@kodax-ai/kodax 0.7.73 → 0.7.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +26 -6
  3. package/README_CN.md +23 -5
  4. package/config-templates/config.example.jsonc +7 -0
  5. package/dist/chunks/{agent-T6PGT6AU.js → agent-2ABIG5NW.js} +1 -1
  6. package/dist/chunks/argument-completer-C45DEO34.js +2 -0
  7. package/dist/chunks/chunk-5GH3SVI3.js +74 -0
  8. package/dist/chunks/{chunk-6JTWXGVU.js → chunk-7W6KYJU2.js} +225 -225
  9. package/dist/chunks/chunk-C7TZAT3S.js +369 -0
  10. package/dist/chunks/chunk-KPGVY6PF.js +321 -0
  11. package/dist/chunks/{chunk-6RI2RBTF.js → chunk-MVCAFD6M.js} +1 -1
  12. package/dist/chunks/{chunk-BUPJGG3C.js → chunk-PP53SIIL.js} +1 -1
  13. package/dist/chunks/chunk-QH56WEHK.js +2 -0
  14. package/dist/chunks/chunk-UJS7JAO6.js +770 -0
  15. package/dist/chunks/{chunk-M7FXNPWV.js → chunk-VK5QTHS6.js} +6 -6
  16. package/dist/chunks/chunk-VPURXE2F.js +427 -0
  17. package/dist/chunks/chunk-XM724XAA.js +329 -0
  18. package/dist/chunks/{chunk-MLEAQTCD.js → chunk-XMP66YWG.js} +11 -4
  19. package/dist/chunks/{compaction-config-IHJBP4GO.js → compaction-config-QYCDOW3Q.js} +1 -1
  20. package/dist/chunks/{construction-bootstrap-EGRUHKLP.js → construction-bootstrap-BKNPJI7O.js} +1 -1
  21. package/dist/chunks/dist-6EK4H7TP.js +2 -0
  22. package/dist/chunks/host-KS4TJKWQ.js +2 -0
  23. package/dist/chunks/{run-manager-AY7RQOWM.js → run-manager-L7MKGQNX.js} +1 -1
  24. package/dist/chunks/{utils-2GAE4XIM.js → utils-GTJLL4GT.js} +1 -1
  25. package/dist/index.d.ts +12 -11
  26. package/dist/index.js +5 -5
  27. package/dist/kodax_cli.js +1295 -1253
  28. package/dist/runtime-worker.js +1208 -1166
  29. package/dist/sdk-a2a.d.ts +9 -8
  30. package/dist/sdk-a2a.js +1 -1
  31. package/dist/sdk-agent.d.ts +107 -59
  32. package/dist/sdk-agent.js +1 -1
  33. package/dist/sdk-coding.d.ts +32 -16
  34. package/dist/sdk-coding.js +1 -1
  35. package/dist/sdk-mcp.js +1 -1
  36. package/dist/sdk-media.js +1 -1
  37. package/dist/sdk-repl.d.ts +27 -20
  38. package/dist/sdk-repl.js +2 -2
  39. package/dist/sdk-runtime.d.ts +151 -14
  40. package/dist/sdk-runtime.js +1 -1
  41. package/dist/sdk-session.d.ts +4 -4
  42. package/dist/sdk-session.js +1 -1
  43. package/dist/sdk-skills.js +1 -1
  44. package/dist/semantic-worker.js +16 -14
  45. package/dist/types-chunks/{bash-prefix-extractor.d-bApf5nGw.d.ts → bash-prefix-extractor.d-r1beOESM.d.ts} +48 -12
  46. package/dist/types-chunks/{capsule.d-CGWltwno.d.ts → capsule.d-zeqV4IQX.d.ts} +10 -2
  47. package/dist/types-chunks/{commands.d-CEHGwRok.d.ts → commands.d-DUxnK2TU.d.ts} +10 -7
  48. package/dist/types-chunks/{guardrail.d-CC4vAQl7.d.ts → guardrail.d-CWYD1bdL.d.ts} +1 -1
  49. package/dist/types-chunks/{guardrail.d-CXIPlWP3.d.ts → guardrail.d-qjuKJZ31.d.ts} +72 -11
  50. package/dist/types-chunks/history-retrieval.d-BKTJIrVd.d.ts +57 -0
  51. package/dist/types-chunks/{public-api.d-FBmX0kxo.d.ts → public-api.d-CX4B11qY.d.ts} +17 -4
  52. package/dist/types-chunks/{run-manager.d-DUNnJcpE.d.ts → run-manager.d-B9fEIjZk.d.ts} +1 -1
  53. package/dist/types-chunks/{sdk-session-XqacVOs8.d.ts → sdk-session-B0fhAOPa.d.ts} +2 -2
  54. package/dist/types-chunks/{types.d-DKr6TCXL.d.ts → types.d-Bm_y6YuM.d.ts} +2 -2
  55. package/dist/types-chunks/{types.d-DgciGHoG.d.ts → types.d-DEctY20M.d.ts} +9 -2
  56. package/dist/types-chunks/{types.d-BnPhGmar.d.ts → types.d-sRLugmjy.d.ts} +61 -14
  57. package/dist/types-chunks/{utils.d-BPYX7-KN.d.ts → utils.d-D0wPxz8y.d.ts} +5 -5
  58. package/docs/SDK_EMBEDDER_GUIDE.md +1956 -1684
  59. package/package.json +8 -1
  60. package/dist/chunks/argument-completer-QGMWA2C3.js +0 -2
  61. package/dist/chunks/chunk-AGDDM5AW.js +0 -329
  62. package/dist/chunks/chunk-EAD4SF3Z.js +0 -413
  63. package/dist/chunks/chunk-HVCNAZ52.js +0 -365
  64. package/dist/chunks/chunk-OG6DRNKV.js +0 -755
  65. package/dist/chunks/chunk-PFH53O4M.js +0 -2
  66. package/dist/chunks/chunk-V4F6IYNK.js +0 -73
  67. package/dist/chunks/chunk-WQ6PWGK5.js +0 -320
  68. package/dist/chunks/dist-5N3I2RTW.js +0 -2
  69. package/dist/chunks/host-MECITNE3.js +0 -2
