zcode-acp-server 0.47.10 → 0.48.1

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 (73) hide show
  1. package/README.md +1 -1
  2. package/dist/backend/adapter.d.ts +129 -0
  3. package/dist/backend/adapter.d.ts.map +1 -0
  4. package/dist/backend/adapter.js +71 -0
  5. package/dist/backend/adapter.js.map +1 -0
  6. package/dist/backend/client.d.ts +14 -103
  7. package/dist/backend/client.d.ts.map +1 -1
  8. package/dist/backend/client.js +63 -413
  9. package/dist/backend/client.js.map +1 -1
  10. package/dist/backend/index.d.ts +3 -1
  11. package/dist/backend/index.d.ts.map +1 -1
  12. package/dist/backend/index.js +2 -0
  13. package/dist/backend/index.js.map +1 -1
  14. package/dist/backend/jsonrpc-child.d.ts +128 -0
  15. package/dist/backend/jsonrpc-child.d.ts.map +1 -0
  16. package/dist/backend/jsonrpc-child.js +397 -0
  17. package/dist/backend/jsonrpc-child.js.map +1 -0
  18. package/dist/code-fingerprint.json +2 -2
  19. package/dist/config/options.d.ts.map +1 -1
  20. package/dist/config/options.js +14 -4
  21. package/dist/config/options.js.map +1 -1
  22. package/dist/config/workflow-gate.d.ts +97 -0
  23. package/dist/config/workflow-gate.d.ts.map +1 -0
  24. package/dist/config/workflow-gate.js +159 -0
  25. package/dist/config/workflow-gate.js.map +1 -0
  26. package/dist/handlers/background-tasks.d.ts.map +1 -1
  27. package/dist/handlers/background-tasks.js +56 -0
  28. package/dist/handlers/background-tasks.js.map +1 -1
  29. package/dist/handlers/dispatch.js +4 -0
  30. package/dist/handlers/dispatch.js.map +1 -1
  31. package/dist/handlers/session.d.ts +11 -0
  32. package/dist/handlers/session.d.ts.map +1 -1
  33. package/dist/handlers/session.js +80 -32
  34. package/dist/handlers/session.js.map +1 -1
  35. package/dist/handlers/slash.d.ts +7 -2
  36. package/dist/handlers/slash.d.ts.map +1 -1
  37. package/dist/handlers/slash.js +115 -4
  38. package/dist/handlers/slash.js.map +1 -1
  39. package/dist/i18n.d.ts +2 -0
  40. package/dist/i18n.d.ts.map +1 -1
  41. package/dist/i18n.js +6 -0
  42. package/dist/i18n.js.map +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +7 -3
  45. package/dist/index.js.map +1 -1
  46. package/dist/remote/session-close-endpoint.d.ts.map +1 -1
  47. package/dist/remote/session-close-endpoint.js +6 -0
  48. package/dist/remote/session-close-endpoint.js.map +1 -1
  49. package/dist/remote/settings-endpoint.d.ts.map +1 -1
  50. package/dist/remote/settings-endpoint.js +380 -3
  51. package/dist/remote/settings-endpoint.js.map +1 -1
  52. package/dist/server.d.ts +49 -1
  53. package/dist/server.d.ts.map +1 -1
  54. package/dist/server.js +80 -0
  55. package/dist/server.js.map +1 -1
  56. package/dist/settings/workflow.d.ts +139 -0
  57. package/dist/settings/workflow.d.ts.map +1 -0
  58. package/dist/settings/workflow.js +443 -0
  59. package/dist/settings/workflow.js.map +1 -0
  60. package/dist/translators/tool-helpers.d.ts.map +1 -1
  61. package/dist/translators/tool-helpers.js +4 -0
  62. package/dist/translators/tool-helpers.js.map +1 -1
  63. package/dist/utils.d.ts +9 -0
  64. package/dist/utils.d.ts.map +1 -1
  65. package/dist/utils.js +8 -0
  66. package/dist/utils.js.map +1 -1
  67. package/dist/workflow/poller.d.ts +68 -0
  68. package/dist/workflow/poller.d.ts.map +1 -0
  69. package/dist/workflow/poller.js +290 -0
  70. package/dist/workflow/poller.js.map +1 -0
  71. package/docs/ARCHITECTURE.md +8 -6
  72. package/docs/BACKLOG.md +20 -2
  73. package/package.json +1 -1
@@ -15,6 +15,7 @@ import path from "node:path";
15
15
  import { randomUUID } from "node:crypto";
16
16
  import { realpathSync } from "node:fs";
17
17
  import { RequestError } from "@agentclientprotocol/sdk";
18
+ import { backendCapabilities } from "../backend/adapter.js";
18
19
  import { EventStreamListener, TurnMonitor } from "../backend/listener.js";
19
20
  import { resolveReal } from "../backend/sandbox.js";
