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.
- package/README.md +1 -1
- package/dist/backend/adapter.d.ts +129 -0
- package/dist/backend/adapter.d.ts.map +1 -0
- package/dist/backend/adapter.js +71 -0
- package/dist/backend/adapter.js.map +1 -0
- package/dist/backend/client.d.ts +14 -103
- package/dist/backend/client.d.ts.map +1 -1
- package/dist/backend/client.js +63 -413
- package/dist/backend/client.js.map +1 -1
- package/dist/backend/index.d.ts +3 -1
- package/dist/backend/index.d.ts.map +1 -1
- package/dist/backend/index.js +2 -0
- package/dist/backend/index.js.map +1 -1
- package/dist/backend/jsonrpc-child.d.ts +128 -0
- package/dist/backend/jsonrpc-child.d.ts.map +1 -0
- package/dist/backend/jsonrpc-child.js +397 -0
- package/dist/backend/jsonrpc-child.js.map +1 -0
- package/dist/code-fingerprint.json +2 -2
- package/dist/config/options.d.ts.map +1 -1
- package/dist/config/options.js +14 -4
- package/dist/config/options.js.map +1 -1
- 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 +80 -32
- 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 +115 -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 +380 -3
- package/dist/remote/settings-endpoint.js.map +1 -1
- package/dist/server.d.ts +49 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -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/ARCHITECTURE.md +8 -6
- package/docs/BACKLOG.md +20 -2
- package/package.json +1 -1
package/dist/handlers/session.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
960
|
-
|
|
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
|
|
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.
|
|
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
|