@gajae-code/coding-agent 0.12.4 → 0.12.6
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 +36 -0
- package/README.md +4 -1
- package/dist/types/async/job-manager.d.ts +9 -1
- package/dist/types/eval/backend.d.ts +2 -2
- package/dist/types/exec/bash-executor.d.ts +10 -8
- package/dist/types/extensibility/extensions/runner.d.ts +9 -7
- package/dist/types/gjc-runtime/launch-tmux.d.ts +22 -3
- package/dist/types/gjc-runtime/psmux-detect.d.ts +3 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +3 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +14 -2
- package/dist/types/gjc-runtime/team-store.d.ts +0 -4
- package/dist/types/gjc-runtime/team-workers.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-provider-context.d.ts +57 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +8 -1
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +3 -0
- package/dist/types/gjc-runtime/ultragoal-review-source.d.ts +83 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +57 -4
- package/dist/types/internal-urls/registry-helpers.d.ts +18 -0
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +1 -0
- package/dist/types/modes/components/plugin-settings.d.ts +2 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +18 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +13 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -2
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +7 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +63 -3
- package/dist/types/sdk/host/host.d.ts +23 -2
- package/dist/types/sdk/host/reverse-leases.d.ts +7 -4
- package/dist/types/session/agent-session.d.ts +5 -4
- package/dist/types/session/artifacts.d.ts +8 -5
- package/dist/types/session/attempt-record-store.d.ts +19 -0
- package/dist/types/session/client-bridge.d.ts +14 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -23
- package/dist/types/session/messages.d.ts +4 -1
- package/dist/types/session/session-manager.d.ts +7 -5
- package/dist/types/session/session-storage.d.ts +8 -0
- package/dist/types/session/streaming-output.d.ts +32 -0
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/task/index.d.ts +2 -1
- package/dist/types/task/receipt.d.ts +3 -0
- package/dist/types/task/types.d.ts +194 -0
- package/dist/types/task/worktree.d.ts +13 -6
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/fetch.d.ts +3 -0
- package/dist/types/tools/output-meta.d.ts +37 -5
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +1 -0
- package/dist/types/utils/artifact-id.d.ts +2 -0
- package/dist/types/utils/git.d.ts +1 -0
- package/package.json +8 -8
- package/scripts/capture-sticky-viewport-showcase.ts +131 -5
- package/scripts/verify-gjc-sdk-canonicalization.ts +128 -183
- package/scripts/verify-sticky-viewport-showcase.ts +620 -93
- package/src/async/job-manager.ts +13 -5
- package/src/config/model-profiles.ts +8 -0
- package/src/config/settings-schema.ts +17 -3
- package/src/cursor.ts +20 -13
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +5 -2
- package/src/eval/backend.ts +2 -2
- package/src/exec/bash-executor.ts +425 -151
- package/src/extensibility/extensions/runner.ts +80 -6
- package/src/extensibility/extensions/wrapper.ts +22 -15
- package/src/extensibility/runtime-skill-discovery.ts +4 -6
- package/src/gjc-runtime/launch-tmux.ts +309 -293
- package/src/gjc-runtime/managed-owner-supervisor.ts +6 -2
- package/src/gjc-runtime/psmux-detect.ts +52 -6
- package/src/gjc-runtime/team-launch.ts +50 -2
- package/src/gjc-runtime/team-runtime.ts +716 -251
- package/src/gjc-runtime/team-store.ts +25 -7
- package/src/gjc-runtime/team-workers.ts +161 -1
- package/src/gjc-runtime/tmux-common.ts +16 -4
- package/src/gjc-runtime/tmux-gc.ts +37 -5
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -0
- package/src/gjc-runtime/tmux-provider-context.ts +451 -0
- package/src/gjc-runtime/tmux-sessions.ts +409 -156
- package/src/gjc-runtime/ultragoal-change-set.ts +90 -44
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +7 -0
- package/src/gjc-runtime/ultragoal-review-source.ts +378 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +640 -159
- package/src/gjc-runtime/workflow-manifest.generated.json +40 -1
- package/src/gjc-runtime/workflow-manifest.ts +12 -0
- package/src/internal-urls/agent-protocol.ts +98 -38
- package/src/internal-urls/artifact-protocol.ts +28 -17
- package/src/internal-urls/docs-index.generated.ts +6 -6
- package/src/internal-urls/local-protocol.ts +39 -18
- package/src/internal-urls/registry-helpers.ts +62 -0
- package/src/modes/acp/acp-agent.ts +51 -13
- package/src/modes/acp/acp-client-bridge.ts +154 -11
- package/src/modes/components/gjc-bundle-settings.ts +2 -0
- package/src/modes/components/plugin-settings.ts +5 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/monitor.md +4 -4
- package/src/sdk/acp/adapter.ts +200 -9
- package/src/sdk/broker/broker.ts +93 -10
- package/src/sdk/broker/lifecycle-ledger.ts +227 -11
- package/src/sdk/broker/lifecycle.ts +692 -98
- package/src/sdk/bus/chat-daemon-control.ts +33 -5
- package/src/sdk/bus/index.ts +347 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +7 -3
- package/src/sdk/bus/telegram-daemon.ts +283 -44
- package/src/sdk/bus/topic-registry.ts +133 -10
- package/src/sdk/host/host.ts +187 -9
- package/src/sdk/host/reverse-leases.ts +249 -42
- package/src/sdk/session.ts +8 -6
- package/src/session/agent-session.ts +930 -354
- package/src/session/artifacts.ts +211 -74
- package/src/session/attempt-record-store.ts +106 -0
- package/src/session/client-bridge.ts +15 -0
- package/src/session/internal/managed-session-scope.ts +332 -198
- package/src/session/internal/managed-session-storage.ts +162 -450
- package/src/session/messages.ts +32 -7
- package/src/session/session-manager.ts +31 -28
- package/src/session/session-storage.ts +190 -53
- package/src/session/streaming-output.ts +198 -40
- package/src/task/executor.ts +5 -2
- package/src/task/index.ts +277 -64
- package/src/task/provider-retry-status.ts +4 -1
- package/src/task/receipt.ts +26 -2
- package/src/task/render.ts +42 -1
- package/src/task/types.ts +52 -0
- package/src/task/worktree.ts +122 -43
- package/src/tools/bash.ts +818 -239
- package/src/tools/eval.ts +99 -4
- package/src/tools/fetch.ts +77 -15
- package/src/tools/find.ts +7 -3
- package/src/tools/monitor.ts +7 -1
- package/src/tools/output-meta.ts +706 -116
- package/src/tools/search.ts +9 -4
- package/src/tools/subagent-render.ts +5 -2
- package/src/tools/subagent.ts +8 -0
- package/src/tools/tool-result.ts +1 -0
- package/src/utils/artifact-id.ts +6 -0
- package/src/utils/git.ts +2 -0
package/src/sdk/acp/adapter.ts
CHANGED
|
@@ -42,6 +42,9 @@ 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;
|
|
45
48
|
controller?: AbortController;
|
|
46
49
|
cancelTimer?: NodeJS.Timeout;
|
|
47
50
|
};
|
|
@@ -65,6 +68,15 @@ function isLifecycleOperation(operation: string): boolean {
|
|
|
65
68
|
);
|
|
66
69
|
}
|
|
67
70
|
|
|
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
|
+
|
|
68
80
|
/**
|
|
69
81
|
* Pure ACP-to-SDK adapter. It deliberately owns neither an AgentSession nor an
|
|
70
82
|
* ACP bridge: all session work is performed through authenticated v3 frames.
|
|
@@ -83,10 +95,13 @@ export class AcpSdkAdapter {
|
|
|
83
95
|
#reclaiming?: Promise<void>;
|
|
84
96
|
#reverseRequests = new Map<string, ReverseRequest>();
|
|
85
97
|
#reconnectFailedHandlers = new Set<AcpReconnectFailedHandler>();
|
|
98
|
+
#pendingReverseProviderCalls = new Set<ProviderCallToken>();
|
|
86
99
|
#frameHandlers = new Set<AcpFrameHandler>();
|
|
87
100
|
|
|
88
101
|
#reverseCancelTtlMs: number;
|
|
89
102
|
#closed = false;
|
|
103
|
+
#closePromise?: Promise<void>;
|
|
104
|
+
#terminalCleanupUncertain = false;
|
|
90
105
|
|
|
91
106
|
constructor(options: AcpSdkAdapterOptions) {
|
|
92
107
|
this.#client = options.client ?? new SdkClient(options.url, options.token);
|
|
@@ -140,10 +155,16 @@ export class AcpSdkAdapter {
|
|
|
140
155
|
}
|
|
141
156
|
|
|
142
157
|
async close(): Promise<void> {
|
|
158
|
+
this.#closePromise ??= this.#closeInternal();
|
|
159
|
+
await this.#closePromise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async #closeInternal(): Promise<void> {
|
|
143
163
|
if (this.#closed) return;
|
|
144
164
|
this.#closed = true;
|
|
145
165
|
if (this.#heartbeat) clearInterval(this.#heartbeat);
|
|
146
|
-
for (const request of this.#reverseRequests
|
|
166
|
+
for (const [id, request] of this.#reverseRequests) {
|
|
167
|
+
if (request.state === "pending") this.#sendReverseCancellation(id, request);
|
|
147
168
|
request.controller?.abort();
|
|
148
169
|
if (request.cancelTimer) clearTimeout(request.cancelTimer);
|
|
149
170
|
}
|
|
@@ -151,7 +172,21 @@ export class AcpSdkAdapter {
|
|
|
151
172
|
this.#unsubscribe?.();
|
|
152
173
|
this.#unsubscribeReconnect?.();
|
|
153
174
|
this.#unsubscribeReconnectFailed?.();
|
|
154
|
-
|
|
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;
|
|
155
190
|
}
|
|
156
191
|
|
|
157
192
|
async prompt(params: JsonObject | string): Promise<unknown> {
|
|
@@ -355,7 +390,7 @@ export class AcpSdkAdapter {
|
|
|
355
390
|
)
|
|
356
391
|
return;
|
|
357
392
|
const controller = new AbortController();
|
|
358
|
-
const active: ReverseRequest = { state: "pending", controller };
|
|
393
|
+
const active: ReverseRequest = { state: "pending", connectionId, capability, leaseId, controller };
|
|
359
394
|
this.#reverseRequests.set(id, active);
|
|
360
395
|
try {
|
|
361
396
|
const request = frame.payload as JsonObject | undefined;
|
|
@@ -423,19 +458,175 @@ export class AcpSdkAdapter {
|
|
|
423
458
|
|
|
424
459
|
#abortActiveReverseRequests(): void {
|
|
425
460
|
for (const [id, request] of this.#reverseRequests) {
|
|
461
|
+
if (request.state === "pending") this.#sendReverseCancellation(id, request);
|
|
426
462
|
request.state = "cancelled";
|
|
427
463
|
request.controller?.abort();
|
|
428
464
|
this.#finishReverse(id, request);
|
|
429
465
|
}
|
|
430
466
|
}
|
|
431
467
|
|
|
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
|
+
|
|
432
556
|
async #forwardReverse(method: string, payload: JsonObject, signal: AbortSignal): Promise<unknown> {
|
|
433
557
|
if (!method || !this.#connection) throw new AcpSdkAdapterError("acp_reverse_unavailable");
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
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
|
+
}
|
|
440
631
|
}
|
|
441
632
|
}
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
redactBrokerDiscovery,
|
|
25
25
|
} from "./discovery";
|
|
26
26
|
import { deriveIdempotencyIdentity } from "./identity";
|
|
27
|
-
import { executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
27
|
+
import { canonicalDeleteLocatorPath, executeLifecycle, isCanonicalSessionId } from "./lifecycle";
|
|
28
28
|
|
|
29
29
|
import {
|
|
30
30
|
type LifecycleDurableEffectsReceipt,
|
|
@@ -71,6 +71,7 @@ export type BrokerErrorCode =
|
|
|
71
71
|
export type BrokerCleanupIdentity = {
|
|
72
72
|
dev: string;
|
|
73
73
|
ino: string;
|
|
74
|
+
nlink?: string;
|
|
74
75
|
size: number;
|
|
75
76
|
mtimeNs: string;
|
|
76
77
|
sha256: string;
|
|
@@ -102,6 +103,7 @@ export type BrokerArtifactTree = {
|
|
|
102
103
|
|
|
103
104
|
export type BrokerCleanupEvidence = {
|
|
104
105
|
phase: "artifacts" | "transcript" | "metadata" | "lifecycle";
|
|
106
|
+
cleanupReceiptVersion?: 1;
|
|
105
107
|
/** Ledger-bound deletion target; never reconstructed from a retry request. */
|
|
106
108
|
sessionsRoot?: string;
|
|
107
109
|
transcriptPath?: string;
|
|
@@ -110,6 +112,7 @@ export type BrokerCleanupEvidence = {
|
|
|
110
112
|
sessionId?: string;
|
|
111
113
|
artifactsIdentity?: BrokerCleanupIdentity;
|
|
112
114
|
transcriptIdentity?: BrokerCleanupIdentity;
|
|
115
|
+
transcriptParentIdentity?: { dev: string; ino: string };
|
|
113
116
|
/** Identity-bound lifecycle metadata marker retained when exact cleanup is deferred. */
|
|
114
117
|
metadataIdentity?: BrokerCleanupIdentity;
|
|
115
118
|
metadataPath?: string;
|
|
@@ -122,9 +125,17 @@ export type BrokerCleanupEvidence = {
|
|
|
122
125
|
/** Append-only terminal proof for lifecycle metadata cleanup. */
|
|
123
126
|
metadataCompleted?: true;
|
|
124
127
|
detachedArtifactsPath?: string;
|
|
128
|
+
retainedArtifactsSuccessorPath?: string;
|
|
129
|
+
retainedArtifactsPlaceholderPath?: string;
|
|
130
|
+
retainedArtifactsUnknownPath?: string;
|
|
131
|
+
retainedArtifactsSideAuthority?: "none" | "retained";
|
|
125
132
|
detachedTranscriptPath?: string;
|
|
133
|
+
retainedTranscriptSuccessorPath?: string;
|
|
134
|
+
retainedTranscriptPlaceholderPath?: string;
|
|
135
|
+
retainedTranscriptUnknownPath?: string;
|
|
126
136
|
/** Durable proof that artifact cleanup completed before transcript mutation. */
|
|
127
137
|
artifactsRemoved?: boolean;
|
|
138
|
+
artifactsAbsentAtAuthorization?: true;
|
|
128
139
|
/** Preauthorized no-replace artifact quarantine path persisted before detach. */
|
|
129
140
|
plannedArtifactsPath?: string;
|
|
130
141
|
/** Identity-bound artifact tree authority persisted before broker detach and replayed exactly. */
|
|
@@ -133,6 +144,7 @@ export type BrokerCleanupEvidence = {
|
|
|
133
144
|
plannedTranscriptPath?: string;
|
|
134
145
|
/** Fully identity-bound startup-failure cleanup plan, persisted before any detach. */
|
|
135
146
|
lifecycleFiles?: BrokerLifecycleCleanupFile[];
|
|
147
|
+
lifecycleParentIdentity?: { dev: string; ino: string };
|
|
136
148
|
/** Delete metadata receipts authorize only the canonical marker/ready sibling pair. */
|
|
137
149
|
lifecycleDeleteMetadata?: true;
|
|
138
150
|
};
|
|
@@ -157,6 +169,14 @@ function isCleanupPending(response: BrokerResponse): boolean {
|
|
|
157
169
|
return !response.ok && response.error.code === "cleanup_pending" && response.error.cleanup !== undefined;
|
|
158
170
|
}
|
|
159
171
|
|
|
172
|
+
function cleanupFromResponse(response: unknown): BrokerCleanupEvidence | undefined {
|
|
173
|
+
return isBrokerResponse(response) && !response.ok ? response.error.cleanup : undefined;
|
|
174
|
+
}
|
|
175
|
+
function pendingCleanupSessionId(response: BrokerResponse): string | undefined {
|
|
176
|
+
if (response.ok || response.error.code !== "cleanup_pending") return undefined;
|
|
177
|
+
return typeof response.error.cleanup?.sessionId === "string" ? response.error.cleanup.sessionId : undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
160
180
|
function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
161
181
|
if (response.ok) return "terminal_ok";
|
|
162
182
|
if (isCleanupPending(response)) return "effect_started";
|
|
@@ -165,8 +185,8 @@ function lifecycleResponseState(response: BrokerResponse): LifecycleState {
|
|
|
165
185
|
|
|
166
186
|
type InputNormalization = { input: Record<string, unknown> } | BrokerResponse;
|
|
167
187
|
|
|
168
|
-
function isBrokerResponse(value:
|
|
169
|
-
return "ok" in value;
|
|
188
|
+
function isBrokerResponse(value: unknown): value is BrokerResponse {
|
|
189
|
+
return typeof value === "object" && value !== null && "ok" in value && typeof value.ok === "boolean";
|
|
170
190
|
}
|
|
171
191
|
|
|
172
192
|
function normalizeAliasedString(
|
|
@@ -224,11 +244,12 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
224
244
|
typeof input.target === "object" && input.target !== null && !Array.isArray(input.target)
|
|
225
245
|
? (input.target as Record<string, unknown>)
|
|
226
246
|
: undefined;
|
|
247
|
+
const normalizeLifecycleDirectory = operation === "session.delete" ? canonicalDeleteLocatorPath : path.resolve;
|
|
227
248
|
const cwd = normalizeAliasedString(
|
|
228
249
|
{ cwd: input.cwd, path: input.path, targetPath: target?.path },
|
|
229
250
|
"cwd",
|
|
230
251
|
["path", "targetPath"],
|
|
231
|
-
|
|
252
|
+
normalizeLifecycleDirectory,
|
|
232
253
|
);
|
|
233
254
|
if (cwd.error) return error("invalid_input", cwd.error);
|
|
234
255
|
if (cwd.value !== undefined) {
|
|
@@ -239,7 +260,7 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
239
260
|
{ stateRoot: input.stateRoot, targetStateRoot: target?.stateRoot },
|
|
240
261
|
"stateRoot",
|
|
241
262
|
["targetStateRoot"],
|
|
242
|
-
|
|
263
|
+
normalizeLifecycleDirectory,
|
|
243
264
|
);
|
|
244
265
|
if (stateRoot.error) return error("invalid_input", stateRoot.error);
|
|
245
266
|
if (stateRoot.value !== undefined && (!cwd.value || stateRoot.value !== path.join(cwd.value, ".gjc", "state")))
|
|
@@ -254,6 +275,11 @@ function normalizeBrokerInput(operation: string, input: Record<string, unknown>)
|
|
|
254
275
|
if (Object.keys(normalizedTarget).length > 0) normalized.target = normalizedTarget;
|
|
255
276
|
else delete normalized.target;
|
|
256
277
|
}
|
|
278
|
+
if (operation === "session.delete") {
|
|
279
|
+
const sessionPath = normalizeAliasedString(input, "sessionPath", [], canonicalDeleteLocatorPath);
|
|
280
|
+
if (sessionPath.error) return error("invalid_input", sessionPath.error);
|
|
281
|
+
if (sessionPath.value !== undefined) normalized.sessionPath = sessionPath.value;
|
|
282
|
+
}
|
|
257
283
|
return { input: normalized };
|
|
258
284
|
}
|
|
259
285
|
function canonicalJson(value: unknown): string {
|
|
@@ -798,7 +824,61 @@ export class Broker {
|
|
|
798
824
|
.update(canonicalJson(lifecycleTarget(operation, input)))
|
|
799
825
|
.digest("hex");
|
|
800
826
|
const identity = await deriveIdempotencyIdentity(this.settings.agentDir, operation, idempotencyKey, target);
|
|
801
|
-
|
|
827
|
+
let reconstructedDeleteCleanup: BrokerCleanupEvidence | undefined;
|
|
828
|
+
if (operation === "session.delete" && input.cwd === undefined && input.sessionPath === undefined) {
|
|
829
|
+
const entry = this.ledger.get(identity);
|
|
830
|
+
const cleanup = cleanupFromResponse(entry?.response) ?? cleanupFromResponse(entry?.unresolvedCleanupResponse);
|
|
831
|
+
reconstructedDeleteCleanup = cleanup;
|
|
832
|
+
const requestedSessionId = typeof input.sessionId === "string" ? input.sessionId : undefined;
|
|
833
|
+
if (!cleanup) {
|
|
834
|
+
if (requestedSessionId && this.ledger.hasUncertainCleanupForSession(requestedSessionId, identity))
|
|
835
|
+
return error(
|
|
836
|
+
"terminal_uncertain",
|
|
837
|
+
"Session cleanup authority is uncertain and cannot be deleted safely",
|
|
838
|
+
);
|
|
839
|
+
const pending = requestedSessionId
|
|
840
|
+
? this.ledger.findCleanupPendingBySessionId(requestedSessionId, identity)
|
|
841
|
+
: undefined;
|
|
842
|
+
if (pending) {
|
|
843
|
+
const pendingResponse = cleanupFromResponse(pending.response)
|
|
844
|
+
? pending.response
|
|
845
|
+
: pending.unresolvedCleanupResponse;
|
|
846
|
+
if (isBrokerResponse(pendingResponse)) return pendingResponse;
|
|
847
|
+
return error(
|
|
848
|
+
"terminal_uncertain",
|
|
849
|
+
"Session cleanup authority is pending under another lifecycle identity",
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
if (entry) {
|
|
853
|
+
if (isBrokerResponse(entry.response)) return entry.response;
|
|
854
|
+
return error("terminal_uncertain", "Existing session.delete ledger evidence lacks replayable authority");
|
|
855
|
+
}
|
|
856
|
+
if (requestedSessionId) {
|
|
857
|
+
await this.index.refresh();
|
|
858
|
+
if (this.index.listSessions().sessions.some(session => session.sessionId === requestedSessionId))
|
|
859
|
+
return error(
|
|
860
|
+
"terminal_uncertain",
|
|
861
|
+
"Indexed session requires durable locator authority before deletion",
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return { ok: true, result: requestedSessionId ? { sessionId: requestedSessionId } : undefined };
|
|
865
|
+
}
|
|
866
|
+
if (
|
|
867
|
+
cleanup &&
|
|
868
|
+
cleanup.sessionId === requestedSessionId &&
|
|
869
|
+
typeof cleanup.cwd === "string" &&
|
|
870
|
+
typeof cleanup.transcriptPath === "string"
|
|
871
|
+
)
|
|
872
|
+
input = {
|
|
873
|
+
sessionId: cleanup.sessionId,
|
|
874
|
+
cwd: cleanup.cwd,
|
|
875
|
+
stateRoot: path.join(cleanup.cwd, ".gjc", "state"),
|
|
876
|
+
sessionPath: cleanup.transcriptPath,
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
const storedRequestHash = reconstructedDeleteCleanup ? this.ledger.get(identity)?.requestHash : undefined;
|
|
880
|
+
const requestHash =
|
|
881
|
+
storedRequestHash ?? createHash("sha256").update(canonicalJson({ operation, input })).digest("hex");
|
|
802
882
|
const prev = this.#chains.get(target) ?? Promise.resolve();
|
|
803
883
|
let release!: () => void;
|
|
804
884
|
const current = new Promise<void>(resolve => (release = resolve));
|
|
@@ -812,8 +892,8 @@ export class Broker {
|
|
|
812
892
|
const begun = await this.ledger.begin(identity, requestHash);
|
|
813
893
|
if (begun.kind === "replay") {
|
|
814
894
|
const replay = begun.entry.response as BrokerResponse;
|
|
815
|
-
|
|
816
|
-
|
|
895
|
+
const cleanup = cleanupFromResponse(replay) ?? reconstructedDeleteCleanup;
|
|
896
|
+
if (!cleanup) return replay;
|
|
817
897
|
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
818
898
|
const response = outcome.response;
|
|
819
899
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
@@ -828,11 +908,13 @@ export class Broker {
|
|
|
828
908
|
return error("idempotency_conflict", "idempotency key was used with a different request");
|
|
829
909
|
if (begun.kind === "terminal_uncertain") {
|
|
830
910
|
const replay = (begun.entry.response ?? beforeBegin?.response) as BrokerResponse | undefined;
|
|
831
|
-
|
|
911
|
+
const cleanup = (replay ? cleanupFromResponse(replay) : undefined) ?? reconstructedDeleteCleanup;
|
|
912
|
+
if (!cleanup)
|
|
832
913
|
return replay ?? error("terminal_uncertain", "prior lifecycle operation outcome is uncertain");
|
|
833
|
-
const outcome = await executeLifecycle(this, operation, input, identity,
|
|
914
|
+
const outcome = await executeLifecycle(this, operation, input, identity, cleanup);
|
|
834
915
|
const response = outcome.response;
|
|
835
916
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
917
|
+
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
836
918
|
response,
|
|
837
919
|
responseDigest: createHash("sha256").update(canonicalJson(response)).digest("hex"),
|
|
838
920
|
...(outcome.durableEffects ? { durableEffects: outcome.durableEffects } : {}),
|
|
@@ -844,6 +926,7 @@ export class Broker {
|
|
|
844
926
|
const outcome = await executeLifecycle(this, operation, input, identity);
|
|
845
927
|
const response = outcome.response;
|
|
846
928
|
await this.ledger.transition(identity, lifecycleResponseState(response), {
|
|
929
|
+
...(pendingCleanupSessionId(response) ? { intendedSessionId: pendingCleanupSessionId(response) } : {}),
|
|
847
930
|
resultSessionId:
|
|
848
931
|
response.ok && typeof (response.result as { sessionId?: unknown } | undefined)?.sessionId === "string"
|
|
849
932
|
? (response.result as { sessionId: string }).sessionId
|