@gajae-code/coding-agent 0.12.6 → 0.12.7
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/CHANGELOG.md +3 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +8 -10
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
- package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/session/artifacts.d.ts +5 -8
- package/dist/types/session/client-bridge.d.ts +0 -14
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +4 -5
- package/dist/types/session/streaming-output.d.ts +0 -32
- package/dist/types/task/receipt.d.ts +1 -2
- package/dist/types/task/types.d.ts +43 -178
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +0 -3
- package/dist/types/tools/output-meta.d.ts +5 -37
- package/dist/types/tools/subagent.d.ts +15 -0
- package/dist/types/tools/todo-write.d.ts +3 -0
- package/dist/types/tools/tool-result.d.ts +0 -1
- package/package.json +8 -8
- package/src/async/job-manager.ts +430 -46
- package/src/cursor.ts +13 -20
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +151 -425
- package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
- package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
- package/src/gjc-runtime/workflow-manifest.ts +0 -12
- package/src/internal-urls/agent-protocol.ts +38 -98
- package/src/internal-urls/artifact-protocol.ts +17 -28
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/bus/chat-daemon-control.ts +3 -5
- package/src/sdk/bus/index.ts +8 -324
- package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/session/agent-session.ts +15 -178
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-storage.ts +5 -104
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +21 -12
- package/src/session/streaming-output.ts +40 -198
- package/src/task/index.ts +230 -64
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/bash.ts +239 -818
- package/src/tools/eval.ts +4 -99
- package/src/tools/fetch.ts +15 -77
- package/src/tools/find.ts +3 -7
- package/src/tools/output-meta.ts +114 -696
- package/src/tools/search.ts +4 -9
- package/src/tools/subagent.ts +97 -75
- package/src/tools/todo-write.ts +47 -0
- package/src/tools/tool-result.ts +0 -1
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
- package/dist/types/utils/artifact-id.d.ts +0 -2
- package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
- package/src/utils/artifact-id.ts +0 -6
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* `AgentSession`) can route through the client when it advertises the
|
|
5
5
|
* relevant capabilities at `initialize` time.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
9
7
|
import type {
|
|
10
8
|
PermissionOption as AcpPermissionOption,
|
|
11
9
|
TerminalHandle as AcpTerminalHandle,
|
|
@@ -21,25 +19,10 @@ import type {
|
|
|
21
19
|
ClientBridgePermissionOption,
|
|
22
20
|
ClientBridgePermissionOutcome,
|
|
23
21
|
ClientBridgePermissionToolCall,
|
|
24
|
-
ClientBridgeProviderLeaseIdentity,
|
|
25
22
|
ClientBridgeTerminalHandle,
|
|
26
23
|
} from "../../session/client-bridge";
|
|
27
24
|
import { resolveAcpPermissionMode } from "./permission-mode";
|
|
28
25
|
|
|
29
|
-
const ACP_TERMINAL_PUBLICATION_WAIT_MS = 500;
|
|
30
|
-
|
|
31
|
-
async function settleTerminalCleanup(operation: () => Promise<unknown>): Promise<boolean> {
|
|
32
|
-
return await Promise.race([
|
|
33
|
-
Promise.resolve()
|
|
34
|
-
.then(operation)
|
|
35
|
-
.then(
|
|
36
|
-
() => true,
|
|
37
|
-
() => false,
|
|
38
|
-
),
|
|
39
|
-
Bun.sleep(ACP_TERMINAL_PUBLICATION_WAIT_MS).then(() => false),
|
|
40
|
-
]);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
26
|
export { type AcpPermissionMode, resolveAcpPermissionMode } from "./permission-mode";
|
|
44
27
|
|
|
45
28
|
export function createAcpClientBridge(
|
|
@@ -56,15 +39,6 @@ export function createAcpClientBridge(
|
|
|
56
39
|
};
|
|
57
40
|
|
|
58
41
|
const bridge: ClientBridge = { capabilities, deferAgentInitiatedTurns: true };
|
|
59
|
-
const connectionId = `acp:${sessionId}`;
|
|
60
|
-
const connectionGeneration = 1;
|
|
61
|
-
const terminalProviderLeaseId = `${connectionId}:${randomUUID()}`;
|
|
62
|
-
const terminalProviderIdentity: ClientBridgeProviderLeaseIdentity = {
|
|
63
|
-
leaseId: terminalProviderLeaseId,
|
|
64
|
-
connectionId,
|
|
65
|
-
connectionGeneration,
|
|
66
|
-
fence: randomUUID(),
|
|
67
|
-
};
|
|
68
42
|
|
|
69
43
|
if (capabilities.readTextFile) {
|
|
70
44
|
bridge.readTextFile = async params => {
|
|
@@ -89,49 +63,8 @@ export function createAcpClientBridge(
|
|
|
89
63
|
}
|
|
90
64
|
|
|
91
65
|
if (capabilities.terminal) {
|
|
92
|
-
let terminalPublicationFailed = false;
|
|
93
|
-
bridge.quarantineProviderLease = async (providerLease, _reason) => {
|
|
94
|
-
if (
|
|
95
|
-
typeof providerLease === "string" ||
|
|
96
|
-
providerLease.leaseId !== terminalProviderIdentity.leaseId ||
|
|
97
|
-
providerLease.connectionId !== terminalProviderIdentity.connectionId ||
|
|
98
|
-
providerLease.connectionGeneration !== terminalProviderIdentity.connectionGeneration ||
|
|
99
|
-
providerLease.fence !== terminalProviderIdentity.fence
|
|
100
|
-
)
|
|
101
|
-
throw new Error("terminal provider lease mismatch");
|
|
102
|
-
terminalPublicationFailed = true;
|
|
103
|
-
};
|
|
104
|
-
let terminalCreationTail = Promise.resolve();
|
|
105
|
-
const withTerminalCreationAdmission = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
106
|
-
const previous = terminalCreationTail;
|
|
107
|
-
const release = Promise.withResolvers<void>();
|
|
108
|
-
terminalCreationTail = previous.then(() => release.promise);
|
|
109
|
-
await previous;
|
|
110
|
-
try {
|
|
111
|
-
return await operation();
|
|
112
|
-
} finally {
|
|
113
|
-
release.resolve();
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
66
|
bridge.createTerminal = (params: ClientBridgeCreateTerminalParams) =>
|
|
117
|
-
|
|
118
|
-
if (terminalPublicationFailed) throw new Error("terminal provider quarantined after publication failure");
|
|
119
|
-
const creationTimeout = AbortSignal.timeout(ACP_TERMINAL_PUBLICATION_WAIT_MS);
|
|
120
|
-
const creationSignal = params.signal ? AbortSignal.any([params.signal, creationTimeout]) : creationTimeout;
|
|
121
|
-
return await createTerminalHandle(
|
|
122
|
-
connection,
|
|
123
|
-
sessionId,
|
|
124
|
-
params,
|
|
125
|
-
creationSignal,
|
|
126
|
-
terminalProviderIdentity,
|
|
127
|
-
() => {
|
|
128
|
-
terminalPublicationFailed = true;
|
|
129
|
-
},
|
|
130
|
-
() => {
|
|
131
|
-
terminalPublicationFailed = true;
|
|
132
|
-
},
|
|
133
|
-
);
|
|
134
|
-
});
|
|
67
|
+
createTerminalHandle(connection, sessionId, params);
|
|
135
68
|
}
|
|
136
69
|
|
|
137
70
|
if (promptPermission) {
|
|
@@ -146,97 +79,21 @@ async function createTerminalHandle(
|
|
|
146
79
|
connection: AgentSideConnection,
|
|
147
80
|
sessionId: string,
|
|
148
81
|
params: ClientBridgeCreateTerminalParams,
|
|
149
|
-
creationSignal: AbortSignal,
|
|
150
|
-
providerIdentity: ClientBridgeProviderLeaseIdentity,
|
|
151
|
-
onCreationUncertain: () => void,
|
|
152
|
-
onPublicationFailure: () => void,
|
|
153
82
|
): Promise<ClientBridgeTerminalHandle> {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}),
|
|
164
|
-
);
|
|
165
|
-
const creationAborted = Promise.withResolvers<"aborted">();
|
|
166
|
-
const onCreationAbort = () => creationAborted.resolve("aborted");
|
|
167
|
-
creationSignal.addEventListener("abort", onCreationAbort, { once: true });
|
|
168
|
-
if (creationSignal.aborted) creationAborted.resolve("aborted");
|
|
169
|
-
const creationOutcome = await Promise.race([
|
|
170
|
-
creation.then(
|
|
171
|
-
handle => ({ status: "created" as const, handle }),
|
|
172
|
-
error => ({ status: "failed" as const, error }),
|
|
173
|
-
),
|
|
174
|
-
creationAborted.promise.then(() => ({ status: "aborted" as const })),
|
|
175
|
-
]);
|
|
176
|
-
creationSignal.removeEventListener("abort", onCreationAbort);
|
|
177
|
-
if (creationOutcome.status === "aborted") {
|
|
178
|
-
onCreationUncertain();
|
|
179
|
-
void creation.then(
|
|
180
|
-
async handle => {
|
|
181
|
-
await settleTerminalCleanup(() => handle.kill());
|
|
182
|
-
await settleTerminalCleanup(() => handle.release());
|
|
183
|
-
},
|
|
184
|
-
() => undefined,
|
|
185
|
-
);
|
|
186
|
-
throw new Error("terminal creation aborted");
|
|
187
|
-
}
|
|
188
|
-
if (creationOutcome.status === "failed") {
|
|
189
|
-
onCreationUncertain();
|
|
190
|
-
throw creationOutcome.error;
|
|
191
|
-
}
|
|
192
|
-
const handle = creationOutcome.handle;
|
|
193
|
-
const publication = Promise.resolve().then(() =>
|
|
194
|
-
connection.sessionUpdate({
|
|
195
|
-
sessionId,
|
|
196
|
-
update: {
|
|
197
|
-
sessionUpdate: "tool_call_update",
|
|
198
|
-
toolCallId: params.toolCallId,
|
|
199
|
-
status: "in_progress",
|
|
200
|
-
content: [{ type: "terminal", terminalId: handle.id }],
|
|
201
|
-
},
|
|
202
|
-
}),
|
|
203
|
-
);
|
|
204
|
-
const aborted = Promise.withResolvers<"aborted">();
|
|
205
|
-
const onAbort = () => aborted.resolve("aborted");
|
|
206
|
-
params.signal?.addEventListener("abort", onAbort, { once: true });
|
|
207
|
-
if (params.signal?.aborted) aborted.resolve("aborted");
|
|
208
|
-
const outcome = await Promise.race([
|
|
209
|
-
publication.then(
|
|
210
|
-
() => ({ status: "published" as const }),
|
|
211
|
-
error => ({ status: "failed" as const, error }),
|
|
212
|
-
),
|
|
213
|
-
Bun.sleep(ACP_TERMINAL_PUBLICATION_WAIT_MS).then(() => ({ status: "timeout" as const })),
|
|
214
|
-
aborted.promise.then(() => ({ status: "aborted" as const })),
|
|
215
|
-
]);
|
|
216
|
-
params.signal?.removeEventListener("abort", onAbort);
|
|
217
|
-
if (outcome.status !== "published") {
|
|
218
|
-
const killed = await settleTerminalCleanup(() => handle.kill());
|
|
219
|
-
const released = await settleTerminalCleanup(() => handle.release());
|
|
220
|
-
if (!params.signal?.aborted || !killed || !released) onPublicationFailure();
|
|
221
|
-
if (outcome.status === "failed") throw outcome.error;
|
|
222
|
-
throw new Error(
|
|
223
|
-
outcome.status === "aborted"
|
|
224
|
-
? "terminal publication aborted"
|
|
225
|
-
: `terminal publication did not settle within ${ACP_TERMINAL_PUBLICATION_WAIT_MS}ms`,
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
return wrapTerminalHandle(handle, providerIdentity.leaseId, providerIdentity);
|
|
83
|
+
const handle = await connection.createTerminal({
|
|
84
|
+
sessionId,
|
|
85
|
+
command: params.command,
|
|
86
|
+
...(params.args ? { args: params.args } : {}),
|
|
87
|
+
...(params.env ? { env: params.env } : {}),
|
|
88
|
+
...(params.cwd ? { cwd: params.cwd } : {}),
|
|
89
|
+
...(typeof params.outputByteLimit === "number" ? { outputByteLimit: params.outputByteLimit } : {}),
|
|
90
|
+
});
|
|
91
|
+
return wrapTerminalHandle(handle);
|
|
229
92
|
}
|
|
230
93
|
|
|
231
|
-
function wrapTerminalHandle(
|
|
232
|
-
handle: AcpTerminalHandle,
|
|
233
|
-
providerLeaseId: string,
|
|
234
|
-
providerLeaseIdentity: ClientBridgeProviderLeaseIdentity,
|
|
235
|
-
): ClientBridgeTerminalHandle {
|
|
94
|
+
function wrapTerminalHandle(handle: AcpTerminalHandle): ClientBridgeTerminalHandle {
|
|
236
95
|
return {
|
|
237
96
|
terminalId: handle.id,
|
|
238
|
-
providerLeaseId,
|
|
239
|
-
providerLeaseIdentity,
|
|
240
97
|
async currentOutput() {
|
|
241
98
|
const out = await handle.currentOutput();
|
|
242
99
|
return {
|
|
@@ -65,5 +65,5 @@ Use it only for sanctioned GJC workflow CLI persistence or state read/write/cont
|
|
|
65
65
|
# Output minimizer
|
|
66
66
|
|
|
67
67
|
- Bash stdout/stderr may be rewritten before you see it: long output keeps only the last 1 KiB by default to reduce noise and input-token use. Explicit `tools.artifactTailBytes` / `tools.artifactHeadBytes` settings can set the tail budget or retain both ends. Prefer focused commands and dedicated `search`/`find` tools over producing broad output. Test/lint runners (e.g. `bun test`, `cargo test`, ESLint) are also passed through heuristic filters that drop noise and keep failures.
|
|
68
|
-
- When the local minimizer changes visible text, successful artifact storage appends a footer containing an `artifact://<id>` reference.
|
|
69
|
-
- ACP/client-terminal output can arrive already truncated from the beginning.
|
|
68
|
+
- When the local minimizer changes visible text, successful artifact storage appends a footer containing an `artifact://<id>` reference. Complete artifacts are labeled as full output; hard-capped artifacts report omitted bytes instead. If artifact allocation/storage is unavailable before a writer/save operation is attempted, truncation may have no reference or diagnostic. If an artifact writer/save operation is attempted and fails, a bounded diagnostic is emitted without inventing an artifact URI.
|
|
69
|
+
- ACP/client-terminal output can arrive already truncated from the beginning. Treat any truncation notice or metadata as evidence that the visible tail is incomplete. Recover omitted output only when an `artifact://` footer or metadata reference is present; truncation evidence without a reference means the visible output is incomplete and no artifact is recoverable. Output with neither truncation evidence nor an artifact reference is the complete emitted output.
|
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -42,9 +42,6 @@ export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
|
|
|
42
42
|
export type AcpFrameHandler = (frame: SdkFrame) => void;
|
|
43
43
|
type ReverseRequest = {
|
|
44
44
|
state: "pending" | "cancelled";
|
|
45
|
-
connectionId?: string;
|
|
46
|
-
capability?: string;
|
|
47
|
-
leaseId?: string;
|
|
48
45
|
controller?: AbortController;
|
|
49
46
|
cancelTimer?: NodeJS.Timeout;
|
|
50
47
|
};
|
|
@@ -68,15 +65,6 @@ function isLifecycleOperation(operation: string): boolean {
|
|
|
68
65
|
);
|
|
69
66
|
}
|
|
70
67
|
|
|
71
|
-
const MAX_PENDING_REVERSE_PROVIDER_CALLS = 64;
|
|
72
|
-
|
|
73
|
-
type ProviderCallToken = {
|
|
74
|
-
method: string;
|
|
75
|
-
promise?: Promise<unknown>;
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const PROVIDER_CLOSE_SETTLEMENT_MS = 1_000;
|
|
79
|
-
|
|
80
68
|
/**
|
|
81
69
|
* Pure ACP-to-SDK adapter. It deliberately owns neither an AgentSession nor an
|
|
82
70
|
* ACP bridge: all session work is performed through authenticated v3 frames.
|
|
@@ -95,13 +83,10 @@ export class AcpSdkAdapter {
|
|
|
95
83
|
#reclaiming?: Promise<void>;
|
|
96
84
|
#reverseRequests = new Map<string, ReverseRequest>();
|
|
97
85
|
#reconnectFailedHandlers = new Set<AcpReconnectFailedHandler>();
|
|
98
|
-
#pendingReverseProviderCalls = new Set<ProviderCallToken>();
|
|
99
86
|
#frameHandlers = new Set<AcpFrameHandler>();
|
|
100
87
|
|
|
101
88
|
#reverseCancelTtlMs: number;
|
|
102
89
|
#closed = false;
|
|
103
|
-
#closePromise?: Promise<void>;
|
|
104
|
-
#terminalCleanupUncertain = false;
|
|
105
90
|
|
|
106
91
|
constructor(options: AcpSdkAdapterOptions) {
|
|
107
92
|
this.#client = options.client ?? new SdkClient(options.url, options.token);
|
|
@@ -155,16 +140,10 @@ export class AcpSdkAdapter {
|
|
|
155
140
|
}
|
|
156
141
|
|
|
157
142
|
async close(): Promise<void> {
|
|
158
|
-
this.#closePromise ??= this.#closeInternal();
|
|
159
|
-
await this.#closePromise;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
async #closeInternal(): Promise<void> {
|
|
163
143
|
if (this.#closed) return;
|
|
164
144
|
this.#closed = true;
|
|
165
145
|
if (this.#heartbeat) clearInterval(this.#heartbeat);
|
|
166
|
-
for (const
|
|
167
|
-
if (request.state === "pending") this.#sendReverseCancellation(id, request);
|
|
146
|
+
for (const request of this.#reverseRequests.values()) {
|
|
168
147
|
request.controller?.abort();
|
|
169
148
|
if (request.cancelTimer) clearTimeout(request.cancelTimer);
|
|
170
149
|
}
|
|
@@ -172,21 +151,7 @@ export class AcpSdkAdapter {
|
|
|
172
151
|
this.#unsubscribe?.();
|
|
173
152
|
this.#unsubscribeReconnect?.();
|
|
174
153
|
this.#unsubscribeReconnectFailed?.();
|
|
175
|
-
|
|
176
|
-
const terminalSettled = await this.#awaitTerminalProviderSettlement();
|
|
177
|
-
let clientError: unknown;
|
|
178
|
-
try {
|
|
179
|
-
await this.#client.close();
|
|
180
|
-
} catch (error) {
|
|
181
|
-
clientError = error;
|
|
182
|
-
}
|
|
183
|
-
if (!terminalSettled) {
|
|
184
|
-
throw new AcpSdkAdapterError(
|
|
185
|
-
"ownership_uncertain",
|
|
186
|
-
"ACP terminal provider cleanup did not settle before adapter close.",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
if (clientError) throw clientError;
|
|
154
|
+
await this.#client.close();
|
|
190
155
|
}
|
|
191
156
|
|
|
192
157
|
async prompt(params: JsonObject | string): Promise<unknown> {
|
|
@@ -390,7 +355,7 @@ export class AcpSdkAdapter {
|
|
|
390
355
|
)
|
|
391
356
|
return;
|
|
392
357
|
const controller = new AbortController();
|
|
393
|
-
const active: ReverseRequest = { state: "pending",
|
|
358
|
+
const active: ReverseRequest = { state: "pending", controller };
|
|
394
359
|
this.#reverseRequests.set(id, active);
|
|
395
360
|
try {
|
|
396
361
|
const request = frame.payload as JsonObject | undefined;
|
|
@@ -458,175 +423,19 @@ export class AcpSdkAdapter {
|
|
|
458
423
|
|
|
459
424
|
#abortActiveReverseRequests(): void {
|
|
460
425
|
for (const [id, request] of this.#reverseRequests) {
|
|
461
|
-
if (request.state === "pending") this.#sendReverseCancellation(id, request);
|
|
462
426
|
request.state = "cancelled";
|
|
463
427
|
request.controller?.abort();
|
|
464
428
|
this.#finishReverse(id, request);
|
|
465
429
|
}
|
|
466
430
|
}
|
|
467
431
|
|
|
468
|
-
#sendReverseCancellation(id: string, request: ReverseRequest): void {
|
|
469
|
-
if (!request.connectionId || !request.leaseId) return;
|
|
470
|
-
try {
|
|
471
|
-
const cancellation = this.#client.send({
|
|
472
|
-
type: "reverse_cancel",
|
|
473
|
-
id,
|
|
474
|
-
connectionId: request.connectionId,
|
|
475
|
-
leaseId: request.leaseId,
|
|
476
|
-
});
|
|
477
|
-
void Promise.resolve(cancellation).catch(() => {});
|
|
478
|
-
} catch {
|
|
479
|
-
// Cancellation is best effort while local retirement remains authoritative.
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
async #awaitTerminalProviderSettlement(): Promise<boolean> {
|
|
484
|
-
const deadline = Date.now() + PROVIDER_CLOSE_SETTLEMENT_MS;
|
|
485
|
-
for (;;) {
|
|
486
|
-
const pending = [...this.#pendingReverseProviderCalls].filter(token =>
|
|
487
|
-
["terminal.create", "terminal.kill", "terminal.release"].includes(token.method),
|
|
488
|
-
);
|
|
489
|
-
if (pending.length === 0) return !this.#terminalCleanupUncertain;
|
|
490
|
-
const remaining = deadline - Date.now();
|
|
491
|
-
if (remaining <= 0) return false;
|
|
492
|
-
const promises = pending.flatMap(token => (token.promise ? [token.promise] : []));
|
|
493
|
-
if (promises.length === 0) {
|
|
494
|
-
await Bun.sleep(Math.min(remaining, 1));
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
await Promise.race([Promise.allSettled(promises), Bun.sleep(remaining)]);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
#reserveProviderCalls(method: string): { call: ProviderCallToken; cleanup: ProviderCallToken[] } {
|
|
502
|
-
const cleanup =
|
|
503
|
-
method === "terminal.create"
|
|
504
|
-
? ([{ method: "terminal.kill" }, { method: "terminal.release" }] satisfies ProviderCallToken[])
|
|
505
|
-
: [];
|
|
506
|
-
const required = 1 + cleanup.length;
|
|
507
|
-
if (this.#pendingReverseProviderCalls.size + required > MAX_PENDING_REVERSE_PROVIDER_CALLS)
|
|
508
|
-
throw new AcpSdkAdapterError("acp_reverse_capacity", "ACP reverse provider call capacity reached.");
|
|
509
|
-
const call: ProviderCallToken = { method };
|
|
510
|
-
this.#pendingReverseProviderCalls.add(call);
|
|
511
|
-
for (const token of cleanup) this.#pendingReverseProviderCalls.add(token);
|
|
512
|
-
return { call, cleanup };
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
#releaseReservedProviderCalls(tokens: readonly ProviderCallToken[]): void {
|
|
516
|
-
for (const token of tokens) {
|
|
517
|
-
if (!token.promise) this.#pendingReverseProviderCalls.delete(token);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
#startProviderCall(
|
|
522
|
-
method: string,
|
|
523
|
-
payload: JsonObject,
|
|
524
|
-
signal: AbortSignal | undefined,
|
|
525
|
-
token: ProviderCallToken,
|
|
526
|
-
): Promise<unknown> {
|
|
527
|
-
let providerCall: Promise<unknown>;
|
|
528
|
-
try {
|
|
529
|
-
if (!this.#connection) throw new AcpSdkAdapterError("acp_reverse_unavailable");
|
|
530
|
-
if (this.#connection.request)
|
|
531
|
-
providerCall = Promise.resolve(this.#connection.request(method, payload, { cancellationSignal: signal }));
|
|
532
|
-
else {
|
|
533
|
-
const target = this.#connection[method];
|
|
534
|
-
if (typeof target !== "function")
|
|
535
|
-
throw new AcpSdkAdapterError("acp_reverse_unsupported", `ACP client does not support ${method}.`);
|
|
536
|
-
providerCall = Promise.resolve((target as (params: JsonObject) => Promise<unknown>)(payload));
|
|
537
|
-
}
|
|
538
|
-
} catch (error) {
|
|
539
|
-
if (token.method === "terminal.kill" || token.method === "terminal.release")
|
|
540
|
-
this.#terminalCleanupUncertain = true;
|
|
541
|
-
this.#pendingReverseProviderCalls.delete(token);
|
|
542
|
-
return Promise.reject(error);
|
|
543
|
-
}
|
|
544
|
-
token.promise = providerCall;
|
|
545
|
-
void providerCall.then(
|
|
546
|
-
() => this.#pendingReverseProviderCalls.delete(token),
|
|
547
|
-
() => {
|
|
548
|
-
if (token.method === "terminal.kill" || token.method === "terminal.release")
|
|
549
|
-
this.#terminalCleanupUncertain = true;
|
|
550
|
-
this.#pendingReverseProviderCalls.delete(token);
|
|
551
|
-
},
|
|
552
|
-
);
|
|
553
|
-
return providerCall;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
432
|
async #forwardReverse(method: string, payload: JsonObject, signal: AbortSignal): Promise<unknown> {
|
|
557
433
|
if (!method || !this.#connection) throw new AcpSdkAdapterError("acp_reverse_unavailable");
|
|
558
|
-
if (
|
|
559
|
-
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
if (signal.aborted) cancelled.resolve({ status: "cancelled" });
|
|
565
|
-
let outcome:
|
|
566
|
-
| { status: "resolved"; result: unknown }
|
|
567
|
-
| { status: "rejected"; error: unknown }
|
|
568
|
-
| { status: "cancelled" };
|
|
569
|
-
try {
|
|
570
|
-
outcome = await Promise.race([
|
|
571
|
-
providerCall.then(
|
|
572
|
-
result => ({ status: "resolved" as const, result }),
|
|
573
|
-
error => ({ status: "rejected" as const, error }),
|
|
574
|
-
),
|
|
575
|
-
cancelled.promise,
|
|
576
|
-
]);
|
|
577
|
-
} finally {
|
|
578
|
-
signal.removeEventListener("abort", onAbort);
|
|
579
|
-
}
|
|
580
|
-
if (outcome.status === "cancelled" || signal.aborted) {
|
|
581
|
-
if (method === "terminal.create") {
|
|
582
|
-
void providerCall
|
|
583
|
-
.then(
|
|
584
|
-
result => this.#cleanupCancelledTerminalCreate(result, reservation.cleanup),
|
|
585
|
-
() => {
|
|
586
|
-
this.#releaseReservedProviderCalls(reservation.cleanup);
|
|
587
|
-
},
|
|
588
|
-
)
|
|
589
|
-
.catch(() => {
|
|
590
|
-
this.#terminalCleanupUncertain = true;
|
|
591
|
-
this.#releaseReservedProviderCalls(reservation.cleanup);
|
|
592
|
-
});
|
|
593
|
-
} else this.#releaseReservedProviderCalls(reservation.cleanup);
|
|
594
|
-
throw new AcpSdkAdapterError("request_cancelled", "Reverse request was cancelled.");
|
|
595
|
-
}
|
|
596
|
-
this.#releaseReservedProviderCalls(reservation.cleanup);
|
|
597
|
-
if (outcome.status === "rejected") throw outcome.error;
|
|
598
|
-
return outcome.result;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async #cleanupCancelledTerminalCreate(result: unknown, cleanupTokens: readonly ProviderCallToken[]): Promise<void> {
|
|
602
|
-
const terminalId =
|
|
603
|
-
result && typeof result === "object" && typeof (result as { terminalId?: unknown }).terminalId === "string"
|
|
604
|
-
? (result as { terminalId: string }).terminalId
|
|
605
|
-
: undefined;
|
|
606
|
-
if (!terminalId || !this.#connection) {
|
|
607
|
-
this.#terminalCleanupUncertain = true;
|
|
608
|
-
this.#releaseReservedProviderCalls(cleanupTokens);
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
for (const [index, method] of (["terminal.kill", "terminal.release"] as const).entries()) {
|
|
612
|
-
const token = cleanupTokens[index];
|
|
613
|
-
if (!token) continue;
|
|
614
|
-
let providerCall: Promise<unknown>;
|
|
615
|
-
try {
|
|
616
|
-
providerCall = this.#startProviderCall(method, { terminalId }, AbortSignal.timeout(500), token);
|
|
617
|
-
} catch {
|
|
618
|
-
this.#terminalCleanupUncertain = true;
|
|
619
|
-
this.#releaseReservedProviderCalls([token]);
|
|
620
|
-
continue;
|
|
621
|
-
}
|
|
622
|
-
const cleanupOutcome = await Promise.race([
|
|
623
|
-
providerCall.then(
|
|
624
|
-
() => "settled" as const,
|
|
625
|
-
() => "failed" as const,
|
|
626
|
-
),
|
|
627
|
-
Bun.sleep(500).then(() => "timeout" as const),
|
|
628
|
-
]);
|
|
629
|
-
if (cleanupOutcome === "failed") this.#terminalCleanupUncertain = true;
|
|
630
|
-
}
|
|
434
|
+
if (this.#connection.request)
|
|
435
|
+
return await this.#connection.request(method, payload, { cancellationSignal: signal });
|
|
436
|
+
const target = this.#connection[method];
|
|
437
|
+
if (typeof target !== "function")
|
|
438
|
+
throw new AcpSdkAdapterError("acp_reverse_unsupported", `ACP client does not support ${method}.`);
|
|
439
|
+
return await (target as (params: JsonObject) => Promise<unknown>)(payload);
|
|
631
440
|
}
|
|
632
441
|
}
|
|
@@ -40,13 +40,11 @@ export type ChatDaemonAction = "stop" | "reload";
|
|
|
40
40
|
* daemon transports. Generation 16 applies Telegram sound-policy configuration
|
|
41
41
|
* through shared notification parsing. Generation 17 bound managed-session
|
|
42
42
|
* replacement to exact native filesystem authority; generation 18 retired that
|
|
43
|
-
* binding, generation 19
|
|
44
|
-
* generation 20 refreshed retained cleanup semantics, and generation 21 hardens
|
|
45
|
-
* exact Bash process-tree ownership shared by chat daemon cleanup.
|
|
43
|
+
* binding, and generation 19 binds exact cleanup to parent/link-count authority.
|
|
46
44
|
*/
|
|
47
45
|
export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
|
|
48
|
-
discord:
|
|
49
|
-
slack:
|
|
46
|
+
discord: 20,
|
|
47
|
+
slack: 20,
|
|
50
48
|
};
|
|
51
49
|
|
|
52
50
|
export function chatDaemonGeneration(kind: ChatDaemonKind): number {
|