classifyAndGate's decision pipeline moves to core: PolicyService runs the
full gate order (classify + transport escalation, plan-first, syntax,
host-only/host-lxc, VM QGA preflight, dedup, approval-flood, window
routing) over ports.GovernanceStore; ExecutionService records and
dispatches (auto-run via ssh.CommandExecutor + TargetResolver, queue via
ExecutionRecorder) with one converged path for run/docker_exec. Gating
matrix test added (risk x window x declared risk -> outcome); pair
coverage 95.6%.
Bug fix surfaced by the matrix: the flag-space syntax regex was inverted
— it refused valid 'tail -n 3' and missed the actual 'head - n' typo.
Fixed to match dash-space-value only.
Remaining Phase 4 items tracked in the plan: ApprovalService.Decide
convergence, execlog fold, execworker poller. VERSION 0.35.0.
Final phase of the hexagonal architecture refactor (plans/2026-08-15-hexagonal-architecture.md). Verifies depguard rules, updates CONTRIBUTING.md to reflect final architecture, marks plan as complete, and bumps minor version to 0.34.0.
Problem: knowledge and learning operations were scattered across
httpapi and mcp handlers with no shared service layer. The hexagonal
refactor needs a single use-case service for both surfaces.
Change:
- app/knowledge.go: KnowledgeService (Search, Upsert, GetContent,
SoftDelete, Restore) and LearningService (ListPatterns,
UpsertPattern, Validate, Quarantine) wrapping the port interfaces.
- adapters/postgres/knowledge.go: KnowledgeRepo implements
KnowledgeRepository — Search, GetContent, Upsert, SoftDelete,
Restore with inline SQL matching the existing handler patterns
(full-text search ILIKE, upsert on conflict, soft-delete).
Verification: go build/vet, full test suite (18 pkgs), DB integration
(postgres + mcp — green).
Problem: signal lifecycle (upsert, resolve, health aggregation) and
observe-pass orchestration (load checks, resolve targets, run probes,
aggregate health) were embedded in scheduler/scheduler.go — 1095 lines
of monolith with no port abstraction.
Change:
- app/signals.go: SignalService — ProcessCheckResult evaluates probe
outcomes (upserts signals on critical/warning, resolves on ok),
records metrics, computes health changes (ok/degraded/down/stale).
WorstHealthForTarget aggregates open signals into entity health.
- app/observation.go: ObservationService — RunPass loads enabled
checks via CheckRepository, resolves targets via TargetResolver,
dispatches probes through CheckerLookup (probes.Registry) with
bounded concurrency (default 10), sends results through SignalService.
- adapters/postgres/signals.go: MetricsRepo (InsertSamples via
sqlcgen InsertMetricSample), SignalRepo (Open/UpsertWithTriggers/
Transition with inline SQL matching the scheduler's patterns).
Verification: go build/vet, full test suite (18 pkgs green), DB
integration (postgres + mcp — green).
Problem: probe logic (checkHTTP, checkTCP, checkPing, checkDNS,
checkSSHScript, etc.) was embedded inside scheduler/scheduler.go as
unexported functions coupled to sqlcgen types — unreachable from the
core ObservationService the hexagonal refactor needs.
Change:
- adapters/probes/network.go: HTTP, TCP, ping, and DNS probe adapters
implementing ports.Checker. Each parses CheckDef.Config (json map),
runs the probe against the Target, and returns a ports.CheckResult.
configMap helper unmarshals config JSON; parseStr/parseFloat extract
typed values.
- adapters/probes/ssh.go: SSHChecker wraps actuator.Dial +
RunCombinedOutput with a SignerSource for key resolution. Registry
(map[string]ports.Checker) with NewRegistry() pre-populating all
known kinds (ssh-script, vm-status, backup-freshness, cert-expiry
set to nil — filled by the ObservationService when signers are
available).
Verification: go build/vet, full test suite (19 pkgs), DB integration
(postgres + mcp — green).
Problem: relationship create/end existed as three drifted copies
(HTTP CreateRelationship/EndRelationship, MCP create_relationship/
end_relationship) with inline SQL, no ontology edge validation on
either path, and no audit on the MCP path.
Change:
- Internal/adapters/postgres/repositories.go: RelRepo implements
ports.RelationshipRepository (Create/End/ListFor) over the pool,
with in-tx upsert + audit/event side effects on Create.
- Internal/core/app/relationships.go: RelationshipService validates
edges against the cached ontology TypeTree (tree.ValidateEdge) and
delegates the tx to the repository. The adapter resolves slug→entity
and extracts types before calling the service.
- HTTP CreateRelationship: resolves source/target via ReadModels,
passes resolved types to RelationshipService for edge validation.
EndRelationship calls the service directly (audit stays in the
adapter for End — a simple toggle with no ontology check).
- MCP create_relationship/end_relationship: rewired to the service
(pool resolves entity IDs inline for the tool handlers; the service
validates edges and writes audit). The MCP path now gets ontology
validation and audit coverage for the first time.
- Composition root: RelationshipService built with RelRepo + Ontology
and wired through httpapi.NewHandler, ListenAndServe, and MCP
constructors.
Verification: go build/vet, full test suite (19 pkgs, DB integration
postgres+mcp green).
Problem: httpapi.NewHandler built its service dependencies internally
(entity repo, entity service, read models), making the handler a
god-object that knew how to construct its own dependencies. ADR 0016
§3.5 wants cmd/oikos/main.go to be the composition root.
Change:
- httpapi.NewHandler: services (EntityService, EntityRepo, ReadModels)
are now injected as parameters instead of constructed inside.
- httpapi.ListenAndServe: passes the injected services to NewHandler.
- cmd/oikos/main.go runAPI + the 'all' role handler: construct
entityRepo, readModels, and EntityService at the composition root
and pass them down. The router stays in httpapi for now; ownership
moves to main in a later phase.
- Tests: newTestHandler updated to construct and inject test doubles.
Verification: full build/vet, non-DB suite (19 pkgs), DB integration
(postgres + mcp — green). httpapi DB tests have the pre-existing set
of failures (TestAPIEndToEnd, TestPhase3* — verified at ec11956).
Problem: entity/relationship/graph/blast-radius reads were embedded
as inline SQL in the httpapi handlers, duplicating the recursive type
tree CTE, the blast_radius function call, and the topology-picking
query across the REST and MCP surfaces with no port abstraction.
Change:
- ports.ReadModels interface: ListEntities, GetEntity, GetEntityBySlug,
GetEntityRelations, GetBlastRadius, GetGraph (with health),
ListEntityTypes. Returns EntityWithHealth (domain.Entity + health
from entity_status join) and domain.Relationship — no gen types
in the port.
- adapters/postgres/readmodels.go: EntityReader implements ReadModels
with the existing SQL verbatim (recursive type filter, blast_radius,
most-connected-first topology, graph edge listing).
- httpapi/entities.go: ListEntities, GetEntity, GetEntityRelations,
GetBlastRadius, GetGraph rewired to ReadModels. SQL moved to the
adapter; handlers map domain/ports types to gen wire shapes.
entityWithHealthToGen, sqlcEntityToGen helpers added.
- Old sqlcEntityToGen (sqlcgen.Entity → gen.Entity) preserved for
client_lifecycle.go; mutation handlers use domainToGen.
Verification: go build/vet, full test suite (19 pkgs), DB integration
(postgres + mcp — both green). httpapi DB tests have the pre-existing
set of failures (TestAPIEndToEnd entity_types=60/501, TestPhase3*)
verified at ec11956.
Problem: entity mutations (create/update/state) existed as three drifted
copies — HTTP CreateEntity/PatchEntity, MCP create_entity/
update_entity_attributes/set_entity_state — each with its own inline
SQL, its own validation subset (MCP validated lifecycle states, HTTP
did not; HTTP patched attributes without regenerating derived checks,
MCP did; MCP wrote no audit trail), the exact drift ADR 0016's first
vertical slice exists to collapse.
Change:
- internal/core/ports: DerivedCheck, Idempotency (adapter-owned request
hash + cached-body renderer so the replay record commits in the
create's transaction), IdempotentResponse + GetIdempotent read,
AuditEntry gains Method/Path/CorrelationID, Event gains
CorrelationID; EntityUpdateInput carries ExpectedVersion +
RederiveChecks (derivation for updates runs repo-side: the graph
host fallback reads relationships through the open tx).
- internal/adapters/postgres/repositories.go: EntityRepo (Create/
Update/SetState/reads/idempotency) preserving the load-bearing
check-then-act invariants in-tx: version WHERE-clause, declared
transitions + preconditions (ValidateTransition), duplicate-slug
mapping, audit/event/writeCheck all inside one BEGIN…COMMIT.
OntologyRepo: TTL-cached OntologyStore.
- internal/core/app/entities.go: EntityService — ontology validation
(type exists, concrete, state declared — the stricter MCP rule now
governs both surfaces), default-state resolution, id generation,
derivation for creates, audit/event construction, idempotency
pass-through.
- httpapi CreateEntity/PatchEntity rewired to the service; PATCH now
regenerates derived checks (the A2 parity gap). MCP create/update/
set-state tools call the same service — and now write audit + event
rows like the HTTP surface always did.
- Integration-test seed paths fixed for the adapters/postgres package
depth (../../../seeds).
Pre-existing failures documented: TestAPIEndToEnd (entity_types 60 vs
59; 501-endpoint now 200), TestClientLifecycleEndToEnd, TestPhase3*
rows — verified failing identically at ec11956 (scratch approval-
notifier commit test drift), unrelated to this change. All mutation
integration tests (create/patch/idempotency/audit/regeneration) pass.
Verification: make test-db (postgres + mcp green, httpapi green except
the pre-existing set), full non-DB suite (19 pkgs), golangci on new
packages — 0 issues.
Problem: the repo carried cruft that predates the Phase 1 client split:
tracked web/node_modules and web/dist content (committed before the
ignore rules existed — ~2.6M lines), seven stale Claude worktrees plus
two stale Agent Manager worktrees (1.5GB on disk, all fully merged),
their 29 merged experiment branches, a pre-DB root inventory.yaml,
Playwright MCP session logs, a config screenshot, and the executed
one-shot apps/105 webhook-cleanup script. The dockerignore's own
comment documents how this cruft once starved the mac-mini disk
mid-build.
Change:
- git rm: web/ + cmd/desktop/ tracked remnants (node_modules, dist),
root inventory.yaml (stale pre-DB copy; seeds/inventory.yaml is
authoritative and what tests read), .playwright-mcp/ logs,
config-screen.png, .claude/launch.json,
scripts/cleanup-apps105-webhooks.sh (job done; pattern lives on in
oikos-web's webhook setup).
- Removed 9 stale worktrees (nested-first) + pruned; deleted 29 fully
merged branches (claude/*, feature/*, frontend-os-apps,
judicious-freckle, code-quality-* pair, impartial-height). The one
branch with an unmerged commit, chore/vendor-orby-engine, vendored
web/vendor — that work moved to dtoro/oikos-web in Phase 1, so it is
superseded.
- Disk cleanup: web/, cmd/desktop/, stray desktop binary, bin/, build
artifacts. Kept: root oikos + webhook binaries (referenced by the
launchd deploy unit and oikos-web's installer), .env bootstrap,
.infisical-credentials.
- .gitignore: .claude/, .playwright-mcp/, config-screen.png now
clone-safe instead of relying on local info/exclude.
Risk: none functional — deletions are either merged history (branches
recoverable from reflog) or content that moved repos; build, vet,
tests, and generate-check all green post-purge.
Verification: go build/vet, make test (19 pkgs ok), generate-check,
git ls-files web cmd/desktop → 0; worktree list → main only.
Problem: the hexagon's Phase 2 (plans/2026-08-15-hexagonal-architecture.md)
must give the use-cases-to-be their contract surface: driven-port
interfaces, test fakes, the secrets interface moved into core, and the
postgres package inside the adapters tree — before the first vertical
slice (Phase 3) can wire a composition root.
Change:
- internal/core/ports: full driven-port catalog per plan §3.3 —
repositories as transaction-scoped aggregates whose inputs carry
derived checks, audit, and events (§3.6), plus CommandExecutor,
TargetResolver, Checker, Secrets, EventPublisher, Provisioner.
Port-local payload types (Event, AuditEntry, CheckDef, KnowledgeEntry,
ExecResult) keep signatures off infrastructure; TypeTree aliases
internal/ontology (pure over domain) until checkdefaults is absorbed.
ReadModels intentionally not declared yet — it materializes with the
Phase 3 slice and grows as report handlers rewire.
- secrets.Backend is now an alias of ports.Secrets; implementations
(Infisical, SOPS, Manager) unchanged. mcp's local secretBackend
subset is deleted; tool constructors take ports.Secrets.
- internal/db → internal/adapters/postgres (mechanical import rewrite;
package identifier stays db until the Phase 3 repository split).
sqlc.yaml, Makefile, golangci exclusions, and docs follow the move;
make generate-check verified.
- internal/adapters/ssh: Executor implements ports.CommandExecutor over
the actuator dial pool + RunStreaming (10-min default timeout carried
over from the httpapi path).
- internal/adapters/remote: Resolver implements ports.TargetResolver
delegating to internal/remote (still pool-based; drops onto
ports.EntityRepository when repositories land in Phase 3 — documented
transitional import).
- internal/core/ports/portstest: importable fakes — in-memory
EntityRepo (with check-then-act SetState, side-effect recording),
RecordingExecutor, FakeChecker, SpyPublisher; port-satisfaction
guards; tests.
Risk: ports are declared ahead of implementations — signatures firm up
per phase as slices land (documented in the package doc); the
remote→postgres transitional import is explicit and dissolves in
Phase 3.
Verification: go vet, make test (race, 19 packages), generate-check,
golangci on core+adapters — 0 issues; full-repo baseline down
365→344.
Problem: the hexagonal refactor churns the backend tree for nine more
phases; the UI delivery stack (web/ SPA, cmd/desktop Wails wrapper,
compose/web image) must move to its own repo first so doc/layout
rewrites land once on a backend-only tree.
Change:
- New repo git.hubris.network/dtoro/oikos-web (v0.33.0): web/, desktop/
(updateURL repointed to oikos-web releases), compose/, own CI (web +
desktop jobs), own deploy script (CI-green gate, TOCTOU guard,
version-tagged images, prune-to-3), own webhook receiver on :9798 +
launchd unit, own compose project publishing the same 8091:80.
- Cutover executed on mac-mini in order: oikos stack's web service
stopped+removed, oikos-web project brought up on 8091; outer Caddy
untouched (targets the published port) — serving + Authentik flow +
/wails 404 quirk verified post-cutover.
- Stripped from oikos: web/, cmd/desktop/, compose/web/, desktop CI
workflow, ci.yml web job, Makefile ui/desktop/desktop-package/install
targets, the compose web service, oikos-web from deploy.sh's fallback
prune list; wails + go-keyring dropped from go.mod, vendor synced.
- README / CONTRIBUTING / AGENTS.md / .agents dev+operations docs now
point at the new repo; mbse + mascot design docs carry a path note.
Risk: production SPA serving depends on the new pipeline now; rollback
is versioned-image re-up of the old web service from a pre-split
checkout (port 8091). Desktop builds installed before the split still
check dtoro/oikos releases — one manual reinstall, noted in the
oikos-web release notes.
Verification: go vet, make test (race), make generate-check, golangci
(no new findings; baseline down 400→365); post-cutover curls —
localhost:8091 200, /wails/runtime.js 404, outer Caddy 302 Authentik.
Problem: the hexagonal-architecture plan (plans/2026-08-15-hexagonal-
architecture.md) needs its foundation — an accepted ADR, the target
directory tree, and machine-checked dependency rules — before any
service extraction starts. Also folds the four outstanding review
findings (F3.1/F5/F6/F7) into the plan: ObservationService owns the
bounded probe-concurrency contract (scheduler.go:133), Phase 9 gates
ExecutionService+PolicyService ≥ 90% with a gating-matrix test,
per-phase abort criteria, and the §3.2 internal/config note.
Change:
- docs/adr/0016-hexagonal-ports-adapters.md records context, decision,
and consequences of the ports & adapters migration.
- internal/domain → internal/core/domain (mechanical import rewrite,
20 files), new internal/core/{ports,app}, internal/adapters trees
with package docs.
- .golangci.yml: depguard rules for §3.1 (core purity, no agent-client
tech in core, nomos isolation — the nomos rules self-activate when
internal/nomos exists in Phase 8). Config migrated to golangci-lint
v2 format so it loads at all (the v1 config errored under v2, masked
by CI's advisory continue-on-error). Verified depguard fires on a
planted openai-go import in internal/core/app.
- CONTRIBUTING.md layout section now shows the core/adapters tree.
Risk: import path churn is mechanical and tests pass unchanged; the
lint config migration surfaces the pre-existing 400-issue baseline
(advisory in CI, unchanged policy) — new/moved packages lint clean.
Verification: go vet ./..., make test (race, core/domain at 100%
coverage), make generate-check, golangci-lint on internal/core/... and
internal/adapters/... — 0 issues; depguard violation probe confirmed.
P0: Transport escalation in classifyAndGate (server.go:745) now exempts
log-inspection commands (tail/head/cat/journalctl on *.log or /logs/)
from the /opt//etc//var/lib/ gating on LXC targets. Fixes the
'tail -3 /opt/seanime/data/logs/seanime.log queued for approval' bug.
P1: queryEntity returns actionable error when slug_or_id param is empty
('slug_or_id is required') instead of silent 'entity not found: '.
P2: Added slugArg() helper (server.go) so get_entity, get_relations,
get_blast_radius, and explain accept 'slug' as an alias for their
declared param name. Solves the discoverability inconsistency where
every tool used a different param name for the same concept.
P3: Two new MCP tools:
- restart_service(target, service) — systemctl restart wrapper,
correctly classified config_mutation (requires approval)
- push_file(target, source_path, dest_path, backup=true) — pct push
from Proxmox host into LXC, with optional backup. Classified
config_mutation. LXC-only for now.
P4: Updated homelab-lxc-ops skill with MCP tools preference table.
Plus: Wails desktop build now uses build-tag approach for frontend embed
(assets_embed.go + assets_stub.go), so go build ./... works on
clean checkout without the frontend built first.
Version: 0.31.0 → 0.32.0
Adds pvecm_quorum_check.sh probe script and wires it into the
checkdefaults system as a new 'quorum' monitoring kind on proxmox-host
entities. Runs every 60s via ssh-script, surfaces unhealthy signal when
cluster loses quorum.
Closes the monitoring blind spot that let the 2026-08-12 3.5h corosync
flapping outage go undetected (ping passed, cluster was non-quorate).
Changes:
- seeds/ontology.yaml: proxmox-host declares monitoring: [quorum]
- internal/checkdefaults/defaults.go: KindQuorum builder
- internal/checkdefaults/build_test.go: 2 new test cases
- checks/pvecm_quorum_check.sh: new probe (deployed to hubris + strong)
- VERSION: 0.30.2 -> 0.31.0
New EntityGraph.svelte app (sigma.js + graphology):
- Theme-aware Gruvbox palettes (light/dark) with reactive color switching
- Lucide icons rendered synchronously via Path2D canvas per entity type
- Color mode toggle (Health / Type) with per-type distinct colors
- Health distribution bar with clickable filters
- Entity type filters grouped by ontological layer (collapsible)
- Relationship type edge filters with color-coded swatches
- Quick presets: All / Problems / Infra
- Node selection with live blast radius from API
- Hover neighborhood highlighting with muted fade
- Isolated node hiding, edge alpha tuning, dot-grid background
- Search with camera focus on highest-degree match
The healthcheck subcommand was reachable only after main()'s Infisical secrets
resolution (4x retries/key, ~30s when Infisical is down), which blew the 5s
Docker healthcheck timeout — so nomos stayed docker-unhealthy despite serving
/healthz fine. Short-circuit 'nomos healthcheck' at the top of main() before
any secrets init; measured 0.58s, no Infisical retries.
The nomos runtime image is gcr.io/distroless/static (no shell/wget), so the
wget-based healthcheck (D5) could never run — nomos showed docker-unhealthy
despite serving /healthz fine. Add a 'nomos healthcheck' subcommand that
self-probes NOMOS_LISTEN/healthz (exit 0 on 200), and point the compose
healthcheck at ["/nomos", "healthcheck"].
Measured startup is ~93s: NewHandler stalls on Infisical auth retries (~40s)
and OIDC discovery timeouts to auth.hubris.network (~35s) before binding
:8090. The api IS healthy once bound (serves /healthz); the window just needs
to clear both external-timeout phases so nomos (depends_on: api-healthy) can
start and the deploy completes.
The api retries Infisical at startup (4x with backoff) before binding :8090.
When Infisical is unreachable that adds ~60s, and the old start_period (10s)
+ 10 retries (~60s grace) ran out just before the bind — marking the api
unhealthy and failing the deploy (nomos depends_on api-healthy). 90s covers
the slow-startup window; the api genuinely serves /healthz once bound.
The procedural-glyph-engine dep pointed at a non-portable file:/private/tmp/orby-pkg
path, breaking npm ci in Docker and every main deploy since v0.20.0 (the build
cache masked it until it busted ~Aug 5). Vendor Orby v5.0.0 into web/vendor/,
switch the dep to file:../vendor, and use npm install in the web Dockerfile
(file: deps need install, not ci). Cherry-picked from 3cd4cf9.
- classifyAndGate: escalate read-only commands on lxc: targets that
touch /opt/, /etc/, /var/lib/ to config_mutation. The classifier
scores command text only, not the SSH transport layer — SSH-ing into
a container to read config is riskier than pct exec from the host.
- ontology: standalone-server monitoring override from inherited
[ping, resource, updates] to [http]. VPS-like machines may not be
SSH/ICMP-reachable from the scheduler; HTTP is the LCD liveness
signal. Entities with full SSH can override per-entity.
- get_health_summary: filter out state=destroyed entities (was noise
from 20+ destroyed test LXCs, deprecated services, etc.)
- create_entity: document the monitoring footgun in the tool description
(creating a type=check entity does NOT wire a check_def; the correct
path is update_entity_attributes with monitoring + url attributes)
Adds a new 'dns' semantic monitoring kind that probes whether a DNS name
resolves. Uses net.LookupNS (NS records) with fallback to net.LookupHost
(A/AAAA). Supports an explicit server config for split-horizon resolution.
Changes:
- seeds/ontology.yaml: dns-zone monitoring: none → [dns] (was deferred
since 2026-06 with a comment 'no dns checker exists yet')
- seeds/inventory.yaml: host:netbird-vps monitoring: [http] (was none;
VPS was invisible for 7 days during the 2026-07-29 outage)
- internal/checkdefaults/defaults.go: add KindDNS, buildKind case for 'dns'
that creates a check_def at 5-minute intervals
- internal/scheduler/scheduler.go: add checkDNS probe + wire in executeCheck
The DNS checker catches stale/unreachable zones (e.g. matrix.hubris.network
pointing to a dead VPS IP). The VPS HTTP check probes the public endpoint
every 60s, closing the 7-day monitoring gap.
get_relations now accepts an optional 'types' (comma-separated) parameter
to filter relationship types — filters out the noisy exec/targets edges
that previously drowned useful host/provides edges.
get_health_summary now accepts an optional 'health' (comma-separated)
parameter to return only entities in specific health states (e.g.
'health=down,stale') instead of the full 100+ entity list.
ping_service now:
- Falls back to e.attributes->>'public_host' when 'url' is not set
(covers LXCs that only have public_host in the graph)
- Performs a live HTTP HEAD probe against the resolved URL, returning
the actual status code instead of just the scheduler's stale health
state
Also: fixed matrix.hubris.network DNS record (was pointing to dead VPS),
pruned 6 dead graph edges, wired url attributes on 7 LXCs, added VPS
HTTP monitoring check, and resolved the 18k-occurrence unmonitored signal.
This session's audit is documented as
document:nomos/2026-08-05-dns-monitoring-improvements-for-strong-hosted-services.
- Add MCP tool — lightweight connectivity check returning server
identity, no DB hit (resolves agent connection-test friction)
- Tighten 6 tool descriptions (get_relations, get_health_summary,
query_metrics, get_trend, get_event_timeline, ping) to be searchable
in the first 8-12 words
- Document Hermes MCP client setup in ADR-0012 with token security caveat
- Move completed plan to plans/done/
Backend:
- Add isThinking flag to agentEvent for text before tool calls
- Separate thinking from response text in runChatTurn and continue.go
- Persist thinking in a dedicated field in message content
Frontend:
- Add thinking field to MessageContent, ChatMessage, ChatTextEvent types
- Create ThinkingBlock.svelte — collapsible block with brain icon
- SSE handler moves text_delta content to thinking on isThinking flag
- Render thinking block between tools and response in ChatThread
- Fix chat window scroll reset on focus change (stable windowKeys order)
- Remove redundant #key id wrapper in WindowLayer
- Enlarge sidebar rail (24→32 default, 40→60 max)
- Remove glyph from sidebar, square graph at top
- Replace AgentTrace/ToolCallCard/UnifiedTimeline with TurnTrace/ToolLine
The "Working — message will queue" strip lived inside the sized input Pane, so
appearing/disappearing ate the textarea's fixed height and clipped it, forcing
a resize. Move it into the message Pane alongside the connection/error banners
— those correctly consume transcript space (flex-1) rather than the input's
fixed height. The input Pane is now stable whether or not a background turn is
running. Styling/idiom unchanged.
VERSION: 0.17.2 -> 0.17.3
The background-working hint was a plain muted text line bolted above the
textarea — misaligned with the input column and off-idiom. Restyle it as a
terminal status strip: spinner + uppercase fg "Working" label + muted detail,
hairline primary-tinted border (matching .trace.running), square, aligned to
the textarea's max-w-3xl column. Reads as part of the working state now.
VERSION: 0.17.1 -> 0.17.2
Move ten completed plans from plans/ to plans/done/ and update the index:
- 2026-07-18 session-review-three-sessions, 2026-07-20 desktop-mascot,
2026-07-20 session-review-ten-sessions, 2026-07-21 chat-full-polish,
2026-07-29 health-check-reality-and-knowledge-graph,
2026-07-30 session-review-plan-drift, and the four 2026-08-03 chat plans
(changes-review, reliability-and-ux-audit, cyberspace-style-adoption,
working-visibility).
- Refresh two stale statuses: cyberspace-style-adoption ("Draft" -> shipped as
full replacement in v0.16.0/757ef2f) and health-check-reality ("ready for
implementation" -> shipped across the v0.14.x-0.16.x check commits).
- .gitignore: ignore local tooling artifacts (.playwright-mcp/, config-screen.png).
No code change. index.md Active/Done tables now match the filesystem (no orphans).
VERSION: 0.17.0 -> 0.17.1
Make background/long/desynced turns visible and queueable, fixing the four
symptoms that survived the v0.15.0 chat reliability pass.
F1 - status-driven working signal (workspace.ts taskWorking/currentWorking =
streaming OR status in {planning,executing}). Drives the chat trace, indicator,
and activity spinner so a turn with no live stream (background resume, a dropped
SSE, an idle-close mid long turn) still looks alive.
F2 - operator messages sent during an in-flight turn are now QUEUED and
auto-run when the gate frees, replacing the "still finishing a previous step...
send it again" rejection. Per-session in-memory FIFO (messagequeue.go, capped at
20) drained one-at-a-time under the turn gate; a `queued` SSE event drives a
"Queued" hint. drainQueued releases via a per-iteration deferred closure so a
runChatTurn panic can't deadlock the session's gate.
F3 - SSE keepalive (12s `:keepalive` comment) in handleChat so 20-40s
inter-iteration gaps no longer trip a proxy/browser idle close (the desync root
cause). All SSE writes serialized through one mutex.
F4 - generation-aware activity timeline (only the last propose_plan renders;
superseded ones collapse to one "Earlier plan revised" marker; step-attribution
follows only the current generation) + debounced plan refetch on lifecycle
events so a missed plan.proposed self-heals.
Verified against the last session (23da10db: 6m33s turn, operator "status"
deferred at 19:48:05). go test ./cmd/nomos/ green (new messagequeue tests);
web vitest 72/72 (new F4 generation tests); vite build clean.
VERSION: 0.16.0 -> 0.17.0
Rebrand light/dark themes to the cyberspace.online look: warm cream-on-black
palette (light/dark are exact inverses), self-hosted JetBrains Mono + VT323,
square corners, border-driven surfaces with no soft shadows. Adds a terminal
design-system CSS layer (DOS double-border modals with hatched corner, fg focus,
inversion-on-hover), a theme-aware <RasterImage> (Atkinson-dithered canvas with
img fallback), and unifies desktop icons, taskbar, window controls, pills and
links under one idiom. Pins window titlebars to a fixed height and switches chat
auto-scroll off scrollIntoView to avoid titlebar reflow.
VERSION 0.15.1 -> 0.16.0
The Send button's disabled={!input.trim()} threw "trim is not a function" when
input was initialized from a non-string initialDraft (a Svelte 5 prop-init edge
where a null/undefined draft reached $state). Coerce at init so the composer
state is always a string.
VERSION: 0.15.0 -> 0.15.1
The agent could run two turns for one session at once (a reconnect resumed
while the live turn was still going), and their interleaved tool calls
corrupted the activity panel, fabricated a confusing "parallel/nested"
sequence, and made tasks feel stuck/never-ending. Several UX gaps compounded it.
Turn serialization (F1):
- turnGate: at most one in-flight turn per session. Background resume paths
(continuation worker, idle sweep, answer-question, /resume, reconnect)
skip non-blocking when busy; the live chat path waits briefly then bails
cleanly instead of stacking a second turn.
- resumeSession returns whether it ran; continueSession marks an execution
"continued" only after a real run (review P0) so a busy-skip can't lose a
finished-execution result. Idle nudge bumps only after delivery (P1).
Connection state (F2/F3, web):
- humanize/bucket raw errors ("model connection dropped..."); one surface
per drop; a terminal task.status event clears stuck streaming/disconnected
state and dismisses the connection toast. Reconnect no longer spawns turns.
Streaming where you look (F4, web):
- live command output in the global activity timeline and in the inline
tool card (auto-opened, tail-pinned) -- not just the per-window rail.
Other (web): artifact/knowledge deep links (F5); step-first stable
"thinking" headline (F6); stable chat layout, no empty->content reflow (F7);
lazy event sync (P2.2); reconnect skips a terminal session (P2.1).
VERSION: 0.14.2 -> 0.15.0