Commit Graph

112 Commits

Author SHA1 Message Date
Tianyi Cui
f40dcb5f7b Merge branch 'split/session-persistence' into split/agent-factory
# Conflicts:
#	docs/adr/README.md
#	packages/agent-loop/package.json
#	yarn.lock
2026-06-16 17:04:54 +08:00
Tianyi Cui
96331432b8 Merge branch 'split/turn-enclosure' into split/session-persistence
# Conflicts:
#	docs/adr/README.md
#	packages/agent-loop/package.json
#	yarn.lock
2026-06-16 17:01:36 +08:00
Tianyi Cui
c4fd22f0fa Merge branch 'split/session-meta' into split/turn-enclosure
# Conflicts:
#	docs/adr/README.md
2026-06-16 16:53:37 +08:00
Tianyi Cui
3a6ebd1954 docs(session): renumber session-persistence ADR reference 0016→0018
master's pnpm migration claimed ADR 0016 (0016-pnpm-over-yarn), which
collides with this stack's session-persistence ADR. Renumbering the
session-persistence ADR to 0018 (turn-enclosure stays 0017); update the
json.ts module-doc reference accordingly.
2026-06-16 16:51:59 +08:00
Tianyi Cui
a7fbd93f4f Merge remote-tracking branch 'origin/master' into split/session-meta 2026-06-16 16:45:32 +08:00
Tianyi Cui
5e36f990ab Merge pull request #39 from deepseek-ai/feat/pnpm
feat: migrate yarn to pnpm
2026-06-16 16:31:43 +08:00
Tianyi Cui
6c931b8869 docs: tighten allowBuilds comments 2026-06-16 16:25:24 +08:00
Tianyi Cui
ce484c0e2d build: deny build scripts for unused pi-ai transitive deps
@google/genai and protobufjs are pulled in transitively by the
dsh-llm-pi-ai adapter and consumed prebuilt; their lifecycle scripts are
no-ops we don't need. Set them to false in allowBuilds (was true) so no
unnecessary install-time code runs, and document why each entry is the
way it is.
2026-06-16 16:17:03 +08:00
Tianyi Cui
49e74ed8d0 docs: add ADR 0016 for the pnpm migration
Records the why behind the Yarn 4 → pnpm move (PR #39): ecosystem
alignment, strict-linker phantom-dependency safety, build-script
allowlisting, and the package-manager-independent constraints script.
Captures migration-time install benchmarks and notes the gate suite
passes unchanged on pnpm.
2026-06-16 15:55:21 +08:00
07akioni
dabc2ff411 feat: migrate to pnpm 2026-06-16 14:55:37 +08:00
Tianyi Cui
b218e027d5 Merge branch 'split/session-persistence' into split/agent-factory 2026-06-16 00:44:10 +08:00
Tianyi Cui
1b1385e4f7 fix(session-persistence-jsonl): never wedge a published log on temp-cleanup failure (review #33)
materialize() removed the temp hard link in a finally that ran BEFORE
syncDir() and before state.materialized/cursor advanced. If link()
succeeded (log published) but the temp rm then threw, materialize()
rejected after publishing — leaving state.materialized false, so the
buffered events stayed unpersisted and every retry wedged on the
"already exists" exists() backstop.

Restructured to the robust shape: track link() success; on link failure
remove the temp (the only reference) before propagating; on success
fsync the directory, mark materialized, THEN best-effort remove the
now-redundant temp link (a leftover *.tmp is harmless and never read).
A temp-rm failure can no longer reject a session whose log published.
2026-06-16 00:43:40 +08:00
Tianyi Cui
d76d02b666 Merge branch 'split/session-persistence' into split/agent-factory
# Conflicts:
#	docs/adr/0016-session-persistence.md
2026-06-16 00:40:10 +08:00
Tianyi Cui
9838fdb626 docs(adr-0016): soften 'typed error' to 'clear error' (review #34)
The resume seam intentionally throws a plain Error (the JSDoc and #34
were aligned to "clear error"). Match the ADR 0016 prose, which still
said "typed error". Docs-only; no behavior change. (Also carries the
#32 finalizer-containment fixes via the forward merge.)
2026-06-16 00:38:55 +08:00
Tianyi Cui
091dd12531 Merge branch 'split/turn-enclosure' into split/session-persistence 2026-06-16 00:38:07 +08:00
Tianyi Cui
3e1ca8a425 fix(agent-loop): contain finalizer append-listener throws (review #32 round 2)
The prior fix handled a throwing session/event listener on the turn/start
append, but the SAME push-before-notify hazard remained on the three
FINALIZER appends. Session.append pushes the event before notifying, so a
throwing listener on a finalizer event left the event logged but aborted
the rest of finalization — stranding the turn open.

- failTurn(): set `reason` BEFORE appending the `error` event, and contain
  a throwing session/event listener on it (the event is already logged
  either way). Otherwise reason stayed unset, agent/error was skipped, and
  the caller's closeTurn(false) never ran → open turn.
- closeStep(): the try/catch wrapped only the agent/step-end EMIT, not the
  step/end APPEND. A throwing session/event listener on step/end escaped —
  fatal when closeStep runs from the outer catch during finalization
  (turn/start + step/end but no turn/end). Now both the append and the emit
  are contained and surface as a turn error via failTurn.
- closeTurn(): contain a throwing session/event listener on the turn/end
  append (it would propagate to the runLoop backstop from closeTurn(false),
  or skip the turn-end emit from closeTurn(true)). turn/end is logged
  either way, so the turn stays balanced.

Regressions: a throwing session/event listener on the error event, on
step/end during finalization (driven by a throwing agent/step-start), and
on turn/end — each leaves a balanced turn and the loop survives.
2026-06-16 00:37:18 +08:00
Tianyi Cui
5284ed4806 docs(agent): sync inject wording + resume error wording with code (review #34)
- agent/README: the inject() line said "without triggering a turn",
  regressing the #32 turn-enclosure model. Restored the running-vs-idle
  wording (idle inject wraps a one-shot injection turn; ADR 0017) to match
  the interface JSDoc.
- agent-loop resume() JSDoc said "throws a typed error" but the code
  throws a plain Error (consistent with the sibling assertAgentIdFree
  throw). Softened to "rejects with a clear error" — no behavior change;
  plain Error is intentional (no consumer needs a structured code here).
2026-06-15 23:55:40 +08:00
Tianyi Cui
06d5f60bac Merge branch 'split/session-persistence' into split/agent-factory 2026-06-15 23:53:37 +08:00
Tianyi Cui
01621d38b6 fix(session-persistence-jsonl): surface non-ENOENT storage errors; harden sidecar; broaden contract (review #33)
A durable persistence backend must not treat a storage fault as absence.
listCwdDirs() and exists() swallowed EVERY error and reported "no
sessions" / "not found", so EACCES/ENOTDIR/transient I/O could make
list() return nothing, load() report not-found, and collision checks
proceed under a false absence assumption.

- Add an isENOENT() helper; listCwdDirs() and exists() now return the
  empty/absent result ONLY for ENOENT and rethrow every other error.
  Regression tests drive ENOTDIR through both paths.

TODO-level hardening also addressed:
- writeSidecar() now uses an exclusive owner-only temp open ('wx', 0o600)
  like the log-materialization path, instead of a truncating writeFile —
  the sidecar can carry user data (title/firstPrompt), so a predictable/
  pre-existing temp path must never be silently followed.
- The shared runPersistenceContract serializability case now exercises
  EVERY value isJsonValue rejects (BigInt, undefined, Infinity, function,
  symbol, Map, circular), not just BigInt, so a backend cannot pass the
  contract while accepting values that corrupt the round-trip. The mock
  MemoryPersistence now validates via the canonical isJsonValue.
2026-06-15 23:53:03 +08:00
Tianyi Cui
b4785e598b Merge branch 'split/turn-enclosure' into split/session-persistence 2026-06-15 23:45:09 +08:00
Tianyi Cui
4535bfab75 fix(agent-loop): decide turn balance + idle-injection flush from the log (review #32)
Session.append pushes the event BEFORE notifying session/event listeners,
so a throwing listener leaves the event in the log while the line after
the append (a boolean flag) never runs. Both turn-balance decisions were
gated on such flags, so a throwing listener could strand an open turn or
skip a durability checkpoint.

- loop.ts: the outer catch decided "turn/end owed" from `turnStarted`.
  A throwing listener on the turn/start append left turn/start logged but
  the flag false → catch rethrew and skipped turn/end → permanently open
  turn (violating ADR 0017). Now decided from the log (this turn's
  turn/start present), so the turn is always balanced; only a genuine
  pre-push failure (non-serializable trigger — turn/start never logged) is
  rethrown to the runLoop backstop. Removed the now-dead `turnStarted`.

- agent.ts inject(): the idle one-shot-turn flush was gated on a
  `turnRecorded` flag set after append('turn/end'); a throwing turn/end
  listener skipped the flush, losing the balanced in-memory injection turn
  on crash. Now the flush decision is read from the log, the synthetic
  turn/end append contains a throwing listener (turn stays balanced), and
  a failing idle flush is reported via agent/error (step 0 convention) AND
  the logger — mirroring the loop's post-turn/end flush path — with a
  throwing agent/error listener contained.

Rewrote the test that encoded the old (buggy) "turn/start listener throw
is rethrown, no turn/end" semantics to assert the balanced-turn contract,
and added regressions for the throwing-turn/end-listener flush and the
agent/error report. Updated Agent.inject JSDoc.
2026-06-15 23:44:54 +08:00
Tianyi Cui
da81709a31 Merge branch 'split/session-meta' into split/turn-enclosure 2026-06-15 23:33:25 +08:00
Tianyi Cui
5299e43bed fix(session): snapshot seed + appended data at the boundary (review #31)
The source-level JSON-serializability invariant was only a preflight: the
Session constructor copied the seed array but shared every event/data
object with the caller, and append() stored the caller's `data` reference
verbatim. A post-create/post-append mutation could rewrite the durable
log or reintroduce a non-JSON-serializable value AFTER validation, so
session.events could diverge from what was validated / what a backend
can persist.

- ctor deep-clones each seed event after validation (not just the array).
- append() stores structuredClone(data) (serializability already checked,
  so the clone is safe); the returned event carries the same snapshot.

Regression tests: mutating the original seed / the passed append object
after the call leaves session.events unchanged. Adapted the dev-freeze
invariants test to assert on the logged clone (append no longer freezes
the caller's input). Documented isJsonValue's exact scope (own enumerable
string keys, matching JSON.stringify) and synced the README create()
signature with meta.createdAt.
2026-06-15 23:33:00 +08:00
Tianyi Cui
6455a1600d Merge branch 'split/session-persistence' into split/agent-factory 2026-06-15 22:13:51 +08:00
Tianyi Cui
5f3a1e4d60 Merge branch 'split/turn-enclosure' into split/session-persistence 2026-06-15 22:13:40 +08:00
Tianyi Cui
3cc074ba5c Merge branch 'split/session-meta' into split/turn-enclosure
# Conflicts:
#	packages/agent-loop/tests/review-fixes.spec.ts
2026-06-15 22:13:22 +08:00
Tianyi Cui
611791ba7f Merge remote-tracking branch 'origin/master' into split/session-meta 2026-06-15 22:10:21 +08:00
Tianyi Cui
30765bd6d5 Merge pull request #24 from deepseek-ai/fix/registration-atomicity
fix: registration atomicity across the six registration methods
2026-06-15 22:09:50 +08:00
Tianyi Cui
328863e458 Merge pull request #23 from deepseek-ai/fix/agent-loop-tool-result-callid
fix(agent-loop): log tool/result under the originating call.id
2026-06-15 22:09:32 +08:00
Tianyi Cui
c0257dafcb Merge pull request #22 from deepseek-ai/fix/agent-loop-turn-step-balance
fix(agent-loop): always close a started turn and any open step on error
2026-06-15 22:09:11 +08:00
Tianyi Cui
247c408e75 Merge pull request #21 from deepseek-ai/fix/agent-loop-step-start-order
fix(agent-loop): append step/start before emitting agent/step-start
2026-06-15 22:08:52 +08:00
Tianyi Cui
4e3524f859 Merge pull request #20 from deepseek-ai/docs/sync-doc-sync-gate-and-invariants
docs: sync high-authority docs with the doc-sync gate and dsh-invariants
2026-06-15 22:07:39 +08:00
Tianyi Cui
9a4006cb2b feat(agent): create/resume factory seam
Add the agent-creation factory seam on ctx.agents (AgentRegistry):
setFactory/create/resume plus the AgentFactory interface and
CreateAgentOptions/ResumeAgentOptions. AgentLoop implements AgentFactory
and registers itself via ctx.agents.setFactory(this), so plugins
create/resume agents through the interface without depending on the
concrete loop package.

- create({ agentId, sessionId, meta?, agentOptions? }) — programmatic
  create on a caller-supplied session id (e.g. an ACP-generated id).
- resume({ agentId, resumeSessionId, agentOptions? }) — load a persisted
  session via ctx.sessionPersistence (RFC 009) and resume an agent on it;
  the live session id is the resumed id, turn numbering and derived
  history continue from the loaded log. sessionPersistence is NOT
  hard-injected (non-persistent demos still work); resume rejects with a
  typed error when it is absent. assertAgentIdFree runs before any
  session is created (and again after the load await) so a duplicate id
  never leaves an orphaned live session.

Adds the runtime dsh-session-persistence dependency to agent-loop.
2026-06-15 21:12:14 +08:00
Tianyi Cui
df4b7d3d9a feat(session-persistence): abstract seam + JSONL backend + wiring
Add the durable session-persistence capability seam (ADR 0016): an
abstract SessionPersistence service (dsh-session-persistence,
ctx.sessionPersistence) defining create/append/load/list/has/delete/
update over the existing SessionEvent — no parallel persisted type — and
a first implementation (dsh-session-persistence-jsonl): an append-only
JSONL log per session with crash-safe atomic writes, truncation-repair
of a never-committed crash tail, and a read/replay path. SessionMeta
(format version, cwd, lineage) travels out-of-log via session.header.

A shared runPersistenceContract suite holds every backend to the same
append-only / contiguous-seq / lazy-materialization / serializability
semantics.

Config-driven create() now uses a per-run ${id}-session-<uuid> session
id so a fixed name no longer collides with an on-disk log once a durable
backend is loaded; each run is a new session (a demo simplification). The
examples drop their hand-rolled session-jsonl.ts and load the JSONL
backend via cordis.yml; CI smoke-loads it too.

The agent-facing create/resume factory that consumes load() is a
separate seam, deferred to a follow-up; this change stops at the load
primitive and does not reach into the loop.
2026-06-15 21:05:46 +08:00
Tianyi Cui
b0bc0b5792 feat(agent-loop): turn-enclosure invariant + post-turn error model
Every session event now lives inside a turn (between turn/start and its
turn/end). The loop records queued user/message events AFTER turn/start;
an idle agent.inject() wraps its context/message in a one-shot injection
turn. This makes the turn the single durability/replay boundary so a
persistence backend can treat anything after the last turn/end as a
crash tail without dropping legitimate between-turn context.

A failure once the turn is already closed (rejecting session/flush, a
throwing agent/turn-end listener) has no in-turn position for a session
error event, so it is reported via agent/error + logger only; the turn
stays balanced. failTurn appends an error event only while the turn is
open.

The dsh-invariants plugin enforces turn-enclosure via a default case:
every non-boundary event type — including plugin-added merge-extensible
keys — must sit inside an open turn or it throws.

Documented in ADR 0017 + architecture.md.
2026-06-15 20:56:17 +08:00
Tianyi Cui
0731ed374b feat(session): metadata seam + JSON-serializability invariant
Adds the durable-session metadata seam and enforces the log's
JSON-serializability invariant at the source:

- SessionHeader / SessionSummary / SessionMeta and CreateSessionOptions in
  dsh-session; Session gains a readonly `header`; SessionStore.create takes
  `(id?, options?: { seed?; meta? })` (validated absolute cwd, parentSession
  lineage). The injection TurnTrigger variant is added for the idle-inject
  one-shot turn that a later change introduces.
- isJsonValue (new json.ts): a value round-trips through JSON losslessly —
  rejects BigInt, function, symbol, undefined, non-finite numbers, sparse
  arrays, circular refs, and exotic objects (Map/Set/Date/class instances).
- Session.append throws on non-JSON-serializable data, and the Session
  constructor validates every seed event (isJsonValue + contiguous seq from
  0), so a replay/fork seed can never build a live log no backend can
  persist — the source-level guarantee a durable backend relies on.

Migrates the ~3 internal positional-seed `create(id, seed)` call sites to
`{ seed }`, and adapts the invariants tests forced by the new guard (the
bad-seq seed is now caught by the constructor; the cyclic deep-freeze test
drives via session/event since append rejects cyclic data; a direct
session/event drives the invariants seq-monotonicity check). Docs kept
backend-agnostic (the persistence packages arrive in a later PR).
2026-06-15 17:54:55 +08:00
Tianyi Cui
3783f3e178 fix(agent-loop): surface throwing step-end listener as turn error via failTurn
closeStep() previously caught and silently swallowed a throw from
agent/step-end emit. In the normal no-tool/no-steering path, this
caused runTurn to reach closeTurn(true) with reason still
{kind:completed}, so the session recorded a completed turn with
zero error events — even though a plugin had failed at a loop
boundary. This violates the contract that a throwing plugin is
contained as a turn error, not a silent success.

Now the catch calls failTurn(toError(error)), which appends the
single error event and sets reason={kind:error,…}. failTurn is
idempotent (errorReported guard), so existing error paths that
call failTurn after closeStep are unaffected.

Add regression test: a throwing agent/step-end listener during a
successful step now produces exactly one error event, a turn/end
with reason error, balanced boundaries (step/end before turn/end),
and a surviving loop.
2026-06-15 17:43:33 +08:00
Tianyi Cui
76602a0844 Merge pull request #26 from deepseek-ai/worktree-docs+rfc012-multi-backend
docs: note multi-language Code Mode backends in RFC 012
2026-06-15 08:45:15 +08:00
Tianyi Cui
a1eea4d36e docs: note multi-language Code Mode backends in RFC 012
Clarify that the CodeRuntime seam can host backends differing by
language/runtime, not just trust level — e.g. an AssemblyScript/WASM
backend (naturally sandboxed) and a Python backend over CPython or a
more controllable/embeddable interpreter. Note the execution contract
is language-agnostic while SDK codegen/prompt presentation is per-
language, and add these backends to the deferred follow-up list.
2026-06-15 08:37:45 +08:00
Tianyi Cui
2638ec9f3f Merge pull request #19 from deepseek-ai/worktree-rfc-012-code-mode
docs: RFC 012 — optional Code Mode for all tools
2026-06-15 01:54:47 +08:00
Tianyi Cui
1b7c376aec docs: address second-round PR review on RFC 012 (alternatives, framing)
- Add an Alternatives section comparing Code Mode against the narrower
  result-elision/summarization route over native tool-calling (solves
  context-bloat but not composition/round-trips) and against parallel native
  dispatch (a core-loop change that still lacks composition); states why
  Code Mode is chosen and why the new code-execution surface is the price.
- Fix the Problem-section framing: it said the model "can run independent
  calls concurrently," which contradicted the serialize-by-default decision.
  Reworded to "express fan-out, initially serialized until concurrency-safety
  metadata exists" — early win is composition + fewer round-trips, not parallelism.
2026-06-15 01:46:26 +08:00
Tianyi Cui
6d9934103d docs: address PR review on RFC 012 (concurrency, vm guard, prompt budget)
- Concurrency: change from "may serialize" to mandatory serialize-by-default
  via a per-run dispatch queue in the SDK bindings, with a non-overlap test as
  a hard acceptance criterion (the binding shape otherwise makes Promise.all
  dispatch concurrently before the tool contract has concurrency-safety metadata).
- node:vm guard: make it enforceable, not a README warning — CodeRuntime exposes
  safe:boolean, the VM stub throws unless constructed { unsafe:true }, and
  code-mode refuses to register run_code over an unsafe runtime unless separately
  acknowledged (allowUnsafeRuntime); refusal path is tested.
- Prompt budget: drop the "zero prompt tokens" claim (the SDK .d.ts is injected
  into the system prompt, so types do consume context) and add the explicit
  budget/caching tradeoff — Code Mode's saving is on output/round-trips, not the
  input-side tool description.
2026-06-15 01:39:21 +08:00
Tianyi Cui
1cc6e1caf7 docs: add RFC 012 (optional Code Mode for all tools)
Proposes an optional Code Mode where the model writes a TypeScript program
against a generated SDK wrapping every registered tool, instead of emitting
one native tool-call per step. Implemented Cordis-style as a capability-seam
trio (code-runtime interface / code-runtime-vm node:vm reference stub /
code-mode consumer plugin) with zero core-package changes; the hardened
execution substrate is deferred to a follow-up RFC.
2026-06-15 01:20:09 +08:00
Tianyi Cui
2df41ee1d3 fix: make the six registration methods atomic under a throwing change-listener (P1-1)
llm.registerAdapter, agents.register, sessions.create, systemPrompt.section,
systemPrompt.tools, and tools.register each mutated state, emitted a change
event, then returned the disposer. In Cordis a synchronous throw before the
effect returns its disposer leaves nothing for the fiber to collect, so a
throwing change-listener leaked the registry entry permanently — HMR/dispose
could not clean it, and the duplicate-name/already-exists check stayed wedged
until restart.

Convert each to the generator-effect pattern already proven in
AgentLoop.create: mutate state, `yield` the disposer that undoes it (collected
before the next step runs, so it is torn down if a later step throws), THEN
emit the change event. The existing duplicate-name throws are unchanged — they
fire before any mutation, so they correctly leak nothing. No public API change:
generator effects are still synchronous SyncEffects and register() keeps
returning its fire-and-forget disposer wrapper.

Tests: a listener-throw rollback test for all six methods — register with a
change-listener that throws, assert the call throws AND the registry is clean
(entry absent; a subsequent listener-free register of the same name succeeds
and contributes exactly once). For systemPrompt (no duplicate-name check) the
two tests assert assembly is clean. Verified each fails against the pre-fix
emit-before-return-disposer form.
2026-06-15 01:06:13 +08:00
Tianyi Cui
37576ade6a fix(agent-loop): log tool/result under the originating call.id (P1-7)
The loop passed the authoritative call.id into ctx.tools.execute() but then
appended tool/result using result.callId — the value a tools/execute waterfall
listener returns — with no check. A listener returning a mismatched id silently
recorded the result under the wrong call. callId is the model-transcript
correlation id: deriveMessages() turns it into the tool-result block's
toolCallId, which must pair with the assistant tool-call block; a wrong id
orphans that pairing in the next model request.

Append tool/result with callId: call.id (the loop's authoritative id). A
listener-internal id, if ever worth keeping, belongs in a separate diagnostic
field — never overloaded onto callId.

Test: a tools/execute listener returns a wrong callId; assert the logged
tool/result.callId equals call.id AND deriveMessages() yields a tool-result
block whose toolCallId equals call.id (not the wrong returned id). Verified the
test fails on the pre-fix result.callId behavior.
2026-06-15 01:06:13 +08:00
Tianyi Cui
22e9152d8b fix(agent-loop): always close a started turn and any open step on error (P1-5)
After turn/start was appended, nothing guaranteed a matching turn/end: a throw
from a boundary emit (agent/turn-start, agent/step-start, the normal-path
agent/turn-end) escaped runTurn, and the outer runLoop backstop logged an
error but never appended turn/end — leaving an unbalanced turn that replay,
telemetry, and the invariants plugin all assume is impossible.

runTurn is restructured around idempotent finalizers that satisfy the four
traps a naive finally would hit:
- closeStep()/closeTurn(emit) are guarded (stepOpen/turnEnded) so they run at
  most once; the agent/step-end and agent/error emits are contained so a
  throwing listener can't strand the turn open.
- failTurn() records the single error event + reason and emits agent/error
  exactly once (errorReported guard) — no double-logging when the outer catch
  also runs (e.g. a step error followed by a throwing turn-end listener).
- the catch closes an open step BEFORE turn/end (invariants reject turn/end
  while a step is open), and rethrows ONLY pre-turn throws (turnStarted false),
  where no turn/end is owed, so the backstop still nets them.
- disposal precedence: reason stays disposed only when disposed AND no error
  was reported; otherwise the error reason wins.

Tests (with the invariants plugin loaded as a balance oracle): throwing
turn-start (one error, one turn/end, no step), throwing step-start (step/end
before turn/end), throwing agent/error on a step-error path (balanced, loop
survives), disposal mid-turn (reason disposed, no error event), a pre-turn
turn/start-append throw (rethrown to the backstop, no turn/end owed), and a
step error + throwing turn-end listener (error logged exactly once). Verified
all six fail against a simulated finalizer bypass. dsh-invariants added as an
agent-loop devDependency (test-only oracle; no package cycle).
2026-06-15 01:06:13 +08:00
Tianyi Cui
9d5b3ab832 fix(agent-loop): append step/start before emitting agent/step-start (P1-6)
Every loop boundary appends the session event before emitting the Cordis
event (ADR 0003's append-before-emit rule) — except step/start, which was
inverted. A listener on agent/step-start that inspected session.events could
not see the step it was just told had started.

- Swap the two lines so session.append('step/start') precedes the emit.
- Fix the two stale pseudo-code copies (the runLoop JSDoc STEP-loop block and
  docs/architecture.md) so neither shows step-start emitted before the append.
- Regression test: a step-start listener observes the matching step/start
  event already at the tail of session.events. Verified the test fails on the
  pre-fix (emit-first) order.
2026-06-14 23:18:41 +08:00
Tianyi Cui
bcfff6f1ee docs: correct the doc-sync gate claim in the code-review skill (PR1 review)
Codex review of PR1 found a semantically-identical stale claim outside the
three files first touched: .agents/skills/dsh-code-review/SKILL.md said "the
doc-sync rule has no gate", which is the same P1-16 drift. doc-sync DOES gate
compilable ts blocks and the event-taxonomy table; only prose drift (config
keys, defaults, error codes, wire fields) is ungated. Reworded to say exactly
that.

(ADR 0014 was also checked and is correct as-is — it is the decision record
that establishes the gate and already describes it as existing.)
2026-06-14 23:14:46 +08:00
Tianyi Cui
b8d4790e98 docs: sync high-authority docs with the doc-sync gate and dsh-invariants (P1-16)
The `yarn doc-sync` gate (doc-typecheck + verify-event-taxonomy) and the
@deepseek-ai/dsh-invariants package both exist now, but the instruction docs
never caught up and the gate's markdown scope (README.md, docs/**/*.md,
packages/*/README.md) does not cover AGENTS.md / packages/AGENTS.md, so they
drifted silently.

- AGENTS.md: add invariants/ to the Repository Layout; add doc-typecheck /
  verify-event-taxonomy / doc-sync to Commands; rewrite the false "CI has no
  doc-sync gate" sentence to describe the gate's actual coverage and what
  remains outside it (AGENTS.md, packages/README.md, prose drift).
- packages/AGENTS.md: fix the same stale "no doc-sync gate" line.
- packages/README.md: add dsh-invariants to the dependency graph and the
  package table.

Verification: `yarn doc-sync` green; `grep -rn "no doc-sync gate"` returns
nothing; the three command names + dsh-invariants are present.
2026-06-14 23:07:52 +08:00
Tianyi Cui
5cec4c2f65 Merge pull request #18 from deepseek-ai/feat/rfc009-011-acp-and-persistence
docs: RFCs 009-011 — session persistence + ACP support
2026-06-14 22:02:02 +08:00