Commit Graph

89 Commits

Author SHA1 Message Date
Tianyi Cui
6fdd048123 fix(agent-loop): harden lifecycle edge cases 2026-06-17 21:25:47 +08:00
Tianyi Cui
f860474f8b Merge branch 'feat/acp-3-multi-session' into feat/acp-4-session-cwd 2026-06-17 15:27:16 +08:00
Tianyi Cui
ff82254fca Merge branch 'feat/acp-2-bridge' into feat/acp-3-multi-session 2026-06-17 15:24:09 +08:00
Tianyi Cui
a9d5a5ba68 Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge 2026-06-17 15:20:36 +08:00
Tianyi Cui
977c23d31f Merge remote-tracking branch 'origin/master' into feat/acp-1-max-tokens-turn-end
# Conflicts:
#	AGENTS.md
2026-06-17 15:16:26 +08:00
Tianyi Cui
f3906af225 feat(acp): honor per-session cwd — run each ACP session in its own workspace
Lifts the RFC 010 § Deferred restriction that the server had to launch in the
workspace ("cwd must equal the launch directory"). An editor can now open any
project folder, and N concurrent sessions over one connection can each target a
different directory.

- packages/acp: drop the `cwd === process.cwd()` guard in validateWorkspaceParams
  (keep "must be absolute" — the cwd becomes the session header / bash workdir),
  and drop the persisted-cwd-vs-launch-dir check in session/load (a resumed
  session keeps its original header.cwd, so its bash tools run in its workspace).
- packages/tool-bash: the missing link — default the bash workdir to the calling
  agent's session cwd (`exec.agent.session.header.cwd`) via a new resolveWorkdir
  helper. An explicit model `workdir` still wins; a relative one resolves against
  the session cwd. This is the only correct spot for multi-session: N sessions
  share one ctx.bash executor, so the workdir must come per-call from exec.agent,
  not executor config. Falls back to the executor default when no session cwd is
  available (preserves non-ACP behavior).