20
21
  import { buildModes, buildConfigOptions, DEFAULT_MODEL_ID, DEFAULT_PROVIDER_ID, formatModelValue, loadAllModels, modelContextWindow, parseModelValue, } from "../config/options.js";
@@ -24,13 +25,14 @@ import { buildProviderRegistry } from "../config/provider-registry.js";
24
25
  import { pushAccountProviderConfig } from "../config/account-provider.js";
25
26
  import { initialSessionMode } from "../config/settings.js";
26
27
  import { applyModelSwitch, buildResumeRuntimeModel } from "../config/runtime-model.js";
28
+ import { filterWorkflowCommands, rememberGate, workflowGateNow } from "../config/workflow-gate.js";
27
29
  import { messages } from "../i18n.js";
28
30
  import { lookupLazySession, lookupModelChoiceByZcodeSid, recordMaterializedSession, rememberLazySession, } from "../lazy-sessions.js";
29
31
  import { refreshTerminalTabTitle } from "../terminal-title.js";
30
32
  import { buildDiffContent, EventTranslator, extractLocations, formatTurnError, isBackendLostError, isTransientTurnError, ProjectionDiffer, } from "../translators/index.js";
31
33
  import { clientConnectionRoot, log, warn } from "../utils.js";
32
34
  import { dispatchEvent } from "./dispatch.js";