@@ -22,15 +22,18 @@ are NOT obvious from inspecting the type definitions alone:
22
22
  12. [Provider credential verification — `verifyProviderCredential`](#12-provider-credential-verification--verifyprovidercredential-feature_216-v0745)
23
23
  13. [Inject your product's manual — `selfManual`](#13-inject-your-products-manual--selfmanual-feature_221-v0747)
24
24
  14. [Media input artifacts — `@kodax-ai/kodax/media`](#14-media-input-artifacts--kodax-aikodaxmedia-feature_239-v0756)
25
- 15. [Space v0.7.57 follow-up ledger](#15-space-v0757-follow-up-ledger)
26
- 16. [SDK agent-profile surface — `KodaXAgentProfile`](#16-sdk-agent-profile-surface--kodaxagentprofile-feature_247-v0758)
27
- 17. [Runtime SDK, Worker isolation, and local daemon](#17-runtime-sdk-worker-isolation-and-local-daemon-feature_253-feature_257)
28
- 18. [External-agent executor plane](#18-external-agent-executor-plane-feature_258-v0767)
29
- 19. [Session surface filtering and cursor pagination](#19-session-surface-filtering-and-cursor-pagination-feature_261-v0767)
30
- 20. [Cost-disciplined workflow routing and telemetry](#20-cost-disciplined-workflow-routing-and-telemetry-feature_259-v0767)
31
- 21. [Experimental governed memory — `/experimental-memory`](#21-experimental-governed-memory--experimental-memory-feature_260-v0768)
25
+ 15. [Space v0.7.57 follow-up ledger](#15-space-v0757-follow-up-ledger)
26
+ 16. [SDK agent-profile surface — `KodaXAgentProfile`](#16-sdk-agent-profile-surface--kodaxagentprofile-feature_247-v0758)
27
+ 17. [Runtime SDK, Worker isolation, and local daemon](#17-runtime-sdk-worker-isolation-and-local-daemon-feature_253-feature_257)
28
+ 18. [External-agent executor plane](#18-external-agent-executor-plane-feature_258-v0767)
29
+ 19. [Session surface filtering and cursor pagination](#19-session-surface-filtering-and-cursor-pagination-feature_261-v0767)
30
+ 20. [Cost-disciplined workflow routing and telemetry](#20-cost-disciplined-workflow-routing-and-telemetry-feature_259-v0767)
31
+ 21. [Experimental governed memory — `/experimental-memory`](#21-experimental-governed-memory--experimental-memory-feature_260-v0768)
32
32
  22. [Bidirectional A2A 1.0 — `/a2a`](#22-bidirectional-a2a-10--a2a-feature_267-v0769)
33
33
  23. [Shared Coder daemon for Space and IDE hosts](#23-shared-coder-daemon-for-space-and-ide-hosts-feature_269-v0769)
34
+ 24. [Runtime-owned Auto Mode and plan-approval bridges](#24-runtime-owned-auto-mode-and-plan-approval-bridges-v0772v0773)
35
+ 25. [Always-on context compaction and bounded transcript recovery](#25-always-on-context-compaction-and-bounded-transcript-recovery-v0774)
36
+ 26. [Agent mailbox control versus SDK event telemetry](#26-agent-mailbox-control-versus-sdk-event-telemetry-v0774)
34
37
 
35
38
  §1–§3 (and the Phase-7/8 MCP-popout surface in §1) land in v0.7.42
36
39
  under FEATURE_186 (see [ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742)).
@@ -697,33 +700,48 @@ await runKodaX(
697
700
  // ✓ ~/.kodax/sessions/s_my_chat.jsonl now exists after the run.
698
701
 
699
702
  // Same `storage` instance reads back through SessionManager:
700
- const recent = await listSessions({
701
- scope: 'user',
702
- surface: 'acp',
703
- limit: 50,
704
- });
705
- const nextPage = recent.at(-1)?.cursor
706
- ? await listSessions({
707
- scope: 'user',
708
- surface: 'acp',
709
- limit: 50,
710
- cursor: recent.at(-1)?.cursor,
711
- })
712
- : [];
703
+ const recent = await listSessions({
704
+ scope: 'user',
705
+ surface: 'acp',
706
+ limit: 50,
707
+ });
708
+ const nextPage = recent.at(-1)?.cursor
709
+ ? await listSessions({
710
+ scope: 'user',
711
+ surface: 'acp',
712
+ limit: 50,
713
+ cursor: recent.at(-1)?.cursor,
714
+ })
715
+ : [];
713
716
  const replay = await loadSession('s_my_chat');
714
717
  const scrollback = await loadFullTranscript('s_my_chat');
715
718
  const compacted = await compactSession('s_my_chat', { dryRun: true });
716
719
 
717
- await appendClientNotice('s_my_chat', {
718
- source: 'space',
719
- content: '/doctor ok',
720
- });
721
- ```
722
-
723
- ### What `createSessionManager()` returns (v0.7.43+)
720
+ await appendClientNotice('s_my_chat', {
721
+ source: 'space',
722
+ content: '/doctor ok',
723
+ });
724
+ ```
725
+
726
+ ### Auto-resume selection in v0.7.74
727
+
728
+ With `session.autoResume: true` (or `resume: true`) and no explicit ID, KodaX
729
+ calls `storage.list(context.gitRoot, { limit: 1000 })` and chooses the first
730
+ newest-first summary whose `msgCount > 0`. This prevents newer zero-message
731
+ ACP/bootstrap placeholders from shadowing a real conversation. An explicit
732
+ `session.id` always wins. Custom `KodaXSessionStorage` implementations should
733
+ therefore honor the optional `limit` argument and return `msgCount` accurately;
734
+ they may return fewer than 1000 records.
735
+
736
+ The standalone interactive CLI additionally restores the persisted
737
+ workspace/runtime identity before the next turn. SDK embedders still own
738
+ `context.gitRoot`, `context.executionCwd`, and storage construction; do not rely
739
+ on process cwd as a substitute for host-owned runtime context.
740
+
741
+ ### What `createSessionManager()` returns (v0.7.43+)
724
742
 
725
743
  ```ts
726
- interface SessionManager {
744
+ interface SessionManager {
727
745
  // Read side (FEATURE_173 v0.7.42)
728
746
  listSessions(...): Promise<SessionSummary[]>;
729
747
  loadSession(id): Promise<...>;
@@ -738,13 +756,13 @@ interface SessionManager {
738
756
  watchSessions(cb): () => void;
739
757
  // Write side (v0.7.43 follow-up)
740
758
  storage: FileSessionStorage; // ← NEW — pass into runKodaX
741
- }
742
- ```
743
-
744
- `surface` is an exact filter applied before `limit`. Each returned summary may
745
- include an opaque `cursor`; pass the last summary's cursor back unchanged to
746
- continue the stable newest-first listing. Callers must not parse or construct
747
- cursors themselves.
759
+ }
760
+ ```
761
+
762
+ `surface` is an exact filter applied before `limit`. Each returned summary may
763
+ include an opaque `cursor`; pass the last summary's cursor back unchanged to
764
+ continue the stable newest-first listing. Callers must not parse or construct
765
+ cursors themselves.
748
766
 
749
767
  ### Active context vs full transcript vs UI replay
750
768
 
@@ -967,7 +985,7 @@ running your project's test suite against `main` — you can `npm link`
967
985
  the in-tree KodaX checkout instead of waiting for a published version.
968
986
 
969
987
  As of v0.7.43 the root `package.json` is in **already-published shape**:
970
- `"name": "@kodax-ai/kodax"` is baked in along with all 10 SDK subpath
988
+ `"name": "@kodax-ai/kodax"` is baked in along with all 10 SDK subpath
971
989
  exports. `npm link` "just works" — no need to run `scripts/release.mjs`
972
990
  first.
973
991
 
@@ -1296,7 +1314,7 @@ import {
1296
1314
  ```ts
1297
1315
  interface KodaXModelCapabilities {
1298
1316
  provider: string; // 'anthropic' | 'kimi' | 'ark-coding' | <custom-name>
1299
- model: string; // model id (e.g. 'claude-sonnet-4-6', 'kimi-k2.7-code')
1317
+ model: string; // model id (e.g. 'claude-sonnet-4-6', 'kimi-k2.7-code')
1300
1318
  displayName: string; // human label — falls back to model id
1301
1319
  supportsThinking: boolean; // native reasoning is available?
1302
1320
  reasoningCapability: 'native-budget' | 'native-effort' | 'native-toggle' | 'prompt-only' | 'none' | 'unknown'; // legacy mechanism label
@@ -1328,8 +1346,8 @@ for (const caps of listAllModelCapabilities()) {
1328
1346
  ```ts
1329
1347
  import { resolveModelCapabilities } from '@kodax-ai/kodax/llm';
1330
1348
 
1331
- const caps = resolveModelCapabilities('kimi', 'kimi-k2.7-code');
1332
- // => { contextWindow: 262_144, supportsThinking: true, reasoningProfile: { defaultEffort: 'high', ... }, ... }
1349
+ const caps = resolveModelCapabilities('kimi', 'kimi-k2.7-code');
1350
+ // => { contextWindow: 262_144, supportsThinking: true, reasoningProfile: { defaultEffort: 'high', ... }, ... }
1333
1351
  ```
1334
1352
 
1335
1353
  For picker/status UIs, use `reasoningProfile.supportedEfforts` and
@@ -1466,11 +1484,11 @@ and loaded into the in-memory `KODAX_PROVIDER_SNAPSHOTS` export. When upstream
1466
1484
  providers publish a new model or change a context-window cap, the JSON file is
1467
1485
  the patch site — the new value flows to runtime (via `buildProviderConfig`) AND
1468
1486
  to SDK consumers (via the getters) in a single edit. The current snapshot is
1469
- dated 2026-07-16 and includes the GPT-5.4, Kimi K2.7 Code / HighSpeed, GLM-5.2, MiniMax
1487
+ dated 2026-07-16 and includes the GPT-5.4, Kimi K2.7 Code / HighSpeed, GLM-5.2, MiniMax
1470
1488
  M3/M2.7, DeepSeek V4, and Doubao Seed 2.0 route refreshes where supported. The
1471
1489
  test suite at
1472
1490
  [`packages/llm/src/providers/model-capabilities.test.ts`](../packages/llm/src/providers/model-capabilities.test.ts)
1473
- locks in specific values (e.g. the public Kimi lineup at 262,144 tokens, deepseek-v4-pro at 1M)
1491
+ locks in specific values (e.g. the public Kimi lineup at 262,144 tokens, deepseek-v4-pro at 1M)
1474
1492
  so accidental drift is caught at PR time.
1475
1493
 
1476
1494
  The probe scripts that surveyed upstream APIs live at
@@ -1905,12 +1923,12 @@ Each provider has one `verifyStrategy` value baked into `provider-capabilities.j
1905
1923
 
1906
1924
  | Strategy | What runs | Cost | Used by built-ins |
1907
1925
  |---|---|---|---|
1908
- | `count-tokens` | `client.messages.countTokens({ messages: [{role:'user',content:'hi'}] })` | 0 token | `anthropic`, `kimi-code`, `qwen-token-plan`, `zhipu-coding`, `zai-coding`, `minimax-coding`, `ark-coding` |
1926
+ | `count-tokens` | `client.messages.countTokens({ messages: [{role:'user',content:'hi'}] })` | 0 token | `anthropic`, `kimi-code`, `qwen-token-plan`, `zhipu-coding`, `zai-coding`, `minimax-coding`, `ark-coding` |
1909
1927
  | `models-list` | `client.models.list()` | 0 token | `openai`, `deepseek`, `kimi`, `qwen` |
1910
1928
  | `minimal-message` | `chat.completions.create({max_tokens:1, content:'hi'})` (or Anthropic equivalent) | ~6–7 token | `zhipu`, `mimo`, `mimo-coding` |
1911
1929
  | `unsupported` | nothing — short-circuits | — | `gemini-cli`, `codex-cli` (cli-bridge: credentials live in CLI binary) |
1912
1930
 
1913
- `models-list` is NOT used as a universal default because (a) some providers' `/v1/models` is publicly accessible (so a bad key returns 200 — false positive), and (b) some compat layers don't implement it (404) or 401 even for valid keys (false negative). The 2026-05-28 provider probe matrix captured the original per-provider evidence (12 providers at the time; 15 built-in aliases as of 2026-06-28). The current capability catalog has 16 aliases, including `qwen-token-plan` with `count-tokens`; opencode's `setup-recording-env.ts` makes the same per-provider decision across its 20+ providers.
1931
+ `models-list` is NOT used as a universal default because (a) some providers' `/v1/models` is publicly accessible (so a bad key returns 200 — false positive), and (b) some compat layers don't implement it (404) or 401 even for valid keys (false negative). The 2026-05-28 provider probe matrix captured the original per-provider evidence (12 providers at the time; 15 built-in aliases as of 2026-06-28). The current capability catalog has 16 aliases, including `qwen-token-plan` with `count-tokens`; opencode's `setup-recording-env.ts` makes the same per-provider decision across its 20+ providers.
1914
1932
 
1915
1933
  ### Custom providers
1916
1934
 
@@ -1954,7 +1972,7 @@ Cli-bridge providers (`gemini-cli`, `codex-cli`) return their CLI binary's known
1954
1972
 
1955
1973
  ### Reference
1956
1974
 
1957
- - Source: `packages/llm/src/providers/verify-credential.ts` (orchestrator + classifier) + `verify-credential.test.ts` (27 unit tests) + `verify-credential-integration.test.ts` (12 gated real-key/fake-key tests, enabled by `KODAX_INTEGRATION_TEST=1`).
1975
+ - Source: `packages/llm/src/providers/verify-credential.ts` (orchestrator + classifier) + `verify-credential.test.ts` (27 unit tests) + `verify-credential-integration.test.ts` (12 gated real-key/fake-key tests, enabled by `KODAX_INTEGRATION_TEST=1`).
1958
1976
  - Data: `packages/llm/src/providers/provider-capabilities.json` `verifyStrategy` field per provider.
1959
1977
  - Design notes + probe matrix: [docs/features/v0.7.45.md FEATURE_216](features/v0.7.45.md#feature_216-provider-credential-verification-api).
1960
1978
 
@@ -2003,14 +2021,14 @@ By default your `topics` **extend** the KodaX base manual. v0.7.58 adds
2003
2021
 
2004
2022
  `KODAX_UNDERLYING_CAPABILITY_TOPICS` (exported) is the recommended mechanism-topic
2005
2023
  subset a product built on KodaX should keep even in a full replace — so your users
2006
- still get correct answers about the underlying engine (providers / config /
2007
- permissions / tools / skills / extensions / mcp / repo-intelligence / sessions /
2008
- sdk / custom-providers) without the KodaX brand:
2009
-
2010
- The default full manual also includes the `memory` topic. It is intentionally
2011
- not part of `KODAX_UNDERLYING_CAPABILITY_TOPICS` because `/experimental-memory`
2012
- is opt-in; hosts that expose it should add `memory` explicitly or provide a
2013
- product-specific override.
2024
+ still get correct answers about the underlying engine (providers / config /
2025
+ permissions / tools / skills / extensions / mcp / repo-intelligence / sessions /
2026
+ sdk / custom-providers) without the KodaX brand:
2027
+
2028
+ The default full manual also includes the `memory` topic. It is intentionally
2029
+ not part of `KODAX_UNDERLYING_CAPABILITY_TOPICS` because `/experimental-memory`
2030
+ is opt-in; hosts that expose it should add `memory` explicitly or provide a
2031
+ product-specific override.
2014
2032
 
2015
2033
  ```ts
2016
2034
  import {
@@ -2169,30 +2187,32 @@ For streaming follow-ups, use `enqueueWithArtifacts()` instead of the raw
2169
2187
  message queue:
2170
2188
 
2171
2189
  ```ts
2172
- enqueueWithArtifacts({
2173
- provider: selectedProvider,
2174
- model: selectedModel,
2175
- sessionId: activeSessionId,
2176
- content: followupText,
2177
- inputArtifacts: [artifact],
2178
- });
2179
- ```
2180
-
2181
- The helper validates first and then stores `inputArtifacts` on the queued prompt.
2182
- Queued image follow-ups are rebuilt as multimodal content blocks on the next
2183
- runner turn. Unsupported file/video attachments are rejected before enqueueing.
2184
- Pass `sessionId` whenever the host can run more than one session concurrently;
2185
- it targets that Actor session's root queue without exposing Actor paths. For
2186
- backward compatibility, omitting both `sessionId` and `agentId` still binds to
2187
- the sole active Actor root, or uses the legacy unscoped SA queue when no Actor
2188
- run is active. If multiple Actor roots are active, the helper rejects the
2189
- ambiguous call instead of risking cross-session delivery. Low-level child-Actor
2190
- producers may continue to pass an explicit `agentId`.
2191
-
2192
- `enqueueWithArtifacts()` is an in-process queue helper for direct/inline runs.
2193
- Runtime Worker and daemon clients must use `runtime.runs.submitInput(...)` (with
2194
- the same `sessionId`, `afterRunId`, and `delivery:'after_turn'`) because a
2195
- process-local MessageQueue cannot cross those transport boundaries.
2190
+ enqueueWithArtifacts({
2191
+ provider: selectedProvider,
2192
+ model: selectedModel,
2193
+ sessionId: activeSessionId,
2194
+ content: followupText,
2195
+ inputArtifacts: [artifact],
2196
+ });
2197
+ ```
2198
+
2199
+ The helper validates first and then stores `inputArtifacts` on the queued prompt.
2200
+ Queued image follow-ups are rebuilt as multimodal content blocks on the next
2201
+ runner turn. Unsupported file/video attachments are rejected before enqueueing.
2202
+ Pass `sessionId` whenever the host can run more than one session concurrently;
2203
+ it targets that Actor session's root queue without exposing Actor paths. For
2204
+ backward compatibility, omitting both `sessionId` and `agentId` still binds to
2205
+ the sole active Actor root, or uses the legacy unscoped SA queue when no Actor
2206
+ run is active. If multiple Actor roots are active, the helper rejects the
2207
+ ambiguous call instead of risking cross-session delivery. Low-level child-Actor
2208
+ producers may continue to pass an explicit `agentId`.
2209
+
2210
+ `enqueueWithArtifacts()` is an in-process queue helper for direct/inline runs.
2211
+ Runtime Worker and daemon clients must use `runtime.runs.submitInput(...)` (with
2212
+ the same `sessionId` and `afterRunId`) because a process-local MessageQueue
2213
+ cannot cross those transport boundaries. Use `delivery:'after_turn'` to create
2214
+ a continuation Run after the current Run ends, or `delivery:'interrupt'` to
2215
+ inject into the current active Actor Run at its next safe Runner boundary.
2196
2216
 
2197
2217
  ### Boundaries
2198
2218
 
@@ -2361,14 +2381,14 @@ The important creation options are:
2361
2381
  | `worker.resourceLimits` | unset | Optional V8 heap/stack limits; requires `isolation: 'worker'`. |
2362
2382
  | `worker.shutdownTimeoutMs` | `2000` | Grace before the parent terminates the Runtime Worker. |
2363
2383
  | `requirements.hardDispose` | `false` | Rejects inline and daemon forms; prevents an accidental weaker ownership form. |
2364
- | `homeDir` | unset | When omitted, use the exact resolved `KODAX_HOME`. When set, this is the base directory that owns `.kodax`, with the same meaning as CLI `daemon --home`; daemon state/config live under `<homeDir>/.kodax`. |
2384
+ | `homeDir` | unset | When omitted, use the exact resolved `KODAX_HOME`. When set, this is the base directory that owns `.kodax`, with the same meaning as CLI `daemon --home`; daemon state/config live under `<homeDir>/.kodax`. |
2365
2385
  | `profile` | `'default'` | Daemon uniqueness and runtime configuration namespace. |
2366
2386
  | `sessionsDir` | `<homeDir>/.kodax/sessions` | Explicit session storage override. |
2367
2387
  | `daemonStartupTimeoutMs` | `60000` | Total cold-start/concurrent-owner wait budget. |
2368
- | `daemonConnectTimeoutMs` | `2000` | Per-socket connection timeout. |
2369
- | `autoStartDaemon` | conditional | For `createKodaXRuntime({mode:'daemon'})`, true only when no explicit endpoint/transport is supplied. |
2370
- | `externalAgents` | unset | Host-installed executor factories, dispatch policy, optional credential/artifact policies, and default dispatch context. Inline owner only; see §18. |
2371
- | `requirements.externalAgents` | `false` | Reject a Runtime/daemon connection that does not advertise an installed external-agent plane. |
2388
+ | `daemonConnectTimeoutMs` | `2000` | Per-socket connection timeout. |
2389
+ | `autoStartDaemon` | conditional | For `createKodaXRuntime({mode:'daemon'})`, true only when no explicit endpoint/transport is supplied. |
2390
+ | `externalAgents` | unset | Host-installed executor factories, dispatch policy, optional credential/artifact policies, and default dispatch context. Inline owner only; see §18. |
2391
+ | `requirements.externalAgents` | `false` | Reject a Runtime/daemon connection that does not advertise an installed external-agent plane. |
2372
2392
 
2373
2393
  KodaX rejects contradictory options. Worker settings without Worker isolation,
2374
2394
  `requirements.hardDispose` on inline/daemon forms, and any explicit isolation
@@ -2442,20 +2462,20 @@ no explicit `daemonEndpoint` or `daemonTransport` is supplied it starts or reuse
2442
2462
  the local profile daemon. `connectKodaXRuntime()` is attach-only unless
2443
2463
  `autoStart: true` is passed.
2444
2464
 
2445
- SDK auto-start allows `daemonStartupTimeoutMs` (default 60 seconds) and
2446
- `daemonConnectTimeoutMs`. The longer startup budget covers cold machines and
2447
- concurrent test/desktop startup without weakening PID, endpoint, token, or
2448
- runtime-identity validation.
2449
-
2450
- `homeDir` and `KODAX_HOME` deliberately name different levels. Runtime SDK and
2451
- CLI daemon `--home` accept the **base directory that contains `.kodax`**;
2452
- lower-level `KODAX_HOME` points at the **data directory itself** and need not be
2453
- named `.kodax`. To share the default CLI daemon, omit `homeDir`; this honors the
2454
- exact resolved `KODAX_HOME`. Passing `os.homedir()` explicitly instead selects
2455
- `<os.homedir()>/.kodax`, regardless of an ambient custom `KODAX_HOME`. For an
2456
- isolated embedder namespace, pass a private base directory and expect data at
2457
- `<homeDir>/.kodax`. Passing `~/.kodax` as `homeDir` would instead select
2458
- `~/.kodax/.kodax` and a different daemon namespace.
2465
+ SDK auto-start allows `daemonStartupTimeoutMs` (default 60 seconds) and
2466
+ `daemonConnectTimeoutMs`. The longer startup budget covers cold machines and
2467
+ concurrent test/desktop startup without weakening PID, endpoint, token, or
2468
+ runtime-identity validation.
2469
+
2470
+ `homeDir` and `KODAX_HOME` deliberately name different levels. Runtime SDK and
2471
+ CLI daemon `--home` accept the **base directory that contains `.kodax`**;
2472
+ lower-level `KODAX_HOME` points at the **data directory itself** and need not be
2473
+ named `.kodax`. To share the default CLI daemon, omit `homeDir`; this honors the
2474
+ exact resolved `KODAX_HOME`. Passing `os.homedir()` explicitly instead selects
2475
+ `<os.homedir()>/.kodax`, regardless of an ambient custom `KODAX_HOME`. For an
2476
+ isolated embedder namespace, pass a private base directory and expect data at
2477
+ `<homeDir>/.kodax`. Passing `~/.kodax` as `homeDir` would instead select
2478
+ `~/.kodax/.kodax` and a different daemon namespace.
2459
2479
 
2460
2480
  ### Worker-hosted embedded usage
2461
2481
 
@@ -2534,16 +2554,16 @@ For a given profile, one owner wins an atomic lock. Concurrent starters wait for
2534
2554
  the winner and connect once it is ready. Stale state is cleaned only after pid,
2535
2555
  endpoint, token, and runtime identity checks. If ownership cannot be verified,
2536
2556
  KodaX reports the daemon as unhealthy instead of killing an arbitrary process.
2537
- SDK auto-start launches a detached `kodax daemon serve` process; it never treats
2538
- an in-process socket listener as daemon mode. Closing the SDK client detaches
2539
- without stopping that shared process. Use `kodax daemon stop` or the explicit
2540
- runtime shutdown protocol to stop the owner.
2541
-
2542
- CLI and SDK startup retain the exact spawned candidate until health confirms
2543
- that candidate PID. Early exit, timeout, identity mismatch, cancellation, or a
2544
- different owner winning the race reclaims only the unsuccessful candidate
2545
- process tree. Once healthy, the owner detaches normally and is not tied to the
2546
- creating client process.
2557
+ SDK auto-start launches a detached `kodax daemon serve` process; it never treats
2558
+ an in-process socket listener as daemon mode. Closing the SDK client detaches
2559
+ without stopping that shared process. Use `kodax daemon stop` or the explicit
2560
+ runtime shutdown protocol to stop the owner.
2561
+
2562
+ CLI and SDK startup retain the exact spawned candidate until health confirms
2563
+ that candidate PID. Early exit, timeout, identity mismatch, cancellation, or a
2564
+ different owner winning the race reclaims only the unsuccessful candidate
2565
+ process tree. Once healthy, the owner detaches normally and is not tied to the
2566
+ creating client process.
2547
2567
 
2548
2568
  ### Daemon startup, conflict, and recovery behavior
2549
2569
 
@@ -2559,18 +2579,18 @@ recovered as `interrupted` with a runtime event; they are not resumed
2559
2579
  automatically. Session and bounded event records remain available for explicit
2560
2580
  reconnect/retry decisions.
2561
2581
 
2562
- Operational guidance:
2563
-
2564
- - use one stable profile for cooperating desktop clients;
2565
- - use a separate `homeDir` or profile for tests, previews, and incompatible
2566
- configurations;
2567
- - test harnesses that auto-start a process daemon must send authenticated
2568
- `runtime.shutdown` (or run `kodax daemon stop --home <dir> --profile <name>`)
2569
- before deleting their temporary home; `runtime.close()` only detaches;
2570
- - KodaX's own Vitest harness also supplies an internal worker-PID marker so a
2571
- forcibly terminated worker cannot strand its test daemon; this is a test-only
2572
- fallback, not a public SDK option or a production idle-shutdown policy;
2573
- - query `kodax daemon status --json` before deciding to restart;
2582
+ Operational guidance:
2583
+
2584
+ - use one stable profile for cooperating desktop clients;
2585
+ - use a separate `homeDir` or profile for tests, previews, and incompatible
2586
+ configurations;
2587
+ - test harnesses that auto-start a process daemon must send authenticated
2588
+ `runtime.shutdown` (or run `kodax daemon stop --home <dir> --profile <name>`)
2589
+ before deleting their temporary home; `runtime.close()` only detaches;
2590
+ - KodaX's own Vitest harness also supplies an internal worker-PID marker so a
2591
+ forcibly terminated worker cannot strand its test daemon; this is a test-only
2592
+ fallback, not a public SDK option or a production idle-shutdown policy;
2593
+ - query `kodax daemon status --json` before deciding to restart;
2574
2594
  - inspect `kodax daemon logs --lines 100` when startup times out;
2575
2595
  - custom endpoints and injected transports are attach-only unless the caller
2576
2596
  implements their owner lifecycle explicitly.
@@ -2606,10 +2626,10 @@ Every `KodaXRuntime` exposes the same service set in inline, Worker, and daemon
2606
2626
  | `mcp` | MCP server CRUD, validation, reload, and tool catalog listing. |
2607
2627
  | `artifacts` | Create/get/delete runtime artifact references for file/image/video inputs. |
2608
2628
  | `status` | Runtime snapshot with sessions, runs, permissions, workflows, and daemon counters. |
2609
- | `diagnostics` | Latest context-budget and tool-exposure decisions for GUI/debug surfaces. |
2610
- | `admin.agentRegistrations` | List/upsert, atomically set `enabled` while preserving the full registration, or remove redacted external-agent registrations. Owner/revision-conditional mutation prevents a stale manager from changing a same-ID replacement. With no plane, list is empty and mutations fail clearly. |
2611
- | `agents` | Check `enabled`, list/describe policy-filtered dispatchable agents, and preflight a selected route. |
2612
- | `agentTasks` | Start/list/get/wait/continue/cancel/reconcile durable external-agent tasks and read their ordered event stream. |
2629
+ | `diagnostics` | Latest context-budget and tool-exposure decisions for GUI/debug surfaces. |
2630
+ | `admin.agentRegistrations` | List/upsert, atomically set `enabled` while preserving the full registration, or remove redacted external-agent registrations. Owner/revision-conditional mutation prevents a stale manager from changing a same-ID replacement. With no plane, list is empty and mutations fail clearly. |
2631
+ | `agents` | Check `enabled`, list/describe policy-filtered dispatchable agents, and preflight a selected route. |
2632
+ | `agentTasks` | Start/list/get/wait/continue/cancel/reconcile durable external-agent tasks and read their ordered event stream. |
2613
2633
 
2614
2634
  ### Sessions, runs, and queueing
2615
2635
 
@@ -2621,14 +2641,14 @@ runtime close.
2621
2641
  ```ts
2622
2642
  const session = await runtime.sessions.create({ title: 'Shared session' });
2623
2643
 
2624
- const sub = runtime.events.subscribe({ sessionId: session.id }, (event) => {
2625
- if (event.type === 'assistant.delta') {
2626
- // Render streaming text in the host UI.
2627
- }
2628
- });
2629
- // Daemon subscriptions establish a remote handshake. Await this before
2630
- // starting work whose first event must not be missed; local subscriptions omit it.
2631
- await sub.ready;
2644
+ const sub = runtime.events.subscribe({ sessionId: session.id }, (event) => {
2645
+ if (event.type === 'assistant.delta') {
2646
+ // Render streaming text in the host UI.
2647
+ }
2648
+ });
2649
+ // Daemon subscriptions establish a remote handshake. Await this before
2650
+ // starting work whose first event must not be missed; local subscriptions omit it.
2651
+ await sub.ready;
2632
2652
 
2633
2653
  const artifact = await runtime.artifacts.create({
2634
2654
  kind: 'image',
@@ -2656,14 +2676,14 @@ sub.close();
2656
2676
 
2657
2677
  `runs.start({ options })` is a DTO boundary in Worker and daemon forms. Do not
2658
2678
  pass process-local objects such as `extensionRuntime`, callbacks, `AbortSignal`,
2659
- LSP services, class instances, or cyclic structures. KodaX rejects them before
2660
- transport instead of silently dropping fields.
2661
-
2662
- `events.beforeToolExecute` is an executable policy hook, not an observation
2663
- callback. It is preserved in embedded mode and rejected in daemon mode; install
2664
- the equivalent policy in the daemon owner instead. The KodaX REPL explicitly
2665
- marks and removes only its own legacy approval hook because Runtime-owned
2666
- permission brokering replaces that hook.
2679
+ LSP services, class instances, or cyclic structures. KodaX rejects them before
2680
+ transport instead of silently dropping fields.
2681
+
2682
+ `events.beforeToolExecute` is an executable policy hook, not an observation
2683
+ callback. It is preserved in embedded mode and rejected in daemon mode; install
2684
+ the equivalent policy in the daemon owner instead. The KodaX REPL explicitly
2685
+ marks and removes only its own legacy approval hook because Runtime-owned
2686
+ permission brokering replaces that hook.
2667
2687
 
2668
2688
  Use the runtime APIs for cross-boundary behavior:
2669
2689
 
@@ -2692,7 +2712,7 @@ Space-style client can subscribe to permission events and answer a request
2692
2712
  created by a REPL-style client.
2693
2713
 
2694
2714
  ```ts
2695
- const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) => {
2715
+ const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) => {
2696
2716
  const payload = event.payload;
2697
2717
  if (!payload || typeof payload !== 'object' || typeof payload.id !== 'string') {
2698
2718
  return;
@@ -2701,35 +2721,35 @@ const sub = runtime.events.subscribe({ type: 'permission.requested' }, (event) =
2701
2721
  payload.id,
2702
2722
  { type: 'allow_once' },
2703
2723
  { runId: payload.runId },
2704
- );
2705
- });
2706
- await sub.ready;
2707
- ```
2708
-
2709
- Await `ready` before another client starts work that may request permission;
2710
- this creates an explicit cross-connection ordering boundary. Only the first
2711
- valid response wins. Wrong-run or stale responses are rejected.
2712
- Abort, runtime close, daemon stop, and timeout reject unresolved permission
2713
- requests so tool approval promises do not hang forever.
2714
-
2715
- SDK clients that create a concrete request may pass `toolInput` and
2716
- `executionCwd` to `runtime.permissions.request(...)` in both embedded and
2717
- daemon mode. The Runtime removes raw `toolInput` before publishing the pending
2718
- request, derives the bounded/redacted preview from that concrete input,
2719
- canonicalizes it into a matcher, and returns only opaque `grantSuggestions`.
2720
- A caller-supplied `inputPreview` cannot override this trusted summary.
2721
- `projectRoot`, classifier signals, and other owner-only safety context are
2722
- deliberately not client request fields.
2723
-
2724
- Grant administration is typed through `RuntimePermissionScope` and the
2725
- exported `RuntimePermissionMatcher` union (`exact-command`, `exact-path`, and
2726
- `exact-call`). These types are available from both the package root and
2727
- `@kodax-ai/kodax/runtime`; matcher construction remains Runtime-owned.
2728
- Legacy `allow_always.scope` responses remain accepted for 0.7.x clients, but
2729
- the Runtime narrows them to its concrete candidate and never persists the
2730
- client-provided coarse scope. Legacy persisted grants that lack a Runtime
2731
- matcher remain inspectable and revocable, but never authorize a concrete tool
2732
- call; the user must approve a fresh Runtime-issued exact suggestion.
2724
+ );
2725
+ });
2726
+ await sub.ready;
2727
+ ```
2728
+
2729
+ Await `ready` before another client starts work that may request permission;
2730
+ this creates an explicit cross-connection ordering boundary. Only the first
2731
+ valid response wins. Wrong-run or stale responses are rejected.
2732
+ Abort, runtime close, daemon stop, and timeout reject unresolved permission
2733
+ requests so tool approval promises do not hang forever.
2734
+
2735
+ SDK clients that create a concrete request may pass `toolInput` and
2736
+ `executionCwd` to `runtime.permissions.request(...)` in both embedded and
2737
+ daemon mode. The Runtime removes raw `toolInput` before publishing the pending
2738
+ request, derives the bounded/redacted preview from that concrete input,
2739
+ canonicalizes it into a matcher, and returns only opaque `grantSuggestions`.
2740
+ A caller-supplied `inputPreview` cannot override this trusted summary.
2741
+ `projectRoot`, classifier signals, and other owner-only safety context are
2742
+ deliberately not client request fields.
2743
+
2744
+ Grant administration is typed through `RuntimePermissionScope` and the
2745
+ exported `RuntimePermissionMatcher` union (`exact-command`, `exact-path`, and
2746
+ `exact-call`). These types are available from both the package root and
2747
+ `@kodax-ai/kodax/runtime`; matcher construction remains Runtime-owned.
2748
+ Legacy `allow_always.scope` responses remain accepted for 0.7.x clients, but
2749
+ the Runtime narrows them to its concrete candidate and never persists the
2750
+ client-provided coarse scope. Legacy persisted grants that lack a Runtime
2751
+ matcher remain inspectable and revocable, but never authorize a concrete tool
2752
+ call; the user must approve a fresh Runtime-issued exact suggestion.
2733
2753
 
2734
2754
  ### Config, catalogs, MCP, and Space-style admin APIs
2735
2755
 
@@ -2802,18 +2822,18 @@ import {
2802
2822
  The schema is additive within this patch line. Removing or changing required
2803
2823
  fields requires a protocol version bump.
2804
2824
 
2805
- ### Current verification status
2806
-
2807
- The v0.7.69 release validation covers the runtime migration, the Worker
2808
- isolation follow-ups delivered ahead of their original v0.7.71/v0.7.72
2809
- planning slots, the v0.7.67 external-agent/session additions, the v0.7.68
2810
- experimental Memory Agent surface, and the v0.7.69 A2A/integration/shared-
2811
- daemon delivery:
2812
-
2813
- - Node 20 and Node 22 post-review regression for process cleanup, lock
2814
- ownership, frozen eval inputs, A2A transport, and the built-in manual;
2815
- - root `tsc --noEmit`, package builds, bundle builds, and all 12 public DTS
2816
- entries on Node 20 and Node 22;
2825
+ ### Current verification status
2826
+
2827
+ The v0.7.69 release validation covers the runtime migration, the Worker
2828
+ isolation follow-ups delivered ahead of their original v0.7.71/v0.7.72
2829
+ planning slots, the v0.7.67 external-agent/session additions, the v0.7.68
2830
+ experimental Memory Agent surface, and the v0.7.69 A2A/integration/shared-
2831
+ daemon delivery:
2832
+
2833
+ - Node 20 and Node 22 post-review regression for process cleanup, lock
2834
+ ownership, frozen eval inputs, A2A transport, and the built-in manual;
2835
+ - root `tsc --noEmit`, package builds, bundle builds, and all 12 public DTS
2836
+ entries on Node 20 and Node 22;
2817
2837
  - runtime/daemon/SDK/ACP/REPL integration tests, including process-distinct SDK
2818
2838
  auto-start and multi-client sessions/permissions;
2819
2839
  - Worker Runtime identity, service parity, hard close, capability requirements,
@@ -2821,1525 +2841,1774 @@ daemon delivery:
2821
2841
  - constructed-handler reverse tool RPC, abort bridging, CPU-loop termination,
2822
2842
  respawn, and revoke/dispose queue drainage;
2823
2843
  - context/tool-exposure eval gate;
2824
- - embedded and hosted-daemon external-agent catalog/task parity, durable task
2825
- recovery, policy/credential/artifact gates, and disabled-plane failure;
2826
- - exact session `surface` filtering plus opaque cursor continuation through the
2827
- narrow `/session` API, embedded Runtime, and daemon transport;
2828
- - scoped zero-wait memory recall, read-only deliberate query, trace-only
2829
- receipts, bounded episode review, governed consult-before-write promotion,
2830
- and the self-contained `/experimental-memory` bundle/DTS surface;
2831
- - bidirectional A2A 1.0 discovery/call/serve, durable task replay, no-code
2832
- configuration, local-Agent binding, exact Skill-script admission, SSRF/
2833
- credential boundaries, and the self-contained `/a2a` bundle/DTS surface;
2834
- - split MCP/A2A/Extension configuration, lossless migration, canonical-template
2835
- drift checks, last-known-good hot reload, draining, and restart-required
2836
- classification;
2837
- - atomic shared-daemon observation/resync, durable operations and same-session
2838
- ordering, settings/grant CAS, AskUser/permission transport, credential/Host
2839
- Tool reverse bridges, owner fencing, restart outcomes, and process-distinct
2840
- client/daemon smokes with credential-canary scans;
2841
- - a fresh `0.7.69` tarball consumer importing all 11 public subpaths, creating a
2842
- Worker-hosted session, running the packaged CLI, and checking packaged DTS and
2843
- Worker sidecars; plus a Windows x64 binary/version/sidecar smoke;
2844
- - external fresh npm consumer installation of the `0.7.67` tarball, proving
2845
- Worker isolation and a distinct daemon PID through the published subpath;
2844
+ - embedded and hosted-daemon external-agent catalog/task parity, durable task
2845
+ recovery, policy/credential/artifact gates, and disabled-plane failure;
2846
+ - exact session `surface` filtering plus opaque cursor continuation through the
2847
+ narrow `/session` API, embedded Runtime, and daemon transport;
2848
+ - scoped zero-wait memory recall, read-only deliberate query, trace-only
2849
+ receipts, bounded episode review, governed consult-before-write promotion,
2850
+ and the self-contained `/experimental-memory` bundle/DTS surface;
2851
+ - bidirectional A2A 1.0 discovery/call/serve, durable task replay, no-code
2852
+ configuration, local-Agent binding, exact Skill-script admission, SSRF/
2853
+ credential boundaries, and the self-contained `/a2a` bundle/DTS surface;
2854
+ - split MCP/A2A/Extension configuration, lossless migration, canonical-template
2855
+ drift checks, last-known-good hot reload, draining, and restart-required
2856
+ classification;
2857
+ - atomic shared-daemon observation/resync, durable operations and same-session
2858
+ ordering, settings/grant CAS, AskUser/permission transport, credential/Host
2859
+ Tool reverse bridges, owner fencing, restart outcomes, and process-distinct
2860
+ client/daemon smokes with credential-canary scans;
2861
+ - a fresh `0.7.69` tarball consumer importing all 11 public subpaths, creating a
2862
+ Worker-hosted session, running the packaged CLI, and checking packaged DTS and
2863
+ Worker sidecars; plus a Windows x64 binary/version/sidecar smoke;
2864
+ - external fresh npm consumer installation of the `0.7.67` tarball, proving
2865
+ Worker isolation and a distinct daemon PID through the published subpath;
2846
2866
  - Ubuntu Node 22 Unix-domain-socket daemon gate, including two clients sharing
2847
2867
  one runtime and cross-client permission resolution.
2848
2868
 
2849
- The portable manual gates remain in
2850
- `docs/test-guides/FEATURE_255_v0.7.66_TEST_GUIDE.md`,
2851
- `FEATURE_256_v0.7.71_TEST_GUIDE.md`, and
2852
- `FEATURE_257_v0.7.72_TEST_GUIDE.md` for release-machine verification; the latter
2853
- two filenames retain their original planning slots while their content records
2854
- the v0.7.66 delivery. v0.7.67 adds
2855
- `FEATURE_258_v0.7.67_TEST_GUIDE.md`, `FEATURE_259_v0.7.67_TEST_GUIDE.md`, and
2856
- `FEATURE_261_v0.7.67_TEST_GUIDE.md`; v0.7.69 adds
2857
- `FEATURE_267_v0.7.69_TEST_GUIDE.md`, `FEATURE_268_v0.7.69_TEST_GUIDE.md`, and
2858
- `FEATURE_269_v0.7.69_TEST_GUIDE.md`. The earlier release-preparation Actions
2859
- run is not reused after the severe-fix review. Fresh GitHub Actions run
2860
- `29385073422` passes the Node 20/22 build, bundle, DTS, and full-test matrix plus
2861
- the Node 22 Unix-domain-socket daemon gate. npm publication remains a
2862
- maintainer-owned manual step.
2863
-
2864
- ---
2865
-
2866
- ## 18. External-agent executor plane (FEATURE_258, v0.7.67)
2867
-
2868
- FEATURE_258 lets an SDK host register remote agents without teaching the coding
2869
- runtime a specific A2A, MCP, or HTTP client. The public contracts live in
2870
- `@kodax-ai/kodax/agent`; the host-facing catalog and task services live on
2871
- `@kodax-ai/kodax/runtime`.
2872
-
2873
- ### Ownership rule
2874
-
2875
- An `AgentExecutorFactory` contains functions, so it cannot cross a Worker or
2876
- daemon DTO boundary. Install factories where the Runtime owner executes:
2877
-
2878
- | Desired owner | Supported construction |
2879
- |---|---|
2880
- | Private in-process owner | `createKodaXRuntime({ mode: 'embedded', isolation: 'inline', externalAgents })` |
2881
- | New locally hosted daemon owner | `createKodaXRuntime({ mode: 'daemon', profile: '<unique>', externalAgents })` |
2882
- | Existing daemon | Configure its owner, then attach with `connectKodaXRuntime({ requirements: { externalAgents: true } })`; a client cannot inject factories. |
2883
- | Runtime Worker | The Worker owner must install factories itself; passing `externalAgents` from the parent is rejected. |
2884
-
2885
- When `mode: 'daemon'` and `externalAgents` are supplied, the caller must win a
2886
- new in-process daemon lease. KodaX rejects an already-running profile instead of
2887
- silently replacing its executor configuration. Closing that owner facade shuts
2888
- down the host it created; closing an ordinary attached client only detaches.
2889
-
2890
- ### Minimal owner and task flow
2891
-
2892
- The reference executor below is a contract/conformance adapter. Replace it with
2893
- your own `AgentExecutorFactory` for a real remote protocol.
2894
-
2895
- ```ts
2896
- import {
2897
- createReferenceAgentExecutorFactory,
2898
- type ExternalAgentRegistration,
2899
- } from '@kodax-ai/kodax/agent';
2900
- import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
2901
-
2902
- const runtime = await createKodaXRuntime({
2903
- mode: 'embedded',
2904
- isolation: 'inline',
2905
- externalAgents: {
2906
- factories: [createReferenceAgentExecutorFactory({
2907
- executorId: 'example-http',
2908
- protocol: 'http',
2909
- })],
2910
- policy: ({ registration, query }) => ({
2911
- allowed: registration.enabled && query.readOnly === true,
2912
- reasons: query.readOnly === true ? [] : ['This host allows read-only dispatch only.'],
2913
- }),
2914
- defaultContext: { actorId: 'desktop-host' },
2915
- },
2916
- });
2917
-
2918
- const registration: ExternalAgentRegistration = {
2919
- agentId: 'external:reviewer',
2920
- displayName: 'Remote Reviewer',
2921
- enabled: true,
2922
- executorId: 'example-http',
2923
- protocol: 'http',
2924
- configurationRevision: 'reviewer-config-v1',
2925
- endpointIdentityHash: 'sha256:replace-with-stable-endpoint-identity',
2926
- skills: ['code-review'],
2927
- inputModalities: ['text'],
2928
- outputModalities: ['text'],
2929
- capabilities: {
2930
- streaming: 'supported',
2931
- durableTasks: 'supported',
2932
- inputRequired: 'conditional',
2933
- cancellation: 'supported',
2934
- artifacts: 'unsupported',
2935
- },
2936
- effects: { remote: 'read', workspace: 'proposal' },
2937
- maxConcurrency: 1,
2938
- };
2939
-
2940
- try {
2941
- await runtime.admin.agentRegistrations.upsert(registration);
2942
-
2943
- const query = {
2944
- actorId: 'desktop-host',
2945
- requiredSkills: ['code-review'],
2946
- readOnly: true,
2947
- } as const;
2948
- const available = await runtime.agents.listDispatchable(query);
2949
- const preflight = await runtime.agents.preflight({
2950
- agentId: 'external:reviewer',
2951
- query,
2952
- expectedConfigurationRevision: registration.configurationRevision,
2953
- });
2954
- if (!preflight.ok) throw new Error(preflight.reasons.join('; '));
2955
-
2956
- const started = await runtime.agentTasks.start({
2957
- agentId: 'external:reviewer',
2958
- objective: 'Review the supplied immutable patch and return cited findings.',
2959
- context: { actorId: 'desktop-host', runId: 'host-run-42' },
2960
- readOnly: true,
2961
- requiredSkills: ['code-review'],
2962
- expectedConfigurationRevision: registration.configurationRevision,
2963
- });
2964
- const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
2965
- console.log(available, terminal.state, terminal.output, terminal.usage);
2966
- } finally {
2967
- await runtime.close();
2968
- }
2969
- ```
2970
-
2971
- `runtime.agents.enabled` is the cheap external-plane feature check. If it is
2972
- false, catalog queries can still return built-in local agents but no external
2973
- agents; registration and task lists are empty, while point reads and mutations
2974
- fail clearly. Set
2975
- `requirements.externalAgents: true` when absence must abort connection.
2976
-
2977
- ### Service reference
2978
-
2979
- | Surface | Methods | Contract |
2980
- |---|---|---|
2981
- | `runtime.admin.agentRegistrations` | `list`, `upsert`, `setEnabled`, `remove` | Durable owner configuration. `setEnabled` preserves the complete captured executor registration while changing admission. Mutations accept both `expectedConfigurationRevision` and `expectedManagementOwner`; `setEnabled` can also atomically `claimOwner` on an unowned registration and rejects another owner. List results expose `managementOwner` and `credentialConfigured`, never a credential value. The same contract is carried across the daemon transport. With no plane, `list()` is empty and mutations fail clearly. |
2982
- | `runtime.agents` | `enabled`, `listDispatchable`, `describe`, `preflight` | Applies health, capability, effect, concurrency, credential-presence, configuration-revision, and host-policy checks before dispatch. |
2983
- | `runtime.agentTasks` | `start`, `list`, `get`, `events`, `wait`, `sendInput`, `cancel`, `reconcile` | Durable snapshots and append-only events for external tasks. The task keeps the immutable registration/executor binding captured at start. |
2984
-
2985
- `agentTasks.events(taskId, cursor)` uses the last seen numeric event `seq` as its
2986
- cursor and returns events with a greater sequence. `wait()` resolves only at a
2987
- terminal task state and rejects on a positive `timeoutMs` expiry. `sendInput()`
2988
- is valid only while the task reports `input-required` or `auth-required`.
2989
- `reconcile()` asks the bound executor for authoritative remote state after an
2990
- owner restart or uncertain failure.
2991
-
2992
- For external tasks, the built-in stores persist an internal full registration
2993
- snapshot before the public task ledger. It is keyed by Agent ID and revision,
2994
- is never returned by task or daemon APIs, and lets an admitted task keep using
2995
- its original executor route after registration update/removal and Runtime
2996
- restart. The internal form fixes `enabled: true` and omits management ownership
2997
- and health diagnostics. The task's public route summary is validated against that internal
2998
- snapshot before recovery. Terminal task state is durable before the last
2999
- unreferenced snapshot is removed; startup cleans crash-window orphans.
3000
-
3001
- Custom `AgentExecutorPlaneStore` implementations should implement
3002
- `loadTaskRegistrationSnapshots()` and `saveTaskRegistrationSnapshots()` as a
3003
- pair and give one Runtime exclusive write ownership of that store. Omitting
3004
- both remains compatible, but restart recovery then succeeds only while the
3005
- exact current registration still exists. Store only non-secret executor config
3006
- or secret references in `executorConfig`/`credentialRef`; the broker resolves
3007
- the current referenced credential just in time, so removing a registration is
3008
- not equivalent to revoking that credential at its issuer.
3009
-
3010
- The owner plane has a terminal close contract. Closing it rejects every pending
3011
- `wait()` (including a wait without `timeoutMs`), disposes its executor instances,
3012
- and makes subsequent registration, catalog, preflight, and task calls reject
3013
- with `Agent executor plane is closed.` One overall deadline covers admitted
3014
- work plus executor disposal: the default is 30 seconds, and direct
3015
- `createAgentExecutorPlane()` hosts may supply a positive finite
3016
- `closeTimeoutMs`. A timeout rejects visibly even though already-admitted cleanup
3017
- may finish in the background. Repeated `close()` calls are safe. SDK hosts
3018
- should stop accepting work before closing the owner and must not retain a plane
3019
- service as a reusable handle after Runtime shutdown.
3020
-
3021
- Restricted Workflow scripts use the same route as direct SDK calls. Both
3022
- `wf.spawnAgent()` and `wf.runAgent()` validate and forward
3023
- `target: { agentId, expectedConfigurationRevision? }`; `phase` is forwarded as
3024
- well. A blank ID or revision is rejected at the script boundary instead of
3025
- silently falling back to the native child backend.
3026
-
3027
- ### Credential, artifact, and failure boundaries
3028
-
3029
- - Put only a `credentialRef` in a registration. Resolve secret material through
3030
- `AgentCredentialBroker.withCredential()`; do not place tokens in
3031
- `executorConfig`, events, diagnostics, or task output.
3032
- - Remote artifacts are denied by default. Supply `artifactPolicy` to authorize
3033
- each artifact before it materializes in the host boundary.
3034
- - External agents may declare workspace effect `none` or `proposal`; direct
3035
- workspace mutation is intentionally not a valid external registration.
3036
- - Use `expectedConfigurationRevision` for dispatch. For registration mutations,
3037
- compare both it and `expectedManagementOwner` so a same-revision ownership
3038
- change cannot be overwritten from an earlier catalog read. Config managers
3039
- should set a stable `managementOwner`; they may atomically claim an unowned
3040
- legacy registration while disabling it, but must not mutate a registration
3041
- owned by another manager.
3042
- - Treat `configurationRevision` as the stable identity of immutable execution
3043
- content, not as a small counter. The same content may deterministically reuse
3044
- the same revision across remove/re-add or Runtime restart, but different
3045
- endpoint, protocol, executor/auth config, capabilities, effects, Skills,
3046
- modalities, or resource limits must never reuse it. Built-in A2A
3047
- configuration derives it from content.
3048
- - A remote start followed by uncertain local persistence is recorded as
3049
- `unknown` with its executor reference preserved; reconcile it rather than
3050
- blindly starting a duplicate. Stable idempotency keys protect retries.
3051
- - The durable plane records provider-reported usage when available. It never
3052
- invents missing token or cost fields.
3053
-
3054
- For a production adapter, implement `preflight?`, `start`, `events`, `get`,
3055
- `sendInput`, `cancel`, `reconcile`, and `dispose` on `AgentExecutor`. The factory
3056
- receives `withCredential()` and `authorizeArtifact()` callbacks so protocol code
3057
- cannot bypass the host's secret/artifact policies.
3058
-
3059
- ---
3060
-
3061
- ## 19. Session surface filtering and cursor pagination (FEATURE_261, v0.7.67)
3062
-
3063
- The narrow session SDK and Runtime facade now share the same listing semantics:
3064
- `surface` is an exact filter applied before `limit`, and `cursor` is an opaque
3065
- continuation token carried by each returned summary.
3066
-
3067
- ### Narrow `/session` API
3068
-
3069
- ```ts
3070
- import { listSessions, type SessionSummary } from '@kodax-ai/kodax/session';
3071
-
3072
- const all: SessionSummary[] = [];
3073
- let cursor: string | undefined;
3074
- do {
3075
- const page = await listSessions({
3076
- scope: 'user',
3077
- surface: 'partner',
3078
- limit: 50,
3079
- ...(cursor ? { cursor } : {}),
3080
- });
3081
- all.push(...page);
3082
- cursor = page.length === 50 ? page.at(-1)?.cursor : undefined;
3083
- } while (cursor);
3084
- ```
3085
-
3086
- ### Embedded/daemon Runtime API
3087
-
3088
- ```ts
3089
- const first = await runtime.sessions.list({ surface: 'acp', limit: 25 });
3090
- const nextCursor = first.at(-1)?.cursor;
3091
- const second = nextCursor
3092
- ? await runtime.sessions.list({ surface: 'acp', limit: 25, cursor: nextCursor })
3093
- : [];
3094
- ```
3095
-
3096
- The filter also composes with `projectRoot`, `scope`, `includeArchived`, `before`,
3097
- and `tag`. Treat cursors as opaque: do not parse them, compare them to session
3098
- IDs, or manufacture them. An invalid cursor produces an empty page on the narrow
3099
- session API. A page shorter than the requested limit is terminal; a full page
3100
- may continue with its last item's cursor.
3101
-
3102
- The interactive `kodax -r` picker is a consumer of these session semantics, not
3103
- a separate SDK API. Headless hosts should build their own UI on `listSessions()`
3104
- or `runtime.sessions.list()` and resume by the selected full session ID.
3105
-
3106
- ---
3107
-
3108
- ## 20. Cost-disciplined workflow routing and telemetry (FEATURE_259, v0.7.67)
3109
-
3110
- FEATURE_259 adds a public cost/quality contract without making the authoring LLM
3111
- choose provider-specific model names. The SDK host maps semantic tiers to routes;
3112
- workflow/child briefs request intent.
3113
-
3114
- ### Configure tiers and bounded concurrency
3115
-
3116
- ```ts
3117
- import { join } from 'node:path';
3118
- import { runKodaX } from '@kodax-ai/kodax/coding';
3119
-
3120
- const workflowRunsBaseDir = join(process.cwd(), '.kodax-host', 'workflow-runs');
3121
- const result = await runKodaX({
3122
- provider: 'zai-coding',
3123
- model: 'glm-5.2',
3124
- agentMode: 'amaw',
3125
- workflowRunsBaseDir,
3126
- modelTiers: {
3127
- fast: { provider: 'deepseek', model: 'deepseek-v4-flash' },
3128
- deep: { provider: 'zai-coding', model: 'glm-5.2' },
3129
- },
3130
- workflow: { maxConcurrency: 3 },
3131
- events: {
3132
- onWorkflowAgentDigest: ({ runId, event }) => {
3133
- if (event.type === 'agent_completed' || event.type === 'agent_unverified'
3134
- || event.type === 'agent_failed') {
3135
- console.log(runId, event.data);
3136
- }
3137
- },
3138
- },
3139
- }, 'Review this change using scoped evidence packets.');
3140
-
3141
- console.log(result.lastText);
3142
- ```
3143
-
3144
- Tier rules are deliberately small:
3145
-
3146
- - `fast`: mechanical read-only lookup. A write child is ineligible and safely
3147
- inherits the parent route.
3148
- - `balanced`: ordinary implementation/investigation/review; uses the parent
3149
- route, so there is no separate `balanced` mapping.
3150
- - `deep`: architecture, adversarial verification, severity calibration, and
3151
- final synthesis.
3152
- - An unconfigured or selector-shadowed tier inherits the appropriate explicit,
3153
- specialist, or parent route and records why; it does not silently claim the
3154
- requested route was applied.
3155
-
3156
- The workflow authoring contract also supports `scopeSummary`, `constraints`,
3157
- `evidenceRefs`, `verification`, `readOnly`, `outputSchema`, and `terseResult`.
3158
- Use those fields to transfer a compact immutable packet instead of asking every
3159
- child to rediscover the same repository context.
3160
-
3161
- ### Live route facts
3162
-
3163
- Terminal `WorkflowEvent.data` may include `requestedTier`, `tierOutcome`,
3164
- `providerSource`, `modelSource`, initial/final provider and model,
3165
- `fallbackReason`, `iterations`, `durationMs`, `usage`, and `digestUsage`.
3166
- Treat absent fields as unknown. In particular, KodaX does not fabricate usage
3167
- for external executors that did not report it.
3168
-
3169
- Direct child-dispatch consumers receive the typed `KodaXChildAgentResult.routeFacts`
3170
- surface, including resolved effort and input/cache-read/output/digest token
3171
- breakdown when known. Inline workflow consumers can subscribe to the raw
3172
- `onWorkflowAgentDigest` event as above; GUI progress remains available through
3173
- `onWorkflowProcessEvent` / `runtime.workflows`.
3174
-
3175
- ### Durable efficiency report
3176
-
3177
- When `workflowRunsBaseDir` is supplied, every terminal workflow writes:
3178
-
3179
- ```text
3180
- <workflowRunsBaseDir>/<runId>/run.json
3181
- <workflowRunsBaseDir>/<runId>/events.jsonl
3182
- <workflowRunsBaseDir>/<runId>/artifacts/*.json
3183
- ```
3184
-
3185
- `run.json.efficiencyReport` includes:
3186
-
3187
- - total/input/cache-read/output/digest tokens and wall-clock duration;
3188
- - total starts, child turns, starts by `role/tier`, and primary-review starts;
3189
- - duplicate primary packet reads plus verification/synthesis packet reads;
3190
- - review/fix/re-review waves and structured review quality-gate outcomes;
3191
- - `tokenCoverage.ok` plus missing local task IDs;
3192
- - `excludedExternalTaskIds` for external tasks whose executor reported no usage.
3193
-
3194
- Do not interpret `totalModelTokens: 0` as free execution unless
3195
- `tokenCoverage.ok` is true and the relevant external task IDs are not excluded.
3196
- The report is an audit/optimization artifact; correctness still comes from the
3197
- workflow's structured findings, verification results, and quality gates.
3198
-
3199
- ---
3200
-
3201
- ## 21. Experimental governed memory — `/experimental-memory` (FEATURE_260, v0.7.68)
3202
-
3203
- KodaX has one durable memory plane: the F228 Memory Control Plane. FEATURE_260
3204
- adds a thin, opt-in agent/session API over that plane; it does not add a second
3205
- database, filesystem memory actions, a resident memory specialist, or online
3206
- self-modification.
3207
-
3208
- Top-level `runKodaX()` coding runs wire this lifecycle automatically. They build
3209
- an exact-scoped memory pack at session start, keep passive recall off the
3210
- blocking hot path, expose `memory_recall` only when the memory session starts,
3211
- record bounded observations/outcomes, and close the session at the run boundary.
3212
- Use the direct SDK below when a custom host needs to own those boundaries.
3213
-
3214
- ### Minimal direct session
3215
-
3216
- ```ts
3217
- import { createMemoryControlPlane } from '@kodax-ai/kodax/agent';
3218
- import { createMemoryAgent } from '@kodax-ai/kodax/experimental-memory';
3219
-
3220
- const identity = {
3221
- tenantId: 'tenant:acme',
3222
- workspaceId: 'workspace:desktop',
3223
- userId: 'user:42',
3224
- agentId: 'agent:reviewer',
3225
- projectId: 'project:kodax',
3226
- sessionId: 'session:20260712',
3227
- };
3228
-
3229
- const controlPlane = createMemoryControlPlane({
3230
- cwd: process.cwd(),
3231
- identity,
3232
- projectDocs: [],
3233
- discoverSkills: false,
3234
- });
3235
- const memory = createMemoryAgent({ controlPlane });
3236
- const session = await memory.startSession({
3237
- identity,
3238
- objective: 'Review the runtime shutdown change',
3239
- });
3240
-
3241
- const immediate = session.recall({
3242
- decisionRevision: 'decision:1',
3243
- objective: 'Review the runtime shutdown change',
3244
- decisionContext: 'Choosing the first verification step',
3245
- decisionIntent: 'runtime shutdown regression',
3246
- throughSequence: 0,
3247
- });
3248
- const deliberate = await session.query({
3249
- decisionRevision: 'decision:2',
3250
- need: 'What uncommon daemon cleanup failure happened before?',
3251
- throughSequence: 0,
3252
- });
3253
-
3254
- await session.complete({
3255
- status: 'succeeded',
3256
- summary: 'Shutdown state replacement verified',
3257
- evidence: [],
3258
- });
3259
- await session.close();
3260
- ```
3261
-
3262
- `recall()` is synchronous: exact observations/hints can be returned immediately,
3263
- while an optional semantic prefetch finishes for a later matching decision.
3264
- `query()` is deliberate and read-only; one distinct query is admitted per
3265
- decision epoch, and the result is bounded to at most three prompt-safe hints and
3266
- 512 estimated tokens. `undefined` means there is no governed reminder to inject.
3267
-
3268
- ### Evidence, tracing, and persistence boundaries
3269
-
3270
- - Recalled content is low-authority evidence. Current repository/config/runtime
3271
- facts must still come from current tools or host state.
3272
- - `observe()` accepts monotonic, evidence-linked observations and rejects secret
3273
- material. `rewind()` removes observations after a sequence boundary.
3274
- - `complete()` can emit an Outcome Digest through `persistOutcomeDigest` and run
3275
- bounded episode review through `reviewEpisode`; cancellation creates neither.
3276
- - `onTrace` receives policy-versioned `MemoryDecisionReceipt` metadata that links
3277
- candidates, selection, injection, and later outcome influence. Receipts are
3278
- trace-only and contain no hidden reasoning.
3279
- - Durable memory mutation remains owned by F228's
3280
- proposal/preview/fingerprint/apply flow. `/memory` is the CLI governance
3281
- surface; direct file/shell writes to managed memory roots are denied.
3282
- - Identity/applicability matching is exact and fail-closed. Hosts should supply
3283
- stable tenant/workspace/user/agent/project/session identifiers and must not put
3284
- credentials into those identifiers.
3285
-
3286
- The subpath is experimental and ESM-only. Treat its exported types as opt-in
3287
- v0.7.x contracts; keep persistence and product policy behind your own adapter.
3288
-
3289
- ---
3290
-
3291
- ## 22. Bidirectional A2A 1.0 — `/a2a` (FEATURE_267, v0.7.69)
3292
-
3293
- `@kodax-ai/kodax/a2a` is the protocol edge for the A2A 1.0 JSON-RPC/SSE
3294
- profile. It composes the protocol-neutral F258 executor plane with the Runtime
3295
- facade; `/agent` and `/coding` remain free of A2A wire types and dependencies.
3296
-
3297
- ### Call an A2A Agent through F258
3298
-
3299
- Discovery is an explicit host action. The URL must match `allowedOrigins`, and
3300
- the default safe transport pins the validated DNS address for each connection
3301
- and independently revalidates redirects, rejects public plain HTTP, bounds
3302
- time/body/redirects, and strips authorization on a cross-origin redirect. A
3303
- custom `fetch` option is a trusted transport override: the embedder then owns
3304
- equivalent DNS-to-connection binding in that transport or proxy.
3305
-
3306
- The selected interface must remain on the Card's trusted origin. KodaX parses
3307
- typed Card-level and Skill-level security declarations: requirement objects are
3308
- alternatives (OR), every scheme inside one object is conjunctive (AND), and an
3309
- empty requirement is anonymous. A configured credential is used only when one
3310
- complete requirement is satisfiable; protected Skills that the configured
3311
- profile cannot satisfy are not advertised to the Runtime catalog.
3312
-
3313
- The built-in profiles are HTTP Bearer and OAuth 2.0 Client Credentials. The
3314
- OAuth profile pins the Card scheme, issuer, exact token endpoint, client ID,
3315
- secret reference, scopes, optional RFC 8707 resource, and client authentication
3316
- method. The external Authorization Server—not the Agent and not KodaX—issues
3317
- the access token. KodaX resolves the client secret only for refresh, keeps an
3318
- expiring token in process memory, coalesces refreshes, and retries one RPC once
3319
- with a fresh token after `401`. Card, Agent RPC, and token endpoints remain
3320
- separate safe-fetch trust boundaries, so a remote Agent cannot redirect a task
3321
- payload to the token origin. API key, Basic, interactive OAuth, OIDC, mTLS, and
3322
- multi-scheme AND requirements fail explicitly in the built-in client.
3323
-
3324
- ```ts
3325
- import {
3326
- createA2AAgentExecutorFactory,
3327
- discoverA2ARegistration,
3328
- } from '@kodax-ai/kodax/a2a';
3329
- import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
3330
-
3331
- const client = {
3332
- networkPolicy: {
3333
- // Card/RPC and OAuth token endpoints are separate trust boundaries.
3334
- allowedOrigins: ['https://reviewer.example', 'https://identity.example'],
3335
- allowPrivateAddresses: false,
3336
- requestTimeoutMs: 10_000,
3337
- maxResponseBytes: 1_048_576,
3338
- maxRedirects: 2,
3339
- },
3340
- pollIntervalMs: 500,
3341
- } as const;
3342
-
3343
- const discovered = await discoverA2ARegistration({
3344
- agentId: 'external:a2a-reviewer',
3345
- agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
3346
- credentialRef: 'a2a/reviewer',
3347
- effects: { remote: 'read' },
3348
- }, client);
3349
-
3350
- const runtime = await createKodaXRuntime({
3351
- mode: 'embedded',
3352
- isolation: 'inline',
3353
- externalAgents: {
3354
- factories: [createA2AAgentExecutorFactory(client)],
3355
- credentialBroker: {
3356
- async withCredential(ref, use) {
3357
- const value = ref === 'a2a/reviewer'
3358
- ? process.env.A2A_REVIEWER_TOKEN
3359
- : ref === 'a2a/reviewer-client-secret'
3360
- ? process.env.A2A_REVIEWER_CLIENT_SECRET
3361
- : undefined;
3362
- if (!value) throw new Error(`Missing credential for reference: ${ref}.`);
3363
- return use(value);
3364
- },
3365
- },
3366
- policy: ({ registration }) => ({ allowed: registration.effects.remote === 'read' }),
3367
- defaultContext: { actorId: 'a2a-host' },
3368
- },
3369
- });
3370
-
3371
- await runtime.admin.agentRegistrations.upsert(discovered.registration);
3372
- const started = await runtime.agentTasks.start({
3373
- agentId: discovered.registration.agentId,
3374
- objective: 'Review this change and return cited findings.',
3375
- context: { actorId: 'a2a-host' },
3376
- readOnly: true,
3377
- expectedConfigurationRevision: discovered.registration.configurationRevision,
3378
- });
3379
- const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
3380
- ```
3381
-
3382
- For OAuth, replace the legacy `credentialRef` input with the structured form;
3383
- the same F258 `credentialBroker` must resolve `clientSecretRef`. The shared
3384
- network policy must admit both origins, while each Card, RPC, and token request
3385
- is still narrowed to its own exact origin:
3386
-
3387
- ```ts
3388
- const discovered = await discoverA2ARegistration({
3389
- agentId: 'external:a2a-reviewer',
3390
- agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
3391
- authentication: {
3392
- type: 'oauth2-client-credentials',
3393
- scheme: 'enterprise-oauth',
3394
- issuer: 'https://identity.example/',
3395
- tokenUrl: 'https://identity.example/oauth/token',
3396
- clientId: 'kodax-reviewer',
3397
- clientSecretRef: 'a2a/reviewer-client-secret',
3398
- scopes: ['a2a.invoke'],
3399
- resource: 'https://reviewer.example/',
3400
- clientAuthentication: 'client-secret-basic',
3401
- },
3402
- effects: { remote: 'read' },
3403
- }, client);
3404
- ```
3405
-
3406
- The executor supports durable task start/get, input continuation, cancel,
3407
- reconcile, SSE events, and polling fallback. An ambiguous start is not retried
3408
- automatically. A `credentialRef` is resolved just in time by the F258 broker;
3409
- the registration, task store, and diagnostics never contain the credential.
3410
- Authenticated SSE uses that same broker. JSON-RPC ID/version and task/context
3411
- correlation are validated before an event is accepted; if a stream ends
3412
- normally before a terminal snapshot, the executor resumes bounded polling.
3413
- Streamed `artifactUpdate` chunks are accumulated by artifact ID according to
3414
- `append`, and direct Message file Parts are preserved as authorized artifact
3415
- references.
3416
-
3417
- ### Built-in configured path (no host code)
3418
-
3419
- The CLI product path stores one user document at
3420
- `~/.kodax/integrations/a2a.json` and uses the same F258 plane:
3421
-
3422
- ```bash
3423
- kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
3424
- --credential-env A2A_REVIEWER_TOKEN --effect read
3425
- kodax a2a test reviewer
3426
- kodax a2a call reviewer "Review this document"
3427
- ```
3428
-
3429
- The no-code OAuth path stores only the environment-variable name for the client
3430
- secret. It can be staged disabled and hot-activated later:
3431
-
3432
- ```bash
3433
- export A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
3434
- # PowerShell: $env:A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
3435
- # PowerShell: use one line or replace each trailing \ with a backtick.
3436
- kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
3437
- --disabled --effect read --oauth-scheme enterprise-oauth \
3438
- --oauth-issuer https://identity.example/ \
3439
- --oauth-token-url https://identity.example/oauth/token \
3440
- --oauth-client-id kodax-reviewer \
3441
- --oauth-client-secret-env A2A_REVIEWER_CLIENT_SECRET \
3442
- --oauth-scope a2a.invoke --oauth-resource https://reviewer.example/
3443
- kodax a2a enable reviewer
3444
- kodax a2a disable reviewer
3445
- ```
3446
-
3447
- Embedded CLI Runtimes and the user-owned daemon automatically reconcile these
3448
- entries as `external:<name>`. Discovery/update failure retains that entry's
3449
- last-known-good registration; another entry can still update. The environment
3450
- broker resolves `credentialEnv` only at call time. Automatic Runtime
3451
- registration accepts public HTTPS and exact loopback targets; explicit private
3452
- network access remains an operator action on the direct CLI/SDK path.
3453
-
3454
- `enabled` is desired state in `a2a.json`, not a fabricated cross-process live
3455
- flag. `a2a list` reports configured entries and that desired state. The owning
3456
- Runtime's `admin.agentRegistrations.list()` is authoritative for applied
3457
- registrations. Automatic reconciliation handles disables/removals first,
3458
- skips unchanged peers, performs no Card or token request for disabled entries,
3459
- and rediscovers before re-enable. Once the owning Runtime observes and applies
3460
- the revision, disable blocks all new starts, including an explicit
3461
- `external:<name>` target, but does not cancel or break an already admitted task.
3462
- The CLI mutation returning is not cross-process acknowledgement. A failed
3463
- activation remains retryable through the owning
3464
- `ConfiguredA2ARuntimeHandle.reload()` even when the disk revision is unchanged;
3465
- the passive `kodax integrations reload` command validates only its own process.
3466
-
3467
- `kodax a2a test` performs Card discovery and security planning only. It never
3468
- requests an OAuth access token; token acquisition starts at `a2a call` or the
3469
- first Runtime dispatch.
3470
-
3471
- Inbound publication is also no-code:
3472
-
3473
- ```bash
3474
- export KODAX_A2A_TOKEN='replace-with-a-long-random-token'
3475
- # PowerShell: $env:KODAX_A2A_TOKEN='replace-with-a-long-random-token'
3476
- kodax a2a expose # Runtime default Agent
3477
- kodax a2a expose document-agent # ~/.kodax/agents/document-agent.md
3478
- kodax a2a serve --port 8765
3479
- ```
3480
-
3481
- The fixed token above is the compatibility profile. For dynamic production
3482
- tokens, configure KodaX as an OAuth Resource Server and point it at an external
3483
- issuer:
3484
-
3485
- ```bash
3486
- kodax a2a expose document-agent --auth oauth2-jwt \
3487
- --oauth-scheme enterprise-oauth \
3488
- --oauth-issuer https://identity.example/ \
3489
- --oauth-audience https://kodax.example/a2a \
3490
- --oauth-jwks-url https://identity.example/.well-known/jwks.json \
3491
- --oauth-token-url https://identity.example/oauth/token \
3492
- --oauth-metadata-url https://identity.example/.well-known/oauth-authorization-server \
3493
- --required-scope a2a.invoke
3494
- kodax a2a serve --port 8765
3495
- ```
3496
-
3497
- The Authorization Server authenticates clients, provisions client IDs/secrets,
3498
- issues/rotates/revokes tokens and, for JWT access tokens, signs them and
3499
- publishes metadata/JWKS. The calling A2A
3500
- client obtains a token out of band or with Client Credentials and sends it in
3501
- the Bearer header. KodaX validates JWT type, asymmetric signature, issuer,
3502
- audience, lifetime, subject, and required scopes before task lookup, then maps
3503
- `sub` to the A2A principal. Missing/invalid credentials return `401`; a valid
3504
- token without the required scope returns `403 insufficient_scope`. KodaX does
3505
- not hold the issuer signing key or expose token, refresh, client-registration,
3506
- login, or consent endpoints. Opaque-token introspection and mTLS deployments
3507
- must use a host authentication adapter or reverse proxy. Offline JWT/JWKS
3508
- validation also cannot observe immediate per-token revocation: use short access
3509
- token lifetimes, signing-key rotation, or an introspecting proxy/adapter when
3510
- that property is required.
3511
-
3512
- #### Upgrade retained pre-realm tasks
3513
-
3514
- Realm-aware task ownership intentionally has no normal-request legacy fallback:
3515
- an authority switch must never adopt tasks merely because it reuses a subject.
3516
- If a v0.7.70 task store must remain addressable after upgrading, stop the A2A
3517
- server and first inspect an exact-owner migration plan:
3518
-
3519
- ```bash
3520
- kodax a2a migrate-tasks
3521
- kodax a2a migrate-tasks --apply --confirm-server-stopped
3522
-
3523
- # OAuth identity is token-specific, so provide the known historical subject.
3524
- kodax a2a migrate-tasks --subject trusted-orchestrator
3525
- ```
3526
-
3527
- The configured Bearer profile supplies its fixed `principalId`; OAuth requires
3528
- `--subject`. Dry-run does not rewrite `tasks.json`. Apply rekeys only exact
3529
- matches, preserves unmatched records, and refuses a live task-store owner.
3530
- Custom SDK hosts can plan multiple known owners without exposing raw tokens:
3531
-
3532
- ```ts
3533
- import { migrateA2ALegacyTaskOwners } from '@kodax-ai/kodax/a2a';
3534
-
3535
- const mappings = [{
3536
- securityRealm: 'oauth2-jwt:https://identity.example/',
3537
- subject: 'trusted-orchestrator',
3538
- }] as const;
3539
- const plan = migrateA2ALegacyTaskOwners({
3540
- dataDir: '/var/lib/kodax/a2a', mappings, apply: false,
3541
- });
3542
-
3543
- // After the host/operator verifies the plan:
3544
- if (plan.matchedLegacyTaskCount > 0) {
3545
- migrateA2ALegacyTaskOwners({
3546
- dataDir: '/var/lib/kodax/a2a', mappings, apply: true,
3547
- });
3548
- }
3549
- ```
3550
-
3551
- The SDK also accepts `tenant` when a custom authentication adapter historically
3552
- returned one. Two mappings that claim the same legacy owner for different
3553
- realms are ambiguous and rejected; split or guessed ownership is never applied.
3554
-
3555
- `a2a serve` resolves its Runtime provider in this order: explicit CLI option,
3556
- environment, core configuration, then the built-in default. Provider-compatible
3557
- model selection follows the normal hosted Runtime rule. A selected Markdown
3558
- Agent may declare its own validated `provider`; remote A2A input cannot choose
3559
- or override provider, model, reasoning, profile, workspace, or tools.
3560
-
3561
- `expose` validates a named user Markdown Agent before writing its reference.
3562
- `serve` loads configured MCP and Extensions before it resolves the execution
3563
- binding or opens a socket. Native workspace read tools are admitted by
3564
- workspace access; writes, narrow Extension Tools, MCP capabilities, subagents,
3565
- and isolated Skill scripts require their corresponding exact `toolPolicy`
3566
- authority. Internal Skills come from `~/.kodax/skills`, `~/.agents/skills`,
3567
- plugins, and built-ins; public Agent Card skills are a separate explicit
3568
- projection and never reveal the private Skill inventory.
3569
-
3570
- The running server pins Agent, Skill, workspace, tool registration, process and
3571
- store revisions. Card/auth/limits can hot reload; execution-authority changes
3572
- require an explicit restart. Managed contexts live below
3573
- `~/kodax_a2a_server_workspace/<runtime-profile>/contexts/<context-key>/`. Exact Skill scripts require
3574
- `process: isolated`, an admitted `scripts/...` path, and a passing
3575
- `kodax sandbox doctor`; KodaX never falls back to an unsandboxed shell.
3576
-
3577
- Every concrete file reached by `read`, `grep`, or `glob` is checked against the
3578
- bound workspace. Child runs inherit ceilings for native reads, tools, Skills,
3579
- and Skill scripts; they cannot expand the parent's admitted authority.
3580
-
3581
- ### Publish one KodaX Agent
3582
-
3583
- Publication is host-owned and opt-in. The public card describes only the
3584
- configured Agent, media types, and skills. Authentication runs before task
3585
- lookup; authorization runs per operation; task visibility is principal-scoped.
3586
-
3587
- ```ts
3588
- import {
3589
- createBearerEnvA2AAuthentication,
3590
- createKodaXA2AServer,
3591
- } from '@kodax-ai/kodax/a2a';
3592
- import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
3593
-
3594
- const runtime = await createKodaXRuntime({ mode: 'embedded', isolation: 'inline' });
3595
- const server = createKodaXA2AServer({
3596
- runtime,
3597
- dataDir: '/var/lib/kodax/a2a',
3598
- agent: {
3599
- name: 'KodaX Reviewer',
3600
- description: 'Reviews bounded code changes.',
3601
- version: '1.0.0',
3602
- publicBaseUrl: 'https://kodax.example',
3603
- skills: [{ id: 'review', name: 'Review', description: 'Review code.', tags: ['code'] }],
3604
- inputModes: ['text/plain'],
3605
- outputModes: ['text/plain'],
3606
- },
3607
- authentication: createBearerEnvA2AAuthentication({
3608
- type: 'bearer-env',
3609
- tokenEnv: 'KODAX_A2A_TOKEN',
3610
- principalId: 'trusted-orchestrator',
3611
- }),
3612
- async authorize({ principal }) { return principal.scopes.includes('a2a:invoke'); },
3613
- limits: {
3614
- maxRequestBytes: 1_048_576,
3615
- maxPartBytes: 524_288,
3616
- maxConcurrentTasks: 8,
3617
- maxTaskWaitMs: 30_000,
3618
- maxActiveTasksPerPrincipal: 8,
3619
- maxRetainedTasksPerPrincipal: 64,
3620
- maxEventsPerTask: 1_000,
3621
- maxEventBytesPerTask: 16_777_216,
3622
- maxWorkspaceBytesPerContext: 1_073_741_824,
3623
- },
3624
- });
3625
-
3626
- // Development only: the built-in listener refuses non-loopback hosts.
3627
- const localBaseUrl = await server.listen({ hostname: '127.0.0.1', port: 0 });
3628
- ```
3629
-
3630
- Production hosts route `GET /.well-known/agent-card.json` and canonical
3631
- JSON-RPC `POST /a2a` to `server.handle(request)` behind their own TLS
3632
- terminator. `POST /` remains an accepted compatibility alias. `listen()` waits for durable recovery before
3633
- it resolves. A host that wires `handle()` directly may explicitly await
3634
- `server.whenReady()` before it starts accepting traffic; `handle()` also waits
3635
- for the same recovery promise. The durable edge store supports get/list,
3636
- continuation, cancellation, ordered SSE subscription, and surviving Runtime-run
3637
- reattachment after an edge restart. Push notifications, A2A 0.3, gRPC, and
3638
- HTTP+JSON are not advertised; unsupported push methods return the standard
3639
- `PushNotificationNotSupportedError`.
3640
-
3641
- Non-streaming `SendMessage` waits at most `maxTaskWaitMs` (30 seconds by
3642
- default). When that bound is reached the response contains the current working
3643
- task; it does not cancel the Runtime run, and clients can continue with
3644
- `GetTask` or `SubscribeToTask`.
3645
-
3646
- When a task enters `INPUT_REQUIRED`, the next accepted input answers the pending
3647
- interaction on the original Runtime run; it does not start a replacement run.
3648
- History length and list filters are validated and bounded. Task listing uses a
3649
- stable opaque cursor, while per-principal retention prunes only the oldest
3650
- terminal records. Terminal subscriptions and failed-start resources are closed
3651
- by their owning lifecycle.
3652
-
3653
- Remote messages are ordinary user inputs. They cannot select provider, model,
3654
- profile, tools, working directory, permission mode, or Runtime configuration.
3655
- URL parts are rejected; inline raw/data parts are bounded and materialized under
3656
- the server-owned data directory. Responses expose final approved output only,
3657
- not system prompts, reasoning deltas, tool payloads, credentials, or local paths.
3658
-
3659
- Generated files are published only through the trusted output broker: a normal
3660
- tool or Extension stages a file in the context's `.kodax-a2a-staging` area, or
3661
- a successfully admitted `run_skill_script` promotes one of its declared
3662
- outputs. The server rechecks that the result is a regular non-symlink file in
3663
- the real bound workspace and applies part-size/output-mode limits before
3664
- inlining it. A declaration from a failed Skill run, an ordinary `write`/`edit`
3665
- elsewhere in the workspace, and a local path in model text never become A2A
3666
- artifacts implicitly.
3667
-
3668
- The normative baseline is A2A repository commit
3669
- `2183794bfb9b67af4aee1be0a0ef726050642873`, protocol `1.0`, with
3670
- `specification/a2a.proto` SHA-256
3671
- `e195bf96ab630c69797851970203e1b2b6b19528f2e9803b7d904b91a5104016`.
3672
-
3673
- ---
3674
-
3675
- ## 23. Shared Coder daemon for Space and IDE hosts (FEATURE_269, v0.7.69)
3676
-
3677
- FEATURE_269 makes one local daemon the source of truth for a Coder profile.
3678
- CLI, Space, IDE, and SDK clients can observe and control the same sessions and
3679
- runs. The transport remains local to the current OS user; it is not a remote
3680
- collaboration protocol. Closing a client detaches that client and does not stop
3681
- the daemon or another client's run.
3682
-
3683
- Partner is deliberately outside this migration. Keep Partner on its existing
3684
- inline callbacks and give it a distinct product data root and sessions root.
3685
- Do not point a Partner inline Runtime at the Coder daemon profile or the Coder
3686
- data root.
3687
-
3688
- ### Connect and fail closed on required capabilities
3689
-
3690
- Space should own the daemon SDK client in Electron Main. Persist a random,
3691
- stable `instanceId` and a separate 32+ character `instanceSecret` per Space
3692
- installation. Store the secret in the OS keychain; never accept either value
3693
- from renderer or model output. `connectKodaXRuntime()` is attach-only unless `autoStart: true`.
3694
- An explicit inline rollback policy blocks auto-start until the owner policy is
3695
- explicitly changed back to daemon.
3696
-
3697
- For Electron, `homeDir` is still the CLI-style base directory, not
3698
- `process.env.KODAX_HOME`. Packaged/asar applications may use `autoStart: true`
3699
- directly; the SDK launches only the daemon child in Electron's Node execution
3700
- mode and does not mutate the application's environment or start a second GUI
3701
- instance. `ELECTRON_RUN_AS_NODE` exists only at the child exec boundary and is
3702
- removed before daemon application code loads, so Bash, MCP, LSP, sandboxed
3703
- commands, and ordinary external processes do not inherit Electron Node mode.
3704
-
3705
- Packaged auto-start requires Electron's `RunAsNode` fuse, which Electron enables
3706
- by default. If an embedder deliberately disables that fuse, the packaged
3707
- executable cannot serve as a detached Node host: start the daemon with an
3708
- ordinary Node/CLI process and use attach-only mode instead. A packaged
3709
- `autoStart: true` timeout includes this fuse requirement in its diagnostic; the
3710
- SDK does not relaunch the GUI or silently fall back to an inline Runtime.
3711
-
3712
- ```ts
3713
- import { connectKodaXRuntime } from '@kodax-ai/kodax/runtime';
3714
-
3715
- const runtime = await connectKodaXRuntime({
3716
- profile: 'coder',
3717
- autoStart: true,
3718
- homeDir: coderRuntimeBaseDir, // owns <coderRuntimeBaseDir>/.kodax
3719
- clientInfo: {
3720
- name: 'kodax-space',
3721
- version: '0.1.32',
3722
- instanceId: spaceInstallationId,
3723
- instanceSecret: await spaceKeychain.readRuntimeClientSecret(),
3724
- },
3725
- capabilities: {
3726
- richEvents: true,
3727
- permissionPrompts: true,
3728
- operationDeduplication: true,
3729
- },
3730
- requirements: {
3731
- operationDeduplication: 1,
3732
- sessionObservation: 1,
3733
- afterTurnInput: 1,
3734
- askUserTransport: 1,
3735
- permissionCas: 1,
3736
- providerCredentialBroker: 1,
3737
- runBoundHostTools: 1,
3738
- coderOwnerFencing: 1,
3739
- crashOutcomeModel: 1,
3740
- coderFeatureMatrix: 1,
3741
- sessionAdmission: 1,
3742
- completeObservationSnapshot: 1,
3743
- connectionLifecycle: 1,
3744
- typedRuntimeEvents: 1,
3745
- daemonSafeRunInput: 1,
3746
- sharedSessionSettings: 1,
3747
- durableRecoveryQueries: 1,
3748
- daemonManagement: 1,
3749
- runtimeAutoModeGuardrail: 3,
3750
- },
3751
- });
3752
- ```
3753
-
3754
- Requirements are server facts, not authorization requests. Check
3755
- `runtime.grantedScopes` before enabling controls. Missing capabilities or
3756
- scopes must disable the affected UI; Space must not silently start inline
3757
- Coder. FEATURE_269 does not advertise `interruptInput`, so an interrupt-only
3758
- product must require `{ interruptInput: 1 }` and fail connection. The supported
3759
- fallback is `delivery: 'after_turn'` only when that is the user's intent.
3760
-
3761
- The v0.7.73 SDK requires `runtimeAutoModeGuardrail:3` automatically for
3762
- `autoStart: true`, even when the caller omits it from `requirements`. If the healthy
3763
- profile daemon advertises v1 or v2, the SDK first requires `daemonManagement:1`,
3764
- takes a revision/owner-policy fenced preflight, and replaces it only when no
3765
- active or queued run, Workflow, Agent turn, pending permission/user input, or
3766
- other logical client exists. A busy or still-older daemon is never stopped: the
3767
- connection rejects with `RuntimeDaemonCapabilityUpgradeError`, whose
3768
- `recoverable` and `restartRequired` fields are `true` and whose optional
3769
- `preflight` explains the blockers. Attach-only connections never mutate daemon
3770
- ownership and must request `runtimeAutoModeGuardrail:1` explicitly when they
3771
- depend only on the v1 owner contract, v2 for bounded input, effective-default
3772
- metadata, structured diagnostics, and speculative-window parity, or v3 for
3773
- opaque exact grant suggestions and concrete permission matchers. Capability
3774
- requirements are minimum versions: v3 satisfies v1/v2, v2 satisfies v1, and an
3775
- older daemon never satisfies a newer requirement.
3776
-
3777
- The `coderFeatureMatrix` capability reports daemon availability for managed
3778
- runs, transcript/session operations, Todo projection, managed tasks, Workflow,
3779
- MCP, Reference External Agent, Memory, and Runtime artifacts. Reference
3780
- External Agent is `false` when the daemon owner did not install its executor
3781
- plane.
3782
-
3783
- The packaged daemon authenticates one local OS-user/profile trust domain with
3784
- a random token stored beside daemon state and a user-only local endpoint. It
3785
- does not issue a different daemon token to each application in v0.7.69. The
3786
- returned scope set is chosen by the host (the packaged host grants the public
3787
- local-user set). `clientInfo.instanceId` is stable attribution for origin and
3788
- operation deduplication. `instanceSecret` proves that a new authenticated
3789
- connection is the same stable client when it resumes that client's credential
3790
- or Host Tool leases; only its hash participates in daemon-owned bridge state.
3791
- Keep all three values in Electron Main. Mutually distrusting processes running
3792
- as the same OS account remain outside this release's threat model.
3793
-
3794
- ### Join atomically and resync after disconnect
3795
-
3796
- `sessions.observe()` installs the live subscription before taking the
3797
- snapshot. Its snapshot contains one authoritative `runtimeId`, cursor,
3798
- `transcriptRevision`, complete transcript, versioned settings, run/queue state,
3799
- queued continuation IDs/order/origin/safe previews, pending permission and
3800
- AskUser requests, and live assistant/thinking/tool/Todo/managed-task
3801
- projection. Run requirements include the current credential/Host Tool
3802
- availability. Listener events are strictly after the returned cursor.
3803
-
3804
- ```ts
3805
- let observedRuntimeId: string | undefined;
3806
- let lastCursor = 0;
3807
-
3808
- async function openCoderSession(sessionId: string) {
3809
- const observation = await runtime.sessions.observe(sessionId, (event) => {
3810
- if (event.seq <= lastCursor) return;
3811
- applyRuntimeEvent(event);
3812
- lastCursor = event.seq;
3813
- });
3814
-
3815
- const { snapshot } = observation;
3816
- const runtimeChanged = observedRuntimeId !== undefined
3817
- && observedRuntimeId !== snapshot.runtimeId;
3818
- observedRuntimeId = snapshot.runtimeId;
3819
- lastCursor = snapshot.cursor;
3820
- replaceSessionProjection(snapshot, { runtimeChanged });
3821
- return observation;
3822
- }
3823
- ```
3824
-
3825
- Subscribe to `runtime.connection` to freeze mutation UI immediately rather
3826
- than waiting for a status poll:
3827
-
3828
- ```ts
3829
- runtime.connection?.subscribe((state) => {
3830
- setCoderConnectionState(state.state, state.reason);
3831
- if (state.state === 'disconnected' && state.reconnectable) {
3832
- scheduleReconnect();
3833
- }
3834
- });
3835
- ```
3836
-
3837
- The SDK reports the current `connectionId`, `runtimeEpoch`, optional
3838
- `journalEpoch`, disconnect reason, and whether a new connection may be
3839
- attempted. It does not transparently replay requests or subscriptions. Space
3840
- creates a replacement Runtime client, checks its new epochs, resumes eligible
3841
- leases, and observes the session again.
3842
-
3843
- On transport failure, Runtime change, expired history, or `resync_required`,
3844
- discard the local derived projection and call `sessions.observe()` again. Do
3845
- not merge a new snapshot into the old projection. The handshake buffer is
3846
- bounded; overflow fails explicitly instead of dropping events. A Runtime
3847
- restart changes `runtimeId`, marks persisted non-terminal runs with a durable
3848
- terminal fact, and closes old in-memory AskUser/permission requests through the
3849
- reset boundary.
3850
-
3851
- ### Durable mutations, stable ordering, and settings CAS
3852
-
3853
- Every durable public control mutation uses an operation envelope. Credential
3854
- and Host Tool register/revoke/supply/complete requests are reverse-bridge
3855
- control frames and are deliberately excluded from the control journal so
3856
- secrets/results are not persisted. They still enter the daemon management
3857
- draining fence: once an atomic stop begins, they fail with typed `conflict` and
3858
- cannot change reverse-bridge state. The SDK creates an operation ID for
3859
- ordinary one-shot calls. A
3860
- product-level retry after a lost response must reuse its own stable operation
3861
- ID; changing its method, payload, resource, or authenticated principal is
3862
- rejected.
3863
-
3864
- ```ts
3865
- const session = await runtime.sessions.create({
3866
- sessionId: stableSpaceSessionId,
3867
- title: 'Shared session',
3868
- surface: 'space-desktop',
3869
- operation: { operationId: loadOrCreatePendingOperationId('space-session-draft-7') },
3870
- });
3871
-
3872
- const operationId = loadOrCreatePendingOperationId('space-run-draft-42');
3873
- const handle = await runtime.runs.start({
3874
- sessionId: session.id,
3875
- input: { type: 'text', text: prompt },
3876
- options: { provider: 'anthropic' },
3877
- operation: { operationId },
3878
- });
3879
-
3880
- const current = await runtime.sessions.getSettingsVersioned(session.id);
3881
- const updated = await runtime.sessions.updateSettingsVersioned(
3882
- session.id,
3883
- { model: 'claude-sonnet-4-5' },
3884
- {
3885
- operationId: loadOrCreatePendingOperationId('space-settings-draft-9'),
3886
- expectedRevision: current.revision,
3887
- },
3888
- );
3889
- ```
3890
-
3891
- Create retries with the same explicit session and operation IDs cannot overwrite
3892
- an existing session. Same-session starts and after-turn inputs receive a durable `sessionOrder`.
3893
- Retries with the same operation ID return the canonical result and do not
3894
- create another run. Settings use compare-and-swap; a stale revision returns a
3895
- structured conflict and must be reloaded, never silently overwritten. The
3896
- shared settings keys are `provider`, `model`, `effort`, `thinking`,
3897
- `reasoningMode`, `permissionMode`, `executionCwd`, `agentMode`, and
3898
- `autoModeEngine`, `autoModeClassifierModel`, `autoModeTimeoutMs`, and
3899
- `autoModeSpeculativeWindowMs`.
3900
-
3901
- ```ts
3902
- const queued = await runtime.runs.submitInput({
3903
- sessionId: session.id,
3904
- afterRunId: handle.runId,
3905
- delivery: 'after_turn',
3906
- input: { type: 'text', text: 'Also update the tests.' },
3907
- operation: { operationId: loadOrCreatePendingOperationId('space-input-17') },
3908
- });
3909
-
2869
+ The portable manual gates remain in
2870
+ `docs/test-guides/FEATURE_255_v0.7.66_TEST_GUIDE.md`,
2871
+ `FEATURE_256_v0.7.71_TEST_GUIDE.md`, and
2872
+ `FEATURE_257_v0.7.72_TEST_GUIDE.md` for release-machine verification; the latter
2873
+ two filenames retain their original planning slots while their content records
2874
+ the v0.7.66 delivery. v0.7.67 adds
2875
+ `FEATURE_258_v0.7.67_TEST_GUIDE.md`, `FEATURE_259_v0.7.67_TEST_GUIDE.md`, and
2876
+ `FEATURE_261_v0.7.67_TEST_GUIDE.md`; v0.7.69 adds
2877
+ `FEATURE_267_v0.7.69_TEST_GUIDE.md`, `FEATURE_268_v0.7.69_TEST_GUIDE.md`, and
2878
+ `FEATURE_269_v0.7.69_TEST_GUIDE.md`. The earlier release-preparation Actions
2879
+ run is not reused after the severe-fix review. Fresh GitHub Actions run
2880
+ `29385073422` passes the Node 20/22 build, bundle, DTS, and full-test matrix plus
2881
+ the Node 22 Unix-domain-socket daemon gate. npm publication remains a
2882
+ maintainer-owned manual step.
2883
+
2884
+ ---
2885
+
2886
+ ## 18. External-agent executor plane (FEATURE_258, v0.7.67)
2887
+
2888
+ FEATURE_258 lets an SDK host register remote agents without teaching the coding
2889
+ runtime a specific A2A, MCP, or HTTP client. The public contracts live in
2890
+ `@kodax-ai/kodax/agent`; the host-facing catalog and task services live on
2891
+ `@kodax-ai/kodax/runtime`.
2892
+
2893
+ ### Ownership rule
2894
+
2895
+ An `AgentExecutorFactory` contains functions, so it cannot cross a Worker or
2896
+ daemon DTO boundary. Install factories where the Runtime owner executes:
2897
+
2898
+ | Desired owner | Supported construction |
2899
+ |---|---|
2900
+ | Private in-process owner | `createKodaXRuntime({ mode: 'embedded', isolation: 'inline', externalAgents })` |
2901
+ | New locally hosted daemon owner | `createKodaXRuntime({ mode: 'daemon', profile: '<unique>', externalAgents })` |
2902
+ | Existing daemon | Configure its owner, then attach with `connectKodaXRuntime({ requirements: { externalAgents: true } })`; a client cannot inject factories. |
2903
+ | Runtime Worker | The Worker owner must install factories itself; passing `externalAgents` from the parent is rejected. |
2904
+
2905
+ When `mode: 'daemon'` and `externalAgents` are supplied, the caller must win a
2906
+ new in-process daemon lease. KodaX rejects an already-running profile instead of
2907
+ silently replacing its executor configuration. Closing that owner facade shuts
2908
+ down the host it created; closing an ordinary attached client only detaches.
2909
+
2910
+ ### Minimal owner and task flow
2911
+
2912
+ The reference executor below is a contract/conformance adapter. Replace it with
2913
+ your own `AgentExecutorFactory` for a real remote protocol.
2914
+
2915
+ ```ts
2916
+ import {
2917
+ createReferenceAgentExecutorFactory,
2918
+ type ExternalAgentRegistration,
2919
+ } from '@kodax-ai/kodax/agent';
2920
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
2921
+
2922
+ const runtime = await createKodaXRuntime({
2923
+ mode: 'embedded',
2924
+ isolation: 'inline',
2925
+ externalAgents: {
2926
+ factories: [createReferenceAgentExecutorFactory({
2927
+ executorId: 'example-http',
2928
+ protocol: 'http',
2929
+ })],
2930
+ policy: ({ registration, query }) => ({
2931
+ allowed: registration.enabled && query.readOnly === true,
2932
+ reasons: query.readOnly === true ? [] : ['This host allows read-only dispatch only.'],
2933
+ }),
2934
+ defaultContext: { actorId: 'desktop-host' },
2935
+ },
2936
+ });
2937
+
2938
+ const registration: ExternalAgentRegistration = {
2939
+ agentId: 'external:reviewer',
2940
+ displayName: 'Remote Reviewer',
2941
+ enabled: true,
2942
+ executorId: 'example-http',
2943
+ protocol: 'http',
2944
+ configurationRevision: 'reviewer-config-v1',
2945
+ endpointIdentityHash: 'sha256:replace-with-stable-endpoint-identity',
2946
+ skills: ['code-review'],
2947
+ inputModalities: ['text'],
2948
+ outputModalities: ['text'],
2949
+ capabilities: {
2950
+ streaming: 'supported',
2951
+ durableTasks: 'supported',
2952
+ inputRequired: 'conditional',
2953
+ cancellation: 'supported',
2954
+ artifacts: 'unsupported',
2955
+ },
2956
+ effects: { remote: 'read', workspace: 'proposal' },
2957
+ maxConcurrency: 1,
2958
+ };
2959
+
2960
+ try {
2961
+ await runtime.admin.agentRegistrations.upsert(registration);
2962
+
2963
+ const query = {
2964
+ actorId: 'desktop-host',
2965
+ requiredSkills: ['code-review'],
2966
+ readOnly: true,
2967
+ } as const;
2968
+ const available = await runtime.agents.listDispatchable(query);
2969
+ const preflight = await runtime.agents.preflight({
2970
+ agentId: 'external:reviewer',
2971
+ query,
2972
+ expectedConfigurationRevision: registration.configurationRevision,
2973
+ });
2974
+ if (!preflight.ok) throw new Error(preflight.reasons.join('; '));
2975
+
2976
+ const started = await runtime.agentTasks.start({
2977
+ agentId: 'external:reviewer',
2978
+ objective: 'Review the supplied immutable patch and return cited findings.',
2979
+ context: { actorId: 'desktop-host', runId: 'host-run-42' },
2980
+ readOnly: true,
2981
+ requiredSkills: ['code-review'],
2982
+ expectedConfigurationRevision: registration.configurationRevision,
2983
+ });
2984
+ const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
2985
+ console.log(available, terminal.state, terminal.output, terminal.usage);
2986
+ } finally {
2987
+ await runtime.close();
2988
+ }
2989
+ ```
2990
+
2991
+ `runtime.agents.enabled` is the cheap external-plane feature check. If it is
2992
+ false, catalog queries can still return built-in local agents but no external
2993
+ agents; registration and task lists are empty, while point reads and mutations
2994
+ fail clearly. Set
2995
+ `requirements.externalAgents: true` when absence must abort connection.
2996
+
2997
+ ### Service reference
2998
+
2999
+ | Surface | Methods | Contract |
3000
+ |---|---|---|
3001
+ | `runtime.admin.agentRegistrations` | `list`, `upsert`, `setEnabled`, `remove` | Durable owner configuration. `setEnabled` preserves the complete captured executor registration while changing admission. Mutations accept both `expectedConfigurationRevision` and `expectedManagementOwner`; `setEnabled` can also atomically `claimOwner` on an unowned registration and rejects another owner. List results expose `managementOwner` and `credentialConfigured`, never a credential value. The same contract is carried across the daemon transport. With no plane, `list()` is empty and mutations fail clearly. |
3002
+ | `runtime.agents` | `enabled`, `listDispatchable`, `describe`, `preflight` | Applies health, capability, effect, concurrency, credential-presence, configuration-revision, and host-policy checks before dispatch. |
3003
+ | `runtime.agentTasks` | `start`, `list`, `get`, `events`, `wait`, `sendInput`, `cancel`, `reconcile` | Durable snapshots and append-only events for external tasks. The task keeps the immutable registration/executor binding captured at start. |
3004
+
3005
+ `agentTasks.events(taskId, cursor)` uses the last seen numeric event `seq` as its
3006
+ cursor and returns events with a greater sequence. `wait()` resolves only at a
3007
+ terminal task state and rejects on a positive `timeoutMs` expiry. `sendInput()`
3008
+ is valid only while the task reports `input-required` or `auth-required`.
3009
+ `reconcile()` asks the bound executor for authoritative remote state after an
3010
+ owner restart or uncertain failure.
3011
+
3012
+ For external tasks, the built-in stores persist an internal full registration
3013
+ snapshot before the public task ledger. It is keyed by Agent ID and revision,
3014
+ is never returned by task or daemon APIs, and lets an admitted task keep using
3015
+ its original executor route after registration update/removal and Runtime
3016
+ restart. The internal form fixes `enabled: true` and omits management ownership
3017
+ and health diagnostics. The task's public route summary is validated against that internal
3018
+ snapshot before recovery. Terminal task state is durable before the last
3019
+ unreferenced snapshot is removed; startup cleans crash-window orphans.
3020
+
3021
+ Custom `AgentExecutorPlaneStore` implementations should implement
3022
+ `loadTaskRegistrationSnapshots()` and `saveTaskRegistrationSnapshots()` as a
3023
+ pair and give one Runtime exclusive write ownership of that store. Omitting
3024
+ both remains compatible, but restart recovery then succeeds only while the
3025
+ exact current registration still exists. Store only non-secret executor config
3026
+ or secret references in `executorConfig`/`credentialRef`; the broker resolves
3027
+ the current referenced credential just in time, so removing a registration is
3028
+ not equivalent to revoking that credential at its issuer.
3029
+
3030
+ The owner plane has a terminal close contract. Closing it rejects every pending
3031
+ `wait()` (including a wait without `timeoutMs`), disposes its executor instances,
3032
+ and makes subsequent registration, catalog, preflight, and task calls reject
3033
+ with `Agent executor plane is closed.` One overall deadline covers admitted
3034
+ work plus executor disposal: the default is 30 seconds, and direct
3035
+ `createAgentExecutorPlane()` hosts may supply a positive finite
3036
+ `closeTimeoutMs`. A timeout rejects visibly even though already-admitted cleanup
3037
+ may finish in the background. Repeated `close()` calls are safe. SDK hosts
3038
+ should stop accepting work before closing the owner and must not retain a plane
3039
+ service as a reusable handle after Runtime shutdown.
3040
+
3041
+ Restricted Workflow scripts use the same route as direct SDK calls. Both
3042
+ `wf.spawnAgent()` and `wf.runAgent()` validate and forward
3043
+ `target: { agentId, expectedConfigurationRevision? }`; `phase` is forwarded as
3044
+ well. A blank ID or revision is rejected at the script boundary instead of
3045
+ silently falling back to the native child backend.
3046
+
3047
+ ### Credential, artifact, and failure boundaries
3048
+
3049
+ - Put only a `credentialRef` in a registration. Resolve secret material through
3050
+ `AgentCredentialBroker.withCredential()`; do not place tokens in
3051
+ `executorConfig`, events, diagnostics, or task output.
3052
+ - Remote artifacts are denied by default. Supply `artifactPolicy` to authorize
3053
+ each artifact before it materializes in the host boundary.
3054
+ - External agents may declare workspace effect `none` or `proposal`; direct
3055
+ workspace mutation is intentionally not a valid external registration.
3056
+ - Use `expectedConfigurationRevision` for dispatch. For registration mutations,
3057
+ compare both it and `expectedManagementOwner` so a same-revision ownership
3058
+ change cannot be overwritten from an earlier catalog read. Config managers
3059
+ should set a stable `managementOwner`; they may atomically claim an unowned
3060
+ legacy registration while disabling it, but must not mutate a registration
3061
+ owned by another manager.
3062
+ - Treat `configurationRevision` as the stable identity of immutable execution
3063
+ content, not as a small counter. The same content may deterministically reuse
3064
+ the same revision across remove/re-add or Runtime restart, but different
3065
+ endpoint, protocol, executor/auth config, capabilities, effects, Skills,
3066
+ modalities, or resource limits must never reuse it. Built-in A2A
3067
+ configuration derives it from content.
3068
+ - A remote start followed by uncertain local persistence is recorded as
3069
+ `unknown` with its executor reference preserved; reconcile it rather than
3070
+ blindly starting a duplicate. Stable idempotency keys protect retries.
3071
+ - The durable plane records provider-reported usage when available. It never
3072
+ invents missing token or cost fields.
3073
+
3074
+ For a production adapter, implement `preflight?`, `start`, `events`, `get`,
3075
+ `sendInput`, `cancel`, `reconcile`, and `dispose` on `AgentExecutor`. The factory
3076
+ receives `withCredential()` and `authorizeArtifact()` callbacks so protocol code
3077
+ cannot bypass the host's secret/artifact policies.
3078
+
3079
+ ---
3080
+
3081
+ ## 19. Session surface filtering and cursor pagination (FEATURE_261, v0.7.67)
3082
+
3083
+ The narrow session SDK and Runtime facade now share the same listing semantics:
3084
+ `surface` is an exact filter applied before `limit`, and `cursor` is an opaque
3085
+ continuation token carried by each returned summary.
3086
+
3087
+ ### Narrow `/session` API
3088
+
3089
+ ```ts
3090
+ import { listSessions, type SessionSummary } from '@kodax-ai/kodax/session';
3091
+
3092
+ const all: SessionSummary[] = [];
3093
+ let cursor: string | undefined;
3094
+ do {
3095
+ const page = await listSessions({
3096
+ scope: 'user',
3097
+ surface: 'partner',
3098
+ limit: 50,
3099
+ ...(cursor ? { cursor } : {}),
3100
+ });
3101
+ all.push(...page);
3102
+ cursor = page.length === 50 ? page.at(-1)?.cursor : undefined;
3103
+ } while (cursor);
3104
+ ```
3105
+
3106
+ ### Embedded/daemon Runtime API
3107
+
3108
+ ```ts
3109
+ const first = await runtime.sessions.list({ surface: 'acp', limit: 25 });
3110
+ const nextCursor = first.at(-1)?.cursor;
3111
+ const second = nextCursor
3112
+ ? await runtime.sessions.list({ surface: 'acp', limit: 25, cursor: nextCursor })
3113
+ : [];
3114
+ ```
3115
+
3116
+ The filter also composes with `projectRoot`, `scope`, `includeArchived`, `before`,
3117
+ and `tag`. Treat cursors as opaque: do not parse them, compare them to session
3118
+ IDs, or manufacture them. An invalid cursor produces an empty page on the narrow
3119
+ session API. A page shorter than the requested limit is terminal; a full page
3120
+ may continue with its last item's cursor.
3121
+
3122
+ The interactive `kodax -r` picker is a consumer of these session semantics, not
3123
+ a separate SDK API. Headless hosts should build their own UI on `listSessions()`
3124
+ or `runtime.sessions.list()` and resume by the selected full session ID.
3125
+
3126
+ ---
3127
+
3128
+ ## 20. Cost-disciplined workflow routing and telemetry (FEATURE_259, v0.7.67)
3129
+
3130
+ FEATURE_259 adds a public cost/quality contract without making the authoring LLM
3131
+ choose provider-specific model names. The SDK host maps semantic tiers to routes;
3132
+ workflow/child briefs request intent.
3133
+
3134
+ ### Configure tiers and bounded concurrency
3135
+
3136
+ ```ts
3137
+ import { join } from 'node:path';
3138
+ import { runKodaX } from '@kodax-ai/kodax/coding';
3139
+
3140
+ const workflowRunsBaseDir = join(process.cwd(), '.kodax-host', 'workflow-runs');
3141
+ const result = await runKodaX({
3142
+ provider: 'zai-coding',
3143
+ model: 'glm-5.2',
3144
+ agentMode: 'amaw',
3145
+ workflowRunsBaseDir,
3146
+ modelTiers: {
3147
+ fast: { provider: 'deepseek', model: 'deepseek-v4-flash' },
3148
+ deep: { provider: 'zai-coding', model: 'glm-5.2' },
3149
+ },
3150
+ workflow: { maxConcurrency: 3 },
3151
+ events: {
3152
+ onWorkflowAgentDigest: ({ runId, event }) => {
3153
+ if (event.type === 'agent_completed' || event.type === 'agent_unverified'
3154
+ || event.type === 'agent_failed') {
3155
+ console.log(runId, event.data);
3156
+ }
3157
+ },
3158
+ },
3159
+ }, 'Review this change using scoped evidence packets.');
3160
+
3161
+ console.log(result.lastText);
3162
+ ```
3163
+
3164
+ Tier rules are deliberately small:
3165
+
3166
+ - `fast`: mechanical read-only lookup. A write child is ineligible and safely
3167
+ inherits the parent route.
3168
+ - `balanced`: ordinary implementation/investigation/review; uses the parent
3169
+ route, so there is no separate `balanced` mapping.
3170
+ - `deep`: architecture, adversarial verification, severity calibration, and
3171
+ final synthesis.
3172
+ - An unconfigured or selector-shadowed tier inherits the appropriate explicit,
3173
+ specialist, or parent route and records why; it does not silently claim the
3174
+ requested route was applied.
3175
+
3176
+ The workflow authoring contract also supports `scopeSummary`, `constraints`,
3177
+ `evidenceRefs`, `verification`, `readOnly`, `outputSchema`, and `terseResult`.
3178
+ Use those fields to transfer a compact immutable packet instead of asking every
3179
+ child to rediscover the same repository context.
3180
+
3181
+ ### Live route facts
3182
+
3183
+ Terminal `WorkflowEvent.data` may include `requestedTier`, `tierOutcome`,
3184
+ `providerSource`, `modelSource`, initial/final provider and model,
3185
+ `fallbackReason`, `iterations`, `durationMs`, `usage`, and `digestUsage`.
3186
+ Treat absent fields as unknown. In particular, KodaX does not fabricate usage
3187
+ for external executors that did not report it.
3188
+
3189
+ Direct child-dispatch consumers receive the typed `KodaXChildAgentResult.routeFacts`
3190
+ surface, including resolved effort and input/cache-read/output/digest token
3191
+ breakdown when known. Inline workflow consumers can subscribe to the raw
3192
+ `onWorkflowAgentDigest` event as above; GUI progress remains available through
3193
+ `onWorkflowProcessEvent` / `runtime.workflows`.
3194
+
3195
+ ### Durable efficiency report
3196
+
3197
+ When `workflowRunsBaseDir` is supplied, every terminal workflow writes:
3198
+
3199
+ ```text
3200
+ <workflowRunsBaseDir>/<runId>/run.json
3201
+ <workflowRunsBaseDir>/<runId>/events.jsonl
3202
+ <workflowRunsBaseDir>/<runId>/artifacts/*.json
3203
+ ```
3204
+
3205
+ `run.json.efficiencyReport` includes:
3206
+
3207
+ - total/input/cache-read/output/digest tokens and wall-clock duration;
3208
+ - total starts, child turns, starts by `role/tier`, and primary-review starts;
3209
+ - duplicate primary packet reads plus verification/synthesis packet reads;
3210
+ - review/fix/re-review waves and structured review quality-gate outcomes;
3211
+ - `tokenCoverage.ok` plus missing local task IDs;
3212
+ - `excludedExternalTaskIds` for external tasks whose executor reported no usage.
3213
+
3214
+ Do not interpret `totalModelTokens: 0` as free execution unless
3215
+ `tokenCoverage.ok` is true and the relevant external task IDs are not excluded.
3216
+ The report is an audit/optimization artifact; correctness still comes from the
3217
+ workflow's structured findings, verification results, and quality gates.
3218
+
3219
+ ---
3220
+
3221
+ ## 21. Experimental governed memory — `/experimental-memory` (FEATURE_260, v0.7.68)
3222
+
3223
+ KodaX has one durable memory plane: the F228 Memory Control Plane. FEATURE_260
3224
+ adds a thin, opt-in agent/session API over that plane; it does not add a second
3225
+ database, filesystem memory actions, a resident memory specialist, or online
3226
+ self-modification.
3227
+
3228
+ Top-level `runKodaX()` coding runs wire this lifecycle automatically. They build
3229
+ an exact-scoped memory pack at session start, keep passive recall off the
3230
+ blocking hot path, expose `memory_recall` only when the memory session starts,
3231
+ record bounded observations/outcomes, and close the session at the run boundary.
3232
+ Use the direct SDK below when a custom host needs to own those boundaries.
3233
+
3234
+ ### Minimal direct session
3235
+
3236
+ ```ts
3237
+ import { createMemoryControlPlane } from '@kodax-ai/kodax/agent';
3238
+ import { createMemoryAgent } from '@kodax-ai/kodax/experimental-memory';
3239
+
3240
+ const identity = {
3241
+ tenantId: 'tenant:acme',
3242
+ workspaceId: 'workspace:desktop',
3243
+ userId: 'user:42',
3244
+ agentId: 'agent:reviewer',
3245
+ projectId: 'project:kodax',
3246
+ sessionId: 'session:20260712',
3247
+ };
3248
+
3249
+ const controlPlane = createMemoryControlPlane({
3250
+ cwd: process.cwd(),
3251
+ identity,
3252
+ projectDocs: [],
3253
+ discoverSkills: false,
3254
+ });
3255
+ const memory = createMemoryAgent({ controlPlane });
3256
+ const session = await memory.startSession({
3257
+ identity,
3258
+ objective: 'Review the runtime shutdown change',
3259
+ });
3260
+
3261
+ const immediate = session.recall({
3262
+ decisionRevision: 'decision:1',
3263
+ objective: 'Review the runtime shutdown change',
3264
+ decisionContext: 'Choosing the first verification step',
3265
+ decisionIntent: 'runtime shutdown regression',
3266
+ throughSequence: 0,
3267
+ });
3268
+ const deliberate = await session.query({
3269
+ decisionRevision: 'decision:2',
3270
+ need: 'What uncommon daemon cleanup failure happened before?',
3271
+ throughSequence: 0,
3272
+ });
3273
+
3274
+ await session.complete({
3275
+ status: 'succeeded',
3276
+ summary: 'Shutdown state replacement verified',
3277
+ evidence: [],
3278
+ });
3279
+ await session.close();
3280
+ ```
3281
+
3282
+ `recall()` is synchronous: exact observations/hints can be returned immediately,
3283
+ while an optional semantic prefetch finishes for a later matching decision.
3284
+ `query()` is deliberate and read-only; one distinct query is admitted per
3285
+ decision epoch, and the result is bounded to at most three prompt-safe hints and
3286
+ 512 estimated tokens. `undefined` means there is no governed reminder to inject.
3287
+
3288
+ ### Evidence, tracing, and persistence boundaries
3289
+
3290
+ - Recalled content is low-authority evidence. Current repository/config/runtime
3291
+ facts must still come from current tools or host state.
3292
+ - `observe()` accepts monotonic, evidence-linked observations and rejects secret
3293
+ material. `rewind()` removes observations after a sequence boundary.
3294
+ - `complete()` can emit an Outcome Digest through `persistOutcomeDigest` and run
3295
+ bounded episode review through `reviewEpisode`; cancellation creates neither.
3296
+ - `onTrace` receives policy-versioned `MemoryDecisionReceipt` metadata that links
3297
+ candidates, selection, injection, and later outcome influence. Receipts are
3298
+ trace-only and contain no hidden reasoning.
3299
+ - Durable memory mutation remains owned by F228's
3300
+ proposal/preview/fingerprint/apply flow. `/memory` is the CLI governance
3301
+ surface; direct file/shell writes to managed memory roots are denied.
3302
+ - Identity/applicability matching is exact and fail-closed. Hosts should supply
3303
+ stable tenant/workspace/user/agent/project/session identifiers and must not put
3304
+ credentials into those identifiers.
3305
+
3306
+ The subpath is experimental and ESM-only. Treat its exported types as opt-in
3307
+ v0.7.x contracts; keep persistence and product policy behind your own adapter.
3308
+
3309
+ ---
3310
+
3311
+ ## 22. Bidirectional A2A 1.0 — `/a2a` (FEATURE_267, v0.7.69)
3312
+
3313
+ `@kodax-ai/kodax/a2a` is the protocol edge for the A2A 1.0 JSON-RPC/SSE
3314
+ profile. It composes the protocol-neutral F258 executor plane with the Runtime
3315
+ facade; `/agent` and `/coding` remain free of A2A wire types and dependencies.
3316
+
3317
+ ### Call an A2A Agent through F258
3318
+
3319
+ Discovery is an explicit host action. The URL must match `allowedOrigins`, and
3320
+ the default safe transport pins the validated DNS address for each connection
3321
+ and independently revalidates redirects, rejects public plain HTTP, bounds
3322
+ time/body/redirects, and strips authorization on a cross-origin redirect. A
3323
+ custom `fetch` option is a trusted transport override: the embedder then owns
3324
+ equivalent DNS-to-connection binding in that transport or proxy.
3325
+
3326
+ The selected interface must remain on the Card's trusted origin. KodaX parses
3327
+ typed Card-level and Skill-level security declarations: requirement objects are
3328
+ alternatives (OR), every scheme inside one object is conjunctive (AND), and an
3329
+ empty requirement is anonymous. A configured credential is used only when one
3330
+ complete requirement is satisfiable; protected Skills that the configured
3331
+ profile cannot satisfy are not advertised to the Runtime catalog.
3332
+
3333
+ The built-in profiles are HTTP Bearer and OAuth 2.0 Client Credentials. The
3334
+ OAuth profile pins the Card scheme, issuer, exact token endpoint, client ID,
3335
+ secret reference, scopes, optional RFC 8707 resource, and client authentication
3336
+ method. The external Authorization Server—not the Agent and not KodaX—issues
3337
+ the access token. KodaX resolves the client secret only for refresh, keeps an
3338
+ expiring token in process memory, coalesces refreshes, and retries one RPC once
3339
+ with a fresh token after `401`. Card, Agent RPC, and token endpoints remain
3340
+ separate safe-fetch trust boundaries, so a remote Agent cannot redirect a task
3341
+ payload to the token origin. API key, Basic, interactive OAuth, OIDC, mTLS, and
3342
+ multi-scheme AND requirements fail explicitly in the built-in client.
3343
+
3344
+ ```ts
3345
+ import {
3346
+ createA2AAgentExecutorFactory,
3347
+ discoverA2ARegistration,
3348
+ } from '@kodax-ai/kodax/a2a';
3349
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
3350
+
3351
+ const client = {
3352
+ networkPolicy: {
3353
+ // Card/RPC and OAuth token endpoints are separate trust boundaries.
3354
+ allowedOrigins: ['https://reviewer.example', 'https://identity.example'],
3355
+ allowPrivateAddresses: false,
3356
+ requestTimeoutMs: 10_000,
3357
+ maxResponseBytes: 1_048_576,
3358
+ maxRedirects: 2,
3359
+ },
3360
+ pollIntervalMs: 500,
3361
+ } as const;
3362
+
3363
+ const discovered = await discoverA2ARegistration({
3364
+ agentId: 'external:a2a-reviewer',
3365
+ agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
3366
+ credentialRef: 'a2a/reviewer',
3367
+ effects: { remote: 'read' },
3368
+ }, client);
3369
+
3370
+ const runtime = await createKodaXRuntime({
3371
+ mode: 'embedded',
3372
+ isolation: 'inline',
3373
+ externalAgents: {
3374
+ factories: [createA2AAgentExecutorFactory(client)],
3375
+ credentialBroker: {
3376
+ async withCredential(ref, use) {
3377
+ const value = ref === 'a2a/reviewer'
3378
+ ? process.env.A2A_REVIEWER_TOKEN
3379
+ : ref === 'a2a/reviewer-client-secret'
3380
+ ? process.env.A2A_REVIEWER_CLIENT_SECRET
3381
+ : undefined;
3382
+ if (!value) throw new Error(`Missing credential for reference: ${ref}.`);
3383
+ return use(value);
3384
+ },
3385
+ },
3386
+ policy: ({ registration }) => ({ allowed: registration.effects.remote === 'read' }),
3387
+ defaultContext: { actorId: 'a2a-host' },
3388
+ },
3389
+ });
3390
+
3391
+ await runtime.admin.agentRegistrations.upsert(discovered.registration);
3392
+ const started = await runtime.agentTasks.start({
3393
+ agentId: discovered.registration.agentId,
3394
+ objective: 'Review this change and return cited findings.',
3395
+ context: { actorId: 'a2a-host' },
3396
+ readOnly: true,
3397
+ expectedConfigurationRevision: discovered.registration.configurationRevision,
3398
+ });
3399
+ const terminal = await runtime.agentTasks.wait(started.taskId, 60_000);
3400
+ ```
3401
+
3402
+ For OAuth, replace the legacy `credentialRef` input with the structured form;
3403
+ the same F258 `credentialBroker` must resolve `clientSecretRef`. The shared
3404
+ network policy must admit both origins, while each Card, RPC, and token request
3405
+ is still narrowed to its own exact origin:
3406
+
3407
+ ```ts
3408
+ const discovered = await discoverA2ARegistration({
3409
+ agentId: 'external:a2a-reviewer',
3410
+ agentCardUrl: 'https://reviewer.example/.well-known/agent-card.json',
3411
+ authentication: {
3412
+ type: 'oauth2-client-credentials',
3413
+ scheme: 'enterprise-oauth',
3414
+ issuer: 'https://identity.example/',
3415
+ tokenUrl: 'https://identity.example/oauth/token',
3416
+ clientId: 'kodax-reviewer',
3417
+ clientSecretRef: 'a2a/reviewer-client-secret',
3418
+ scopes: ['a2a.invoke'],
3419
+ resource: 'https://reviewer.example/',
3420
+ clientAuthentication: 'client-secret-basic',
3421
+ },
3422
+ effects: { remote: 'read' },
3423
+ }, client);
3424
+ ```
3425
+
3426
+ The executor supports durable task start/get, input continuation, cancel,
3427
+ reconcile, SSE events, and polling fallback. An ambiguous start is not retried
3428
+ automatically. A `credentialRef` is resolved just in time by the F258 broker;
3429
+ the registration, task store, and diagnostics never contain the credential.
3430
+ Authenticated SSE uses that same broker. JSON-RPC ID/version and task/context
3431
+ correlation are validated before an event is accepted; if a stream ends
3432
+ normally before a terminal snapshot, the executor resumes bounded polling.
3433
+ Streamed `artifactUpdate` chunks are accumulated by artifact ID according to
3434
+ `append`, and direct Message file Parts are preserved as authorized artifact
3435
+ references.
3436
+
3437
+ ### Built-in configured path (no host code)
3438
+
3439
+ The CLI product path stores one user document at
3440
+ `~/.kodax/integrations/a2a.json` and uses the same F258 plane:
3441
+
3442
+ ```bash
3443
+ kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
3444
+ --credential-env A2A_REVIEWER_TOKEN --effect read
3445
+ kodax a2a test reviewer
3446
+ kodax a2a call reviewer "Review this document"
3447
+ ```
3448
+
3449
+ The no-code OAuth path stores only the environment-variable name for the client
3450
+ secret. It can be staged disabled and hot-activated later:
3451
+
3452
+ ```bash
3453
+ export A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
3454
+ # PowerShell: $env:A2A_REVIEWER_CLIENT_SECRET='provisioned-out-of-band'
3455
+ # PowerShell: use one line or replace each trailing \ with a backtick.
3456
+ kodax a2a add reviewer https://reviewer.example/.well-known/agent-card.json \
3457
+ --disabled --effect read --oauth-scheme enterprise-oauth \
3458
+ --oauth-issuer https://identity.example/ \
3459
+ --oauth-token-url https://identity.example/oauth/token \
3460
+ --oauth-client-id kodax-reviewer \
3461
+ --oauth-client-secret-env A2A_REVIEWER_CLIENT_SECRET \
3462
+ --oauth-scope a2a.invoke --oauth-resource https://reviewer.example/
3463
+ kodax a2a enable reviewer
3464
+ kodax a2a disable reviewer
3465
+ ```
3466
+
3467
+ Embedded CLI Runtimes and the user-owned daemon automatically reconcile these
3468
+ entries as `external:<name>`. Discovery/update failure retains that entry's
3469
+ last-known-good registration; another entry can still update. The environment
3470
+ broker resolves `credentialEnv` only at call time. Automatic Runtime
3471
+ registration accepts public HTTPS and exact loopback targets; explicit private
3472
+ network access remains an operator action on the direct CLI/SDK path.
3473
+
3474
+ `enabled` is desired state in `a2a.json`, not a fabricated cross-process live
3475
+ flag. `a2a list` reports configured entries and that desired state. The owning
3476
+ Runtime's `admin.agentRegistrations.list()` is authoritative for applied
3477
+ registrations. Automatic reconciliation handles disables/removals first,
3478
+ skips unchanged peers, performs no Card or token request for disabled entries,
3479
+ and rediscovers before re-enable. Once the owning Runtime observes and applies
3480
+ the revision, disable blocks all new starts, including an explicit
3481
+ `external:<name>` target, but does not cancel or break an already admitted task.
3482
+ The CLI mutation returning is not cross-process acknowledgement. A failed
3483
+ activation remains retryable through the owning
3484
+ `ConfiguredA2ARuntimeHandle.reload()` even when the disk revision is unchanged;
3485
+ the passive `kodax integrations reload` command validates only its own process.
3486
+
3487
+ `kodax a2a test` performs Card discovery and security planning only. It never
3488
+ requests an OAuth access token; token acquisition starts at `a2a call` or the
3489
+ first Runtime dispatch.
3490
+
3491
+ Inbound publication is also no-code:
3492
+
3493
+ ```bash
3494
+ export KODAX_A2A_TOKEN='replace-with-a-long-random-token'
3495
+ # PowerShell: $env:KODAX_A2A_TOKEN='replace-with-a-long-random-token'
3496
+ kodax a2a expose # Runtime default Agent
3497
+ kodax a2a expose document-agent # ~/.kodax/agents/document-agent.md
3498
+ kodax a2a serve --port 8765
3499
+ ```
3500
+
3501
+ The fixed token above is the compatibility profile. For dynamic production
3502
+ tokens, configure KodaX as an OAuth Resource Server and point it at an external
3503
+ issuer:
3504
+
3505
+ ```bash
3506
+ kodax a2a expose document-agent --auth oauth2-jwt \
3507
+ --oauth-scheme enterprise-oauth \
3508
+ --oauth-issuer https://identity.example/ \
3509
+ --oauth-audience https://kodax.example/a2a \
3510
+ --oauth-jwks-url https://identity.example/.well-known/jwks.json \
3511
+ --oauth-token-url https://identity.example/oauth/token \
3512
+ --oauth-metadata-url https://identity.example/.well-known/oauth-authorization-server \
3513
+ --required-scope a2a.invoke
3514
+ kodax a2a serve --port 8765
3515
+ ```
3516
+
3517
+ The Authorization Server authenticates clients, provisions client IDs/secrets,
3518
+ issues/rotates/revokes tokens and, for JWT access tokens, signs them and
3519
+ publishes metadata/JWKS. The calling A2A
3520
+ client obtains a token out of band or with Client Credentials and sends it in
3521
+ the Bearer header. KodaX validates JWT type, asymmetric signature, issuer,
3522
+ audience, lifetime, subject, and required scopes before task lookup, then maps
3523
+ `sub` to the A2A principal. Missing/invalid credentials return `401`; a valid
3524
+ token without the required scope returns `403 insufficient_scope`. KodaX does
3525
+ not hold the issuer signing key or expose token, refresh, client-registration,
3526
+ login, or consent endpoints. Opaque-token introspection and mTLS deployments
3527
+ must use a host authentication adapter or reverse proxy. Offline JWT/JWKS
3528
+ validation also cannot observe immediate per-token revocation: use short access
3529
+ token lifetimes, signing-key rotation, or an introspecting proxy/adapter when
3530
+ that property is required.
3531
+
3532
+ #### Upgrade retained pre-realm tasks
3533
+
3534
+ Realm-aware task ownership intentionally has no normal-request legacy fallback:
3535
+ an authority switch must never adopt tasks merely because it reuses a subject.
3536
+ If a v0.7.70 task store must remain addressable after upgrading, stop the A2A
3537
+ server and first inspect an exact-owner migration plan:
3538
+
3539
+ ```bash
3540
+ kodax a2a migrate-tasks
3541
+ kodax a2a migrate-tasks --apply --confirm-server-stopped
3542
+
3543
+ # OAuth identity is token-specific, so provide the known historical subject.
3544
+ kodax a2a migrate-tasks --subject trusted-orchestrator
3545
+ ```
3546
+
3547
+ The configured Bearer profile supplies its fixed `principalId`; OAuth requires
3548
+ `--subject`. Dry-run does not rewrite `tasks.json`. Apply rekeys only exact
3549
+ matches, preserves unmatched records, and refuses a live task-store owner.
3550
+ Custom SDK hosts can plan multiple known owners without exposing raw tokens:
3551
+
3552
+ ```ts
3553
+ import { migrateA2ALegacyTaskOwners } from '@kodax-ai/kodax/a2a';
3554
+
3555
+ const mappings = [{
3556
+ securityRealm: 'oauth2-jwt:https://identity.example/',
3557
+ subject: 'trusted-orchestrator',
3558
+ }] as const;
3559
+ const plan = migrateA2ALegacyTaskOwners({
3560
+ dataDir: '/var/lib/kodax/a2a', mappings, apply: false,
3561
+ });
3562
+
3563
+ // After the host/operator verifies the plan:
3564
+ if (plan.matchedLegacyTaskCount > 0) {
3565
+ migrateA2ALegacyTaskOwners({
3566
+ dataDir: '/var/lib/kodax/a2a', mappings, apply: true,
3567
+ });
3568
+ }
3569
+ ```
3570
+
3571
+ The SDK also accepts `tenant` when a custom authentication adapter historically
3572
+ returned one. Two mappings that claim the same legacy owner for different
3573
+ realms are ambiguous and rejected; split or guessed ownership is never applied.
3574
+
3575
+ `a2a serve` resolves its Runtime provider in this order: explicit CLI option,
3576
+ environment, core configuration, then the built-in default. Provider-compatible
3577
+ model selection follows the normal hosted Runtime rule. A selected Markdown
3578
+ Agent may declare its own validated `provider`; remote A2A input cannot choose
3579
+ or override provider, model, reasoning, profile, workspace, or tools.
3580
+
3581
+ `expose` validates a named user Markdown Agent before writing its reference.
3582
+ `serve` loads configured MCP and Extensions before it resolves the execution
3583
+ binding or opens a socket. Native workspace read tools are admitted by
3584
+ workspace access; writes, narrow Extension Tools, MCP capabilities, subagents,
3585
+ and isolated Skill scripts require their corresponding exact `toolPolicy`
3586
+ authority. Internal Skills come from `~/.kodax/skills`, `~/.agents/skills`,
3587
+ plugins, and built-ins; public Agent Card skills are a separate explicit
3588
+ projection and never reveal the private Skill inventory.
3589
+
3590
+ The running server pins Agent, Skill, workspace, tool registration, process and
3591
+ store revisions. Card/auth/limits can hot reload; execution-authority changes
3592
+ require an explicit restart. Managed contexts live below
3593
+ `~/kodax_a2a_server_workspace/<runtime-profile>/contexts/<context-key>/`. Exact Skill scripts require
3594
+ `process: isolated`, an admitted `scripts/...` path, and a passing
3595
+ `kodax sandbox doctor`; KodaX never falls back to an unsandboxed shell.
3596
+
3597
+ Every concrete file reached by `read`, `grep`, or `glob` is checked against the
3598
+ bound workspace. Child runs inherit ceilings for native reads, tools, Skills,
3599
+ and Skill scripts; they cannot expand the parent's admitted authority.
3600
+
3601
+ ### Publish one KodaX Agent
3602
+
3603
+ Publication is host-owned and opt-in. The public card describes only the
3604
+ configured Agent, media types, and skills. Authentication runs before task
3605
+ lookup; authorization runs per operation; task visibility is principal-scoped.
3606
+
3607
+ ```ts
3608
+ import {
3609
+ createBearerEnvA2AAuthentication,
3610
+ createKodaXA2AServer,
3611
+ } from '@kodax-ai/kodax/a2a';
3612
+ import { createKodaXRuntime } from '@kodax-ai/kodax/runtime';
3613
+
3614
+ const runtime = await createKodaXRuntime({ mode: 'embedded', isolation: 'inline' });
3615
+ const server = createKodaXA2AServer({
3616
+ runtime,
3617
+ dataDir: '/var/lib/kodax/a2a',
3618
+ agent: {
3619
+ name: 'KodaX Reviewer',
3620
+ description: 'Reviews bounded code changes.',
3621
+ version: '1.0.0',
3622
+ publicBaseUrl: 'https://kodax.example',
3623
+ skills: [{ id: 'review', name: 'Review', description: 'Review code.', tags: ['code'] }],
3624
+ inputModes: ['text/plain'],
3625
+ outputModes: ['text/plain'],
3626
+ },
3627
+ authentication: createBearerEnvA2AAuthentication({
3628
+ type: 'bearer-env',
3629
+ tokenEnv: 'KODAX_A2A_TOKEN',
3630
+ principalId: 'trusted-orchestrator',
3631
+ }),
3632
+ async authorize({ principal }) { return principal.scopes.includes('a2a:invoke'); },
3633
+ limits: {
3634
+ maxRequestBytes: 1_048_576,
3635
+ maxPartBytes: 524_288,
3636
+ maxConcurrentTasks: 8,
3637
+ maxTaskWaitMs: 30_000,
3638
+ maxActiveTasksPerPrincipal: 8,
3639
+ maxRetainedTasksPerPrincipal: 64,
3640
+ maxEventsPerTask: 1_000,
3641
+ maxEventBytesPerTask: 16_777_216,
3642
+ maxWorkspaceBytesPerContext: 1_073_741_824,
3643
+ },
3644
+ });
3645
+
3646
+ // Development only: the built-in listener refuses non-loopback hosts.
3647
+ const localBaseUrl = await server.listen({ hostname: '127.0.0.1', port: 0 });
3648
+ ```
3649
+
3650
+ Production hosts route `GET /.well-known/agent-card.json` and canonical
3651
+ JSON-RPC `POST /a2a` to `server.handle(request)` behind their own TLS
3652
+ terminator. `POST /` remains an accepted compatibility alias. `listen()` waits for durable recovery before
3653
+ it resolves. A host that wires `handle()` directly may explicitly await
3654
+ `server.whenReady()` before it starts accepting traffic; `handle()` also waits
3655
+ for the same recovery promise. The durable edge store supports get/list,
3656
+ continuation, cancellation, ordered SSE subscription, and surviving Runtime-run
3657
+ reattachment after an edge restart. Push notifications, A2A 0.3, gRPC, and
3658
+ HTTP+JSON are not advertised; unsupported push methods return the standard
3659
+ `PushNotificationNotSupportedError`.
3660
+
3661
+ Non-streaming `SendMessage` waits at most `maxTaskWaitMs` (30 seconds by
3662
+ default). When that bound is reached the response contains the current working
3663
+ task; it does not cancel the Runtime run, and clients can continue with
3664
+ `GetTask` or `SubscribeToTask`.
3665
+
3666
+ When a task enters `INPUT_REQUIRED`, the next accepted input answers the pending
3667
+ interaction on the original Runtime run; it does not start a replacement run.
3668
+ History length and list filters are validated and bounded. Task listing uses a
3669
+ stable opaque cursor, while per-principal retention prunes only the oldest
3670
+ terminal records. Terminal subscriptions and failed-start resources are closed
3671
+ by their owning lifecycle.
3672
+
3673
+ Remote messages are ordinary user inputs. They cannot select provider, model,
3674
+ profile, tools, working directory, permission mode, or Runtime configuration.
3675
+ URL parts are rejected; inline raw/data parts are bounded and materialized under
3676
+ the server-owned data directory. Responses expose final approved output only,
3677
+ not system prompts, reasoning deltas, tool payloads, credentials, or local paths.
3678
+
3679
+ Generated files are published only through the trusted output broker: a normal
3680
+ tool or Extension stages a file in the context's `.kodax-a2a-staging` area, or
3681
+ a successfully admitted `run_skill_script` promotes one of its declared
3682
+ outputs. The server rechecks that the result is a regular non-symlink file in
3683
+ the real bound workspace and applies part-size/output-mode limits before
3684
+ inlining it. A declaration from a failed Skill run, an ordinary `write`/`edit`
3685
+ elsewhere in the workspace, and a local path in model text never become A2A
3686
+ artifacts implicitly.
3687
+
3688
+ The normative baseline is A2A repository commit
3689
+ `2183794bfb9b67af4aee1be0a0ef726050642873`, protocol `1.0`, with
3690
+ `specification/a2a.proto` SHA-256
3691
+ `e195bf96ab630c69797851970203e1b2b6b19528f2e9803b7d904b91a5104016`.
3692
+
3693
+ ---
3694
+
3695
+ ## 23. Shared Coder daemon for Space and IDE hosts (FEATURE_269, v0.7.69)
3696
+
3697
+ FEATURE_269 makes one local daemon the source of truth for a Coder profile.
3698
+ CLI, Space, IDE, and SDK clients can observe and control the same sessions and
3699
+ runs. The transport remains local to the current OS user; it is not a remote
3700
+ collaboration protocol. Closing a client detaches that client and does not stop
3701
+ the daemon or another client's run.
3702
+
3703
+ Partner is deliberately outside this migration. Keep Partner on its existing
3704
+ inline callbacks and give it a distinct product data root and sessions root.
3705
+ Do not point a Partner inline Runtime at the Coder daemon profile or the Coder
3706
+ data root.
3707
+
3708
+ ### Connect and fail closed on required capabilities
3709
+
3710
+ Space should own the daemon SDK client in Electron Main. Persist a random,
3711
+ stable `instanceId` and a separate 32+ character `instanceSecret` per Space
3712
+ installation. Store the secret in the OS keychain; never accept either value
3713
+ from renderer or model output. `connectKodaXRuntime()` is attach-only unless `autoStart: true`.
3714
+ An explicit inline rollback policy blocks auto-start until the owner policy is
3715
+ explicitly changed back to daemon.
3716
+
3717
+ For Electron, `homeDir` is still the CLI-style base directory, not
3718
+ `process.env.KODAX_HOME`. Packaged/asar applications may use `autoStart: true`
3719
+ directly; the SDK launches only the daemon child in Electron's Node execution
3720
+ mode and does not mutate the application's environment or start a second GUI
3721
+ instance. `ELECTRON_RUN_AS_NODE` exists only at the child exec boundary and is
3722
+ removed before daemon application code loads, so Bash, MCP, LSP, sandboxed
3723
+ commands, and ordinary external processes do not inherit Electron Node mode.
3724
+
3725
+ Packaged auto-start requires Electron's `RunAsNode` fuse, which Electron enables
3726
+ by default. If an embedder deliberately disables that fuse, the packaged
3727
+ executable cannot serve as a detached Node host: start the daemon with an
3728
+ ordinary Node/CLI process and use attach-only mode instead. A packaged
3729
+ `autoStart: true` timeout includes this fuse requirement in its diagnostic; the
3730
+ SDK does not relaunch the GUI or silently fall back to an inline Runtime.
3731
+
3732
+ ```ts
3733
+ import { connectKodaXRuntime } from '@kodax-ai/kodax/runtime';
3734
+
3735
+ const runtime = await connectKodaXRuntime({
3736
+ profile: 'coder',
3737
+ autoStart: true,
3738
+ homeDir: coderRuntimeBaseDir, // owns <coderRuntimeBaseDir>/.kodax
3739
+ clientInfo: {
3740
+ name: 'kodax-space',
3741
+ version: '0.1.32',
3742
+ instanceId: spaceInstallationId,
3743
+ instanceSecret: await spaceKeychain.readRuntimeClientSecret(),
3744
+ },
3745
+ capabilities: {
3746
+ richEvents: true,
3747
+ permissionPrompts: true,
3748
+ operationDeduplication: true,
3749
+ },
3750
+ requirements: {
3751
+ operationDeduplication: 1,
3752
+ sessionObservation: 1,
3753
+ afterTurnInput: 1,
3754
+ interruptInput: 1,
3755
+ askUserTransport: 1,
3756
+ permissionCas: 1,
3757
+ providerCredentialBroker: 1,
3758
+ runBoundHostTools: 1,
3759
+ coderOwnerFencing: 1,
3760
+ crashOutcomeModel: 1,
3761
+ coderFeatureMatrix: 1,
3762
+ sessionAdmission: 1,
3763
+ completeObservationSnapshot: 1,
3764
+ connectionLifecycle: 1,
3765
+ typedRuntimeEvents: 1,
3766
+ daemonSafeRunInput: 1,
3767
+ sharedSessionSettings: 1,
3768
+ durableRecoveryQueries: 1,
3769
+ daemonManagement: 1,
3770
+ runtimeAutoModeGuardrail: 3,
3771
+ },
3772
+ });
3773
+ ```
3774
+
3775
+ Requirements are server facts, not authorization requests. Check
3776
+ `runtime.grantedScopes` before enabling controls. Missing capabilities or
3777
+ scopes must disable the affected UI; Space must not silently start inline
3778
+ Coder. Products that depend on same-Run delivery should require
3779
+ `{ interruptInput: 1 }`. Individual active Runs without a safe Actor boundary
3780
+ (for example, SA execution) still return `unsupported_capability`; do not
3781
+ silently substitute `delivery:'after_turn'` unless that is the user's intent.
3782
+
3783
+ The v0.7.73 SDK requires `runtimeAutoModeGuardrail:3` automatically for
3784
+ `autoStart: true`, even when the caller omits it from `requirements`. If the healthy
3785
+ profile daemon advertises v1 or v2, the SDK first requires `daemonManagement:1`,
3786
+ takes a revision/owner-policy fenced preflight, and replaces it only when no
3787
+ active or queued run, Workflow, Agent turn, pending permission/user input, or
3788
+ other logical client exists. A busy or still-older daemon is never stopped: the
3789
+ connection rejects with `RuntimeDaemonCapabilityUpgradeError`, whose
3790
+ `recoverable` and `restartRequired` fields are `true` and whose optional
3791
+ `preflight` explains the blockers. Attach-only connections never mutate daemon
3792
+ ownership and must request `runtimeAutoModeGuardrail:1` explicitly when they
3793
+ depend only on the v1 owner contract, v2 for bounded input, effective-default
3794
+ metadata, structured diagnostics, and speculative-window parity, or v3 for
3795
+ opaque exact grant suggestions and concrete permission matchers. Capability
3796
+ requirements are minimum versions: v3 satisfies v1/v2, v2 satisfies v1, and an
3797
+ older daemon never satisfies a newer requirement.
3798
+
3799
+ The `coderFeatureMatrix` capability reports daemon availability for managed
3800
+ runs, transcript/session operations, Todo projection, managed tasks, Workflow,
3801
+ MCP, Reference External Agent, Memory, and Runtime artifacts. Reference
3802
+ External Agent is `false` when the daemon owner did not install its executor
3803
+ plane.
3804
+
3805
+ The packaged daemon authenticates one local OS-user/profile trust domain with
3806
+ a random token stored beside daemon state and a user-only local endpoint. It
3807
+ does not issue a different daemon token to each application in v0.7.69. The
3808
+ returned scope set is chosen by the host (the packaged host grants the public
3809
+ local-user set). `clientInfo.instanceId` is stable attribution for origin and
3810
+ operation deduplication. `instanceSecret` proves that a new authenticated
3811
+ connection is the same stable client when it resumes that client's credential
3812
+ or Host Tool leases; only its hash participates in daemon-owned bridge state.
3813
+ Keep all three values in Electron Main. Mutually distrusting processes running
3814
+ as the same OS account remain outside this release's threat model.
3815
+
3816
+ ### Join atomically and resync after disconnect
3817
+
3818
+ `sessions.observe()` installs the live subscription before taking the
3819
+ snapshot. Its snapshot contains one authoritative `runtimeId`, cursor,
3820
+ `transcriptRevision`, complete transcript, versioned settings, run/queue state,
3821
+ queued continuation IDs/order/origin/safe previews, pending permission and
3822
+ AskUser requests, and live assistant/thinking/tool/Todo/managed-task
3823
+ projection. Run requirements include the current credential/Host Tool
3824
+ availability. Listener events are strictly after the returned cursor.
3825
+
3826
+ ```ts
3827
+ let observedRuntimeId: string | undefined;
3828
+ let lastCursor = 0;
3829
+
3830
+ async function openCoderSession(sessionId: string) {
3831
+ const observation = await runtime.sessions.observe(sessionId, (event) => {
3832
+ if (event.seq <= lastCursor) return;
3833
+ applyRuntimeEvent(event);
3834
+ lastCursor = event.seq;
3835
+ });
3836
+
3837
+ const { snapshot } = observation;
3838
+ const runtimeChanged = observedRuntimeId !== undefined
3839
+ && observedRuntimeId !== snapshot.runtimeId;
3840
+ observedRuntimeId = snapshot.runtimeId;
3841
+ lastCursor = snapshot.cursor;
3842
+ replaceSessionProjection(snapshot, { runtimeChanged });
3843
+ return observation;
3844
+ }
3845
+ ```
3846
+
3847
+ Subscribe to `runtime.connection` to freeze mutation UI immediately rather
3848
+ than waiting for a status poll:
3849
+
3850
+ ```ts
3851
+ runtime.connection?.subscribe((state) => {
3852
+ setCoderConnectionState(state.state, state.reason);
3853
+ if (state.state === 'disconnected' && state.reconnectable) {
3854
+ scheduleReconnect();
3855
+ }
3856
+ });
3857
+ ```
3858
+
3859
+ The SDK reports the current `connectionId`, `runtimeEpoch`, optional
3860
+ `journalEpoch`, disconnect reason, and whether a new connection may be
3861
+ attempted. It does not transparently replay requests or subscriptions. Space
3862
+ creates a replacement Runtime client, checks its new epochs, resumes eligible
3863
+ leases, and observes the session again.
3864
+
3865
+ On transport failure, Runtime change, expired history, or `resync_required`,
3866
+ discard the local derived projection and call `sessions.observe()` again. Do
3867
+ not merge a new snapshot into the old projection. The handshake buffer is
3868
+ bounded; overflow fails explicitly instead of dropping events. A Runtime
3869
+ restart changes `runtimeId`, marks persisted non-terminal runs with a durable
3870
+ terminal fact, and closes old in-memory AskUser/permission requests through the
3871
+ reset boundary.
3872
+
3873
+ ### Durable mutations, stable ordering, and settings CAS
3874
+
3875
+ Every durable public control mutation uses an operation envelope. Credential
3876
+ and Host Tool register/revoke/supply/complete requests are reverse-bridge
3877
+ control frames and are deliberately excluded from the control journal so
3878
+ secrets/results are not persisted. They still enter the daemon management
3879
+ draining fence: once an atomic stop begins, they fail with typed `conflict` and
3880
+ cannot change reverse-bridge state. The SDK creates an operation ID for
3881
+ ordinary one-shot calls. A
3882
+ product-level retry after a lost response must reuse its own stable operation
3883
+ ID; changing its method, payload, resource, or authenticated principal is
3884
+ rejected.
3885
+
3886
+ ```ts
3887
+ const session = await runtime.sessions.create({
3888
+ sessionId: stableSpaceSessionId,
3889
+ title: 'Shared session',
3890
+ surface: 'space-desktop',
3891
+ operation: { operationId: loadOrCreatePendingOperationId('space-session-draft-7') },
3892
+ });
3893
+
3894
+ const operationId = loadOrCreatePendingOperationId('space-run-draft-42');
3895
+ const handle = await runtime.runs.start({
3896
+ sessionId: session.id,
3897
+ input: { type: 'text', text: prompt },
3898
+ options: { provider: 'anthropic' },
3899
+ operation: { operationId },
3900
+ });
3901
+
3902
+ const current = await runtime.sessions.getSettingsVersioned(session.id);
3903
+ const updated = await runtime.sessions.updateSettingsVersioned(
3904
+ session.id,
3905
+ { model: 'claude-sonnet-4-5' },
3906
+ {
3907
+ operationId: loadOrCreatePendingOperationId('space-settings-draft-9'),
3908
+ expectedRevision: current.revision,
3909
+ },
3910
+ );
3911
+ ```
3912
+
3913
+ Create retries with the same explicit session and operation IDs cannot overwrite
3914
+ an existing session. Same-session starts and after-turn inputs receive a durable `sessionOrder`.
3915
+ Retries with the same operation ID return the canonical result and do not
3916
+ create another run. Settings use compare-and-swap; a stale revision returns a
3917
+ structured conflict and must be reloaded, never silently overwritten. The
3918
+ shared settings keys are `provider`, `model`, `effort`, `thinking`,
3919
+ `reasoningMode`, `permissionMode`, `executionCwd`, `agentMode`, and
3920
+ `autoModeEngine`, `autoModeClassifierModel`, `autoModeTimeoutMs`, and
3921
+ `autoModeSpeculativeWindowMs`.
3922
+
3923
+ ```ts
3924
+ const queued = await runtime.runs.submitInput({
3925
+ sessionId: session.id,
3926
+ afterRunId: handle.runId,
3927
+ delivery: 'after_turn',
3928
+ input: { type: 'text', text: 'Also update the tests.' },
3929
+ operation: { operationId: loadOrCreatePendingOperationId('space-input-17') },
3930
+ });
3931
+
3910
3932
  if (!queued.accepted) {
3911
- // stale_run or unsupported_capability: show the factual result.
3912
- }
3913
- ```
3914
-
3915
- Run status exposes acceptance/start/queue times, authenticated origin,
3916
- `sessionOrder`, and a single terminal fact. Important terminal codes include
3917
- `runtime_restarted`, `daemon_crashed`, `credential_unavailable`,
3918
- `host_not_dispatched`, `host_outcome_unknown`, and
3919
- `control_history_untrusted`. Respect `effectOutcome`; `unknown` must never be
3920
- presented as success or automatically retried. After a lost response, query
3921
- `runtime.operations.get({ operationId, journalEpoch })`; applied receipts
3922
- include the canonical result. Permission grants remain daemon-owned and
3923
- revisioned.
3924
-
3925
- ### AskUser and permission from any client
3926
-
3927
- AskUser is no longer an in-process callback for daemon Coder runs. Any client
3928
- with the responder scope can list the pending request and answer or dismiss it.
3929
- The request revision and run binding prevent a stale UI from answering a new
3930
- request. Exactly one concurrent answer is accepted.
3931
-
3932
- ```ts
3933
- for (const request of await runtime.userInputs.listPending({ sessionId })) {
3934
- const resolution = await runtime.userInputs.respond(request.id, answer, {
3935
- expectedRevision: request.revision,
3936
- runId: request.runId,
3937
- });
3938
- if (!resolution.accepted) refreshPendingInteractions();
3939
- }
3940
-
3941
- for (const request of await runtime.permissions.listPending({ sessionId })) {
3942
- const sessionScope = request.grantSuggestions
3943
- ?.find((candidate) => candidate.kind === 'session');
3944
- const accepted = await runtime.permissions.respond(
3945
- request.id,
3946
- sessionScope
3947
- ? { type: 'allow_session', suggestionId: sessionScope.id }
3948
- : { type: 'allow_once' },
3949
- { runId: request.runId },
3950
- );
3951
- if (!accepted) refreshPendingInteractions();
3933
+ // stale_run, unsupported_capability, or interrupt_window_closed:
3934
+ // show the factual result and preserve the user's unsent input.
3952
3935
  }
3953
- ```
3954
-
3955
- Persistent `allow_always` grants are owned by the daemon and require
3956
- `permission:grant-admin`. Use `permissions.listGrants()` and
3957
- `revokeGrant(grantId, expectedRevision)` for revision-safe administration.
3958
- Clients must return one opaque `grantSuggestions[].id` from the pending request;
3959
- they must not infer, construct, or widen a scope from the display label or input
3960
- preview. A safe request can offer `allow_session` and `allow_always`; a risky or
3961
- dynamic shell request deliberately omits the persistent candidate. Command
3962
- grants match one exact normalized command/cwd/shell/background combination,
3963
- while path grants match one tool and normalized absolute path (the future
3964
- Write/Edit content may differ). Generic extension calls can receive only an
3965
- exact in-memory Session grant. Raw command/argv data is not stored in the
3966
- matcher; grants and audit contain only its fingerprint plus a bounded,
3967
- secret-redacted operator label. Clients must not keep separate persistent
3968
- permission rule stores. Runtime capability `runtimeAutoModeGuardrail` v3 advertises this
3969
- opaque concrete-grant contract; restart or upgrade an older daemon instead of
3970
- falling back to a client-side alias.
3971
-
3972
- ### Broker a Space keychain credential
3973
-
3974
- Credentials remain owned by Space's OS keychain. Register a broker in Electron
3975
- Main, bind the returned lease only to runs that Space starts, and return the
3976
- key only after checking the daemon-provided provider/session/run context.
3977
-
3978
- ```ts
3979
- const credentialLease = await runtime.credentials.register(
3980
- { providers: ['anthropic'] },
3981
- async ({ provider, sessionId: requestedSession, runId }) => {
3982
- authorizeSpaceRunCredential({ provider, sessionId: requestedSession, runId });
3983
- return spaceKeychain.readProviderCredential(provider);
3984
- },
3985
- );
3986
-
3987
- const run = await runtime.runs.start({
3988
- sessionId,
3989
- input: { type: 'text', text: prompt },
3990
- options: { provider: 'anthropic' },
3991
- credential: { leaseId: credentialLease.id, provider: 'anthropic' },
3992
- operation: { operationId: loadOrCreatePendingOperationId('space-run-88') },
3993
- });
3994
- ```
3995
-
3996
- The secret crosses only the authenticated reverse frame and an in-memory
3997
- run/provider scope. It is excluded from events, status, logs, diagnostics,
3998
- operation records, and Runtime persistence. While such a scope is active,
3999
- provider mismatch fails closed and never falls back to daemon environment.
4000
- Without a stable `instanceSecret`, registration ends when the Space connection
4001
- closes. With it, the daemon keeps the registration owned by that stable client;
4002
- a replacement Space process reattaches the callback with
4003
- `runtime.credentials.resume(leaseId, broker)`. An accepted run has already
4004
- acquired its scoped credential, so it reports `requirements.credential.state`
4005
- as `ready` and may continue after disconnect. If the broker cannot answer, the
4006
- start is rejected instead of accepting an indefinitely waiting run. Expiry or
4007
- Runtime restart is explicit (`expired` or terminal); no provider request is
4008
- automatically replayed.
4009
-
4010
- ### Bind Space-owned Host Tools to one run
4011
-
4012
- Register only narrow product capabilities. Descriptors are data; handlers stay
4013
- in Electron Main. A lease grants nothing until its ID is explicitly bound to a
4014
- run.
4015
-
4016
- ```ts
4017
- const hostLease = await runtime.hostTools.register([{
4018
- name: 'space_artifact_create',
4019
- description: 'Create a Space-owned artifact for this run.',
4020
- inputSchema: {
4021
- type: 'object',
4022
- properties: { title: { type: 'string' } },
4023
- required: ['title'],
4024
- additionalProperties: false,
4025
- },
4026
- sideEffect: 'non_idempotent',
4027
- }], {
4028
- async space_artifact_create(invocation) {
4029
- authorizeBoundSpaceInvocation(invocation);
4030
- const artifact = await createSpaceArtifact(invocation.input);
4031
- return { content: `Created artifact ${artifact.displayId}` };
4032
- },
4033
- });
4034
-
4035
- await runtime.runs.start({
4036
- sessionId,
4037
- input: { type: 'text', text: 'Create the report artifact.' },
4038
- hostTools: { leaseId: hostLease.id },
4039
- operation: { operationId: loadOrCreatePendingOperationId('space-artifact-run') },
4040
- });
4041
- ```
4042
-
4043
- The daemon injects session/run/lease/invocation identity; renderer, model, and
4044
- ordinary tool input cannot choose it. CLI runs never inherit a Space lease just
4045
- because Space later observes their session. The client memoizes one handler
4046
- promise per invocation ID, and the daemon never replays a dispatched Host Tool.
4047
- After a stable-client reconnect, call
4048
- `runtime.hostTools.resume(leaseId, handlers)`. Bound run status reports
4049
- `ready`, `waiting_host`, `expired`, or `terminal`. Disconnect or timeout after dispatch produces `host_outcome_unknown`; Space
4050
- must reconcile the product side effect itself before offering a new user
4051
- action. `runtime.hostTools.getInvocation(invocationId)` returns the durable
4052
- metadata state `prepared`, `dispatched`, `completed`, `unknown`, or
4053
- `not_dispatched`; it never returns handler input, result, or credential data.
4054
- The daemon writes the `dispatched` marker before attempting the reverse frame
4055
- and never auto-replays an invocation.
4056
-
4057
- ### Coder admission, typed events, and transport-safe inputs
4058
-
4059
- The daemon enforces Coder session admission on the server for list/create/load,
4060
- run, settings, delete, rewind, fork, compact, transcript, event, interaction,
4061
- and diagnostic paths. Coder
4062
- surfaces are `code`, `cli`, `repl`, `acp`, `a2a`, `sdk`, `ide`, and
4063
- `space-desktop`. A session marked with
4064
- Partner surface/profile metadata, or any unknown product surface, fails with
4065
- typed `session_not_admitted` before mutation. Space must continue marking
4066
- Partner sessions as `surface: 'partner'` and keep their inline storage root
4067
- separate. Legacy sessions without a surface remain admitted for existing Coder
4068
- compatibility, so absence of metadata is not a Partner namespace mechanism.
4069
-
4070
- `RuntimeEventPayloadMap` and `RuntimeTypedEvent` provide the public
4071
- discriminated contract for known events. Existing raw listeners remain
4072
- compatible; consumers can use `parseRuntimeEvent(value)` before exhaustive
4073
- handling. One unknown or malformed event is diagnosed and dropped without
4074
- closing the observation stream.
4075
-
4076
- Daemon clients use `RuntimeDaemonStartRunInput`. Function callbacks,
4077
- `AbortSignal`, Extension Runtime objects, and guardrail instances are excluded
4078
- from that type and rejected at runtime with `RuntimeTransportBoundaryError`
4079
- and an exact value path if an untyped caller supplies them. Host-only values
4080
- remain valid only for embedded Runtime calls.
4081
-
4082
- ### Recovery queries and stop preflight
4083
-
4084
- `runtime.status.preflight()` returns the initialized logical-client count,
4085
- active and queued runs, running/paused Workflows, every non-terminal External
4086
- Agent task (including `unknown`), pending AskUser/permission records, blockers,
4087
- and `canStop`. The background-work blockers are `active_workflows` and
4088
- `active_agent_tasks`. The current facade counts as one; daemon self-connections
4089
- and bounded health probes do not count. A second process changes the count to
4090
- two, and its awaited `close()` makes the count converge back to one.
4091
-
4092
- Preflight is useful for UI, but it is not a stop authorization token. Use
4093
- `runtime.daemon.inspect()` to obtain one consistent management revision,
4094
- verified owner fence, owner-policy revision, and preflight projection. Only
4095
- `runtime.daemon.stopForInline()` atomically rechecks and commits a rollback.
4096
- The management revision also advances when the preflight projection changes,
4097
- so a Workflow or AgentTask lifecycle transition between inspect and commit
4098
- invalidates the stale stop. Capability details
4099
- `daemonManagement.backgroundWorkPreflight` and
4100
- `daemonManagement.reverseBridgeDrainingFence` identify this complete contract.
4101
- `runtime.operations.get()` reconciles durable mutations,
4102
- `hostTools.getInvocation()` reconciles Host Tool metadata, and
4103
- `permissions.listGrants()` returns the daemon-owned persistent grant set.
4104
-
4105
- Terminal notification read/unread state is intentionally client-owned in
4106
- v0.7.69 (`durableRecoveryQueries.terminalAcknowledgement === false`): Space
4107
- persists its own UI acknowledgement cursor against Runtime/run terminal facts.
4108
- This avoids a false claim that one client's acknowledgement is global daemon
4109
- truth.
4110
-
4111
- ### Owner policy, rollback, and Electron boundary
4112
-
4113
- Daemon and inline Coder use one profile fence. Do not compose
4114
- `status.preflight()` with a low-level unconditional stop: another client or run
4115
- can appear between those calls. The public rollback transaction gates new
4116
- clients and mutations, rechecks the same Runtime and management/policy
4117
- revisions, verifies there is no other client or active/queued/pending work,
4118
- commits sticky inline policy while that daemon still owns the fence, and then
4119
- requests shutdown.
4120
-
4121
- ```ts
4122
- import {
4123
- acquireKodaXInlineOwner,
4124
- enableKodaXDaemonOwner,
4125
- getKodaXRuntimeOwnerState,
4126
- } from '@kodax-ai/kodax/runtime';
4127
-
4128
- const management = await runtime.daemon.inspect();
4129
- if (!management.preflight.canStop) {
4130
- showRollbackBlockers(management.preflight.blockers);
4131
- return;
4132
- }
4133
-
4134
- const rollback = await runtime.daemon.stopForInline({
4135
- expectedRuntimeId: management.runtimeId,
4136
- expectedRevision: management.revision,
4137
- expectedOwnerPolicyRevision: management.ownerPolicy.revision,
4138
- operation: { operationId: loadOrCreatePendingOperationId('coder-inline-rollback') },
4139
- });
4140
-
4141
- // `accepted` means inline policy is committed and shutdown is in progress.
4142
- // Wait through the public owner-state query; never infer release from a PID.
4143
- const shutdownDeadline = Date.now() + 30_000;
4144
- while (getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' }).owner?.runtimeId
4145
- === rollback.runtimeId) {
4146
- if (Date.now() >= shutdownDeadline) throw new Error('Timed out waiting for daemon owner release.');
4147
- await delay(25);
4148
- }
4149
- const releasedOwner = getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' });
4150
- if (releasedOwner.ownerStatus !== 'unowned') {
4151
- throw new Error('Coder profile acquired a different owner during rollback.');
4152
- }
4153
- await runtime.close(); // Detach only; it does not perform a second stop.
4154
- const inlineOwner = acquireKodaXInlineOwner({ homeDir: kodaxHome, profile: 'coder' });
4155
-
4156
- // Later, after the inline owner has released its fence:
4157
- inlineOwner.close();
4158
- const daemonPolicy = enableKodaXDaemonOwner({ homeDir: kodaxHome, profile: 'coder' });
4159
- // daemonPolicy.revision is authoritative; no expectedRevision guess is needed.
4160
- ```
4161
-
4162
- Any management revision change, another logical client, active or queued run,
4163
- running/paused Workflow, non-terminal/unknown AgentTask, pending
4164
- AskUser/permission, or in-flight mutation returns structured `conflict`; the
4165
- daemon remains running and policy remains unchanged. Draining also rejects
4166
- credential and Host Tool state changes without journaling their secrets or
4167
- results. The inline policy is sticky: later CLI auto-start is rejected until
4168
- `enableKodaXDaemonOwner()` changes it back to `daemon`. `runtime.close()` still
4169
- only detaches. Stale-owner handling validates the owned lock/state and never
4170
- kills a process merely because a PID was reused.
4171
-
4172
- Keep all trusted objects in Electron Main: daemon token/endpoint, stable client
4173
- identity, operation IDs, owner policy, keychain broker, Host Tool handlers, and
4174
- permission-grant administration. Renderer IPC should expose product-specific
4175
- commands and sanitized projections only. Never pass daemon credentials,
4176
- leases, operation epochs, or trusted session/run context to renderer or model
4177
- tool arguments.
3936
+ ```
3937
+
3938
+ For input that must join the current active Run, submit `delivery:'interrupt'`.
3939
+ This does not create a Run. Each accepted input appears as `queued` in the
3940
+ owning Run's `interruptInputs`. At the next safe boundary, all accumulated
3941
+ interrupts are drained FIFO, remain separate user messages in one next LLM
3942
+ request, and produce one `run.input.delivered` event whose `inputs` array is the
3943
+ complete ordered batch. Exact operation retries return the same `inputId`.
3944
+ The accepted result's `runId` is the existing owning Run (equal to
3945
+ `afterRunId`), not a newly created continuation.
3946
+
3947
+ Interrupt admission closes when the Runner publishes its final completion or
3948
+ terminal error signal, or when the Run's supplied `abortSignal` aborts, even if
3949
+ the outer Run is still settling. Non-terminal observer diagnostics do not close
3950
+ the window. A submission after closure returns `accepted:false` with
3951
+ `reason:'interrupt_window_closed'` and is not queued. Keep the original input
3952
+ available for retry after the Run ends; do not silently change its delivery to
3953
+ `after_turn`. As a final race/recovery
3954
+ guard, inspect terminal Run status: any `interruptInputs` entry whose state is
3955
+ `terminal` was not delivered. Reconcile it by `inputId` and present a visible
3956
+ non-delivery outcome rather than leaving a pending queue indicator.
3957
+
3958
+ ```ts
3959
+ const interrupted = await runtime.runs.submitInput({
3960
+ sessionId: session.id,
3961
+ afterRunId: handle.runId,
3962
+ delivery: 'interrupt',
3963
+ input: { type: 'text', text: 'Also preserve the public API.' },
3964
+ operation: { operationId: loadOrCreatePendingOperationId('space-input-18') },
3965
+ });
3966
+ ```
3967
+
3968
+ Run status exposes acceptance/start/queue times, authenticated origin,
3969
+ `sessionOrder`, and a single terminal fact. Important terminal codes include
3970
+ `runtime_restarted`, `daemon_crashed`, `credential_unavailable`,
3971
+ `host_not_dispatched`, `host_outcome_unknown`, and
3972
+ `control_history_untrusted`. Respect `effectOutcome`; `unknown` must never be
3973
+ presented as success or automatically retried. After a lost response, query
3974
+ `runtime.operations.get({ operationId, journalEpoch })`; applied receipts
3975
+ include the canonical result. Permission grants remain daemon-owned and
3976
+ revisioned.
3977
+
3978
+ ### AskUser and permission from any client
3979
+
3980
+ AskUser is no longer an in-process callback for daemon Coder runs. Any client
3981
+ with the responder scope can list the pending request and answer or dismiss it.
3982
+ The request revision and run binding prevent a stale UI from answering a new
3983
+ request. Exactly one concurrent answer is accepted.
3984
+
3985
+ ```ts
3986
+ for (const request of await runtime.userInputs.listPending({ sessionId })) {
3987
+ const resolution = await runtime.userInputs.respond(request.id, answer, {
3988
+ expectedRevision: request.revision,
3989
+ runId: request.runId,
3990
+ });
3991
+ if (!resolution.accepted) refreshPendingInteractions();
3992
+ }
3993
+
3994
+ for (const request of await runtime.permissions.listPending({ sessionId })) {
3995
+ const sessionScope = request.grantSuggestions
3996
+ ?.find((candidate) => candidate.kind === 'session');
3997
+ const accepted = await runtime.permissions.respond(
3998
+ request.id,
3999
+ sessionScope
4000
+ ? { type: 'allow_session', suggestionId: sessionScope.id }
4001
+ : { type: 'allow_once' },
4002
+ { runId: request.runId },
4003
+ );
4004
+ if (!accepted) refreshPendingInteractions();
4005
+ }
4006
+ ```
4007
+
4008
+ Persistent `allow_always` grants are owned by the daemon and require
4009
+ `permission:grant-admin`. Use `permissions.listGrants()` and
4010
+ `revokeGrant(grantId, expectedRevision)` for revision-safe administration.
4011
+ Clients must return one opaque `grantSuggestions[].id` from the pending request;
4012
+ they must not infer, construct, or widen a scope from the display label or input
4013
+ preview. A safe request can offer `allow_session` and `allow_always`; a risky or
4014
+ dynamic shell request deliberately omits the persistent candidate. Command
4015
+ grants match one exact normalized command/cwd/shell/background combination,
4016
+ while path grants match one tool and normalized absolute path (the future
4017
+ Write/Edit content may differ). Generic extension calls can receive only an
4018
+ exact in-memory Session grant. Raw command/argv data is not stored in the
4019
+ matcher; grants and audit contain only its fingerprint plus a bounded,
4020
+ secret-redacted operator label. Clients must not keep separate persistent
4021
+ permission rule stores. Runtime capability `runtimeAutoModeGuardrail` v3 advertises this
4022
+ opaque concrete-grant contract; restart or upgrade an older daemon instead of
4023
+ falling back to a client-side alias.
4024
+
4025
+ ### Broker a Space keychain credential
4026
+
4027
+ Credentials remain owned by Space's OS keychain. Register a broker in Electron
4028
+ Main, bind the returned lease only to runs that Space starts, and return the
4029
+ key only after checking the daemon-provided provider/session/run context.
4030
+
4031
+ ```ts
4032
+ const credentialLease = await runtime.credentials.register(
4033
+ { providers: ['anthropic'] },
4034
+ async ({ provider, sessionId: requestedSession, runId }) => {
4035
+ authorizeSpaceRunCredential({ provider, sessionId: requestedSession, runId });
4036
+ return spaceKeychain.readProviderCredential(provider);
4037
+ },
4038
+ );
4039
+
4040
+ const run = await runtime.runs.start({
4041
+ sessionId,
4042
+ input: { type: 'text', text: prompt },
4043
+ options: { provider: 'anthropic' },
4044
+ credential: { leaseId: credentialLease.id, provider: 'anthropic' },
4045
+ operation: { operationId: loadOrCreatePendingOperationId('space-run-88') },
4046
+ });
4047
+ ```
4048
+
4049
+ The secret crosses only the authenticated reverse frame and an in-memory
4050
+ run/provider scope. It is excluded from events, status, logs, diagnostics,
4051
+ operation records, and Runtime persistence. While such a scope is active,
4052
+ provider mismatch fails closed and never falls back to daemon environment.
4053
+ Without a stable `instanceSecret`, registration ends when the Space connection
4054
+ closes. With it, the daemon keeps the registration owned by that stable client;
4055
+ a replacement Space process reattaches the callback with
4056
+ `runtime.credentials.resume(leaseId, broker)`. An accepted run has already
4057
+ acquired its scoped credential, so it reports `requirements.credential.state`
4058
+ as `ready` and may continue after disconnect. If the broker cannot answer, the
4059
+ start is rejected instead of accepting an indefinitely waiting run. Expiry or
4060
+ Runtime restart is explicit (`expired` or terminal); no provider request is
4061
+ automatically replayed.
4062
+
4063
+ ### Bind Space-owned Host Tools to one run
4064
+
4065
+ Register only narrow product capabilities. Descriptors are data; handlers stay
4066
+ in Electron Main. A lease grants nothing until its ID is explicitly bound to a
4067
+ run.
4068
+
4069
+ ```ts
4070
+ const hostLease = await runtime.hostTools.register([{
4071
+ name: 'space_artifact_create',
4072
+ description: 'Create a Space-owned artifact for this run.',
4073
+ inputSchema: {
4074
+ type: 'object',
4075
+ properties: { title: { type: 'string' } },
4076
+ required: ['title'],
4077
+ additionalProperties: false,
4078
+ },
4079
+ sideEffect: 'non_idempotent',
4080
+ }], {
4081
+ async space_artifact_create(invocation) {
4082
+ authorizeBoundSpaceInvocation(invocation);
4083
+ const artifact = await createSpaceArtifact(invocation.input);
4084
+ return { content: `Created artifact ${artifact.displayId}` };
4085
+ },
4086
+ });
4087
+
4088
+ await runtime.runs.start({
4089
+ sessionId,
4090
+ input: { type: 'text', text: 'Create the report artifact.' },
4091
+ hostTools: { leaseId: hostLease.id },
4092
+ operation: { operationId: loadOrCreatePendingOperationId('space-artifact-run') },
4093
+ });
4094
+ ```
4095
+
4096
+ The daemon injects session/run/lease/invocation identity; renderer, model, and
4097
+ ordinary tool input cannot choose it. CLI runs never inherit a Space lease just
4098
+ because Space later observes their session. The client memoizes one handler
4099
+ promise per invocation ID, and the daemon never replays a dispatched Host Tool.
4100
+ After a stable-client reconnect, call
4101
+ `runtime.hostTools.resume(leaseId, handlers)`. Bound run status reports
4102
+ `ready`, `waiting_host`, `expired`, or `terminal`. Disconnect or timeout after dispatch produces `host_outcome_unknown`; Space
4103
+ must reconcile the product side effect itself before offering a new user
4104
+ action. `runtime.hostTools.getInvocation(invocationId)` returns the durable
4105
+ metadata state `prepared`, `dispatched`, `completed`, `unknown`, or
4106
+ `not_dispatched`; it never returns handler input, result, or credential data.
4107
+ The daemon writes the `dispatched` marker before attempting the reverse frame
4108
+ and never auto-replays an invocation.
4109
+
4110
+ ### Coder admission, typed events, and transport-safe inputs
4111
+
4112
+ The daemon enforces Coder session admission on the server for list/create/load,
4113
+ run, settings, delete, rewind, fork, compact, transcript, event, interaction,
4114
+ and diagnostic paths. Coder
4115
+ surfaces are `code`, `cli`, `repl`, `acp`, `a2a`, `sdk`, `ide`, and
4116
+ `space-desktop`. A session marked with
4117
+ Partner surface/profile metadata, or any unknown product surface, fails with
4118
+ typed `session_not_admitted` before mutation. Space must continue marking
4119
+ Partner sessions as `surface: 'partner'` and keep their inline storage root
4120
+ separate. Legacy sessions without a surface remain admitted for existing Coder
4121
+ compatibility, so absence of metadata is not a Partner namespace mechanism.
4122
+
4123
+ `RuntimeEventPayloadMap` and `RuntimeTypedEvent` provide the public
4124
+ discriminated contract for known events. Existing raw listeners remain
4125
+ compatible; consumers can use `parseRuntimeEvent(value)` before exhaustive
4126
+ handling. One unknown or malformed event is diagnosed and dropped without
4127
+ closing the observation stream.
4128
+
4129
+ Daemon clients use `RuntimeDaemonStartRunInput`. Function callbacks,
4130
+ `AbortSignal`, Extension Runtime objects, and guardrail instances are excluded
4131
+ from that type and rejected at runtime with `RuntimeTransportBoundaryError`
4132
+ and an exact value path if an untyped caller supplies them. Host-only values
4133
+ remain valid only for embedded Runtime calls.
4134
+
4135
+ ### Recovery queries and stop preflight
4136
+
4137
+ `runtime.status.preflight()` returns the initialized logical-client count,
4138
+ active and queued runs, running/paused Workflows, every non-terminal External
4139
+ Agent task (including `unknown`), pending AskUser/permission records, blockers,
4140
+ and `canStop`. The background-work blockers are `active_workflows` and
4141
+ `active_agent_tasks`. The current facade counts as one; daemon self-connections
4142
+ and bounded health probes do not count. A second process changes the count to
4143
+ two, and its awaited `close()` makes the count converge back to one.
4144
+
4145
+ Preflight is useful for UI, but it is not a stop authorization token. Use
4146
+ `runtime.daemon.inspect()` to obtain one consistent management revision,
4147
+ verified owner fence, owner-policy revision, and preflight projection. Only
4148
+ `runtime.daemon.stopForInline()` atomically rechecks and commits a rollback.
4149
+ The management revision also advances when the preflight projection changes,
4150
+ so a Workflow or AgentTask lifecycle transition between inspect and commit
4151
+ invalidates the stale stop. Capability details
4152
+ `daemonManagement.backgroundWorkPreflight` and
4153
+ `daemonManagement.reverseBridgeDrainingFence` identify this complete contract.
4154
+ `runtime.operations.get()` reconciles durable mutations,
4155
+ `hostTools.getInvocation()` reconciles Host Tool metadata, and
4156
+ `permissions.listGrants()` returns the daemon-owned persistent grant set.
4157
+
4158
+ Terminal notification read/unread state is intentionally client-owned in
4159
+ v0.7.69 (`durableRecoveryQueries.terminalAcknowledgement === false`): Space
4160
+ persists its own UI acknowledgement cursor against Runtime/run terminal facts.
4161
+ This avoids a false claim that one client's acknowledgement is global daemon
4162
+ truth.
4163
+
4164
+ ### Owner policy, rollback, and Electron boundary
4165
+
4166
+ Daemon and inline Coder use one profile fence. Do not compose
4167
+ `status.preflight()` with a low-level unconditional stop: another client or run
4168
+ can appear between those calls. The public rollback transaction gates new
4169
+ clients and mutations, rechecks the same Runtime and management/policy
4170
+ revisions, verifies there is no other client or active/queued/pending work,
4171
+ commits sticky inline policy while that daemon still owns the fence, and then
4172
+ requests shutdown.
4173
+
4174
+ ```ts
4175
+ import {
4176
+ acquireKodaXInlineOwner,
4177
+ enableKodaXDaemonOwner,
4178
+ getKodaXRuntimeOwnerState,
4179
+ } from '@kodax-ai/kodax/runtime';
4180
+
4181
+ const management = await runtime.daemon.inspect();
4182
+ if (!management.preflight.canStop) {
4183
+ showRollbackBlockers(management.preflight.blockers);
4184
+ return;
4185
+ }
4186
+
4187
+ const rollback = await runtime.daemon.stopForInline({
4188
+ expectedRuntimeId: management.runtimeId,
4189
+ expectedRevision: management.revision,
4190
+ expectedOwnerPolicyRevision: management.ownerPolicy.revision,
4191
+ operation: { operationId: loadOrCreatePendingOperationId('coder-inline-rollback') },
4192
+ });
4193
+
4194
+ // `accepted` means inline policy is committed and shutdown is in progress.
4195
+ // Wait through the public owner-state query; never infer release from a PID.
4196
+ const shutdownDeadline = Date.now() + 30_000;
4197
+ while (getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' }).owner?.runtimeId
4198
+ === rollback.runtimeId) {
4199
+ if (Date.now() >= shutdownDeadline) throw new Error('Timed out waiting for daemon owner release.');
4200
+ await delay(25);
4201
+ }
4202
+ const releasedOwner = getKodaXRuntimeOwnerState({ homeDir: kodaxHome, profile: 'coder' });
4203
+ if (releasedOwner.ownerStatus !== 'unowned') {
4204
+ throw new Error('Coder profile acquired a different owner during rollback.');
4205
+ }
4206
+ await runtime.close(); // Detach only; it does not perform a second stop.
4207
+ const inlineOwner = acquireKodaXInlineOwner({ homeDir: kodaxHome, profile: 'coder' });
4208
+
4209
+ // Later, after the inline owner has released its fence:
4210
+ inlineOwner.close();
4211
+ const daemonPolicy = enableKodaXDaemonOwner({ homeDir: kodaxHome, profile: 'coder' });
4212
+ // daemonPolicy.revision is authoritative; no expectedRevision guess is needed.
4213
+ ```
4214
+
4215
+ Any management revision change, another logical client, active or queued run,
4216
+ running/paused Workflow, non-terminal/unknown AgentTask, pending
4217
+ AskUser/permission, or in-flight mutation returns structured `conflict`; the
4218
+ daemon remains running and policy remains unchanged. Draining also rejects
4219
+ credential and Host Tool state changes without journaling their secrets or
4220
+ results. The inline policy is sticky: later CLI auto-start is rejected until
4221
+ `enableKodaXDaemonOwner()` changes it back to `daemon`. `runtime.close()` still
4222
+ only detaches. Stale-owner handling validates the owned lock/state and never
4223
+ kills a process merely because a PID was reused.
4224
+
4225
+ Keep all trusted objects in Electron Main: daemon token/endpoint, stable client
4226
+ identity, operation IDs, owner policy, keychain broker, Host Tool handlers, and
4227
+ permission-grant administration. Renderer IPC should expose product-specific
4228
+ commands and sanitized projections only. Never pass daemon credentials,
4229
+ leases, operation epochs, or trusted session/run context to renderer or model
4230
+ tool arguments.
4231
+
4232
+ ---
4233
+
4234
+ ## 24. Runtime-owned Auto Mode and plan-approval bridges (v0.7.72–v0.7.73)
4235
+
4236
+ Auto Mode is a Runtime session contract, including in shared-daemon mode. Do
4237
+ not implement a second classifier or decide permissions from a client-side
4238
+ `beforeToolExecute` hook before the Runtime has classified the call.
4239
+
4240
+ ### Configure the session, not an individual UI callback
4241
+
4242
+ ```ts
4243
+ await runtime.sessions.updateSettings(session.id, {
4244
+ permissionMode: 'auto',
4245
+ autoModeEngine: 'llm',
4246
+ autoModeClassifierModel: 'zhipu:glm-5.2', // optional; otherwise follow the run model
4247
+ autoModeTimeoutMs: 20_000, // positive safe integer, optional
4248
+ autoModeSpeculativeWindowMs: 0, // non-negative safe integer, optional
4249
+ executionCwd: projectDirectory,
4250
+ });
4251
+ ```
4252
+
4253
+ All three Auto fields are durable session settings in inline, Worker, and
4254
+ daemon forms. A `null` patch removes an optional override. Timeout must be a
4255
+ positive safe integer; speculative window must be non-negative, so `0` is a
4256
+ valid request to wait for the actual verdict. Daemon capability discovery
4257
+ advertises all fields in `sharedSessionSettings.keys`.
4258
+
4259
+ SDK hosts that need config precedence without creating a REPL can reuse the
4260
+ same typed resolver as KodaX:
4261
+
4262
+ ```ts
4263
+ import {
4264
+ loadAutoModeSettings,
4265
+ resolveAutoModeSettings,
4266
+ type ResolveAutoModeSettingsInput,
4267
+ } from '@kodax-ai/kodax/repl';
4268
+
4269
+ const persisted = loadAutoModeSettings(process.env);
4270
+ const preview = resolveAutoModeSettings({
4271
+ settings: { engine: 'llm', speculativeWindowMs: 0 },
4272
+ env: process.env,
4273
+ });
4274
+ ```
4275
+
4276
+ `resolveAutoModeSettings()` is pure. `loadAutoModeSettings()` reads the KodaX
4277
+ config once and delegates to that resolver; `loadConfig()` declares and
4278
+ returns the same optional `autoMode` object.
4279
+
4280
+ Starting with the v0.7.73 patch, `permissionMode: 'auto'` with an omitted
4281
+ `autoModeEngine` still means the documented `llm` default and is still owned by
4282
+ Runtime. If neither `autoModeClassifierModel` nor the effective run/session/
4283
+ Runtime model exists, `runs.start()` rejects with
4284
+ `RuntimeAutoModeConfigurationError` (`code:
4285
+ 'auto_mode_classifier_model_required'`, `recoverable: true`) before provider
4286
+ construction, a classifier call, or a pending permission. Blank and malformed
4287
+ classifier model specs are rejected by the same typed configuration boundary;
4288
+ a live rules-to-LLM switch is blocked rather than converted into approval work.
4289
+
4290
+ Direct consumers of `createAutoModeToolGuardrail()` receive the same terminal
4291
+ model boundary. After resolving CLI/env/session/settings/live-default
4292
+ precedence, an empty effective model returns a local configuration `block`
4293
+ before provider lookup. It does not call `askUser`, mutate the denial or
4294
+ circuit-breaker trackers, or change the engine to rules. An explicit non-empty
4295
+ classifier override remains valid even when the main-session model is empty.
4296
+
4297
+ The Runtime owns one serialized permission-settings stream and one shared
4298
+ engine/denial/breaker state per Session. It reuses bounded context-specific
4299
+ guardrails across turns while provider/model, repository boundary, execution
4300
+ directory, classifier model, and timeout remain the same. Updating one of
4301
+ those inputs selects a new context guardrail by design without copying stale
4302
+ state from a queued turn. Active runs, queued runs, explicit settings updates,
4303
+ and automatic LLM-to-rules fallback merge through the same Session mutation
4304
+ queue; fallback is persisted before a later classification reads the engine.
4305
+
4306
+ ### What an embedder should expect
4307
+
4308
+ The Runtime's execution order is fixed:
4309
+
4310
+ ```text
4311
+ Runtime Auto Mode guardrail -> host permission bridge only for escalate -> tool execution
4312
+ ```
4313
+
4314
+ Consequently, an LLM/rules `allow` does not create a pending permission request
4315
+ just because a host installed a static approval hook. `block` does not become a
4316
+ spurious approval prompt. A real `escalate` uses the existing shared
4317
+ `runtime.permissions` flow, so another authorized client may render and answer
4318
+ it. Hosts should subscribe to permission events to display such a request, but
4319
+ must not treat a missing request as an error for a safe tool call.
4320
+
4321
+ The classifier deadline remains 20 seconds by default and includes connection
4322
+ setup, provider Retry-After/backoff, inference, and stream completion. KodaX
4323
+ does not solve timeouts by extending that deadline indefinitely. Before the
4324
+ provider call it removes assistant prose/thinking and image paths, limits each
4325
+ tool result to 2 KiB and the serialized permission-relevant transcript to
4326
+ 8 KiB, then enforces 16 KiB action and 32 KiB total-prompt ceilings plus a
4327
+ 256-token output cap. An oversized action or prompt escalates without a
4328
+ provider call; it is never truncated into an automatic allow. These limits are
4329
+ owned by `classify()` itself, so custom callers cannot accidentally bypass the
4330
+ session-history boundary.
4331
+
4332
+ `ClassifyDecision.diagnostics` and the lower-level
4333
+ `SideQueryResult.diagnostics` expose provider, model, effective timeout,
4334
+ elapsed time, retry count/wait, and a coarse terminal phase without including
4335
+ the prompt, action, messages, or response text. `pre_output` means no non-empty
4336
+ text delta was observed; `streaming` means output began before termination.
4337
+ `firstOutputMs` and `streamMs` are present only when the provider adapter emits
4338
+ a text delta. The current provider API cannot honestly separate DNS/connect,
4339
+ TLS, provider queueing, and inference, so embedders must not infer those stages
4340
+ from `pre_output`.
4341
+
4342
+ The permission event's `inputPreview` is a display-safe diagnostic projection:
4343
+ it is bounded, credential-redacted, valid JSON, and includes the effective
4344
+ execution directory. Use the Runtime owner’s typed tool input for execution;
4345
+ do not reconstruct or authorize a tool from the preview. `gitRoot` remains the
4346
+ session repository safety boundary, whereas relative operands resolve from the
4347
+ validated `executionCwd`. In particular, quoted Python/JavaScript/regexp source
4348
+ inside a shell command is not a path operand.
4349
+
4350
+ The user-level `.kodax` directory is a credential/configuration boundary, not
4351
+ an ordinary project path. Direct shell mutations, output redirects, and
4352
+ recognized nested-shell payloads whose target is provably beneath that
4353
+ directory are rejected before LLM classification. The check is segment-safe
4354
+ and Windows case-insensitive. KodaX deliberately does not scan arbitrary
4355
+ quoted language source for path-looking substrings: doing so would turn Python,
4356
+ JavaScript, YAML, and regular expressions into false Tier-0 matches. Trusted
4357
+ configuration changes should use the KodaX config CLI or SDK configuration API.
4358
+
4359
+ ### 0.7.x source compatibility
4360
+
4361
+ The v0.7.72 public declarations retain the following migration aliases:
4362
+
4363
+ | Legacy source | Current source | Contract |
4364
+ |---|---|---|
4365
+ | `agentMode: 'amaw'` | `'ama'` | accepted as deprecated input and normalized to AMA; no separate AMAW runtime is restored |
4366
+ | `SkillSource` | `ResolvedSkillSource` | formal source union remains `project \| user \| plugin \| builtin`; only resolved discovery output adds `learned` |
4367
+ | `RuntimeDaemonPreflight.activeAgentTasks` | `activeAgentTurns` | both required fields are returned and reference the same array throughout the 0.7.x line |
4368
+
4369
+ ### Plan capability is opt-in
4370
+
4371
+ `exit_plan_mode` is exposed to a Runtime run only when that run supplies an
4372
+ approval bridge. A daemon or headless host that cannot approve a plan should
4373
+ leave the bridge absent; KodaX removes the tool from that run's scope.
4374
+
4375
+ ```ts
4376
+ const planned = await runtime.runs.start({
4377
+ sessionId: session.id,
4378
+ prompt: 'Draft a migration plan, then ask for approval.',
4379
+ options: {
4380
+ events: {
4381
+ exitPlanMode: async () => showPlanAndAskUser(),
4382
+ },
4383
+ },
4384
+ });
4385
+ await planned.result;
4386
+ ```
4387
+
4388
+ The callback belongs in a trusted host process (for Electron, Main rather than
4389
+ renderer). It is intentionally not inferred from the presence of a permission
4390
+ UI: tool permission and plan approval are different user decisions.
4391
+
4392
+ See [ADR-056](ADR.md#adr-056-runtime-owns-auto-mode-permission-decisions-and-host-capability-exposure)
4393
+ for the ownership decision, [the v0.7.72 design](features/v0.7.72.md#2026-07-18-runtime-permission-queue-and-resume-closure)
4394
+ for the release boundary, and [Known Issue 187](KNOWN_ISSUES.md#187-shared-daemon-auto-permission-ownership-upgrade-fencing-preview-bounds-and-sdk-compatibility-were-incomplete)
4395
+ for the final capability-upgrade and compatibility closure.
4396
+
4397
+ ---
4398
+
4399
+ ## 25. Always-on context compaction and bounded transcript recovery (v0.7.74)
4400
+
4401
+ Automatic large compaction is always enabled. `enabled` remains accepted for
4402
+ v0.7.x source compatibility, but `false` is normalized to `true`.
4403
+ `triggerPercent` defaults to `75` and is clamped to `15..90`. The optional
4404
+ absolute threshold is inactive when omitted or zero; otherwise the smaller
4405
+ percentage, absolute, and physical-capacity threshold wins.
4406
+
4407
+ ```ts
4408
+ const run = await startKodaX({
4409
+ provider: 'zhipu-coding',
4410
+ model: 'glm-5.2',
4411
+ compaction: {
4412
+ triggerPercent: 60,
4413
+ triggerTokens: 300_000,
4414
+ },
4415
+ });
4416
+ ```
4417
+
4418
+ The recent raw tail is 20% of that effective trigger, not 20% of the model's
4419
+ maximum context window. A manual Runtime compact bypasses only the trigger
4420
+ comparison and uses the Session's same effective policy:
4421
+
4422
+ ```ts
4423
+ await runtime.sessions.updateSettings(session.id, {
4424
+ compactionTriggerPercent: 60,
4425
+ compactionTriggerTokens: 300_000,
4426
+ });
4427
+
4428
+ await runtime.sessions.compact({ sessionId: session.id });
4429
+ ```
4430
+
4431
+ Setting `compactionTriggerTokens: 0` removes the absolute Session override.
4432
+ Percentage updates are normalized to `15..90`; negative/fractional absolute
4433
+ values are rejected. Explicit per-run `options.compaction` values override
4434
+ Session settings.
4435
+
4436
+ Large compaction covers the complete eligible prefix once, preserves an atomic
4437
+ recent tail, and installs a synthetic user checkpoint. Every genuine user query
4438
+ is rendered mechanically in its checkpoint ledger; tool-result wire messages
4439
+ and synthetic prompts are excluded. The normal summary request preserves the
4440
+ main request's system, message, tool, model, and reasoning prefix and appends a
4441
+ text-only ephemeral instruction so providers can reuse prompt/KV cache.
4442
+
4443
+ The exact pre-compaction transcript has a separate durability guarantee. The
4444
+ root host persists all pre-compaction messages (including messages created in
4445
+ the active Run) before old payload is evicted from memory. Island records are
4446
+ flushed before a slim main JSONL is published. If the main replacement fails,
4447
+ main and sidecar may temporarily overlap, but stable entry IDs project the
4448
+ logical entry once. A persistence failure keeps the exact live copy; child
4449
+ compaction never writes root Session lineage.
4450
+
4451
+ The in-process `onCompactedMessages` callback may return a `Promise`. KodaX
4452
+ awaits it before the next provider request and before
4453
+ `context.compaction.finished`. Embedded and daemon Runtime execution always
4454
+ uses Runtime-owned Session storage, regardless of a client-side
4455
+ `persistedByHost` value; a daemon client cannot be the durability owner because
4456
+ its callback is not present in the daemon process. Runtime-backed Ink and
4457
+ classic REPL hosts therefore update only their live projection after the
4458
+ Runtime acknowledgement; they do not perform a second Session write. If a
4459
+ headless Runtime Run compacts before its first routine snapshot, Runtime seeds
4460
+ the new Session from explicit Run metadata before applying the exact compact
4461
+ transaction. A rejected durability callback also rolls back the tentative
4462
+ `contextRevision`, so a later successful compact does not expose a phantom gap.
4463
+
4464
+ ### Context-owned events
4465
+
4466
+ `context.compaction.finished` is the canonical post-commit Runtime fact. It
4467
+ includes stable root/child identity, revision, before/after tokens, strategy,
4468
+ effective trigger, protected budget, and component accounting. Consumers must
4469
+ not use `scope: 'worker'` as a parent/child identity substitute.
4470
+
4471
+ ```ts
4472
+ const subscription = runtime.events.subscribe(
4473
+ { sessionId: session.id, types: ['context.compaction.finished'] },
4474
+ (event) => {
4475
+ if (event.type !== 'context.compaction.finished') return;
4476
+ const fact = event.payload;
4477
+ if (fact.contextKind === 'root' && fact.committed) {
4478
+ renderRootContext(fact.tokensAfter, fact.tokensBefore);
4479
+ }
4480
+ },
4481
+ );
4482
+ ```
4483
+
4484
+ Legacy `onCompactStats`/`onCompact` callbacks remain compatibility projections.
4485
+ The old `onCompact` callback now receives the post-compact count; it no longer
4486
+ echoes the pre-compact `currentTokens` value. Hosts that need ownership or
4487
+ component metrics should use `onContextCompactionFinished` or the Runtime
4488
+ event. Compatibility success callbacks fire only after a strict token
4489
+ reduction has restored physical request validity and committed. An unchanged,
4490
+ failed, stale, or still-oversized candidate is not a successful compaction.
4491
+
4492
+ ### Transcript observation below the daemon frame limit
4493
+
4494
+ `sessions.observe()` no longer embeds `FullTranscriptSessionData`. Its snapshot
4495
+ contains a bounded `RuntimeTranscriptSlice`. Inline entries carry the complete
4496
+ transcript entry; an oversized entry carries an explicit descriptor.
4497
+
4498
+ ```ts
4499
+ const observation = await runtime.sessions.observe(session.id, onLiveEvent);
4500
+ let page = observation.snapshot.transcript;
4501
+
4502
+ while (page) {
4503
+ for (const descriptor of page.entries) {
4504
+ if (descriptor.entry) consume(descriptor.entry);
4505
+ else await consumeEntryChunks(runtime, session.id, page.revision, descriptor.index);
4506
+ }
4507
+ if (!page.hasMore) break;
4508
+ page = await runtime.sessions.transcriptPage({
4509
+ sessionId: session.id,
4510
+ cursor: page.nextCursor,
4511
+ });
4512
+ }
4513
+ ```
4514
+
4515
+ `transcriptEntryChunk()` returns lossless `base64-json` chunks. Concatenate the
4516
+ decoded bytes and parse JSON only after `hasMore` becomes false. Page and entry
4517
+ cursors are opaque and revision-bound; a changed transcript produces an
4518
+ explicit resync error, so restart from a fresh observation. The shared daemon's
4519
+ legacy `session.transcript` method rejects payloads above 512 KiB and names the
4520
+ page/chunk methods rather than attempting a frame near the 8 MiB ceiling.
4521
+
4522
+ ### Search compacted history before fetching exact content
4523
+
4524
+ Use `transcriptSearch()` when the host or user knows a historical detail but
4525
+ not its page/index. It searches the authoritative main-plus-sidecar lineage and
4526
+ returns bounded revision-bound hits with stable `entryId`/`logicalId`, entry
4527
+ index, role/source, timestamp, active/compacted status, and a citation:
4528
+
4529
+ ```ts
4530
+ const found = await runtime.sessions.transcriptSearch({
4531
+ sessionId: session.id,
4532
+ query: 'permission test output capture',
4533
+ role: 'assistant',
4534
+ limit: 5,
4535
+ });
4536
+
4537
+ for (const hit of found.hits) {
4538
+ renderSearchHit(hit.citation, hit.snippet);
4539
+ // For an oversized exact entry, pass found.revision + hit.entryIndex to
4540
+ // transcriptEntryChunk(); ordinary entries can be obtained from the page.
4541
+ }
4542
+ ```
4543
+
4544
+ Search is deterministic Unicode lexical/metadata ranking, not an embedding or
4545
+ background-model index. The Action LLM gets the corresponding
4546
+ `session_history_search` and `session_history_read` pair only when its current
4547
+ Run owns full-lineage-capable Session storage. A root Run reads its root
4548
+ lineage. A persistent child Run gets a separately minted hidden
4549
+ `managed-task-worker` Session and can recover only that child's compacted
4550
+ history; it is never given root-history access. Storage-less Runs and a tool
4551
+ visibility policy that hides either member expose neither member. Results are
4552
+ low-authority historical evidence; current instructions and freshly verified
4553
+ workspace state take precedence. System/control entries, hidden-only content,
4554
+ synthetic current or legacy `[对话历史摘要]` checkpoints, and `[compacted]`
4555
+ placeholders are neither searchable nor directly readable. Short ordinary
4556
+ terms do not gain a metadata match merely because they occur inside a random
4557
+ entry ID; direct identifier lookup is reserved for a sufficiently specific ID
4558
+ query. Sessions compacted by older builds without an exact main/sidecar copy
4559
+ cannot reconstruct bytes that were already discarded.
4560
+
4561
+ Clients that depend on these guarantees should require
4562
+ `contextCompaction: 3`, `transcriptPaging: 1`, and `transcriptSearch: 1` during
4563
+ connection.
4178
4564
 
4179
4565
  ---
4180
4566
 
4181
- ## 24. Runtime-owned Auto Mode and plan-approval bridges (v0.7.72–v0.7.73)
4182
-
4183
- Auto Mode is a Runtime session contract, including in shared-daemon mode. Do
4184
- not implement a second classifier or decide permissions from a client-side
4185
- `beforeToolExecute` hook before the Runtime has classified the call.
4186
-
4187
- ### Configure the session, not an individual UI callback
4188
-
4189
- ```ts
4190
- await runtime.sessions.updateSettings(session.id, {
4191
- permissionMode: 'auto',
4192
- autoModeEngine: 'llm',
4193
- autoModeClassifierModel: 'zhipu:glm-5.2', // optional; otherwise follow the run model
4194
- autoModeTimeoutMs: 20_000, // positive safe integer, optional
4195
- autoModeSpeculativeWindowMs: 0, // non-negative safe integer, optional
4196
- executionCwd: projectDirectory,
4197
- });
4198
- ```
4199
-
4200
- All three Auto fields are durable session settings in inline, Worker, and
4201
- daemon forms. A `null` patch removes an optional override. Timeout must be a
4202
- positive safe integer; speculative window must be non-negative, so `0` is a
4203
- valid request to wait for the actual verdict. Daemon capability discovery
4204
- advertises all fields in `sharedSessionSettings.keys`.
4205
-
4206
- SDK hosts that need config precedence without creating a REPL can reuse the
4207
- same typed resolver as KodaX:
4208
-
4209
- ```ts
4210
- import {
4211
- loadAutoModeSettings,
4212
- resolveAutoModeSettings,
4213
- type ResolveAutoModeSettingsInput,
4214
- } from '@kodax-ai/kodax/repl';
4215
-
4216
- const persisted = loadAutoModeSettings(process.env);
4217
- const preview = resolveAutoModeSettings({
4218
- settings: { engine: 'llm', speculativeWindowMs: 0 },
4219
- env: process.env,
4220
- });
4221
- ```
4222
-
4223
- `resolveAutoModeSettings()` is pure. `loadAutoModeSettings()` reads the KodaX
4224
- config once and delegates to that resolver; `loadConfig()` declares and
4225
- returns the same optional `autoMode` object.
4226
-
4227
- Starting with the v0.7.73 patch, `permissionMode: 'auto'` with an omitted
4228
- `autoModeEngine` still means the documented `llm` default and is still owned by
4229
- Runtime. If neither `autoModeClassifierModel` nor the effective run/session/
4230
- Runtime model exists, `runs.start()` rejects with
4231
- `RuntimeAutoModeConfigurationError` (`code:
4232
- 'auto_mode_classifier_model_required'`, `recoverable: true`) before provider
4233
- construction, a classifier call, or a pending permission. Blank and malformed
4234
- classifier model specs are rejected by the same typed configuration boundary;
4235
- a live rules-to-LLM switch is blocked rather than converted into approval work.
4236
-
4237
- Direct consumers of `createAutoModeToolGuardrail()` receive the same terminal
4238
- model boundary. After resolving CLI/env/session/settings/live-default
4239
- precedence, an empty effective model returns a local configuration `block`
4240
- before provider lookup. It does not call `askUser`, mutate the denial or
4241
- circuit-breaker trackers, or change the engine to rules. An explicit non-empty
4242
- classifier override remains valid even when the main-session model is empty.
4243
-
4244
- The Runtime owns one serialized permission-settings stream and one shared
4245
- engine/denial/breaker state per Session. It reuses bounded context-specific
4246
- guardrails across turns while provider/model, repository boundary, execution
4247
- directory, classifier model, and timeout remain the same. Updating one of
4248
- those inputs selects a new context guardrail by design without copying stale
4249
- state from a queued turn. Active runs, queued runs, explicit settings updates,
4250
- and automatic LLM-to-rules fallback merge through the same Session mutation
4251
- queue; fallback is persisted before a later classification reads the engine.
4252
-
4253
- ### What an embedder should expect
4567
+ ## 26. Agent mailbox control versus SDK event telemetry (v0.7.74)
4254
4568
 
4255
- The Runtime's execution order is fixed:
4569
+ The model-visible `wait_agent` tool and the public Runtime Actor event APIs have
4570
+ different jobs. Do not expose `runtime.agents.wait()` to the model as though it
4571
+ were the same operation.
4256
4572
 
4257
- ```text
4258
- Runtime Auto Mode guardrail -> host permission bridge only for escalate -> tool execution
4259
- ```
4260
-
4261
- Consequently, an LLM/rules `allow` does not create a pending permission request
4262
- just because a host installed a static approval hook. `block` does not become a
4263
- spurious approval prompt. A real `escalate` uses the existing shared
4264
- `runtime.permissions` flow, so another authorized client may render and answer
4265
- it. Hosts should subscribe to permission events to display such a request, but
4266
- must not treat a missing request as an error for a safe tool call.
4267
-
4268
- The classifier deadline remains 20 seconds by default and includes connection
4269
- setup, provider Retry-After/backoff, inference, and stream completion. KodaX
4270
- does not solve timeouts by extending that deadline indefinitely. Before the
4271
- provider call it removes assistant prose/thinking and image paths, limits each
4272
- tool result to 2 KiB and the serialized permission-relevant transcript to
4273
- 8 KiB, then enforces 16 KiB action and 32 KiB total-prompt ceilings plus a
4274
- 256-token output cap. An oversized action or prompt escalates without a
4275
- provider call; it is never truncated into an automatic allow. These limits are
4276
- owned by `classify()` itself, so custom callers cannot accidentally bypass the
4277
- session-history boundary.
4278
-
4279
- `ClassifyDecision.diagnostics` and the lower-level
4280
- `SideQueryResult.diagnostics` expose provider, model, effective timeout,
4281
- elapsed time, retry count/wait, and a coarse terminal phase without including
4282
- the prompt, action, messages, or response text. `pre_output` means no non-empty
4283
- text delta was observed; `streaming` means output began before termination.
4284
- `firstOutputMs` and `streamMs` are present only when the provider adapter emits
4285
- a text delta. The current provider API cannot honestly separate DNS/connect,
4286
- TLS, provider queueing, and inference, so embedders must not infer those stages
4287
- from `pre_output`.
4288
-
4289
- The permission event's `inputPreview` is a display-safe diagnostic projection:
4290
- it is bounded, credential-redacted, valid JSON, and includes the effective
4291
- execution directory. Use the Runtime owner’s typed tool input for execution;
4292
- do not reconstruct or authorize a tool from the preview. `gitRoot` remains the
4293
- session repository safety boundary, whereas relative operands resolve from the
4294
- validated `executionCwd`. In particular, quoted Python/JavaScript/regexp source
4295
- inside a shell command is not a path operand.
4296
-
4297
- The user-level `.kodax` directory is a credential/configuration boundary, not
4298
- an ordinary project path. Direct shell mutations, output redirects, and
4299
- recognized nested-shell payloads whose target is provably beneath that
4300
- directory are rejected before LLM classification. The check is segment-safe
4301
- and Windows case-insensitive. KodaX deliberately does not scan arbitrary
4302
- quoted language source for path-looking substrings: doing so would turn Python,
4303
- JavaScript, YAML, and regular expressions into false Tier-0 matches. Trusted
4304
- configuration changes should use the KodaX config CLI or SDK configuration API.
4305
-
4306
- ### 0.7.x source compatibility
4307
-
4308
- The v0.7.72 public declarations retain the following migration aliases:
4309
-
4310
- | Legacy source | Current source | Contract |
4573
+ | Need | API | Wake/data contract |
4311
4574
  |---|---|---|
4312
- | `agentMode: 'amaw'` | `'ama'` | accepted as deprecated input and normalized to AMA; no separate AMAW runtime is restored |
4313
- | `SkillSource` | `ResolvedSkillSource` | formal source union remains `project \| user \| plugin \| builtin`; only resolved discovery output adds `learned` |
4314
- | `RuntimeDaemonPreflight.activeAgentTasks` | `activeAgentTurns` | both required fields are returned and reference the same array throughout the 0.7.x line |
4315
-
4316
- ### Plan capability is opt-in
4575
+ | Let the action model yield until useful coordination evidence exists | `wait_agent({ timeout_ms })` | Caller-scoped Agent mailbox, root user input, interruption, or timeout; returns only a small acknowledgement. |
4576
+ | Render or diagnose Actor activity | `runtime.agents.events(sessionId, afterSequence?)` | Bounded event snapshot/replay, including progress and terminal events. |
4577
+ | Long-poll Actor telemetry from a host | `runtime.agents.wait(sessionId, afterSequence?, timeoutMs?)` | Returns the next sequenced Actor event, including progress. |
4578
+ | Read one known result | `runtime.agents.output(sessionId, actorPath, turnId?)` | Bounded current/terminal output and structured artifact metadata. |
4579
+ | Deliver a real user follow-up to the active root Run | `runtime.runs.submitInput(...)` | Ordered active-run input delivered at the next safe Runner boundary; requires `interruptInput:1`. |
4317
4580
 
4318
- `exit_plan_mode` is exposed to a Runtime run only when that run supplies an
4319
- approval bridge. A daemon or headless host that cannot approve a plan should
4320
- leave the bridge absent; KodaX removes the tool from that run's scope.
4581
+ For example, a host activity view can replay the current tail and then wait
4582
+ from its last sequence without causing another action-model request:
4321
4583
 
4322
4584
  ```ts
4323
- const planned = await runtime.runs.start({
4324
- sessionId: session.id,
4325
- prompt: 'Draft a migration plan, then ask for approval.',
4326
- options: {
4327
- events: {
4328
- exitPlanMode: async () => showPlanAndAskUser(),
4329
- },
4330
- },
4331
- });
4332
- await planned.result;
4333
- ```
4585
+ const snapshot = await runtime.agents.events(session.id);
4586
+ for (const event of snapshot) renderActorEvent(event);
4334
4587
 
4335
- The callback belongs in a trusted host process (for Electron, Main rather than
4336
- renderer). It is intentionally not inferred from the presence of a permission
4337
- UI: tool permission and plan approval are different user decisions.
4588
+ const afterSequence = snapshot.at(-1)?.sequence;
4589
+ const next = await runtime.agents.wait(session.id, afterSequence, 30_000);
4590
+ if (next) renderActorEvent(next);
4591
+ ```
4338
4592
 
4339
- See [ADR-056](ADR.md#adr-056-runtime-owns-auto-mode-permission-decisions-and-host-capability-exposure)
4340
- for the ownership decision, [the v0.7.72 design](features/v0.7.72.md#2026-07-18-runtime-permission-queue-and-resume-closure)
4341
- for the release boundary, and [Known Issue 187](KNOWN_ISSUES.md#187-shared-daemon-auto-permission-ownership-upgrade-fencing-preview-bounds-and-sdk-compatibility-were-incomplete)
4342
- for the final capability-upgrade and compatibility closure.
4593
+ Model `wait_agent` has only `timeout_ms` in its schema (10,000 to 3,600,000 ms,
4594
+ default 120,000). Actor progress and Runtime `system-reminder` messages do not
4595
+ end that wait. A scoped Agent message/completion produces `mailbox`; queued root
4596
+ input produces `user_input_pending`; cancellation and expiry produce
4597
+ `interrupted` and `wait_expired`. Authenticated Agent evidence is drained at the
4598
+ next safe Runner boundary as synthetic context, while root input remains a real
4599
+ user turn. The acknowledgement itself never carries raw event batches.
4600
+
4601
+ Completion delivery is post-transcript and crash-recoverable. The Actor snapshot
4602
+ persists the explicit root completion turn IDs that still await transcript
4603
+ acknowledgement. A hard restart republishes only those IDs; a same-process
4604
+ Runtime rebuild deduplicates the projected queue by child turn ID. Once the
4605
+ parent transcript commits and acknowledges the completion, later restores do
4606
+ not replay it. Legacy snapshots without the explicit pending set do not infer
4607
+ replay work from historical mailbox content.
4608
+
4609
+ This separation changes no Actor event capability or daemon version: existing
4610
+ SDK snapshot, replay, and long-poll clients keep their telemetry surface. It
4611
+ only prevents high-frequency progress from becoming a model control signal.
4343
4612
 
4344
4613
  ---
4345
4614
 
@@ -4347,5 +4616,8 @@ for the final capability-upgrade and compatibility closure.
4347
4616
 
4348
4617
  - [README.md](../README.md) — end-user CLI quick start
4349
4618
  - [docs/ADR.md ADR-024](ADR.md#adr-024-npm-发布物正名-kodax-aikodax--sdk-subpath-exports-形式化-v0739) — SDK subpath architecture rationale
4350
- - [docs/ADR.md ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742) — FEATURE_186 design record (all 8 phases)
4351
- - [docs/features/v0.7.42.md FEATURE_186](features/v0.7.42.md#feature_186-sdk-embedder-surface-closure--kodax-space-gap-list--mcp-popout) — gap-by-gap landing matrix
4619
+ - [docs/ADR.md ADR-032](ADR.md#adr-032-sdk-embedder-surface-closure-feature_186-v0742) — FEATURE_186 design record (all 8 phases)
4620
+ - [docs/ADR.md ADR-057](ADR.md#adr-057-large-compaction-is-an-always-on-context-scoped-full-coverage-transaction) — v0.7.74 compaction and exact-history ownership
4621
+ - [docs/ADR.md ADR-058](ADR.md#adr-058-model-agent-wait-is-mailbox-control-not-event-telemetry) — mailbox control versus Actor telemetry
4622
+ - [docs/features/v0.7.42.md FEATURE_186](features/v0.7.42.md#feature_186-sdk-embedder-surface-closure--kodax-space-gap-list--mcp-popout) — gap-by-gap landing matrix
4623
+ - [docs/features/v0.7.74.md](features/v0.7.74.md) — v0.7.74 release-candidate design and verification record