- Trust: the cwd originates from the ACP client (the user's editor) at
  session/new — same trust level as the old launch dir; no new untrusted-input
  path. `additionalDirectories` (scope widening / sandbox) stays rejected.
- Tests: bridge accepts any absolute cwd + records it on the header; session/load
  honors the persisted cwd; bash defaults to / resolves relative against the
  session cwd; two sessions with different cwds each run bash in their own dir;
  non-absolute cwd still rejected. 100% per-file coverage maintained.
- Docs: RFC 010 status + § Deferred cwd bullet marked RESOLVED; acp README adds a
  Per-session cwd section; tool-bash + example READMEs and e2e comments updated.
2026-06-17 10:53:40 +08:00
Tianyi Cui
dbaef018cc Merge remote-tracking branch 'origin/split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	docs/adr/0018-session-persistence.md
2026-06-16 23:53:54 +08:00
Tianyi Cui
d3c9f6ca1e Merge remote-tracking branch 'origin/split/session-persistence' into split/agent-factory 2026-06-16 23:51:16 +08:00
Tianyi Cui
2bae18f811 fix(session): synthesize tool results for interrupted tool calls on crash recovery (review #33)
The loop logs the assistant/message (carrying tool-call blocks) BEFORE running
the tools, so a crash mid-tool leaves durable tool calls with no matching
tool/result. interruptedTurnClosers only added step/end + turn/end, so a resumed
session's deriveMessages() replayed a dangling assistant tool-call — which every
provider rejects as an invalid transcript on the next request.

interruptedTurnClosers now scans the interrupted turn for tool-call blocks
without a matching tool/result and synthesizes an error tool/result for each
(before the step/end), so the rehydrated history is a valid transcript. Adds a
dedicated repair.spec.ts and a shared-contract case proving both backends pair
every orphaned call with a result. Docs (ADR 0018, both persistence READMEs,
load() JSDoc) updated.

Also fixes the echo-agent README session-cleanup path: demo:echo runs from the
repo root, so sessions land in <repo-root>/.sessions/_no-cwd/, not
examples/echo-agent/.sessions/ (review #33).
2026-06-16 23:50:52 +08:00
Tianyi Cui
5e3df1b2f5 Merge branch 'feat/acp-2-bridge' into feat/acp-3-multi-session 2026-06-16 23:44:27 +08:00
Tianyi Cui
c2f8af30da Merge branch 'feat/acp-1-max-tokens-turn-end' into feat/acp-2-bridge
# Conflicts:
#	AGENTS.md
#	docs/cookbook/extension-cookbook.md
#	yarn.lock
2026-06-16 23:40:23 +08:00
Tianyi Cui
ddf121320b Merge remote-tracking branch 'origin/split/session-persistence-sqlite' into feat/acp-1-max-tokens-turn-end
# Conflicts:
#	packages/session/src/types.ts
2026-06-16 23:31:14 +08:00
Tianyi Cui
ce4b32ace9 feat(session-persistence-sqlite): preserve interrupted turns on load, don't truncate
Mirror the JSONL backend's crash-recovery contract change: load() now PRESERVES
the real events of an interrupted final turn (a turn can be huge — truncating it
would destroy work) and durably CLOSES the orphaned turn with synthetic boundary
events (step/end if open, then turn/end {interrupted}) inside one transaction
that also deletes any torn tail row. load() is therefore mutating; the deferred
truncation-repair on the next append is gone. Replaces cutAtLastTurnEnd with
scanRows (longest preserved prefix + torn-tail offset). Updates the sqlite tests
and README to the preserve-and-close semantics; the shared runPersistenceContract
suite now holds both backends to identical interrupted-turn behavior.
2026-06-16 22:56:17 +08:00
Tianyi Cui
f0d9383f49 Merge branch 'split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	docs/adr/0018-session-persistence.md
#	docs/architecture.md
2026-06-16 22:32:49 +08:00
Tianyi Cui
0000cdb2c2 feat(agent-loop): config-driven session resume via RESUME_SESSION_ID
A config agent with `resumeSessionId` set continues a persisted session
instead of starting a fresh `${id}-session-<uuid>`. The id is sourced from
an env var in cordis.yml, so the coding-agent demo can resume a prior
conversation without code changes. The resume is deferred until the
`sessionPersistence` backend loads (via ctx.inject) and is contained: a
missing/unreadable id logs a warning and starts no agent. Adds a real-API
resume e2e proving cross-process continuity through the JSONL backend.
2026-06-16 22:28:01 +08:00
Tianyi Cui
8511b9639c Merge branch 'split/session-persistence' into split/agent-factory
# Conflicts:
#	docs/adr/0017-turn-enclosure-invariant.md
#	docs/architecture.md
2026-06-16 22:27:51 +08:00
Tianyi Cui
efee449cfe feat(session-persistence): preserve interrupted turns on crash; don't truncate (review #33)
A crash can leave a durable log whose final turn never closed. The old
behavior truncated everything after the last turn/end as a "crash tail".
But a single turn can be HUGE in a long-horizon task (many steps, large
tool output), so truncating it silently destroys real, durably-written
work — truncating a turn is wrong.

New crash recovery (ADR 0018): load() PRESERVES the interrupted turn's
events and CLOSES the orphaned turn by durably appending synthetic
boundary events — a step/end if a step was open, then a turn/end carrying
the new merge-extensible TurnEndReason {kind:'interrupted'}. load()
returns the balanced log, so a resumed session is immediately usable. Only
a never-fully-written TORN tail fragment is discarded; corruption in the
committed region is still unloadable.

- dsh-session: TurnEndReason {kind:'interrupted'} + shared
  interruptedTurnClosers() repair helper.
- JSONL backend: scanLog preserves the longest contiguous prefix
  (including a partial final turn); loadCore truncates a torn fragment and
  durably writes the closers, returning the balanced log.
- runPersistenceContract gains a crash-recovery test (both backends + mock).
- Docs: ADR 0018/0017, architecture.md, package READMEs.

Also (review #33): RFC 013 records the "move event vocabulary to Zod"
question (merge-extensible maps → runtime schema registry) + blast radius;
deferred, not done here.
2026-06-16 21:27:50 +08:00
Tianyi Cui
3bef6b38c7 refactor(session-persistence-sqlite): drop the materialized column; use row existence as the signal (review #35)
The `materialized` INTEGER column was redundant: create()/update() already
keep a lazy session in memory and write no row, so a `sessions` row is
written only by the first append. Its EXISTENCE is the materialization
signal — has()/list() now report exactly the sessions that have a row,
matching the JSONL backend's "file exists ⇔ materialized".

The column only existed to force has()/list() to FALSE for an all-tail
crash (a partial first turn, zero committed events). That actually
DIVERGED from the JSONL backend, whose file (and thus has()=true) survives
a first append that never reached turn/end. Removing the column drops that
special case: an all-tail session keeps its row and stays present, the
same as JSONL. The orphaned tail rows are still removed by the deferred
truncation-repair on the next append, and load() stays non-mutating.

Also: add a TODO to route through a cordis db service if one is adopted,
and correct the README's Node-version framing to the repo's engines (>=24).
2026-06-16 20:35:01 +08:00
Tianyi Cui
b3ea13749c feat(acp): multiplex N concurrent ACP sessions + bash task ownership (RFC 011)
Lifts the RFC 010 single-session-per-connection cap: the bridge now runs N
concurrent sessions over one connection, each mapped to its own LoopAgent.

- packages/acp: live sessions held in a Map<sessionId, SessionRecord> with an
  agent→sessionId reverse WeakMap so agent/* events (which carry only the
  Agent) demux in O(1). Every session/event and agent/status is routed strictly
  to its owning record — concurrent sessions never cross-settle or interleave
  their session/update notifications. Per-session state: one in-flight prompt
  each, session/cancel aborts+settles only its own agent/prompt, session/load
  reserves a per-id load slot (distinct ids load concurrently; re-loading a live
  id is rejected), and disposal drains every live session in parallel to
  quiescence.
- packages/tool-bash: record each background task's owning agent at spawn and
  keep it for the executor's lifetime (NOT cleared on completion).
  bash_output/bash_kill reject a task owned by a different agent (a task with no
  owner is open; a no-agent caller can't access an owned task). Task ids are
  global and predictable, so this is the fence that stops one session's agent
  from reading/killing another session's background task.
- Per-session permission ownership and a per-agent disposer seam stay deferred
  (depend on the deferred permission gate); the reverse map the gate will route
  through is in place. RFC 011 stays `proposed`.
- Tests: two sessions stream concurrently without interleave; cross-session
  cancel isolation; per-session in-flight enforcement; dispose-all-to-quiescence;
  bash cross-session read/kill rejected (+ no-agent and unowned-task cases).
- Docs: RFC 011 implementation-status note; acp + tool-bash READMEs; example
  MVP-limitations updated. 100% per-file coverage maintained.
2026-06-16 19:47:13 +08:00
Tianyi Cui
fb9636db44 feat(acp): ACP bridge — drive the coding agent from an editor over JSON-RPC stdio
Implements the RFC 010 MVP: a new `@deepseek-ai/dsh-acp` package bridges the
harness agent to the Agent Client Protocol (JSON-RPC 2.0 over newline-delimited
stdio), so Zed and other ACP editors can drive the coding agent — streaming
render, tool-call display, and resumable sessions via `session/load`.

- packages/acp: AgentSideConnection wiring; initialize/newSession/loadSession/
  prompt/cancel; a total TurnEndReason→StopReason codec; settle-once with a
  fallback chain (agent/turn-end → logged turn/end → idle); single-session
  guard; cwd-must-equal-launch-dir validation; load replays from the persisted
  event log (assistant/chunk→agent_message_chunk, tool/call/result→tool_call*).
- agent: add Agent.whenIdle() quiescence signal to the interface; LoopAgent
  implements it (resolves on the first running→idle/disposed transition). The
  bridge awaits it on disposal so teardown reaches quiescence, not just abort.
- examples: extract the shared provider/tool core into examples/base.yml;
  coding-agent nest-includes it; new examples/acp-agent serves the agent over
  ACP with JSONL persistence and no stdout logger (stdout is the protocol).
- Permission gate deferred (TODO(rfc010-permission-gate)): tools run with the
  executor's full authority; only the Agent→sessionId ownership seam is laid
  down. Cancel is best-effort for a not-yet-started queued turn
  (TODO(rfc010-cancel-prestep)). RFC 010 stays `proposed`.
- Docs: package README + Zed snippet; client-driver cookbook section; root and
  packages layout/commands; RFC 010 implementation-status note.

48 bridge tests + whenIdle coverage; 100% per-file coverage; e2e boots the
example as a subprocess and verifies a written file on disk (key-gated, with a
no-key stdout-purity check).
2026-06-16 18:44:31 +08:00
Tianyi Cui
c27b1bba6e Merge branch 'split/agent-factory' into split/session-persistence-sqlite
# Conflicts:
#	yarn.lock
2026-06-16 17:09:39 +08:00
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
07akioni
dabc2ff411 feat: migrate to pnpm 2026-06-16 14:55:37 +08:00
Tianyi Cui
add59a3336 feat(agent-loop): surface max-tokens as a distinct turn-end reason
Add a `max-tokens` variant to `TurnEndReasonMap` and carry the model
finish reason up from `runStep` to `runTurn`, applying the rule "any
max-tokens step in the turn surfaces as max-tokens" (disposed/aborted/
error still take precedence). This lets consumers distinguish a clean
stop from a truncated one — the contract RFC 010's ACP bridge maps to
the `max_tokens` stop reason.

Also add an AGENTS.md rule: write an ADR when (and only when) a PR makes
a durable, contested, surprising decision.
2026-06-16 11:27:19 +08:00
Tianyi Cui
11e166db71 Merge branch 'split/agent-factory' into split/session-persistence-sqlite 2026-06-16 00:44:19 +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
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
f1dac1b1ed fix(session-persistence-sqlite): defer crash-tail repair to append; fix all-tail materialized flag; persist schema version (review #35)
- load() no longer DELETEs the crash tail — it stays non-mutating w.r.t.
  the event log and records a repair point (repairFrom). The next append
  runs the DELETE inside its own transaction before inserting. This makes
  the SQLite backend honor the SAME public contract as JSONL (load returns
  the committed prefix; the subsequent append performs the one-time
  physical truncation-repair), instead of mutating during load.

- All-tail load: when the discarded crash tail was the session's only
  committed content (committed.length === 0), the metadata row still read
  materialized = 1 from the prior append, so has()/list() reported a
  session load() had just emptied. load() now flips the row's materialized
  flag to 0 (metadata only — the orphaned event rows are still removed by
  the deferred repair), so has()/list() are immediately consistent.

- Schema version: openDatabase now stores SCHEMA_VERSION in PRAGMA
  user_version on a fresh database and rejects opening one whose
  user_version is newer than this build supports, protecting against a
  future incompatible layout.

Regression tests: load is non-mutating (tail rows survive until the next
append), all-tail load makes has()/list() false, and a newer-schema
database is rejected on open.
2026-06-16 00:05:25 +08:00
Tianyi Cui
ca4e1c3a1c Merge branch 'split/agent-factory' into split/session-persistence-sqlite 2026-06-15 23:55:58 +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
ccbc4f533f fix(session-persistence-sqlite): JSONL parity on append snapshot + corrupt-tail load
Two parity gaps with the JSONL backend found in review:

- append() now validates serializability and structuredClones the batch
  synchronously at call time, BEFORE waiting behind the per-session
  chain. A caller that mutates the passed array (or an event inside it)
  after the call can no longer corrupt the persisted copy or advance the
  cursor past what was written. Matches the JSONL backend.

- load() now computes the last-turn/end cut from the seq+type COLUMNS
  only (cutAtLastTurnEnd is generic over {seq,type}); event `data` is
  JSON-parsed only for the committed prefix, never for the uncommitted
  tail. A malformed `data` in a crash tail is discarded, not treated as
  unloadable — only a parse error/gap in the COMMITTED region is
  unloadable (the SessionPersistence.load contract). Matches scanLog.

Regression tests for both.
2026-06-15 22:23:36 +08:00
Tianyi Cui
bc670e69f3 Merge branch 'split/agent-factory' into split/session-persistence-sqlite 2026-06-15 22:14:02 +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