33
- import { emitSessionTurnState, sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
35
+ import { emitSessionTurnState, sendAvailableCommandsDeferred, sendSessionUpdate, sendTextChunk, withReplayBatch, } from "./io.js";
34
36
  import { EDIT_DIFF_READ_LIMIT, fetchMessages, fetchMessagesSinceAnchor, fullSlice, readTailLimit, REPLY_READ_LIMIT, replayMessages, sliceTail, TURN_READ, } from "./replay.js";
35
37
  import { extractPermDeniedPath, extractSandboxDenial, GENERIC_HINT_KEY, handleSandboxDenial, READ_ONLY_TOOLS, } from "./sandbox-allow.js";
36
38
  import { handleServerRequests } from "./server-requests.js";
@@ -39,6 +41,60 @@ function workspaceFor(cwd) {
39
41
  const p = cwd || process.cwd();
40
42
  return { workspacePath: p, workspaceKey: p };
41
43
  }
44
+ /**
45
+ * Dynamic-workflow session flag (desktop-host parity, server.backendWorkflowGate):
46
+ * `{dynamicWorkflowEnabled:true}` rides every session/create·resume when the
47
+ * gate resolved enabled; otherwise NOTHING is written — the backend's zod
48
+ * schemas are strict, and the flag-absent default is exactly fail-closed.
49
+ * Awaiting the gate on the hot path is free (the verdict resolved at backend
50
+ * spawn); the cold-path bound is the gate fetch's 6s timeout, well under a
51
+ * backend boot. Fork and sub-agent sessions copy the flag inside the backend
52
+ * — no bridge-side injection point for those.
53
+ */
54
+ async function workflowFlag(server) {
55
+ const promise = server.backendWorkflowGate;
56
+ if (!promise)
57
+ return {};
58
+ const gate = await promise;
59
+ // Belt-and-braces vs captureGate (ensureBackend): an awaited verdict becomes
60
+ // synchronously readable immediately, so the send-time menu filter that runs
61
+ // right after create/resume sees it on its FIRST call.
62
+ rememberGate(promise, gate);
63
+ return gate.enabled ? { dynamicWorkflowEnabled: true } : {};
64
+ }
65
+ /**
66
+ * Cold-bridge menu catch-up: the `/` menu snapshot sent at session/new is
67
+ * filtered against the workflow gate, and on a COLD bridge (lazy session/new,
68
+ * no backend yet) that snapshot was taken while the gate was still
69
+ * pending/absent — the two workflow commands get dropped for that whole
70
+ * session. By the time a lazy session MATERIALIZES the gate has necessarily
71
+ * settled (create/resume params awaited it), so re-send the menu here.
72
+ * Overwrite semantics + the deferred helper's timer cancellation make this
73
+ * idempotent and correctly ordered (a late settle supersedes the stale send).
74
+ */
75
+ export function resendMenuAfterGateSettled(server, acpSid) {
76
+ if (!server.allCommands || !workflowGateNow(server)?.enabled)
77
+ return;
78
+ for (const sid of server.sessionAliases(acpSid)) {
79
+ sendAvailableCommandsDeferred(server.clients, sid, filterWorkflowCommands(server, server.allCommands));
80
+ }
81
+ }
82
+ /**
83
+ * Shared `session/resume` params builder: {sessionId, workspace} + optional
84
+ * session-lifetime mcpServers + the conditional dynamic-workflow flag. Every
85
+ * resume construction site (session/resume, /resume adoption, session/load,
86
+ * eviction/respawn reload) goes through here so the param shape cannot drift.
87
+ */
88
+ async function resumeParams(server, zcodeSid, cwd, mcpServers) {
89
+ const params = {
90
+ sessionId: zcodeSid,
91
+ workspace: workspaceFor(cwd),
92
+ ...(await workflowFlag(server)),
93
+ };
94
+ if (mcpServers)
95
+ params.mcpServers = mcpServers;
96
+ return params;
97
+ }
42
98
  /**
43
99
  * A client-supplied cwd is only ever trusted for `session/new` — creating a
44
100
  * session is the editor declaring its worktree. "/" is never a project root:
@@ -212,7 +268,8 @@ export async function newSession(server, params, client) {
212
268
  // connection is martty. Identity is per-connection — recorded at the
213
269
  // connection's own initialize (server.ts), never the sticky process flag,
214
270
  // so a phone app's session/new must not become a quota-dock target.
215
- if (server.marttyConnectionRoots.has(clientConnectionRoot(client))) {
271
+ if (backendCapabilities(server.backendKind).quota &&
272
+ server.marttyConnectionRoots.has(clientConnectionRoot(client))) {
216
273
  startQuotaRefresher(server);
217
274
  }
218
275
  // Hub session-create binding (remote create, ADR-0016): adopt the hub's
@@ -255,7 +312,8 @@ export async function newSession(server, params, client) {
255
312
  // Per-connection martty identity, NOT the sticky process flag: the phone
256
313
  // claims this same bind below, and a sticky-gated arm re-pointed the
257
314
  // handshake at the phone — the TUI's trigger then reached the model.
258
- if (server.marttyConnectionRoots.has(bindRoot)) {
315
+ if (backendCapabilities(server.backendKind).bootResumeHandshake &&
316
+ server.marttyConnectionRoots.has(bindRoot)) {
259
317
  server.bootResumeTriggerConnection = bindRoot;
260
318
  }
261
319
  const modes = await buildModes(server, null);
@@ -289,7 +347,8 @@ export async function newSession(server, params, client) {
289
347
  // into its transcript. The chunks sit BEHIND martty's welcome banner
290
348
  // until text is submitted — the DSH_TUI_AUTOPROMPT handshake armed
291
349
  // below drops the banner for the user.
292
- if (server.marttyConnectionRoots.has(clientConnectionRoot(client))) {
350
+ if (backendCapabilities(server.backendKind).bootResumeHandshake &&
351
+ server.marttyConnectionRoots.has(clientConnectionRoot(client))) {
293
352
  // Scope the handshake to THIS connection: a phone app attached to the
294
353
  // same bridge may prompt during the boot window and must not disarm it.
295
354
  // Per-connection martty identity, NOT the sticky process flag — an
@@ -510,6 +569,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
510
569
  if (opts.ensureResident !== false) {
511
570
  await ensureBackendResident(server, acpSid, record.zcodeSid);
512
571
  }
572
+ // Materialization (resume flight) settled the gate — refresh a menu
573
+ // snapshot that may predate the verdict (see resendMenuAfterGateSettled).
574
+ resendMenuAfterGateSettled(server, acpSid);
513
575
  return record.zcodeSid;
514
576
  }
515
577
  if (record) {
@@ -550,6 +612,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
550
612
  // ZCODE_ACP_MODE; the default stays "yolo" (unrestricted), which is
551
613
  // what the bridge always used.
552
614
  mode: initialSessionMode(),
615
+ // Dynamic-workflow enable rides create only when the gate says so —
616
+ // workflowFlag writes nothing otherwise (strict zod).
617
+ ...(await workflowFlag(server)),
553
618
  };
554
619
  if (pending.mcpServers && pending.mcpServers.length > 0) {
555
620
  createParams.mcpServers = pending.mcpServers;
@@ -598,6 +663,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
598
663
  recordMaterializedSession(acpSid, sid, pending.cwd);
599
664
  log(`session/new ${acpSid} → created ${sid} (lazy, on first use)`);
600
665
  await server.ensureBackgroundListener(sid);
666
+ // Cold-bridge catch-up: the session/new menu snapshot was filtered on a
667
+ // pending gate; the create above settled it — re-send for this session.
668
+ resendMenuAfterGateSettled(server, acpSid);
601
669
  // Sync to the App's tasks-index.sqlite so the App UI shows this session.
602
670
  // Best-effort; failures are logged inside upsertSessionTask and swallowed.
603
671
  const { upsertSessionTask } = await import("../tasks-index.js");
@@ -812,18 +880,12 @@ export async function resumeSession(server, params, cx) {
812
880
  // the faithful resume fails outright). The params deliberately carry NO
813
881
  // apiKey either (the backend's schema rejects it; it resolves auth from
814
882
  // its own config/OAuth store).
815
- const zcParams = {
816
- sessionId: zcodeSid,
817
- workspace: workspaceFor(cwd),
818
- };
819
883
  // ACP session/resume may also carry mcpServers; the backend's resume
820
884
  // schema accepts the same array shape (verified: an unknown key would be
821
885
  // rejected before the session lookup). Remembered as session-lifetime
822
886
  // state so later reloads keep re-sending them (#193).
823
887
  rememberSessionMcpServers(server, acpSid, params.mcpServers);
824
- const storedMcp = server.sessionMcpServers.get(acpSid);
825
- if (storedMcp)
826
- zcParams.mcpServers = storedMcp;
888
+ const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
827
889
  // Push the provider registry BEFORE resume: a resumed session may carry a
828
890
  // third-party model in its history, and the backend needs the provider
829
891
  // registered to even process the resume turn.
@@ -955,14 +1017,9 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
955
1017
  let settledHistory;
956
1018
  try {
957
1019
  await syncProviderRegistry(server, cwd);
958
- const outcome = await resumePreservingModel(server, {
959
- sessionId: zcodeTarget,
960
- workspace: workspaceFor(cwd),
961
- // Session-lifetime MCP set keeps riding every resume (#193).
962
- ...(server.sessionMcpServers.get(acpSid)
963
- ? { mcpServers: server.sessionMcpServers.get(acpSid) }
964
- : {}),
965
- });
1020
+ const outcome = await resumePreservingModel(server,
1021
+ // Session-lifetime MCP set keeps riding every resume (#193).
1022
+ await resumeParams(server, zcodeTarget, cwd, server.sessionMcpServers.get(acpSid)));
966
1023
  settledHistory = outcome.history;
967
1024
  server.markBackendLoaded(acpSid);
968
1025
  await repairUnavailableModel(server, zcodeTarget);
@@ -1032,16 +1089,10 @@ export async function loadSession(server, params, cx, opts = {}) {
1032
1089
  // snapshot is the mid-hydration-prefix guard; undefined = alreadyLive).
1033
1090
  let settledHistory;
1034
1091
  if (!alreadyLive) {
1035
- const zcParams = {
1036
- sessionId: zcodeSid,
1037
- workspace: workspaceFor(cwd),
1038
- };
1039
1092
  // Same mcpServers contract as resume: the client may re-declare them on
1040
1093
  // load; a stored set keeps riding along when it doesn't (#193).
1041
1094
  rememberSessionMcpServers(server, acpSid, params.mcpServers);
1042
- const storedMcp = server.sessionMcpServers.get(acpSid);
1043
- if (storedMcp)
1044
- zcParams.mcpServers = storedMcp;
1095
+ const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
1045
1096
  // Push the provider registry BEFORE resume: a loaded session may carry a
1046
1097
  // third-party model in its history, and the backend needs the provider
1047
1098
  // registered to process it.
@@ -1724,7 +1775,8 @@ async function runPrompt(server, params, cx, requestId, continuationRound = fals
1724
1775
  // trigger verbatim is that handshake; a phone typing the same words is
1725
1776
  // real input (marttyConnectionRoots does not contain it) and goes to the
1726
1777
  // model as typed.
1727
- server.bootResumeTriggerConnection === null &&
1778
+ backendCapabilities(server.backendKind).bootResumeHandshake &&
1779
+ server.bootResumeTriggerConnection === null &&
1728
1780
  text === BOOT_RESUME_TRIGGER &&
1729
1781
  server.marttyConnectionRoots.has(promptRoot) &&
1730
1782
  !server.connectionPromptSeen.has(promptRoot)) {
@@ -2458,17 +2510,13 @@ async function resumeBackendSession(server, zcParams) {
2458
2510
  */
2459
2511
  export async function reloadBackendSession(server, acpSid, zcodeSid) {
2460
2512
  const cwd = server.sessionCwds.get(acpSid) ?? process.cwd();
2461
- const zcParams = {
2462
- sessionId: zcodeSid,
2463
- workspace: workspaceFor(cwd),
2464
- };
2465
2513
  // Eviction/respawn reload: the backend lost the per-load mcpServers runtime
2466
2514
  // config with its resident session — re-send or the tools vanish (#193).
2467
2515
  const reloadMcp = server.sessionMcpServers.get(acpSid);
2468
2516
  if (reloadMcp) {
2469
- zcParams.mcpServers = reloadMcp;
2470
2517
  log(`session/resume (reload) carrying ${reloadMcp.length} client MCP server(s)`);
2471
2518
  }
2519
+ const zcParams = await resumeParams(server, zcodeSid, cwd, reloadMcp);
2472
2520
  // Same pre-resume steps as the ACP resume/load handlers: register the
2473
2521
  // provider registry (a resumed session's history references a model the
2474
2522
  // fresh backend can't process until its provider is registered — sends