@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.
Files changed (70) hide show
  1. package/CHANGELOG.md +3 -1
  2. package/dist/types/async/job-manager.d.ts +36 -1
  3. package/dist/types/eval/backend.d.ts +2 -2
  4. package/dist/types/exec/bash-executor.d.ts +8 -10
  5. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -36
  6. package/dist/types/internal-urls/registry-helpers.d.ts +0 -18
  7. package/dist/types/sdk/bus/chat-daemon-control.d.ts +1 -3
  8. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +2 -4
  9. package/dist/types/sdk/host/host.d.ts +2 -23
  10. package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
  11. package/dist/types/session/artifacts.d.ts +5 -8
  12. package/dist/types/session/client-bridge.d.ts +0 -14
  13. package/dist/types/session/internal/managed-session-storage.d.ts +2 -5
  14. package/dist/types/session/messages.d.ts +1 -4
  15. package/dist/types/session/session-manager.d.ts +4 -5
  16. package/dist/types/session/streaming-output.d.ts +0 -32
  17. package/dist/types/task/receipt.d.ts +1 -2
  18. package/dist/types/task/types.d.ts +43 -178
  19. package/dist/types/tools/bash.d.ts +1 -1
  20. package/dist/types/tools/fetch.d.ts +0 -3
  21. package/dist/types/tools/output-meta.d.ts +5 -37
  22. package/dist/types/tools/subagent.d.ts +15 -0
  23. package/dist/types/tools/todo-write.d.ts +3 -0
  24. package/dist/types/tools/tool-result.d.ts +0 -1
  25. package/package.json +8 -8
  26. package/src/async/job-manager.ts +430 -46
  27. package/src/cursor.ts +13 -20
  28. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -3
  29. package/src/eval/backend.ts +2 -2
  30. package/src/exec/bash-executor.ts +151 -425
  31. package/src/gjc-runtime/ultragoal-runtime.ts +3 -280
  32. package/src/gjc-runtime/workflow-manifest.generated.json +1 -40
  33. package/src/gjc-runtime/workflow-manifest.ts +0 -12
  34. package/src/internal-urls/agent-protocol.ts +38 -98
  35. package/src/internal-urls/artifact-protocol.ts +17 -28
  36. package/src/internal-urls/docs-index.generated.ts +1 -1
  37. package/src/internal-urls/registry-helpers.ts +0 -62
  38. package/src/modes/acp/acp-agent.ts +0 -24
  39. package/src/modes/acp/acp-client-bridge.ts +11 -154
  40. package/src/prompts/tools/bash.md +2 -2
  41. package/src/sdk/acp/adapter.ts +9 -200
  42. package/src/sdk/bus/chat-daemon-control.ts +3 -5
  43. package/src/sdk/bus/index.ts +8 -324
  44. package/src/sdk/bus/telegram-daemon-contract.ts +2 -4
  45. package/src/sdk/host/host.ts +9 -187
  46. package/src/sdk/host/reverse-leases.ts +42 -249
  47. package/src/session/agent-session.ts +15 -178
  48. package/src/session/artifacts.ts +74 -211
  49. package/src/session/client-bridge.ts +0 -15
  50. package/src/session/internal/managed-session-storage.ts +5 -104
  51. package/src/session/messages.ts +7 -32
  52. package/src/session/session-manager.ts +21 -12
  53. package/src/session/streaming-output.ts +40 -198
  54. package/src/task/index.ts +230 -64
  55. package/src/task/receipt.ts +2 -4
  56. package/src/task/render.ts +0 -30
  57. package/src/task/types.ts +10 -34
  58. package/src/tools/bash.ts +239 -818
  59. package/src/tools/eval.ts +4 -99
  60. package/src/tools/fetch.ts +15 -77
  61. package/src/tools/find.ts +3 -7
  62. package/src/tools/output-meta.ts +114 -696
  63. package/src/tools/search.ts +4 -9
  64. package/src/tools/subagent.ts +97 -75
  65. package/src/tools/todo-write.ts +47 -0
  66. package/src/tools/tool-result.ts +0 -1
  67. package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +0 -83
  68. package/dist/types/utils/artifact-id.d.ts +0 -2
  69. package/src/gjc-runtime/ultragoal-review-source.ts +0 -378
  70. 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
- withTerminalCreationAdmission(async () => {
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
- if (creationSignal.aborted) throw new Error("terminal creation aborted");
155
- const creation = Promise.resolve().then(() =>
156
- connection.createTerminal({
157
- sessionId,
158
- command: params.command,
159
- ...(params.args ? { args: params.args } : {}),
160
- ...(params.env ? { env: params.env } : {}),
161
- ...(params.cwd ? { cwd: params.cwd } : {}),
162
- ...(typeof params.outputByteLimit === "number" ? { outputByteLimit: params.outputByteLimit } : {}),
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. Artifact allocation is best-effort on direct, async, and monitor paths, and only decimal session artifact ids are accepted. Native callback delivery bounds both the core-to-native relay and N-API queue with backpressure and preserves a 64 KiB terminal tail after its core stream budget, emitting at most one typed aggregate loss marker. Complete artifacts are labeled as full output; hard-capped artifacts, streams that lost bytes before Bash capture, reader/client captures that cannot prove completeness, and loss counters beyond JavaScript's exact integer range report incompleteness 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. Terminal creation, kill, output-recovery, and release RPCs are settlement-bounded; stalled or failed exit/kill/output operations report source-capture uncertainty instead of hanging or claiming completeness. 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.
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.
@@ -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 [id, request] of this.#reverseRequests) {
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", connectionId, capability, leaseId, controller };
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 (signal.aborted) throw new AcpSdkAdapterError("request_cancelled", "Reverse request was cancelled.");
559
- const reservation = this.#reserveProviderCalls(method);
560
- const providerCall = this.#startProviderCall(method, payload, signal, reservation.call);
561
- const cancelled = Promise.withResolvers<{ status: "cancelled" }>();
562
- const onAbort = () => cancelled.resolve({ status: "cancelled" });
563
- signal.addEventListener("abort", onAbort, { once: true });
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 bound exact cleanup to parent/link-count authority,
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: 21,
49
- slack: 21,
46
+ discord: 20,
47
+ slack: 20,
50
48
  };
51
49
 
52
50
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {