Skip to content

feat(runtime): add child-session runtime primitive - #1376

Merged
likun666661 merged 4 commits into
mainfrom
agent/child-session-runtime-primitive
Jul 23, 2026
Merged

likun666661 merged 4 commits into
mainfrom
agent/child-session-runtime-primitive

Conversation

@likun666661

@likun666661 likun666661 commented Jul 23, 2026 •

Copy link
Copy Markdown
Member

Summary

Part of #1366. This is PR 2 in the proposed child-session sequence and follows #1368.

  • add an internal spawnChildSession runtime primitive without switching agent_spawn or agent_swarm;
  • create a fresh linked Session with a versioned, self-contained runtime definition snapshot;
  • enforce one durable child per spawn identity through the SQLite metadata control plane;
  • copy the parent workspace/backend/model environment at admission time;
  • run the assigned task as the child's first normal session-inline AgentRun, with no parentRunId and no parent transcript inheritance;
  • preserve foreground lifecycle semantics: parent stop reaches linked children, while stopping one child does not affect its parent or siblings;
  • reuse, join, or recover the persisted initial child run when the same invocation is retried;
  • enforce the snapshot tool set as a hard ceiling in CLI/TUI, Desktop, and Headless hosts.

Durable idempotency

SQLite metadata schema v5 owns spawn identities in a dedicated subagent_spawns claim table:

  • regular spawn: (parentSessionId, parentRunId, toolCallId);
  • swarm item: the regular identity plus (swarmId, itemId).

Each claim persists the request fingerprint, child session id, initial turn/run ids, and claim time. The v4 migration backfills claims from live child metadata and removes the old live-row unique index. Claims intentionally have no foreign key to session_metadata, so deleting a child session cannot release its spawn identity and cause the work to execute again; a retry against a deleted target fails closed.

Each new child also persists a SHA-256 request fingerprint covering lineage, profile, prompt, and swarm item. On conflict, storage returns the existing child only when its relation, fingerprint, and claim agree. Runtime joins the in-process invocation, returns a completed durable result, or repairs an inactive interrupted run. Reusing the same identity for different work fails closed.

Runtime snapshot and execution boundary

SubagentSessionRuntime stores:

  • snapshot schema version and profile definition version;
  • exact agent identity, name, profile key, and system prompt;
  • exact tool allowlist and category policy;
  • immutable permission ceiling.

Reopening a child no longer resolves the current builtin profile definition. Runtime reconstructs execution directly from the stored snapshot and resolves only the named tool implementations. Catalog prompt/policy changes therefore do not alter an existing child; a missing or disallowed tool implementation fails closed.

BackendFactoryContext.tools is now explicitly a hard ceiling: hosts may narrow it but may not append or substitute tools. CLI/TUI and Headless consume the persisted prompt and scoped tools directly; Desktop no longer appends plan-control tools or lets the computer-use E2E seam override a child scope. Legacy relation-only child sessions remain readable/projectable but refuse activation when the durable runtime snapshot is absent.

Runtime invariants

  • Child creation requires the exact spawning parent sessionId + runId + turnId to still be active.
  • Admission is revalidated after metadata-only retry resolution to close the crash/retry cancellation race.
  • onReady is exactly-once and fires only after an initial run exists or starts.
  • Child messages and canonical RuntimeEvents are scoped to childSessionId.
  • The first child run is session-inline, so future normal turns can reuse only child history.
  • A child permission mode cannot exceed its immutable creation-time ceiling.
  • Full runtime snapshots remain in the child header; list summaries expose only the bounded non-prompt projection.

Exercised profile

local_read / Local Read:

  • permission mode: explore;
  • tools: Read, Glob, Grep;
  • fresh task context only.

Non-goals

Validation

  • npm run typecheck
  • npm run build:test
  • full @maka/storage suite plus focused SQLite v4→v5/deletion regression
  • full runtime suite: 2389 passed, 7 skipped; the same two unrelated local macOS sandbox failures remain in untouched tests (/usr/local executable-root assumption and Homebrew rg loading pcre2 through the sandbox)
  • focused child-session, CLI/TUI, Desktop, and Headless host boundary tests
  • Biome check and git diff --check
中文说明

概述

这是 #1366 的第 2 个 PR,承接已合并的 #1368。

  • 新增内部 spawnChildSession primitive,但暂不切换 agent_spawn / agent_swarm;
  • 创建全新的关联 Child Session,并持久化版本化、自包含的 runtime definition snapshot;
  • 通过 SQLite metadata control plane 保证同一个 spawn identity 只对应一个 Child Session;
  • 首个任务以 Child Session 内普通 session-inline AgentRun 执行,不设置 parentRunId,也不继承父 transcript;
  • 同一 invocation 重试时复用、等待或恢复已有 Child Session 与首个 Run;
  • CLI/TUI、Desktop、Headless 都把 snapshot tool set 当作不可扩大的硬上限。

持久幂等

SQLite metadata schema v5 新增独立的 subagent_spawns claim 表。普通 spawn 的 key 是 (parentSessionId, parentRunId, toolCallId),swarm item 再加入 (swarmId, itemId)。claim 保存 request fingerprint、child session id、首个 turn/run id 和 claim 时间。

v4→v5 迁移会从现有 child metadata 回填 claim。claim 不依赖可删除的 session_metadata 行,所以删除 Child Session 不会释放 spawn identity;之后同一调用重试会 fail closed,而不会新建 Child 并重复执行。

Runtime snapshot 与执行边界

快照包含 schema version、definition version、精确 system prompt、category policy、tool allowlist、agent identity/profile 和 permission ceiling。Child 重开时不再查询当前 builtin profile definition,而是从持久化 snapshot 恢复,仅按名称解析工具实现。

