zcode-acp-server 0.47.10 → 0.48.0

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 (53) hide show
  1. package/README.md +1 -1
  2. package/dist/code-fingerprint.json +2 -2
  3. package/dist/config/workflow-gate.d.ts +97 -0
  4. package/dist/config/workflow-gate.d.ts.map +1 -0
  5. package/dist/config/workflow-gate.js +159 -0
  6. package/dist/config/workflow-gate.js.map +1 -0
  7. package/dist/handlers/background-tasks.d.ts.map +1 -1
  8. package/dist/handlers/background-tasks.js +56 -0
  9. package/dist/handlers/background-tasks.js.map +1 -1
  10. package/dist/handlers/dispatch.js +4 -0
  11. package/dist/handlers/dispatch.js.map +1 -1
  12. package/dist/handlers/session.d.ts +11 -0
  13. package/dist/handlers/session.d.ts.map +1 -1
  14. package/dist/handlers/session.js +71 -28
  15. package/dist/handlers/session.js.map +1 -1
  16. package/dist/handlers/slash.d.ts +7 -2
  17. package/dist/handlers/slash.d.ts.map +1 -1
  18. package/dist/handlers/slash.js +111 -4
  19. package/dist/handlers/slash.js.map +1 -1
  20. package/dist/i18n.d.ts +2 -0
  21. package/dist/i18n.d.ts.map +1 -1
  22. package/dist/i18n.js +6 -0
  23. package/dist/i18n.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +7 -3
  26. package/dist/index.js.map +1 -1
  27. package/dist/remote/session-close-endpoint.d.ts.map +1 -1
  28. package/dist/remote/session-close-endpoint.js +6 -0
  29. package/dist/remote/session-close-endpoint.js.map +1 -1
  30. package/dist/remote/settings-endpoint.d.ts.map +1 -1
  31. package/dist/remote/settings-endpoint.js +362 -3
  32. package/dist/remote/settings-endpoint.js.map +1 -1
  33. package/dist/server.d.ts +41 -0
  34. package/dist/server.d.ts.map +1 -1
  35. package/dist/server.js +73 -0
  36. package/dist/server.js.map +1 -1
  37. package/dist/settings/workflow.d.ts +139 -0
  38. package/dist/settings/workflow.d.ts.map +1 -0
  39. package/dist/settings/workflow.js +443 -0
  40. package/dist/settings/workflow.js.map +1 -0
  41. package/dist/translators/tool-helpers.d.ts.map +1 -1
  42. package/dist/translators/tool-helpers.js +4 -0
  43. package/dist/translators/tool-helpers.js.map +1 -1
  44. package/dist/utils.d.ts +9 -0
  45. package/dist/utils.d.ts.map +1 -1
  46. package/dist/utils.js +8 -0
  47. package/dist/utils.js.map +1 -1
  48. package/dist/workflow/poller.d.ts +68 -0
  49. package/dist/workflow/poller.d.ts.map +1 -0
  50. package/dist/workflow/poller.js +290 -0
  51. package/dist/workflow/poller.js.map +1 -0
  52. package/docs/BACKLOG.md +20 -2
  53. package/package.json +1 -1
@@ -24,13 +24,14 @@ import { buildProviderRegistry } from "../config/provider-registry.js";
24
24
  import { pushAccountProviderConfig } from "../config/account-provider.js";
25
25
  import { initialSessionMode } from "../config/settings.js";
26
26
  import { applyModelSwitch, buildResumeRuntimeModel } from "../config/runtime-model.js";
27
+ import { filterWorkflowCommands, rememberGate, workflowGateNow } from "../config/workflow-gate.js";
27
28
  import { messages } from "../i18n.js";
28
29
  import { lookupLazySession, lookupModelChoiceByZcodeSid, recordMaterializedSession, rememberLazySession, } from "../lazy-sessions.js";
29
30
  import { refreshTerminalTabTitle } from "../terminal-title.js";
30
31
  import { buildDiffContent, EventTranslator, extractLocations, formatTurnError, isBackendLostError, isTransientTurnError, ProjectionDiffer, } from "../translators/index.js";
31
32
  import { clientConnectionRoot, log, warn } from "../utils.js";
32
33
  import { dispatchEvent } from "./dispatch.js";
33
- import { emitSessionTurnState, sendSessionUpdate, sendTextChunk, withReplayBatch } from "./io.js";
34
+ import { emitSessionTurnState, sendAvailableCommandsDeferred, sendSessionUpdate, sendTextChunk, withReplayBatch, } from "./io.js";
34
35
  import { EDIT_DIFF_READ_LIMIT, fetchMessages, fetchMessagesSinceAnchor, fullSlice, readTailLimit, REPLY_READ_LIMIT, replayMessages, sliceTail, TURN_READ, } from "./replay.js";
