zcode-acp-server 0.47.9 → 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.
- package/README.md +1 -1
- package/dist/code-fingerprint.json +2 -2
- package/dist/config/workflow-gate.d.ts +97 -0
- package/dist/config/workflow-gate.d.ts.map +1 -0
- package/dist/config/workflow-gate.js +159 -0
- package/dist/config/workflow-gate.js.map +1 -0
- package/dist/handlers/background-tasks.d.ts.map +1 -1
- package/dist/handlers/background-tasks.js +56 -0
- package/dist/handlers/background-tasks.js.map +1 -1
- package/dist/handlers/dispatch.js +4 -0
- package/dist/handlers/dispatch.js.map +1 -1
- package/dist/handlers/session.d.ts +11 -0
- package/dist/handlers/session.d.ts.map +1 -1
- package/dist/handlers/session.js +109 -37
- package/dist/handlers/session.js.map +1 -1
- package/dist/handlers/slash.d.ts +7 -2
- package/dist/handlers/slash.d.ts.map +1 -1
- package/dist/handlers/slash.js +111 -4
- package/dist/handlers/slash.js.map +1 -1
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +6 -0
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/remote/session-close-endpoint.d.ts.map +1 -1
- package/dist/remote/session-close-endpoint.js +6 -0
- package/dist/remote/session-close-endpoint.js.map +1 -1
- package/dist/remote/settings-endpoint.d.ts.map +1 -1
- package/dist/remote/settings-endpoint.js +362 -3
- package/dist/remote/settings-endpoint.js.map +1 -1
- package/dist/server.d.ts +55 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +87 -0
- package/dist/server.js.map +1 -1
- package/dist/settings/workflow.d.ts +139 -0
- package/dist/settings/workflow.d.ts.map +1 -0
- package/dist/settings/workflow.js +443 -0
- package/dist/settings/workflow.js.map +1 -0
- package/dist/translators/tool-helpers.d.ts.map +1 -1
- package/dist/translators/tool-helpers.js +4 -0
- package/dist/translators/tool-helpers.js.map +1 -1
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +8 -0
- package/dist/utils.js.map +1 -1
- package/dist/workflow/poller.d.ts +68 -0
- package/dist/workflow/poller.d.ts.map +1 -0
- package/dist/workflow/poller.js +290 -0
- package/dist/workflow/poller.js.map +1 -0
- package/docs/BACKLOG.md +20 -2
- package/package.json +1 -1
package/dist/handlers/session.js
CHANGED
|
@@ -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:
|
|
@@ -252,7 +307,10 @@ export async function newSession(server, params, client) {
|
|
|
252
307
|
// Same banner handshake as boot-resume: the TUI's auto-submitted trigger
|
|
253
308
|
// (DSH_TUI_AUTOPROMPT) drops martty's welcome banner so the window shows
|
|
254
309
|
// the shared conversation instead of waiting for local input.
|
|
255
|
-
|
|
310
|
+
// Per-connection martty identity, NOT the sticky process flag: the phone
|
|
311
|
+
// claims this same bind below, and a sticky-gated arm re-pointed the
|
|
312
|
+
// handshake at the phone — the TUI's trigger then reached the model.
|
|
313
|
+
if (server.marttyConnectionRoots.has(bindRoot)) {
|
|
256
314
|
server.bootResumeTriggerConnection = bindRoot;
|
|
257
315
|
}
|
|
258
316
|
const modes = await buildModes(server, null);
|
|
@@ -286,9 +344,12 @@ export async function newSession(server, params, client) {
|
|
|
286
344
|
// into its transcript. The chunks sit BEHIND martty's welcome banner
|
|
287
345
|
// until text is submitted — the DSH_TUI_AUTOPROMPT handshake armed
|
|
288
346
|
// below drops the banner for the user.
|
|
289
|
-
if (
|
|
347
|
+
if (server.marttyConnectionRoots.has(clientConnectionRoot(client))) {
|
|
290
348
|
// Scope the handshake to THIS connection: a phone app attached to the
|
|
291
349
|
// same bridge may prompt during the boot window and must not disarm it.
|
|
350
|
+
// Per-connection martty identity, NOT the sticky process flag — an
|
|
351
|
+
// attaching phone's session/new can win the race for the env; it must
|
|
352
|
+
// neither arm for itself nor leave a wrong arm for the TUI's trigger.
|
|
292
353
|
server.bootResumeTriggerConnection = clientConnectionRoot(client);
|
|
293
354
|
// Targeted replay — the booting TUI's own connection, never broadcast
|
|
294
355
|
// (see the loadSession comment above).
|
|
@@ -504,6 +565,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
|
|
|
504
565
|
if (opts.ensureResident !== false) {
|
|
505
566
|
await ensureBackendResident(server, acpSid, record.zcodeSid);
|
|
506
567
|
}
|
|
568
|
+
// Materialization (resume flight) settled the gate — refresh a menu
|
|
569
|
+
// snapshot that may predate the verdict (see resendMenuAfterGateSettled).
|
|
570
|
+
resendMenuAfterGateSettled(server, acpSid);
|
|
507
571
|
return record.zcodeSid;
|
|
508
572
|
}
|
|
509
573
|
if (record) {
|
|
@@ -544,6 +608,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
|
|
|
544
608
|
// ZCODE_ACP_MODE; the default stays "yolo" (unrestricted), which is
|
|
545
609
|
// what the bridge always used.
|
|
546
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)),
|
|
547
614
|
};
|
|
548
615
|
if (pending.mcpServers && pending.mcpServers.length > 0) {
|
|
549
616
|
createParams.mcpServers = pending.mcpServers;
|
|
@@ -592,6 +659,9 @@ export async function ensureRealSession(server, acpSid, opts = {}) {
|
|
|
592
659
|
recordMaterializedSession(acpSid, sid, pending.cwd);
|
|
593
660
|
log(`session/new ${acpSid} → created ${sid} (lazy, on first use)`);
|
|
594
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);
|
|
595
665
|
// Sync to the App's tasks-index.sqlite so the App UI shows this session.
|
|
596
666
|
// Best-effort; failures are logged inside upsertSessionTask and swallowed.
|
|
597
667
|
const { upsertSessionTask } = await import("../tasks-index.js");
|
|
@@ -806,18 +876,12 @@ export async function resumeSession(server, params, cx) {
|
|
|
806
876
|
// the faithful resume fails outright). The params deliberately carry NO
|
|
807
877
|
// apiKey either (the backend's schema rejects it; it resolves auth from
|
|
808
878
|
// its own config/OAuth store).
|
|
809
|
-
const zcParams = {
|
|
810
|
-
sessionId: zcodeSid,
|
|
811
|
-
workspace: workspaceFor(cwd),
|
|
812
|
-
};
|
|
813
879
|
// ACP session/resume may also carry mcpServers; the backend's resume
|
|
814
880
|
// schema accepts the same array shape (verified: an unknown key would be
|
|
815
881
|
// rejected before the session lookup). Remembered as session-lifetime
|
|
816
882
|
// state so later reloads keep re-sending them (#193).
|
|
817
883
|
rememberSessionMcpServers(server, acpSid, params.mcpServers);
|
|
818
|
-
const
|
|
819
|
-
if (storedMcp)
|
|
820
|
-
zcParams.mcpServers = storedMcp;
|
|
884
|
+
const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
|
|
821
885
|
// Push the provider registry BEFORE resume: a resumed session may carry a
|
|
822
886
|
// third-party model in its history, and the backend needs the provider
|
|
823
887
|
// registered to even process the resume turn.
|
|
@@ -949,14 +1013,9 @@ export async function resumeIntoSession(server, cx, acpSid, zcodeTarget) {
|
|
|
949
1013
|
let settledHistory;
|
|
950
1014
|
try {
|
|
951
1015
|
await syncProviderRegistry(server, cwd);
|
|
952
|
-
const outcome = await resumePreservingModel(server,
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
// Session-lifetime MCP set keeps riding every resume (#193).
|
|
956
|
-
...(server.sessionMcpServers.get(acpSid)
|
|
957
|
-
? { mcpServers: server.sessionMcpServers.get(acpSid) }
|
|
958
|
-
: {}),
|
|
959
|
-
});
|
|
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)));
|
|
960
1019
|
settledHistory = outcome.history;
|
|
961
1020
|
server.markBackendLoaded(acpSid);
|
|
962
1021
|
await repairUnavailableModel(server, zcodeTarget);
|
|
@@ -1026,16 +1085,10 @@ export async function loadSession(server, params, cx, opts = {}) {
|
|
|
1026
1085
|
// snapshot is the mid-hydration-prefix guard; undefined = alreadyLive).
|
|
1027
1086
|
let settledHistory;
|
|
1028
1087
|
if (!alreadyLive) {
|
|
1029
|
-
const zcParams = {
|
|
1030
|
-
sessionId: zcodeSid,
|
|
1031
|
-
workspace: workspaceFor(cwd),
|
|
1032
|
-
};
|
|
1033
1088
|
// Same mcpServers contract as resume: the client may re-declare them on
|
|
1034
1089
|
// load; a stored set keeps riding along when it doesn't (#193).
|
|
1035
1090
|
rememberSessionMcpServers(server, acpSid, params.mcpServers);
|
|
1036
|
-
const
|
|
1037
|
-
if (storedMcp)
|
|
1038
|
-
zcParams.mcpServers = storedMcp;
|
|
1091
|
+
const zcParams = await resumeParams(server, zcodeSid, cwd, server.sessionMcpServers.get(acpSid));
|
|
1039
1092
|
// Push the provider registry BEFORE resume: a loaded session may carry a
|
|
1040
1093
|
// third-party model in its history, and the backend needs the provider
|
|
1041
1094
|
// registered to process it.
|
|
@@ -1695,16 +1748,39 @@ async function runPrompt(server, params, cx, requestId, continuationRound = fals
|
|
|
1695
1748
|
// connection submitting anything else first means the auto-submit was
|
|
1696
1749
|
// lost — disarm so the trigger text typed manually later stays a normal
|
|
1697
1750
|
// prompt.
|
|
1751
|
+
const promptRoot = clientConnectionRoot(client);
|
|
1752
|
+
const ackBootResumeTrigger = async () => {
|
|
1753
|
+
await sendTextChunk(cx, params.sessionId, messages().bootResumeAck, randomUUID());
|
|
1754
|
+
log("session/prompt: boot-resume banner handshake acknowledged");
|
|
1755
|
+
await emitSessionTurnState(server, params.sessionId, false, cx);
|
|
1756
|
+
return { stopReason: "end_turn" };
|
|
1757
|
+
};
|
|
1698
1758
|
if (server.bootResumeTriggerConnection !== null &&
|
|
1699
|
-
|
|
1759
|
+
promptRoot === server.bootResumeTriggerConnection) {
|
|
1700
1760
|
server.bootResumeTriggerConnection = null;
|
|
1701
1761
|
if (text === BOOT_RESUME_TRIGGER) {
|
|
1702
|
-
await
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1762
|
+
return await ackBootResumeTrigger();
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
else if (
|
|
1766
|
+
// Unarmed fallback: the boot-resume env was consumed by a NON-martty
|
|
1767
|
+
// first session/new (an attaching phone can win the race against the
|
|
1768
|
+
// TUI's boot), so no arm exists for the TUI — its auto-submitted trigger
|
|
1769
|
+
// still needs the ack or it reaches the model (observed from the mobile
|
|
1770
|
+
// app, 2026-09-23). A MARTTY connection's first prompt matching the
|
|
1771
|
+
// trigger verbatim is that handshake; a phone typing the same words is
|
|
1772
|
+
// real input (marttyConnectionRoots does not contain it) and goes to the
|
|
1773
|
+
// model as typed.
|
|
1774
|
+
server.bootResumeTriggerConnection === null &&
|
|
1775
|
+
text === BOOT_RESUME_TRIGGER &&
|
|
1776
|
+
server.marttyConnectionRoots.has(promptRoot) &&
|
|
1777
|
+
!server.connectionPromptSeen.has(promptRoot)) {
|
|
1778
|
+
return await ackBootResumeTrigger();
|
|
1779
|
+
}
|
|
1780
|
+
// First-prompt bookkeeping for the unarmed fallback above: only a martty
|
|
1781
|
+
// connection's FIRST prompt is a handshake candidate.
|
|
1782
|
+
if (promptRoot !== undefined)
|
|
1783
|
+
server.connectionPromptSeen.add(promptRoot);
|
|
1708
1784
|
// Materialize a lazy session/new placeholder on first use. Placed after the
|
|
1709
1785
|
// empty-prompt check so an invalid request doesn't create a backend session.
|
|
1710
1786
|
const zcodeSid = await ensureRealSession(server, params.sessionId);
|
|
@@ -2429,17 +2505,13 @@ async function resumeBackendSession(server, zcParams) {
|
|
|
2429
2505
|
*/
|
|
2430
2506
|
export async function reloadBackendSession(server, acpSid, zcodeSid) {
|
|
2431
2507
|
const cwd = server.sessionCwds.get(acpSid) ?? process.cwd();
|
|
2432
|
-
const zcParams = {
|
|
2433
|
-
sessionId: zcodeSid,
|
|
2434
|
-
workspace: workspaceFor(cwd),
|
|
2435
|
-
};
|
|
2436
2508
|
// Eviction/respawn reload: the backend lost the per-load mcpServers runtime
|
|
2437
2509
|
// config with its resident session — re-send or the tools vanish (#193).
|
|
2438
2510
|
const reloadMcp = server.sessionMcpServers.get(acpSid);
|
|
2439
2511
|
if (reloadMcp) {
|
|
2440
|
-
zcParams.mcpServers = reloadMcp;
|
|
2441
2512
|
log(`session/resume (reload) carrying ${reloadMcp.length} client MCP server(s)`);
|
|
2442
2513
|
}
|
|
2514
|
+
const zcParams = await resumeParams(server, zcodeSid, cwd, reloadMcp);
|
|
2443
2515
|
// Same pre-resume steps as the ACP resume/load handlers: register the
|
|
2444
2516
|
// provider registry (a resumed session's history references a model the
|
|
2445
2517
|
// fresh backend can't process until its provider is registered — sends
|