zcode-acp-server 0.1.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/LICENSE +201 -0
- package/README.md +167 -0
- package/README.zh-CN.md +163 -0
- package/dist/backend/client.d.ts +103 -0
- package/dist/backend/client.d.ts.map +1 -0
- package/dist/backend/client.js +344 -0
- package/dist/backend/client.js.map +1 -0
- package/dist/backend/credentials.d.ts +31 -0
- package/dist/backend/credentials.d.ts.map +1 -0
- package/dist/backend/credentials.js +93 -0
- package/dist/backend/credentials.js.map +1 -0
- package/dist/backend/index.d.ts +7 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +6 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/listener.d.ts +63 -0
- package/dist/backend/listener.d.ts.map +1 -0
- package/dist/backend/listener.js +138 -0
- package/dist/backend/listener.js.map +1 -0
- package/dist/backend/resolve.d.ts +11 -0
- package/dist/backend/resolve.d.ts.map +1 -0
- package/dist/backend/resolve.js +116 -0
- package/dist/backend/resolve.js.map +1 -0
- package/dist/backend/types.d.ts +164 -0
- package/dist/backend/types.d.ts.map +1 -0
- package/dist/backend/types.js +15 -0
- package/dist/backend/types.js.map +1 -0
- package/dist/config/model-cache.d.ts +20 -0
- package/dist/config/model-cache.d.ts.map +1 -0
- package/dist/config/model-cache.js +62 -0
- package/dist/config/model-cache.js.map +1 -0
- package/dist/config/options.d.ts +36 -0
- package/dist/config/options.d.ts.map +1 -0
- package/dist/config/options.js +171 -0
- package/dist/config/options.js.map +1 -0
- package/dist/config/runtime-model.d.ts +33 -0
- package/dist/config/runtime-model.d.ts.map +1 -0
- package/dist/config/runtime-model.js +96 -0
- package/dist/config/runtime-model.js.map +1 -0
- package/dist/handlers/dispatch.d.ts +15 -0
- package/dist/handlers/dispatch.d.ts.map +1 -0
- package/dist/handlers/dispatch.js +183 -0
- package/dist/handlers/dispatch.js.map +1 -0
- package/dist/handlers/extensions.d.ts +43 -0
- package/dist/handlers/extensions.d.ts.map +1 -0
- package/dist/handlers/extensions.js +310 -0
- package/dist/handlers/extensions.js.map +1 -0
- package/dist/handlers/io.d.ts +40 -0
- package/dist/handlers/io.d.ts.map +1 -0
- package/dist/handlers/io.js +55 -0
- package/dist/handlers/io.js.map +1 -0
- package/dist/handlers/server-requests.d.ts +34 -0
- package/dist/handlers/server-requests.d.ts.map +1 -0
- package/dist/handlers/server-requests.js +357 -0
- package/dist/handlers/server-requests.js.map +1 -0
- package/dist/handlers/session.d.ts +46 -0
- package/dist/handlers/session.d.ts.map +1 -0
- package/dist/handlers/session.js +738 -0
- package/dist/handlers/session.js.map +1 -0
- package/dist/handlers/slash.d.ts +16 -0
- package/dist/handlers/slash.d.ts.map +1 -0
- package/dist/handlers/slash.js +107 -0
- package/dist/handlers/slash.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/interaction/adapter.d.ts +136 -0
- package/dist/interaction/adapter.d.ts.map +1 -0
- package/dist/interaction/adapter.js +353 -0
- package/dist/interaction/adapter.js.map +1 -0
- package/dist/server.d.ts +73 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +97 -0
- package/dist/server.js.map +1 -0
- package/dist/tasks-index.d.ts +39 -0
- package/dist/tasks-index.d.ts.map +1 -0
- package/dist/tasks-index.js +152 -0
- package/dist/tasks-index.js.map +1 -0
- package/dist/translators/event-translator.d.ts +40 -0
- package/dist/translators/event-translator.d.ts.map +1 -0
- package/dist/translators/event-translator.js +214 -0
- package/dist/translators/event-translator.js.map +1 -0
- package/dist/translators/index.d.ts +6 -0
- package/dist/translators/index.d.ts.map +1 -0
- package/dist/translators/index.js +5 -0
- package/dist/translators/index.js.map +1 -0
- package/dist/translators/projection-differ.d.ts +48 -0
- package/dist/translators/projection-differ.d.ts.map +1 -0
- package/dist/translators/projection-differ.js +239 -0
- package/dist/translators/projection-differ.js.map +1 -0
- package/dist/translators/tool-helpers.d.ts +60 -0
- package/dist/translators/tool-helpers.d.ts.map +1 -0
- package/dist/translators/tool-helpers.js +308 -0
- package/dist/translators/tool-helpers.js.map +1 -0
- package/dist/translators/types.d.ts +58 -0
- package/dist/translators/types.d.ts.map +1 -0
- package/dist/translators/types.js +27 -0
- package/dist/translators/types.js.map +1 -0
- package/dist/utils.d.ts +111 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +110 -0
- package/dist/utils.js.map +1 -0
- package/docs/ARCHITECTURE.md +299 -0
- package/docs/DEVELOPMENT.md +193 -0
- package/docs/PROTOCOL.md +649 -0
- package/docs/TROUBLESHOOTING.md +251 -0
- package/package.json +66 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal event-dict shape — the seam between translators and the dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Both `EventTranslator` (event-stream path) and `ProjectionDiffer` (snapshot
|
|
5
|
+
* path) emit these discriminated unions; `dispatchEvent` consumes them and
|
|
6
|
+
* serialises each into an ACP `session/update` notification.
|
|
7
|
+
*/
|
|
8
|
+
import type { PlanEntry, ToolCallContent, ToolCallLocation, ToolCallStatus, ToolKind } from "@agentclientprotocol/sdk";
|
|
9
|
+
export interface ToolCallNewEvent {
|
|
10
|
+
kind: "ToolCallNew";
|
|
11
|
+
callId: string;
|
|
12
|
+
tool: string;
|
|
13
|
+
acpKind: ToolKind;
|
|
14
|
+
status: ToolCallStatus;
|
|
15
|
+
title: string;
|
|
16
|
+
input?: unknown;
|
|
17
|
+
output?: string;
|
|
18
|
+
content?: ToolCallContent[];
|
|
19
|
+
diffContent?: ToolCallContent[];
|
|
20
|
+
locations?: ToolCallLocation[];
|
|
21
|
+
}
|
|
22
|
+
export interface ToolCallUpdateEvent {
|
|
23
|
+
kind: "ToolCallUpdate";
|
|
24
|
+
callId: string;
|
|
25
|
+
tool?: string;
|
|
26
|
+
status: ToolCallStatus;
|
|
27
|
+
output?: string;
|
|
28
|
+
rawOutput?: unknown;
|
|
29
|
+
rawResult?: unknown;
|
|
30
|
+
content?: ToolCallContent[];
|
|
31
|
+
diffContent?: ToolCallContent[];
|
|
32
|
+
locations?: ToolCallLocation[];
|
|
33
|
+
}
|
|
34
|
+
export interface UsageDeltaEvent {
|
|
35
|
+
kind: "UsageDelta";
|
|
36
|
+
used: number;
|
|
37
|
+
size: number;
|
|
38
|
+
}
|
|
39
|
+
export interface TextDeltaEvent {
|
|
40
|
+
kind: "TextDelta";
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ReasoningDeltaEvent {
|
|
44
|
+
kind: "ReasoningDelta";
|
|
45
|
+
text: string;
|
|
46
|
+
}
|
|
47
|
+
export interface PlanUpdateEvent {
|
|
48
|
+
kind: "PlanUpdate";
|
|
49
|
+
entries: PlanEntry[];
|
|
50
|
+
}
|
|
51
|
+
/** A plan entry being built before dispatch (status/priority are normalised here). */
|
|
52
|
+
export declare function makePlanEntry(content: string, status: string, priority?: string): PlanEntry;
|
|
53
|
+
export interface FilesChangedEvent {
|
|
54
|
+
kind: "FilesChanged";
|
|
55
|
+
files: string[];
|
|
56
|
+
}
|
|
57
|
+
export type InternalEvent = ToolCallNewEvent | ToolCallUpdateEvent | UsageDeltaEvent | TextDeltaEvent | ReasoningDeltaEvent | PlanUpdateEvent | FilesChangedEvent;
|
|
58
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/translators/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EACV,SAAS,EAGT,eAAe,EACf,gBAAgB,EAChB,cAAc,EACd,QAAQ,EACT,MAAM,0BAA0B,CAAC;AAElC,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,QAAQ,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,eAAe,EAAE,CAAC;IAC5B,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,gBAAgB,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,SAAS,EAAE,CAAC;CACtB;AAED,sFAAsF;AACtF,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAM3F;AAcD,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GACrB,gBAAgB,GAChB,mBAAmB,GACnB,eAAe,GACf,cAAc,GACd,mBAAmB,GACnB,eAAe,GACf,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal event-dict shape — the seam between translators and the dispatcher.
|
|
3
|
+
*
|
|
4
|
+
* Both `EventTranslator` (event-stream path) and `ProjectionDiffer` (snapshot
|
|
5
|
+
* path) emit these discriminated unions; `dispatchEvent` consumes them and
|
|
6
|
+
* serialises each into an ACP `session/update` notification.
|
|
7
|
+
*/
|
|
8
|
+
/** A plan entry being built before dispatch (status/priority are normalised here). */
|
|
9
|
+
export function makePlanEntry(content, status, priority) {
|
|
10
|
+
return {
|
|
11
|
+
content,
|
|
12
|
+
status: normalisePlanStatus(status),
|
|
13
|
+
priority: normalisePlanPriority(priority),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function normalisePlanStatus(s) {
|
|
17
|
+
if (s === "completed" || s === "in_progress" || s === "pending") {
|
|
18
|
+
return s;
|
|
19
|
+
}
|
|
20
|
+
return "pending";
|
|
21
|
+
}
|
|
22
|
+
function normalisePlanPriority(p) {
|
|
23
|
+
if (p === "high" || p === "medium" || p === "low")
|
|
24
|
+
return p;
|
|
25
|
+
return "medium";
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/translators/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AA4DH,sFAAsF;AACtF,MAAM,UAAU,aAAa,CAAC,OAAe,EAAE,MAAc,EAAE,QAAiB;IAC9E,OAAO;QACL,OAAO;QACP,MAAM,EAAE,mBAAmB,CAAC,MAAM,CAAC;QACnC,QAAQ,EAAE,qBAAqB,CAAC,QAAQ,CAAC;KAC1C,CAAC;AACJ,CAAC;AAED,SAAS,mBAAmB,CAAC,CAAS;IACpC,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,aAAa,IAAI,CAAC,KAAK,SAAS,EAAE,CAAC;QAChE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,qBAAqB,CAAC,CAAqB;IAClD,IAAI,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,KAAK;QAAE,OAAO,CAAC,CAAC;IAC5D,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities: logging and project-wide constants.
|
|
3
|
+
*
|
|
4
|
+
* Logging goes to stderr so it never corrupts the stdout ACP protocol stream.
|
|
5
|
+
*/
|
|
6
|
+
/** ACP protocol version this server speaks. */
|
|
7
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
8
|
+
/** Agent identity advertised in the initialize response. */
|
|
9
|
+
export declare const AGENT_INFO: {
|
|
10
|
+
readonly name: "zcode-acp-server";
|
|
11
|
+
readonly title: "ZCode";
|
|
12
|
+
readonly version: "0.1.0";
|
|
13
|
+
};
|
|
14
|
+
/** Path to the ZCode v2 config (credentials + provider/model metadata). */
|
|
15
|
+
export declare const ZCODE_CREDS_PATH: string;
|
|
16
|
+
/**
|
|
17
|
+
* Slash commands surfaced to the editor. Each maps to a ZCode session method
|
|
18
|
+
* that the server forwards when the user types the command.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SLASH_COMMANDS: readonly [{
|
|
21
|
+
readonly name: "compact";
|
|
22
|
+
readonly description: "Compress conversation context (free up tokens)";
|
|
23
|
+
}, {
|
|
24
|
+
readonly name: "goal";
|
|
25
|
+
readonly description: "Set or show the session goal";
|
|
26
|
+
readonly input: {
|
|
27
|
+
readonly hint: "goal description";
|
|
28
|
+
};
|
|
29
|
+
}, {
|
|
30
|
+
readonly name: "fork";
|
|
31
|
+
readonly description: "Fork the session at the latest checkpoint";
|
|
32
|
+
}, {
|
|
33
|
+
readonly name: "rewind";
|
|
34
|
+
readonly description: "Rewind workspace files to the latest checkpoint";
|
|
35
|
+
}, {
|
|
36
|
+
readonly name: "steer";
|
|
37
|
+
readonly description: "Append an instruction to a running turn";
|
|
38
|
+
readonly input: {
|
|
39
|
+
readonly hint: "content";
|
|
40
|
+
};
|
|
41
|
+
}, {
|
|
42
|
+
readonly name: "mode";
|
|
43
|
+
readonly description: "Switch permission mode (plan/build/edit/yolo)";
|
|
44
|
+
readonly input: {
|
|
45
|
+
readonly hint: "plan|build|edit|yolo";
|
|
46
|
+
};
|
|
47
|
+
}, {
|
|
48
|
+
readonly name: "model";
|
|
49
|
+
readonly description: "Switch the session model";
|
|
50
|
+
readonly input: {
|
|
51
|
+
readonly hint: "GLM-5.2|GLM-5-Turbo";
|
|
52
|
+
};
|
|
53
|
+
}, {
|
|
54
|
+
readonly name: "thought";
|
|
55
|
+
readonly description: "Set the reasoning effort";
|
|
56
|
+
readonly input: {
|
|
57
|
+
readonly hint: "max|high|nothink";
|
|
58
|
+
};
|
|
59
|
+
}];
|
|
60
|
+
/** Static metadata for the configOptions selects (model/mode/thought). */
|
|
61
|
+
export declare const CONFIG_META: {
|
|
62
|
+
readonly model: {
|
|
63
|
+
readonly name: "Model";
|
|
64
|
+
readonly category: "model";
|
|
65
|
+
readonly options: Array<{
|
|
66
|
+
value: string;
|
|
67
|
+
name: string;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
readonly mode: {
|
|
71
|
+
readonly name: "Mode";
|
|
72
|
+
readonly category: "mode";
|
|
73
|
+
readonly options: readonly [{
|
|
74
|
+
readonly value: "plan";
|
|
75
|
+
readonly name: "plan";
|
|
76
|
+
}, {
|
|
77
|
+
readonly value: "build";
|
|
78
|
+
readonly name: "build";
|
|
79
|
+
}, {
|
|
80
|
+
readonly value: "edit";
|
|
81
|
+
readonly name: "edit";
|
|
82
|
+
}, {
|
|
83
|
+
readonly value: "yolo";
|
|
84
|
+
readonly name: "yolo";
|
|
85
|
+
}];
|
|
86
|
+
};
|
|
87
|
+
readonly thought: {
|
|
88
|
+
readonly name: "Thought Level";
|
|
89
|
+
readonly category: "thought_level";
|
|
90
|
+
readonly options: readonly [{
|
|
91
|
+
readonly value: "max";
|
|
92
|
+
readonly name: "max";
|
|
93
|
+
}, {
|
|
94
|
+
readonly value: "high";
|
|
95
|
+
readonly name: "high";
|
|
96
|
+
}, {
|
|
97
|
+
readonly value: "nothink";
|
|
98
|
+
readonly name: "nothink";
|
|
99
|
+
}];
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
/** configId → zcode method + param key (model deliberately absent — switch via runtimeModel). */
|
|
103
|
+
export declare const CONFIG_DISPATCH: Record<string, {
|
|
104
|
+
method: string;
|
|
105
|
+
paramKey: string;
|
|
106
|
+
}>;
|
|
107
|
+
/** Verbose diagnostic log. Only emitted when `ZCODE_ACP_DEBUG=1`. */
|
|
108
|
+
export declare function log(msg: string): void;
|
|
109
|
+
/** Warning — always emitted. For perceivable failures. */
|
|
110
|
+
export declare function warn(msg: string): void;
|
|
111
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,+CAA+C;AAC/C,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC,4DAA4D;AAC5D,eAAO,MAAM,UAAU;;;;CAIb,CAAC;AAEX,2EAA2E;AAC3E,eAAO,MAAM,gBAAgB,QAK5B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA6BjB,CAAC;AAEX,0EAA0E;AAC1E,eAAO,MAAM,WAAW;;;;0BAIL,KAAK,CAAC;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqB/C,CAAC;AAEX,iGAAiG;AACjG,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAGhF,CAAC;AAoBF,qEAAqE;AACrE,wBAAgB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAGrC;AAED,0DAA0D;AAC1D,wBAAgB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAEtC"}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities: logging and project-wide constants.
|
|
3
|
+
*
|
|
4
|
+
* Logging goes to stderr so it never corrupts the stdout ACP protocol stream.
|
|
5
|
+
*/
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import process from "node:process";
|
|
8
|
+
/** ACP protocol version this server speaks. */
|
|
9
|
+
export const PROTOCOL_VERSION = 1;
|
|
10
|
+
/** Agent identity advertised in the initialize response. */
|
|
11
|
+
export const AGENT_INFO = {
|
|
12
|
+
name: "zcode-acp-server",
|
|
13
|
+
title: "ZCode",
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
};
|
|
16
|
+
/** Path to the ZCode v2 config (credentials + provider/model metadata). */
|
|
17
|
+
export const ZCODE_CREDS_PATH = path.join(process.env.HOME || process.env.USERPROFILE || "~", ".zcode", "v2", "config.json");
|
|
18
|
+
/**
|
|
19
|
+
* Slash commands surfaced to the editor. Each maps to a ZCode session method
|
|
20
|
+
* that the server forwards when the user types the command.
|
|
21
|
+
*/
|
|
22
|
+
export const SLASH_COMMANDS = [
|
|
23
|
+
{ name: "compact", description: "Compress conversation context (free up tokens)" },
|
|
24
|
+
{
|
|
25
|
+
name: "goal",
|
|
26
|
+
description: "Set or show the session goal",
|
|
27
|
+
input: { hint: "goal description" },
|
|
28
|
+
},
|
|
29
|
+
{ name: "fork", description: "Fork the session at the latest checkpoint" },
|
|
30
|
+
{ name: "rewind", description: "Rewind workspace files to the latest checkpoint" },
|
|
31
|
+
{
|
|
32
|
+
name: "steer",
|
|
33
|
+
description: "Append an instruction to a running turn",
|
|
34
|
+
input: { hint: "content" },
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: "mode",
|
|
38
|
+
description: "Switch permission mode (plan/build/edit/yolo)",
|
|
39
|
+
input: { hint: "plan|build|edit|yolo" },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "model",
|
|
43
|
+
description: "Switch the session model",
|
|
44
|
+
input: { hint: "GLM-5.2|GLM-5-Turbo" },
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "thought",
|
|
48
|
+
description: "Set the reasoning effort",
|
|
49
|
+
input: { hint: "max|high|nothink" },
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
/** Static metadata for the configOptions selects (model/mode/thought). */
|
|
53
|
+
export const CONFIG_META = {
|
|
54
|
+
model: {
|
|
55
|
+
name: "Model",
|
|
56
|
+
category: "model",
|
|
57
|
+
options: [],
|
|
58
|
+
},
|
|
59
|
+
mode: {
|
|
60
|
+
name: "Mode",
|
|
61
|
+
category: "mode",
|
|
62
|
+
options: [
|
|
63
|
+
{ value: "plan", name: "plan" },
|
|
64
|
+
{ value: "build", name: "build" },
|
|
65
|
+
{ value: "edit", name: "edit" },
|
|
66
|
+
{ value: "yolo", name: "yolo" },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
thought: {
|
|
70
|
+
name: "Thought Level",
|
|
71
|
+
category: "thought_level",
|
|
72
|
+
options: [
|
|
73
|
+
{ value: "max", name: "max" },
|
|
74
|
+
{ value: "high", name: "high" },
|
|
75
|
+
{ value: "nothink", name: "nothink" },
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
/** configId → zcode method + param key (model deliberately absent — switch via runtimeModel). */
|
|
80
|
+
export const CONFIG_DISPATCH = {
|
|
81
|
+
mode: { method: "session/setMode", paramKey: "mode" },
|
|
82
|
+
thought: { method: "session/setThoughtLevel", paramKey: "thoughtLevel" },
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Logging. Two levels, both write to stderr (stdout is reserved for the ACP
|
|
86
|
+
* JSON-RPC stream):
|
|
87
|
+
* - `warn(msg)`: always emitted — failures the user can perceive (fatal
|
|
88
|
+
* exit, broken backend pipe, command/permission failures, etc.).
|
|
89
|
+
* - `log(msg)`: verbose diagnostic detail — only emitted when
|
|
90
|
+
* `ZCODE_ACP_DEBUG=1` is set. Default (unset) keeps the log quiet so
|
|
91
|
+
* a stable bridge doesn't spam `Zed.log`.
|
|
92
|
+
*
|
|
93
|
+
* Never use `console.log` — it would corrupt the stdout protocol stream.
|
|
94
|
+
*/
|
|
95
|
+
/** True when the user opted into verbose diagnostics.
|
|
96
|
+
* Read at call time so tests can flip it without re-importing the module. */
|
|
97
|
+
function isDebug() {
|
|
98
|
+
return process.env.ZCODE_ACP_DEBUG === "1";
|
|
99
|
+
}
|
|
100
|
+
/** Verbose diagnostic log. Only emitted when `ZCODE_ACP_DEBUG=1`. */
|
|
101
|
+
export function log(msg) {
|
|
102
|
+
if (!isDebug())
|
|
103
|
+
return;
|
|
104
|
+
process.stderr.write(`[zcode-acp] ${msg}\n`);
|
|
105
|
+
}
|
|
106
|
+
/** Warning — always emitted. For perceivable failures. */
|
|
107
|
+
export function warn(msg) {
|
|
108
|
+
process.stderr.write(`[zcode-acp] ${msg}\n`);
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,OAAO,MAAM,cAAc,CAAC;AAEnC,+CAA+C;AAC/C,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC,4DAA4D;AAC5D,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,IAAI,EAAE,kBAAkB;IACxB,KAAK,EAAE,OAAO;IACd,OAAO,EAAE,OAAO;CACR,CAAC;AAEX,2EAA2E;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CACvC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,GAAG,EAClD,QAAQ,EACR,IAAI,EACJ,aAAa,CACd,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,gDAAgD,EAAE;IAClF;QACE,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,8BAA8B;QAC3C,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE;KACpC;IACD,EAAE,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,2CAA2C,EAAE;IAC1E,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,iDAAiD,EAAE;IAClF;QACE,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,yCAAyC;QACtD,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE;KAC3B;IACD;QACE,IAAI,EAAE,MAAM;QACZ,WAAW,EAAE,+CAA+C;QAC5D,KAAK,EAAE,EAAE,IAAI,EAAE,sBAAsB,EAAE;KACxC;IACD;QACE,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,0BAA0B;QACvC,KAAK,EAAE,EAAE,IAAI,EAAE,qBAAqB,EAAE;KACvC;IACD;QACE,IAAI,EAAE,SAAS;QACf,WAAW,EAAE,0BAA0B;QACvC,KAAK,EAAE,EAAE,IAAI,EAAE,kBAAkB,EAAE;KACpC;CACO,CAAC;AAEX,0EAA0E;AAC1E,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,KAAK,EAAE;QACL,IAAI,EAAE,OAAO;QACb,QAAQ,EAAE,OAAO;QACjB,OAAO,EAAE,EAA4C;KACtD;IACD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM;QACZ,QAAQ,EAAE,MAAM;QAChB,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;YAC/B,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE;YACjC,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;YAC/B,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;SAChC;KACF;IACD,OAAO,EAAE;QACP,IAAI,EAAE,eAAe;QACrB,QAAQ,EAAE,eAAe;QACzB,OAAO,EAAE;YACP,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE;YAC7B,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE;YAC/B,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,SAAS,EAAE;SACtC;KACF;CACO,CAAC;AAEX,iGAAiG;AACjG,MAAM,CAAC,MAAM,eAAe,GAAyD;IACnF,IAAI,EAAE,EAAE,MAAM,EAAE,iBAAiB,EAAE,QAAQ,EAAE,MAAM,EAAE;IACrD,OAAO,EAAE,EAAE,MAAM,EAAE,yBAAyB,EAAE,QAAQ,EAAE,cAAc,EAAE;CACzE,CAAC;AAEF;;;;;;;;;;GAUG;AAEH;8EAC8E;AAC9E,SAAS,OAAO;IACd,OAAO,OAAO,CAAC,GAAG,CAAC,eAAe,KAAK,GAAG,CAAC;AAC7C,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,GAAG,CAAC,GAAW;IAC7B,IAAI,CAAC,OAAO,EAAE;QAAE,OAAO;IACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,0DAA0D;AAC1D,MAAM,UAAU,IAAI,CAAC,GAAW;IAC9B,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# zcode-acp-server Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`zcode-acp-server` bridges the headless ZCode CLI (`zcode app-server --stdio`) to
|
|
6
|
+
ACP (Agent Client Protocol) compatible editors. It is the translation layer
|
|
7
|
+
between ZCode's internal JSON-RPC event stream and the standard ACP protocol.
|
|
8
|
+
|
|
9
|
+
## Layered Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
application-client (Zed / JetBrains)
|
|
13
|
+
session/update
|
|
14
|
+
|
|
|
15
|
+
v
|
|
16
|
+
zcode-acp-server (stdio JSON-RPC ACP)
|
|
17
|
+
|-- handlers/ session, extensions, dispatch, server-requests, io, slash
|
|
18
|
+
|-- translators/ event-translator, projection-differ, tool-helpers
|
|
19
|
+
|-- interaction/ adapter
|
|
20
|
+
|-- config/ options, runtime-model, model-cache
|
|
21
|
+
|-- backend/ client, listener, types
|
|
22
|
+
|-- server.ts ZcodeAcpServer
|
|
23
|
+
|
|
|
24
|
+
v
|
|
25
|
+
zcode app-server --stdio (line-delimited JSON)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Core Data Flow
|
|
29
|
+
|
|
30
|
+
### 1. Session lifecycle
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
session/new → session/create → register EventListener
|
|
34
|
+
|
|
|
35
|
+
prompt request → session/send → EventTranslator translates → dispatchEvent
|
|
36
|
+
| |
|
|
37
|
+
end_turn / cancelled session/update notification
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 2. Event stream subscription
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
EventStreamListener.subscribe()
|
|
44
|
+
|
|
|
45
|
+
session/subscribe (deliveryKind: "desktop-continuous")
|
|
46
|
+
|
|
|
47
|
+
ZCode pushes session/event → handleEvent()
|
|
48
|
+
|
|
|
49
|
+
pollEvent() consumes → EventTranslator.translate()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 3. Dual-path event handling
|
|
53
|
+
|
|
54
|
+
#### Real-time path (EventTranslator)
|
|
55
|
+
- Listens to zcode `session/event` pushes
|
|
56
|
+
- Translates each event to an ACP `session/update` in real time
|
|
57
|
+
- Maintains `seenToolIds` to avoid duplicates
|
|
58
|
+
|
|
59
|
+
#### Snapshot path (ProjectionDiffer)
|
|
60
|
+
- On turn completion, builds a snapshot from `session/messages` + `session/read`
|
|
61
|
+
- Diffs two snapshots to produce new events (PlanUpdate / TextDelta / ToolCallNew, etc.)
|
|
62
|
+
- Used for turn-completion triage and stall recovery
|
|
63
|
+
|
|
64
|
+
### 4. Dual-path deduplication
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
EventTranslator (real-time path)
|
|
68
|
+
├── seenToolIds: Set<string>
|
|
69
|
+
└── turnDone: boolean
|
|
70
|
+
|
|
|
71
|
+
v
|
|
72
|
+
ProjectionDiffer (snapshot path)
|
|
73
|
+
├── seenToolIds: Set<string>
|
|
74
|
+
├── lastToolStatus: Map<string, string>
|
|
75
|
+
└── seenMessageIds: Set<string>
|
|
76
|
+
|
|
|
77
|
+
v
|
|
78
|
+
dispatchEvent (single exit point)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Key: **`seenToolIds` synchronization**
|
|
82
|
+
|
|
83
|
+
In `session.ts:629`, after the event path finishes processing, the state is
|
|
84
|
+
synced to the differ:
|
|
85
|
+
```typescript
|
|
86
|
+
for (const seenId of translator.seenToolIds) {
|
|
87
|
+
differ.markToolSeen(seenId);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
This ensures the snapshot diff does not re-emit tools already handled by the
|
|
92
|
+
event path, preventing Bash terminal output from being overwritten by a
|
|
93
|
+
content-less ToolCallNew.
|
|
94
|
+
|
|
95
|
+
## Module Responsibilities
|
|
96
|
+
|
|
97
|
+
### `backend/` — ZCode process communication
|
|
98
|
+
|
|
99
|
+
| File | Responsibility |
|
|
100
|
+
|------|------|
|
|
101
|
+
| `client.ts` | Spawn/manage the zcode subprocess, reader-loop, request/response multiplexing, process watchdog |
|
|
102
|
+
| `listener.ts` | EventStreamListener (subscribe/consume the event stream) and TurnMonitor (snapshot polling) |
|
|
103
|
+
| `types.ts` | ZCode JSON-RPC message type definitions |
|
|
104
|
+
|
|
105
|
+
### `translators/` — Event translation
|
|
106
|
+
|
|
107
|
+
| File | Responsibility |
|
|
108
|
+
|------|------|
|
|
109
|
+
| `event-translator.ts` | Translate zcode events to InternalEvent (real-time path) |
|
|
110
|
+
| `projection-differ.ts` | Diff two snapshots to produce InternalEvent (snapshot path) |
|
|
111
|
+
| `tool-helpers.ts` | Tool-related pure functions: title generation, output rendering, diff parsing, location extraction |
|
|
112
|
+
| `types.ts` | InternalEvent union type and plan entry builders |
|
|
113
|
+
|
|
114
|
+
### `handlers/` — ACP method handling
|
|
115
|
+
|
|
116
|
+
| File | Responsibility |
|
|
117
|
+
|------|------|
|
|
118
|
+
| `session.ts` | session/new/list/resume/load/prompt/set_config_option/cancel |
|
|
119
|
+
| `extensions.ts` | fork/rewind/rewindCascade/goal/compact/steer/cancelBackgroundTask/setModel/setMode/setThoughtLevel |
|
|
120
|
+
| `dispatch.ts` | dispatchEvent single exit point: InternalEvent → ACP session/update |
|
|
121
|
+
| `server-requests.ts` | Handle zcode interaction/* requests (tool auth, ExitPlanMode, AskUserQuestion), protocol negotiation routing |
|
|
122
|
+
| `io.ts` | ACP notification helpers (including `sendAvailableCommandsDeferred` deferred notification) |
|
|
123
|
+
| `slash.ts` | Interception of `/`-prefixed commands (/compact /goal /fork /rewind /steer /model /mode /thought) |
|
|
124
|
+
|
|
125
|
+
### `interaction/` — Interaction bridging
|
|
126
|
+
|
|
127
|
+
| File | Responsibility |
|
|
128
|
+
|------|------|
|
|
129
|
+
| `adapter.ts` | Conversion adapter from zcode interaction requests to ACP (requestPermission + elicitation form) |
|
|
130
|
+
|
|
131
|
+
### `config/` — Configuration management
|
|
132
|
+
|
|
133
|
+
| File | Responsibility |
|
|
134
|
+
|------|------|
|
|
135
|
+
| `options.ts` | configOptions / modes construction, set_config_option dispatch |
|
|
136
|
+
| `runtime-model.ts` | runtimeModel overlay construction and application |
|
|
137
|
+
| `model-cache.ts` | Model ID cache and usage initialization |
|
|
138
|
+
|
|
139
|
+
## Key State Machines
|
|
140
|
+
|
|
141
|
+
### Turn state
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
subscribe
|
|
145
|
+
|
|
|
146
|
+
turn.started
|
|
147
|
+
|
|
|
148
|
+
+---------------------+
|
|
149
|
+
| model.streaming |
|
|
150
|
+
| tool.updated |
|
|
151
|
+
| session.updated |
|
|
152
|
+
+---------------------+
|
|
153
|
+
|
|
|
154
|
+
+---------------------+
|
|
155
|
+
| turn.completed | -> end_turn
|
|
156
|
+
| turn.failed | -> error
|
|
157
|
+
| turn.cancelled | -> cancelled
|
|
158
|
+
| timeout (120s) | -> max_turn_requests
|
|
159
|
+
| manual cancel | -> cancelled
|
|
160
|
+
+---------------------+
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Tool lifecycle
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
scheduled -> started -> progress -> result/error
|
|
167
|
+
| | |
|
|
168
|
+
ToolCallNew status=in_progress output (stdoutTail)
|
|
169
|
+
|
|
|
170
|
+
(seenToolIds.add)
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Interaction Protocol Negotiation
|
|
174
|
+
|
|
175
|
+
ZCode `interaction/*` requests are routed to different ACP client interaction
|
|
176
|
+
mechanisms via protocol negotiation:
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
zcode interaction request received
|
|
180
|
+
│
|
|
181
|
+
├─ Tool auth (interaction/requestPermission)
|
|
182
|
+
│ └─ Always uses session/request_permission (its native purpose)
|
|
183
|
+
│
|
|
184
|
+
├─ ExitPlanMode (interaction/requestUserInput + plan_approval)
|
|
185
|
+
│ ├─ Client supports elicitation.form → elicitation/create (approve/reject form)
|
|
186
|
+
│ └─ Otherwise → session/request_permission (fallback)
|
|
187
|
+
│
|
|
188
|
+
└─ AskUserQuestion (interaction/requestUserInput)
|
|
189
|
+
├─ Client supports elicitation.form → elicitation/create (single form)
|
|
190
|
+
└─ Otherwise → per-question session/request_permission (fallback)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Key**: `server.supportsElicitationForm()` is detected at `initialize` time from
|
|
194
|
+
`clientCapabilities.elicitation.form`. Tool auth always goes through
|
|
195
|
+
request_permission, since that is its native purpose.
|
|
196
|
+
|
|
197
|
+
## Deferred Notification Mechanism
|
|
198
|
+
|
|
199
|
+
The `available_commands_update` notification must be sent **after** the session
|
|
200
|
+
response; otherwise the client's session state machine is not yet ready and
|
|
201
|
+
drops the notification, leaving the `/` completion menu empty.
|
|
202
|
+
|
|
203
|
+
```
|
|
204
|
+
session/new|resume|load handler
|
|
205
|
+
│
|
|
206
|
+
├─ call newSession()/resumeSession()/loadSession()
|
|
207
|
+
│
|
|
208
|
+
├─ sendAvailableCommandsDeferred(cx, sid, SLASH_COMMANDS)
|
|
209
|
+
│ └─ enqueue, send after 50ms (fire-and-forget)
|
|
210
|
+
│
|
|
211
|
+
└─ return response
|
|
212
|
+
│
|
|
213
|
+
└─ after the response is written to stdout, the 50ms timer fires sendAvailableCommands
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`sendAvailableCommandsDeferred` (`io.ts`) encapsulates the 50ms delay logic,
|
|
217
|
+
mirroring the Python bridge's `_pending_post_notifs` queue +
|
|
218
|
+
`_drain_post_notifs` mechanism.
|
|
219
|
+
|
|
220
|
+
## Mode Reconciliation
|
|
221
|
+
|
|
222
|
+
The session mode can change through four entry points, all of which must
|
|
223
|
+
notify the editor UI:
|
|
224
|
+
|
|
225
|
+
| Trigger | Path | Notifies UI |
|
|
226
|
+
|------|------|:---:|
|
|
227
|
+
| `session/setMode` request | `extensions.ts:setMode` | yes |
|
|
228
|
+
| `session/set_config_option` (mode) | `session.ts:setConfigOptionHandler` → `emitConfigOptionUpdate` | yes |
|
|
229
|
+
| `/mode` slash command | `slash.ts` → `emitConfigOptionUpdate` | yes |
|
|
230
|
+
| In-turn `EnterPlanMode`/`ExitPlanMode` | reconciled at turn completion | yes |
|
|
231
|
+
|
|
232
|
+
The in-turn path bypasses the bridge entirely, so `prompt()` runs
|
|
233
|
+
`emitModeIfChanged` (`session.ts`) at turn completion: it re-reads the
|
|
234
|
+
authoritative mode via `buildModes`, compares against `server.lastMode`
|
|
235
|
+
(the value last advertised to the client), and emits
|
|
236
|
+
`current_mode_update` + `config_option_update` when they differ. Failures are
|
|
237
|
+
swallowed so they cannot break the turn-completion path.
|
|
238
|
+
|
|
239
|
+
## Prompt-Lock Release on Stop
|
|
240
|
+
|
|
241
|
+
`session/stop` is fire-and-forget, but ZCode has a startup delay: when stop
|
|
242
|
+
arrives before the turn truly holds the lock the backend ignores it, the turn
|
|
243
|
+
runs on, and the lock leaks (next `session/send` fails with
|
|
244
|
+
"A prompt is already running").
|
|
245
|
+
|
|
246
|
+
`ensureTurnStopped` (`session.ts`) closes this gap. It mirrors the
|
|
247
|
+
`expectLock:true` strategy from `waitForTurnIdle` (`extensions.ts`):
|
|
248
|
+
|
|
249
|
+
1. send `session/stop`
|
|
250
|
+
2. poll `session/goal show`; first REQUIRE seeing "prompt is running" once
|
|
251
|
+
(proves the turn started), then wait for it to clear
|
|
252
|
+
3. if an 8s grace window elapses without ever seeing the lock, the turn never
|
|
253
|
+
started (stop caught it in time) or already ended → treat as released
|
|
254
|
+
4. hard timeout 30s
|
|
255
|
+
|
|
256
|
+
It is used at every stop site: the cancel check, the stall no-output path,
|
|
257
|
+
the turn cancelled/failed result, the 120s no-progress timeout, and the
|
|
258
|
+
`session/send` error path. Never throws (failures only log) so it cannot
|
|
259
|
+
break the cancel path.
|
|
260
|
+
|
|
261
|
+
## Process Watchdog
|
|
262
|
+
|
|
263
|
+
`close()` reaps the zcode process group via `process.kill(-pid)`, but only
|
|
264
|
+
when the bridge exits cleanly enough for its signal handlers to fire
|
|
265
|
+
(SIGTERM/SIGINT). If the bridge is SIGKILLed (Zed force-kill on reconnect,
|
|
266
|
+
crash, OOM), the handler never runs and the zcode subprocess group is
|
|
267
|
+
orphaned.
|
|
268
|
+
|
|
269
|
+
The watchdog (`backend/client.ts:startWatchdog`) closes that gap. It is a tiny
|
|
270
|
+
detached child that polls the bridge pid every 2s and, once the bridge is
|
|
271
|
+
gone, sends SIGKILL to the zcode process group, then exits. It is its own
|
|
272
|
+
process-group leader and `unref`'d, so it never holds the event loop open and
|
|
273
|
+
is not part of the zcode group it kills. It self-terminates as soon as the
|
|
274
|
+
zcode process exits, so a normal shutdown leaves no lingering watchdog.
|
|
275
|
+
|
|
276
|
+
## Design Decisions
|
|
277
|
+
|
|
278
|
+
### Why a dual path?
|
|
279
|
+
|
|
280
|
+
| Scenario | Real-time path | Snapshot path |
|
|
281
|
+
|------|---------|----------|
|
|
282
|
+
| Normal streaming | Low latency | Must wait for turn end |
|
|
283
|
+
| Lost events | Loses data | Recovers from snapshot |
|
|
284
|
+
| Deduplication | seenToolIds | seenMessageIds + markToolSeen() |
|
|
285
|
+
| Turn-completion triage | Not triggered | PlanUpdate / usage_update |
|
|
286
|
+
|
|
287
|
+
### Why no polling fallback?
|
|
288
|
+
|
|
289
|
+
ZCode CLI 0.14.5 ~ 0.14.7 used `session/read` polling to emulate streaming.
|
|
290
|
+
0.14.8+ introduced `session/subscribe` event push, which has lower latency and
|
|
291
|
+
is more reliable. This project supports only 0.14.8+ and has removed the
|
|
292
|
+
polling fallback code.
|
|
293
|
+
|
|
294
|
+
### Why does ProjectionDiffer need to persist across turns?
|
|
295
|
+
|
|
296
|
+
- `seenMessageIds`: prevents historical messages from being re-emitted after resume
|
|
297
|
+
- `lastToolStatus` + `seenToolIds`: ensures tool state is not lost across turns
|
|
298
|
+
- `lastUsage`: avoids duplicate usage_update pushes
|
|
299
|
+
- `lastPlanSig`: emits only when the plan changes
|