BackendFactoryContext.tools 现在是 host 的硬上限:host 可以进一步收窄,但不能追加或替换工具。CLI/TUI、Desktop、Headless 均遵守该边界。仅有父子 relation、缺少 runtime snapshot 的 legacy Child 仍可读取和展示,但执行时直接 fail closed。

metadata-only crash retry 在启动首个 Run 前会重新检查 parent run 与取消信号;onReady 保证 exactly-once,避免之前的双回调和取消后误启动。

本 PR 仍只打通 local_read / Local Read 只读 profile。切换公共 spawn/swarm、legacy execution-reference union、contextMode: "fork"、Child Session UI 投影和 #1341 graph scheduling 留给后续 PR。

@likun666661

Copy link
Copy Markdown
Member Author

I think two parts of the durable child-session contract need to be completed in this PR before the public spawn path is migrated:

  1. spawnChildSession() needs a durable idempotency key. A retry of the same tool invocation can currently create another child session. For a regular spawn, the identity should be scoped by (parentSessionId, parentRunId, toolCallId); for a swarm item it also needs (swarmId, itemId). This should be enforced by a SQLite unique constraint/index rather than a check-then-insert in application code. On conflict, the runtime should load and reuse/join/recover the existing child, and it should reject identity drift. I would also persist a request fingerprint covering the profile, prompt, and swarm identity so the same key cannot silently refer to different work. A uniqueness violation by itself is not yet an idempotent API.

  2. The persisted runtime snapshot is not sufficient to restore the same profile. SubagentSessionRuntime currently stores the profile key, tool names, and permission ceiling, but reopening resolves the current built-in definition and therefore uses the current systemPrompt and categoryPolicy. Changing a profile definition changes the behavior of an existing child session, and removing/renaming it makes the child unrecoverable. The snapshot should be versioned and include the runtime-affecting fields needed for independent restoration—at minimum a snapshot schema version, profile/definition version, exact system prompt, category policy, tool names, and permission ceiling. Reopen should decode that snapshot directly, resolve only the named tool implementations, and fail closed if any required implementation is unavailable.

These are both much easier to define now, while the persisted schema is being introduced, than after PR3 starts creating real child sessions that require migration compatibility.

@likun666661
likun666661 force-pushed the agent/child-session-runtime-primitive branch from 30c0437 to 35e752b Compare July 23, 2026 08:17
@likun666661

Copy link
Copy Markdown
Member Author

Follow-up review on 35e752b: the two issues from my previous comment are substantially addressed. The SQLite v4 spawn identity/fingerprint design is real, and the runtime snapshot is now versioned and self-contained. I still see four execution-boundary issues; I would treat the first three as blockers.

1. [P1] The snapshot tool allowlist is still not enforced by every host

RuntimeKernel.ensureActive() passes ctx.tools and ctx.systemPrompt to the backend factory, but the CLI/TUI factory still constructs the backend with tools: allTools and builds the normal CLI system prompt without consuming ctx.systemPrompt:

That means a persisted local_read child can still receive Bash, write, subagent, and other full-surface tools on CLI/TUI. Desktop consumes ctx.tools, but then unions planControlTools after the snapshot restriction, so a generic collaboration-mode change can widen the set there as well:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/apps/desktop/src/main/session-stream.ts#L174-L220

BackendFactoryContext.tools should be documented and implemented as a hard ceiling. Every host must use the intersection and must not append tools outside the snapshot.

2. [P1] Legacy relation-only child sessions still activate fail-open

Storage intentionally accepts legacy rows with subagentParent but without subagentRuntime/subagentSpawn:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/storage/src/session-store.ts#L1172-L1197

However, activation currently does if (!snapshot) return undefined, which treats such a linked child as an ordinary main session and builds an unrestricted backend:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/runtime/src/runtime-kernel.ts#L1529-L1552

Legacy rows can remain readable/projectable, but attempting to execute one should fail closed whenever subagentParent exists and the runtime snapshot is missing.

3. [P1] Deleting the child releases the durable spawn identity

The unique spawn constraint lives only on the live session_metadata row:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/storage/src/sqlite-session-metadata-schema.ts#L118-L130

SqliteSessionMetadataStore.remove() deletes that row and retains only a tombstone keyed by sessionId:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/storage/src/sqlite-session-metadata-store.ts#L348-L363

A retry of the same spawn identity can therefore create a new child and execute the work again after the original child is deleted. The durable spawn claim needs to outlive the deletable session row, for example in a separate subagent_spawns table or in a tombstone that retains the spawn key, fingerprint, child id, and initial run identities.

4. [P2] The metadata-only retry path can miss cancellation and calls onReady twice

If the process crashes after child metadata commit but before the initial AgentRun is created, a retry enters resolveExistingChildSpawn(). That method invokes onReady, then returns undefined when the run is absent:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/runtime/src/session-manager.ts#L1477-L1499

The caller then falls through to sendMessage(), whose onRunStarted invokes onReady again:

https://github.com/maka-agent/maka-agent/blob/35e752b850ad848ea41ee66b89f2640b36125a21/packages/runtime/src/session-manager.ts#L1394-L1424

The parent-active/abort check also occurs before the awaited existing-run resolution. If the parent settles or the abort signal fires during that await, the fallback starts without revalidation; because listener registration skips an already-aborted signal, the child can still run. Please revalidate immediately before the metadata-only fallback start, handle an already-aborted signal, and make onReady exactly-once.

The current test, typecheck, and e2e checks are green; these cases are outside the existing coverage.

@likun666661
likun666661 marked this pull request as ready for review July 23, 2026 09:18
@likun666661
likun666661 merged commit a66e1a3 into main Jul 23, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant