Merge branch 'stack/agent-profiles-1-seam' into stack/agent-profiles-3-wire

Rewires this layer onto the standing-mount model:

- serviceForAgent roots its search at the agent's standing mount (parent
  scope key → live mount fiber) — the composition no longer lives under the
  agent's own fiber, and two agents on one preset now address ONE instance,
  which the sharing test asserts instead of distinctness.
- viewFor/historyPage take a registry view SCOPE. A live agent is that scope;
  a cold read uses the recorded preset's standing key via standingKeyFor —
  composing plugins but starting no agent, session, or turn. A header without
  a preset (a pre-roster log) renders through the DEFAULT preset's standing
  layer; an unusable preset degrades the read to generic cards, never fails
  it. This turns produced-files and chat-scroll green structurally, with the
  token counts untouched (no resume, so the projections fold stays detached).
- The detached projections baseline now includes every standing unit's key at
  its empty fold (todos: null): the standing mount registers units
  deterministically, which is what makes the client's "omitted key =
  capability absence → clear" rule safe again. seeded-history's contract test
  asserts the new shape.
- The standard preset's realm preamble no longer claims a shared label pools
  instances — provide() throws on the second registration under one realm
  symbol; labels join REALMS.
This commit is contained in:
Yichen Jiang
2026-08-08 18:41:07 +08:00
24 changed files with 566 additions and 117 deletions

View File

@@ -1,18 +1,21 @@
# The `standard` agent preset: the full coding agent, mounted per session.
# The `standard` agent preset: the full coding agent, mounted once per process.
#
# This file is an AGENT-PLANE composition. It is mounted under one agent's
# scope context, so every tool and prompt section it registers belongs to that
# session alone. The host composition (`base.cordis.yml` + `web.cordis.yml`)
# keeps everything a preset must not own: the registries themselves, the
# sandbox and approval stack, persistence, and the model route.
# This file is an AGENT-PLANE composition. The roster mounts it ONCE under a
# standing scope; every session naming it joins by scope parentage, so the
# tools and prompt sections registered here cover each joined agent while a
# session's own state stays keyed per Session/Agent inside the plugins. The
# host composition (`base.cordis.yml` + `web.cordis.yml`) keeps everything a
# preset must not own: the registries themselves, the sandbox and approval
# stack, persistence, and the model route.
#
# A service row here MUST sit inside a group carrying an `isolate` realm.
# Without one it publishes into the root realm, where it is process-global
# rather than per-session and the second session mounting this preset collides
# with the first; `dsh-agent-presets` rejects that at mount. `true` means an
# entry-local realm — one private instance per mounted session, which is the
# default this deployment wants. A shared label would instead pool one instance
# across every session naming it.
# Without one it publishes into the root realm, where it is process-global
# another preset publishing the same name collides, and a host reader would
# resolve one preset's instance for every session; `dsh-agent-presets` rejects
# that at mount. `true` means an entry-local realm: this standing mount's own
# private instance, apart from every other preset's. (A shared label does NOT
# pool instances — `provide()` throws on the second registration under the
# same realm symbol; labels join REALMS, and are not what this file needs.)
# ── identity ────────────────────────────────────────────────────────────────

View File

@@ -236,14 +236,13 @@ describe('web e2e: seeded history renders through cold resume', () => {
// The seed carries a session/title event: the title unit is host-plane, so
// it folds the detached log and serves the value with nothing composed.
expect(typeof projections?.values.title).toBe('string')
// `todos` is NOT here, and that is the contract rather than a gap. Its unit
// is registered by `tool-todo` inside an agent's preset, so a detached
// session yields it from exactly one place: a durable checkpoint written
// while the session was live. This seed was written straight to persistence
// and never ran, so it recorded none — and the answer no longer depends on
// whether some UNRELATED session happens to be composed right now, which is
// the whole reason the checkpoint row carries its own view.
expect(projections?.values).not.toHaveProperty('todos')
// `todos` IS here, as its empty fold (null). Its unit is registered by
// `tool-todo` inside the default preset's STANDING mount, which the read
// itself ensures — deterministically, not because some unrelated session
// happens to be composed. A present-but-null key is what keeps the
// client's "omitted key = capability absent → clear the row" rule from
// wiping preset-owned projections on cold reads.
expect(projections?.values).toHaveProperty('todos', null)
})
it.skipIf(MODE === 'record')('lists the seeded session cold and renders its history from the log', async () => {