35
36
  import { extractPermDeniedPath, extractSandboxDenial, GENERIC_HINT_KEY, handleSandboxDenial, READ_ONLY_TOOLS, } from "./sandbox-allow.js";
36
37
  import { handleServerRequests } from "./server-requests.js";
@@ -39,6 +40,60 @@ function workspaceFor(cwd) {
39
40
  const p = cwd || process.cwd();
40
41
  return { workspacePath: p, workspaceKey: p };
41
42
  }
43
+ /**
44
+ * Dynamic-workflow session flag (desktop-host parity, server.backendWorkflowGate):
45
+ * `{dynamicWorkflowEnabled:true}` rides every session/create·resume when the
46
+ * gate resolved enabled; otherwise NOTHING is written — the backend's zod
47
+ * schemas are strict, and the flag-absent default is exactly fail-closed.
48
+ * Awaiting the gate on the hot path is free (the verdict resolved at backend
49
+ * spawn); the cold-path bound is the gate fetch's 6s timeout, well under a
50
+ * backend boot. Fork and sub-agent sessions copy the flag inside the backend
51
+ * — no bridge-side injection point for those.
52
+ */
53
+ async function workflowFlag(server) {
54
+ const promise = server.backendWorkflowGate;
55
+ if (!promise)
56
+ return {};
57
+ const gate = await promise;
58
+ // Belt-and-braces vs captureGate (ensureBackend): an awaited verdict becomes
59
+ // synchronously readable immediately, so the send-time menu filter that runs
60
+ // right after create/resume sees it on its FIRST call.
61
+ rememberGate(promise, gate);
62
+ return gate.enabled ? { dynamicWorkflowEnabled: true } : {};
63
+ }
64
+ /**
65
+ * Cold-bridge menu catch-up: the `/` menu snapshot sent at session/new is
66
+ * filtered against the workflow gate, and on a COLD bridge (lazy session/new,
67
+ * no backend yet) that snapshot was taken while the gate was still
68
+ * pending/absent — the two workflow commands get dropped for that whole
69
+ * session. By the time a lazy session MATERIALIZES the gate has necessarily
70
+ * settled (create/resume params awaited it), so re-send the menu here.
71
+ * Overwrite semantics + the deferred helper's timer cancellation make this
72
+ * idempotent and correctly ordered (a late settle supersedes the stale send).
73
+ */
74
+ export function resendMenuAfterGateSettled(server, acpSid) {
75
+ if (!server.allCommands || !workflowGateNow(server)?.enabled)
76
+ return;
77
+ for (const sid of server.sessionAliases(acpSid)) {
78
+ sendAvailableCommandsDeferred(server.clients, sid, filterWorkflowCommands(server, server.allCommands));
79
+ }
80
+ }
81
+ /**
82
+ * Shared `session/resume` params builder: {sessionId, workspace} + optional
83
+ * session-lifetime mcpServers + the conditional dynamic-workflow flag. Every
84
+ * resume construction site (session/resume, /resume adoption, session/load,
85
+ * eviction/respawn reload) goes through here so the param shape cannot drift.
86
+ */
87
+ async function resumeParams(server, zcodeSid, cwd, mcpServers) {
88
+ const params = {
89
+ sessionId: zcodeSid,
90
+ workspace: workspaceFor(cwd),
91
+ ...(await workflowFlag(server)),
92
+ };
93
+ if (mcpServers)
94
+ params.mcpServers = mcpServers;
95
+ return params;
96
+ }
42
97
  /**
43
98
  * A client-supplied cwd is only ever trusted for `session/new` — creating a
44
99
  * session is the editor declaring its worktree. "/" is never a project root:
@@ -510,6 +565,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
510
565
  if (opts.ensureResident !== false) {
511
566
  await ensureBackendResident(server, acpSid, record.zcodeSid);
512
567
  }
568
+ // Materialization (resume flight) settled the gate — refresh a menu
569
+ // snapshot that may predate the verdict (see resendMenuAfterGateSettled).
570
+ resendMenuAfterGateSettled(server, acpSid);
513
571
  return record.zcodeSid;
514
572
  }
515
573
  if (record) {
@@ -550,6 +608,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
550
608
  // ZCODE_ACP_MODE; the default stays "yolo" (unrestricted), which is
551
609
  // what the bridge always used.
552
610
  mode: initialSessionMode(),
611
+ // Dynamic-workflow enable rides create only when the gate says so —
612
+ // workflowFlag writes nothing otherwise (strict zod).
613
+ ...(await workflowFlag(server)),
553
614
  };
554
615
  if (pending.mcpServers && pending.mcpServers.length > 0) {
555
616
  createParams.mcpServers = pending.mcpServers;
@@ -598,6 +659,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
598
659
  recordMaterializedSession(acpSid, sid, pending.cwd);
599
660
  log(`session/new ${acpSid} → created ${sid} (lazy, on first use)`);
600
661
  await server.ensureBackgroundListener(sid);
662
+ // Cold-bridge catch-up: the session/new menu snapshot was filtered on a
663
+ // pending gate; the create above settled it — re-send for this session.
664
+ resendMenuAfterGateSettled(server, acpSid);
601
665
  // Sync to the App's tasks-index.sqlite so the App UI shows this session.
602
666
  // Best-effort; failures are logged inside upsertSessionTask and swallowed.
603
667
  const { upsertSessionTask } = await import("../tasks-index.js");
@@ -812,18 +876,12 @@ export async function resumeSession(server, params, cx) {
812
876
  // the faithful resume fails outright). The params deliberately carry NO
813
877
  // apiKey either (the backend's schema rejects it; it resolves auth from
814
878
  // its own config/OAuth store).
815
- const zcParams = {
816
- sessionId: zcodeSid,
817
- workspace: workspaceFor(cwd),
818
- };
819
879
  // ACP session/resume may also carry mcpServers; the backend's resume
820
880
  // schema accepts the same array shape (verified: an unknown key would be
821
881
  // rejected before the session lookup). Remembered as session-lifetime
822
882
  // state so later reloads keep re-sending them (#193).
823
883
  rememberSessionMcpServers(server, acpSid, params.mcpServers);
824
- const storedMcp = server.sessionMcpServers.get(acpSid);
825
- if (storedMcp)
826
- zcParams.mcpServers = storedMcp;
884
+ const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
827
885
  // Push the provider registry BEFORE resume: a resumed session may carry a
828
886
  // third-party model in its history, and the backend needs the provider
829
887
  // registered to even process the resume turn.
@@ -955,14 +1013,9 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
955
1013
  let settledHistory;
956
1014
  try {
957
1015
  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
- });
1016
+ const outcome = await resumePreservingModel(server,
1017
+ // Session-lifetime MCP set keeps riding every resume (#193).
1018
+ await resumeParams(server, zcodeTarget, cwd, server.sessionMcpServers.get(acpSid)));
966
1019
  settledHistory = outcome.history;
967
1020
  server.markBackendLoaded(acpSid);
968
1021
  await repairUnavailableModel(server, zcodeTarget);
@@ -1032,16 +1085,10 @@ export async function loadSession(server, params, cx, opts = {}) {
1032
1085
  // snapshot is the mid-hydration-prefix guard; undefined = alreadyLive).
1033
1086
  let settledHistory;
1034
1087
  if (!alreadyLive) {
1035
- const zcParams = {
1036
- sessionId: zcodeSid,
1037
- workspace: workspaceFor(cwd),
1038
- };
1039
1088
  // Same mcpServers contract as resume: the client may re-declare them on
1040
1089
  // load; a stored set keeps riding along when it doesn't (#193).
1041
1090
  rememberSessionMcpServers(server, acpSid, params.mcpServers);
1042
- const storedMcp = server.sessionMcpServers.get(acpSid);
1043
- if (storedMcp)
1044
- zcParams.mcpServers = storedMcp;
1091
+ const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
1045
1092
  // Push the provider registry BEFORE resume: a loaded session may carry a
1046
1093
  // third-party model in its history, and the backend needs the provider
1047
1094
  // registered to process it.
@@ -2458,17 +2505,13 @@ async function resumeBackendSession(server, zcParams) {
2458
2505
  */
2459
2506
  export async function reloadBackendSession(server, acpSid, zcodeSid) {
2460
2507
  const cwd = server.sessionCwds.get(acpSid) ?? process.cwd();
2461
- const zcParams = {
2462
- sessionId: zcodeSid,
2463
- workspace: workspaceFor(cwd),
2464
- };
2465
2508
  // Eviction/respawn reload: the backend lost the per-load mcpServers runtime
2466
2509
  // config with its resident session — re-send or the tools vanish (#193).
2467
2510
  const reloadMcp = server.sessionMcpServers.get(acpSid);
2468
2511
  if (reloadMcp) {
2469
- zcParams.mcpServers = reloadMcp;
2470
2512
  log(`session/resume (reload) carrying ${reloadMcp.length} client MCP server(s)`);
2471
2513
  }
2514
+ const zcParams = await resumeParams(server, zcodeSid, cwd, reloadMcp);
2472
2515
  // Same pre-resume steps as the ACP resume/load handlers: register the
2473
2516
  // provider registry (a resumed session's history references a model the
2474
2517
  // fresh backend can't process until its provider is registered — sends