@gajae-code/coding-agent 0.12.6 → 0.12.8
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 +22 -1
- package/dist/types/async/job-manager.d.ts +36 -1
- package/dist/types/commands/notify.d.ts +4 -0
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-discovery-manager.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/resolve-config-value.d.ts +2 -1
- package/dist/types/config/settings-schema.d.ts +17 -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/main.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +20 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +53 -4
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +4 -2
- package/dist/types/sdk/broker/broker.d.ts +5 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -3
- package/dist/types/sdk/bus/config.d.ts +75 -113
- package/dist/types/sdk/bus/discord-live-provider.d.ts +8 -2
- package/dist/types/sdk/bus/discord-provider.d.ts +19 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/notification-orchestration.d.ts +127 -18
- package/dist/types/sdk/bus/notification-service.d.ts +31 -4
- package/dist/types/sdk/bus/session-control.d.ts +8 -3
- package/dist/types/sdk/bus/slack-live-provider.d.ts +9 -2
- package/dist/types/sdk/bus/slack-provider.d.ts +22 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
- package/dist/types/sdk/client/discovery.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -23
- package/dist/types/sdk/host/query/handlers.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +4 -7
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/providers.d.ts +22 -0
- package/dist/types/sdk/session-directory.d.ts +1 -1
- 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-scope.d.ts +17 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +11 -4
- package/dist/types/session/internal/native-publish-outcome.d.ts +1 -0
- package/dist/types/session/internal/session-open-errors.d.ts +5 -0
- package/dist/types/session/messages.d.ts +1 -4
- package/dist/types/session/session-manager.d.ts +7 -6
- package/dist/types/session/session-storage.d.ts +2 -0
- 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 +10 -8
- package/scripts/build-sdk-package-smoke.ts +51 -0
- package/scripts/capture-notifications-settings-showcase.ts +1 -1
- package/src/async/job-manager.ts +430 -46
- package/src/cli/notify-cli.ts +204 -94
- package/src/commands/notify.ts +19 -8
- package/src/config/file-lock.ts +6 -0
- package/src/config/model-discovery-manager.ts +50 -8
- package/src/config/model-registry.ts +862 -75
- package/src/config/resolve-config-value.ts +10 -8
- package/src/config/settings-schema.ts +25 -15
- package/src/config/settings.ts +15 -15
- 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/session-state-sidecar.ts +12 -2
- 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 +7 -7
- package/src/internal-urls/registry-helpers.ts +0 -62
- package/src/main.ts +43 -7
- package/src/modes/DESIGN.md +11 -11
- package/src/modes/acp/acp-agent.ts +0 -24
- package/src/modes/acp/acp-client-bridge.ts +11 -154
- package/src/modes/components/hook-selector.ts +155 -44
- package/src/modes/components/model-selector.ts +242 -49
- package/src/modes/components/notifications-settings-editor.ts +631 -85
- package/src/modes/controllers/extension-ui-controller.ts +114 -15
- package/src/modes/controllers/selector-controller.ts +464 -50
- package/src/prompts/tools/bash.md +2 -2
- package/src/sdk/acp/adapter.ts +9 -200
- package/src/sdk/broker/broker.ts +54 -10
- package/src/sdk/broker/lifecycle-ledger.ts +34 -1
- package/src/sdk/broker/lifecycle.ts +47 -50
- package/src/sdk/bus/chat-daemon-cli.ts +12 -5
- package/src/sdk/bus/chat-daemon-control.ts +35 -8
- package/src/sdk/bus/chat-daemon-runtime.ts +16 -2
- package/src/sdk/bus/config.ts +534 -286
- package/src/sdk/bus/discord-live-provider.ts +57 -2
- package/src/sdk/bus/discord-provider.ts +21 -0
- package/src/sdk/bus/index.ts +105 -359
- package/src/sdk/bus/notification-orchestration.ts +371 -68
- package/src/sdk/bus/notification-service.ts +431 -130
- package/src/sdk/bus/session-control.ts +119 -44
- package/src/sdk/bus/slack-live-provider.ts +78 -2
- package/src/sdk/bus/slack-provider.ts +24 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +9 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
- package/src/sdk/bus/telegram-daemon-control.ts +3 -3
- package/src/sdk/bus/telegram-daemon.ts +23 -15
- package/src/sdk/client/discovery.ts +10 -4
- package/src/sdk/host/host.ts +9 -187
- package/src/sdk/host/query/handlers.ts +20 -1
- package/src/sdk/host/reverse-leases.ts +42 -249
- package/src/sdk/index.ts +1 -0
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +5 -2
- package/src/sdk/providers.ts +51 -0
- package/src/sdk/session-directory.ts +1 -0
- package/src/sdk/session.ts +10 -7
- package/src/session/agent-session.ts +59 -202
- package/src/session/artifacts.ts +74 -211
- package/src/session/client-bridge.ts +0 -15
- package/src/session/internal/managed-session-scope.ts +981 -450
- package/src/session/internal/managed-session-storage.ts +362 -124
- package/src/session/internal/native-publish-outcome.ts +3 -1
- package/src/session/internal/session-open-errors.ts +11 -0
- package/src/session/messages.ts +7 -32
- package/src/session/session-manager.ts +48 -15
- package/src/session/session-storage.ts +72 -5
- package/src/session/streaming-output.ts +40 -198
- package/src/slash-commands/builtin-registry.ts +82 -4
- package/src/task/index.ts +237 -65
- package/src/task/receipt.ts +2 -4
- package/src/task/render.ts +0 -30
- package/src/task/types.ts +10 -34
- package/src/tools/ask.ts +1 -1
- 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/telegram-send.ts +4 -2
- 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
|
@@ -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
|
}
|
package/src/sdk/broker/broker.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
BROKER_HEARTBEAT_TTL_MS,
|
|
14
14
|
type BrokerDiscovery,
|
|
15
|
+
type BrokerPublicationObservation,
|
|
15
16
|
brokerDiscoveryPath,
|
|
16
17
|
brokerProcessIncarnation,
|
|
17
18
|
heartbeatBrokerDiscoveryRetained,
|
|
@@ -396,6 +397,14 @@ type BrokerLockSnapshot = {
|
|
|
396
397
|
|
|
397
398
|
const BROKER_PUBLICATION_CADENCE_MS = 5_000;
|
|
398
399
|
const BROKER_PUBLICATION_GRACE_MS = 15_000;
|
|
400
|
+
// A broker that cannot observe its own publication is not provably the root, but
|
|
401
|
+
// ambiguity is also not proof of replacement, so it must not be treated as a
|
|
402
|
+
// `lost-root` immediately. It must still be bounded: an indefinitely ambiguous
|
|
403
|
+
// broker stops heartbeating, so peers discover it as stale and spawn replacements
|
|
404
|
+
// while it keeps its port and memory forever. Ambiguity therefore accrues against
|
|
405
|
+
// its own deadline, generous enough to absorb transient filesystem faults and far
|
|
406
|
+
// longer than the loss grace.
|
|
407
|
+
const BROKER_AMBIGUITY_GRACE_MS = 120_000;
|
|
399
408
|
const BROKER_SETTLEMENT_MS = 2_000;
|
|
400
409
|
type BrokerPublicationState =
|
|
401
410
|
| "healthy-owned"
|
|
@@ -406,6 +415,8 @@ type BrokerPublicationState =
|
|
|
406
415
|
type BrokerStopMode = "owned-root" | "lost-root";
|
|
407
416
|
|
|
408
417
|
const terminalPersistenceHooksForTest = new WeakMap<Broker, () => void>();
|
|
418
|
+
const ambiguityGraceOverridesForTest = new WeakMap<Broker, number>();
|
|
419
|
+
const publicationObservationOverridesForTest = new WeakMap<Broker, BrokerPublicationObservation>();
|
|
409
420
|
|
|
410
421
|
export class Broker {
|
|
411
422
|
readonly settings: ResolvedBrokerSettings;
|
|
@@ -419,6 +430,7 @@ export class Broker {
|
|
|
419
430
|
#publication: RetainedBrokerDiscovery | null = null;
|
|
420
431
|
#publicationState: BrokerPublicationState = "healthy-owned";
|
|
421
432
|
#lossAt: bigint | null = null;
|
|
433
|
+
#ambiguousAt: bigint | null = null;
|
|
422
434
|
#stopping = false;
|
|
423
435
|
#transport: BrokerTransport | null = null;
|
|
424
436
|
#heartbeatTimer: NodeJS.Timeout | null = null;
|
|
@@ -565,6 +577,7 @@ export class Broker {
|
|
|
565
577
|
this.#stopping = false;
|
|
566
578
|
this.#publicationState = "healthy-owned";
|
|
567
579
|
this.#lossAt = null;
|
|
580
|
+
this.#ambiguousAt = null;
|
|
568
581
|
await Promise.all([this.ledger.assertSupportedStateVersions(), readBrokerDiscovery(this.settings.agentDir)]);
|
|
569
582
|
await fs.mkdir(path.dirname(this.#lock), { recursive: true, mode: 0o700 });
|
|
570
583
|
for (;;) {
|
|
@@ -648,16 +661,35 @@ export class Broker {
|
|
|
648
661
|
#fence(kind: "suspect-unpublished" | "observation-ambiguous" | "heartbeat-ambiguous"): void {
|
|
649
662
|
if (this.#publicationState === "stopping") return;
|
|
650
663
|
this.#publicationState = kind;
|
|
651
|
-
if (kind === "suspect-unpublished")
|
|
652
|
-
|
|
664
|
+
if (kind === "suspect-unpublished") {
|
|
665
|
+
this.#lossAt ??= process.hrtime.bigint();
|
|
666
|
+
this.#ambiguousAt = null;
|
|
667
|
+
} else {
|
|
668
|
+
this.#lossAt = null;
|
|
669
|
+
this.#ambiguousAt ??= process.hrtime.bigint();
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Whether this broker has been unable to confirm it is the published root for
|
|
674
|
+
* longer than the deadline for its current fence. Replacement is proven quickly
|
|
675
|
+
* and ambiguity slowly, but neither may persist indefinitely: a permanently
|
|
676
|
+
* fenced broker never heartbeats, so it is unreachable through discovery while
|
|
677
|
+
* still holding its port and memory.
|
|
678
|
+
*/
|
|
679
|
+
#fencedBeyondDeadline(): boolean {
|
|
680
|
+
const now = process.hrtime.bigint();
|
|
681
|
+
if (this.#lossAt !== null && now - this.#lossAt >= BigInt(BROKER_PUBLICATION_GRACE_MS) * 1_000_000n) return true;
|
|
682
|
+
const ambiguityGraceMs = ambiguityGraceOverridesForTest.get(this) ?? BROKER_AMBIGUITY_GRACE_MS;
|
|
683
|
+
return this.#ambiguousAt !== null && now - this.#ambiguousAt >= BigInt(ambiguityGraceMs) * 1_000_000n;
|
|
653
684
|
}
|
|
654
685
|
async #watchPublication(writeHeartbeat = true): Promise<void> {
|
|
655
686
|
if (!this.#publication || this.#publicationState === "stopping") return;
|
|
656
687
|
let observation: ReturnType<RetainedBrokerDiscovery["observe"]>;
|
|
657
688
|
try {
|
|
658
|
-
observation = this.#publication.observe();
|
|
689
|
+
observation = publicationObservationOverridesForTest.get(this) ?? this.#publication.observe();
|
|
659
690
|
} catch {
|
|
660
691
|
this.#fence("observation-ambiguous");
|
|
692
|
+
if (this.#fencedBeyondDeadline()) void this.#complete("lost-root");
|
|
661
693
|
return;
|
|
662
694
|
}
|
|
663
695
|
if (observation === "owned") {
|
|
@@ -667,15 +699,12 @@ export class Broker {
|
|
|
667
699
|
}
|
|
668
700
|
this.#publicationState = "healthy-owned";
|
|
669
701
|
this.#lossAt = null;
|
|
702
|
+
this.#ambiguousAt = null;
|
|
670
703
|
if (writeHeartbeat) await this.#writeHeartbeat();
|
|
671
704
|
return;
|
|
672
705
|
}
|
|
673
706
|
this.#fence(observation === "ambiguous" ? "observation-ambiguous" : "suspect-unpublished");
|
|
674
|
-
if (
|
|
675
|
-
this.#lossAt !== null &&
|
|
676
|
-
process.hrtime.bigint() - this.#lossAt >= BigInt(BROKER_PUBLICATION_GRACE_MS) * 1_000_000n
|
|
677
|
-
)
|
|
678
|
-
void this.#complete("lost-root");
|
|
707
|
+
if (this.#fencedBeyondDeadline()) void this.#complete("lost-root");
|
|
679
708
|
}
|
|
680
709
|
async #writeHeartbeat(): Promise<void> {
|
|
681
710
|
if (!this.discovery || !this.#publication || this.#publicationState === "stopping") return;
|
|
@@ -691,6 +720,7 @@ export class Broker {
|
|
|
691
720
|
}
|
|
692
721
|
this.#publicationState = "healthy-owned";
|
|
693
722
|
this.#lossAt = null;
|
|
723
|
+
this.#ambiguousAt = null;
|
|
694
724
|
this.discovery = { ...this.discovery, heartbeatAt };
|
|
695
725
|
}
|
|
696
726
|
async heartbeat(): Promise<void> {
|
|
@@ -938,9 +968,8 @@ export class Broker {
|
|
|
938
968
|
});
|
|
939
969
|
if (isCleanupPending(response)) return response;
|
|
940
970
|
const persisted = await this.ledger.readTerminal(identity, requestHash);
|
|
941
|
-
const expectedResponseDigest = createHash("sha256").update(canonicalJson(response)).digest("hex");
|
|
942
971
|
const persistenceVerified =
|
|
943
|
-
persisted
|
|
972
|
+
persisted !== undefined &&
|
|
944
973
|
canonicalJson(persisted.response) === canonicalJson(response) &&
|
|
945
974
|
canonicalJson(persisted.durableEffects) === canonicalJson(outcome.durableEffects) &&
|
|
946
975
|
canonicalJson(persisted.startupFailure) === canonicalJson(outcome.startupFailure);
|
|
@@ -972,3 +1001,18 @@ export function setTerminalPersistenceHookForTest(broker: Broker, hook: (() => v
|
|
|
972
1001
|
if (hook) terminalPersistenceHooksForTest.set(broker, hook);
|
|
973
1002
|
else terminalPersistenceHooksForTest.delete(broker);
|
|
974
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
/** Test-only hook for shortening the bounded ambiguity deadline. */
|
|
1006
|
+
export function setAmbiguityGraceForTest(broker: Broker, graceMs: number | undefined): void {
|
|
1007
|
+
if (graceMs === undefined) ambiguityGraceOverridesForTest.delete(broker);
|
|
1008
|
+
else ambiguityGraceOverridesForTest.set(broker, graceMs);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/** Test-only hook for forcing the observation the publication watchdog sees. */
|
|
1012
|
+
export function setPublicationObservationForTest(
|
|
1013
|
+
broker: Broker,
|
|
1014
|
+
observation: BrokerPublicationObservation | undefined,
|
|
1015
|
+
): void {
|
|
1016
|
+
if (observation === undefined) publicationObservationOverridesForTest.delete(broker);
|
|
1017
|
+
else publicationObservationOverridesForTest.set(broker, observation);
|
|
1018
|
+
}
|
|
@@ -666,7 +666,13 @@ export class LifecycleLedger {
|
|
|
666
666
|
return this.#mutate(async () => {
|
|
667
667
|
const previous = this.#byIdentity.get(identity);
|
|
668
668
|
if (!previous) throw new Error("Unknown lifecycle identity");
|
|
669
|
-
const next = {
|
|
669
|
+
const next = {
|
|
670
|
+
...previous,
|
|
671
|
+
...fields,
|
|
672
|
+
state,
|
|
673
|
+
ts: Date.now(),
|
|
674
|
+
...(fields.response !== undefined ? { response: fields.response } : {}),
|
|
675
|
+
};
|
|
670
676
|
if (this.#isCleanupPending(next)) {
|
|
671
677
|
next.unresolvedCleanupResponse = undefined;
|
|
672
678
|
next.unresolvedCleanupResponseDigest = undefined;
|
|
@@ -796,4 +802,31 @@ export class LifecycleLedger {
|
|
|
796
802
|
get(identity: string): LifecycleLedgerEntry | undefined {
|
|
797
803
|
return this.#byIdentity.get(identity);
|
|
798
804
|
}
|
|
805
|
+
|
|
806
|
+
findPendingCleanupByTarget(
|
|
807
|
+
sessionId: string,
|
|
808
|
+
cwd: string,
|
|
809
|
+
transcriptPath: string,
|
|
810
|
+
): LifecycleLedgerEntry | undefined {
|
|
811
|
+
const expectedCwd = path.resolve(cwd);
|
|
812
|
+
const expectedTranscript = path.resolve(transcriptPath);
|
|
813
|
+
let latest: LifecycleLedgerEntry | undefined;
|
|
814
|
+
for (const entry of this.#byIdentity.values()) {
|
|
815
|
+
if (entry.state !== "effect_started" || !this.#isCleanupPending(entry)) continue;
|
|
816
|
+
const response = entry.response as {
|
|
817
|
+
error?: { cleanup?: { sessionId?: unknown; cwd?: unknown; transcriptPath?: unknown } };
|
|
818
|
+
};
|
|
819
|
+
const cleanup = response.error?.cleanup;
|
|
820
|
+
if (
|
|
821
|
+
cleanup?.sessionId !== sessionId ||
|
|
822
|
+
typeof cleanup.cwd !== "string" ||
|
|
823
|
+
typeof cleanup.transcriptPath !== "string" ||
|
|
824
|
+
path.resolve(cleanup.cwd) !== expectedCwd ||
|
|
825
|
+
path.resolve(cleanup.transcriptPath) !== expectedTranscript
|
|
826
|
+
)
|
|
827
|
+
continue;
|
|
828
|
+
if (!latest || entry.ts > latest.ts) latest = entry;
|
|
829
|
+
}
|
|
830
|
+
return latest;
|
|
831
|
+
}
|
|
799
832
|
}
|
|
@@ -865,7 +865,7 @@ export async function writeSessionLifecycleReady(root: string, id: string, effec
|
|
|
865
865
|
const incarnation = processIncarnation(process.pid);
|
|
866
866
|
if (!incarnation) throw new Error("Lifecycle child has no readable OS incarnation.");
|
|
867
867
|
await fs.mkdir(path.join(root, "sdk"), { recursive: true, mode: 0o700 });
|
|
868
|
-
await fs.writeFile(lifecycleReadyPath(root, id),
|
|
868
|
+
await fs.writeFile(lifecycleReadyPath(root, id), canonicalJson({ pid: process.pid, effectMarker, incarnation }), {
|
|
869
869
|
mode: 0o600,
|
|
870
870
|
});
|
|
871
871
|
}
|
|
@@ -1365,15 +1365,6 @@ function lifecycleMetadataReplayFiles(cleanup: CleanupEvidence): LifecycleCleanu
|
|
|
1365
1365
|
return cleanup.lifecycleFiles?.length ? cleanup.lifecycleFiles : undefined;
|
|
1366
1366
|
}
|
|
1367
1367
|
|
|
1368
|
-
function absentLifecyclePath(file: string): boolean {
|
|
1369
|
-
try {
|
|
1370
|
-
fsSync.lstatSync(file);
|
|
1371
|
-
return false;
|
|
1372
|
-
} catch (error) {
|
|
1373
|
-
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
1368
|
function isLifecycleCleanupResponse(value: LifecycleFileCapture | BrokerResponse | undefined): value is BrokerResponse {
|
|
1378
1369
|
return typeof value === "object" && value !== null && "ok" in value;
|
|
1379
1370
|
}
|
|
@@ -1423,6 +1414,19 @@ function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerRespon
|
|
|
1423
1414
|
try {
|
|
1424
1415
|
current = captureLifecycleFile(candidate, true, true);
|
|
1425
1416
|
} catch {
|
|
1417
|
+
if (
|
|
1418
|
+
file.completed &&
|
|
1419
|
+
[file.detachedPath, file.plannedPath].some(
|
|
1420
|
+
bound => bound && path.resolve(candidate) === path.resolve(bound),
|
|
1421
|
+
)
|
|
1422
|
+
) {
|
|
1423
|
+
try {
|
|
1424
|
+
const stat = fsSync.lstatSync(candidate);
|
|
1425
|
+
if (stat.isFile() && !stat.isSymbolicLink() && stat.nlink === 1 && stat.size === 0) continue;
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1426
1430
|
return fail("terminal_uncertain", "Lifecycle metadata candidate could not be safely inspected.");
|
|
1427
1431
|
}
|
|
1428
1432
|
if (!current) continue;
|
|
@@ -1457,7 +1461,7 @@ function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerRespon
|
|
|
1457
1461
|
if (!markerEntry)
|
|
1458
1462
|
return fail("terminal_uncertain", "Lifecycle readiness metadata lacks canonical marker authority.");
|
|
1459
1463
|
if (!marker) {
|
|
1460
|
-
if (createHash("sha256").update(
|
|
1464
|
+
if (createHash("sha256").update(ready.bytes).digest("hex") !== markerEntry.identity.sha256)
|
|
1461
1465
|
return fail(
|
|
1462
1466
|
"terminal_uncertain",
|
|
1463
1467
|
"Lifecycle readiness metadata is not bound to the completed marker authority.",
|
|
@@ -1473,30 +1477,6 @@ function validateLifecycleMetadataReplay(cleanup: CleanupEvidence): BrokerRespon
|
|
|
1473
1477
|
return undefined;
|
|
1474
1478
|
}
|
|
1475
1479
|
|
|
1476
|
-
function lifecycleMetadataReplayAbsent(cleanup: CleanupEvidence): boolean {
|
|
1477
|
-
const files = lifecycleMetadataReplayFiles(cleanup);
|
|
1478
|
-
if (!files) return true;
|
|
1479
|
-
const root = path.resolve(cleanup.metadataRoot!);
|
|
1480
|
-
const id = cleanup.sessionId!;
|
|
1481
|
-
const required = new Set<string>([lifecycleMarkerPath(root, id), lifecycleReadyPath(root, id)]);
|
|
1482
|
-
for (const file of files) for (const candidate of lifecycleCleanupCandidates(file)) required.add(candidate);
|
|
1483
|
-
for (const candidate of required) {
|
|
1484
|
-
if (absentLifecyclePath(candidate)) continue;
|
|
1485
|
-
// A completed file's recorded retained quarantine is durable evidence, not a
|
|
1486
|
-
// survivor — accept it only at its receipt-bound path and identity.
|
|
1487
|
-
const owner = files.find(
|
|
1488
|
-
file =>
|
|
1489
|
-
file.completed === true &&
|
|
1490
|
-
file.detachedPath !== undefined &&
|
|
1491
|
-
path.resolve(file.detachedPath) === path.resolve(candidate),
|
|
1492
|
-
);
|
|
1493
|
-
if (!owner) return false;
|
|
1494
|
-
const current = captureLifecycleFile(candidate, true, true);
|
|
1495
|
-
if (!current || !sameLifecycleCleanupIdentity(current.identity, owner.identity)) return false;
|
|
1496
|
-
}
|
|
1497
|
-
return true;
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
1480
|
/**
|
|
1501
1481
|
* Base dev persisted metadata cleanup one file at a time. Accept only its
|
|
1502
1482
|
* identity-bound marker receipt and translate it into the current replay plan.
|
|
@@ -1612,7 +1592,7 @@ function legacyMetadataCleanupPlan(cleanup: CleanupEvidence): CleanupEvidence |
|
|
|
1612
1592
|
return undefined;
|
|
1613
1593
|
}
|
|
1614
1594
|
if (marker && !sameEffectMarker(marker, readyMarker)) return undefined;
|
|
1615
|
-
if (!marker && createHash("sha256").update(
|
|
1595
|
+
if (!marker && createHash("sha256").update(ready.capture.bytes).digest("hex") !== persistedIdentity.sha256)
|
|
1616
1596
|
return undefined;
|
|
1617
1597
|
}
|
|
1618
1598
|
|
|
@@ -1785,11 +1765,11 @@ async function reconcileLifecycleCleanup(
|
|
|
1785
1765
|
file.detachedPath &&
|
|
1786
1766
|
path.resolve(candidate) === path.resolve(file.detachedPath) &&
|
|
1787
1767
|
stat.isFile() &&
|
|
1788
|
-
!stat.isSymbolicLink()
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1768
|
+
!stat.isSymbolicLink() &&
|
|
1769
|
+
stat.nlink === 1 &&
|
|
1770
|
+
stat.size === 0
|
|
1771
|
+
)
|
|
1772
|
+
continue;
|
|
1793
1773
|
return fail(
|
|
1794
1774
|
"terminal_uncertain",
|
|
1795
1775
|
"Lifecycle cleanup receipt marks a target complete while an authorized candidate remains.",
|
|
@@ -1892,7 +1872,7 @@ async function reconcileLifecycleCleanup(
|
|
|
1892
1872
|
});
|
|
1893
1873
|
}
|
|
1894
1874
|
const lifecycleFiles = activeCleanup.lifecycleFiles!.map((candidate, candidateIndex) =>
|
|
1895
|
-
candidateIndex === index ? { ...candidate, completed: true as const } : candidate,
|
|
1875
|
+
candidateIndex === index ? { ...candidate, detachedPath: undefined, completed: true as const } : candidate,
|
|
1896
1876
|
);
|
|
1897
1877
|
activeCleanup = { ...activeCleanup, lifecycleFiles };
|
|
1898
1878
|
await broker.ledger.transition(identity, "effect_started", {
|
|
@@ -1900,8 +1880,6 @@ async function reconcileLifecycleCleanup(
|
|
|
1900
1880
|
});
|
|
1901
1881
|
lifecycleCleanupHooksForTest.get(broker)?.();
|
|
1902
1882
|
}
|
|
1903
|
-
if (!lifecycleMetadataReplayAbsent(activeCleanup))
|
|
1904
|
-
return fail("terminal_uncertain", "Lifecycle metadata replay left an authorized sibling behind.");
|
|
1905
1883
|
await syncDirectory(path.join(activeCleanup.metadataRoot!, "sdk"));
|
|
1906
1884
|
return completion;
|
|
1907
1885
|
}
|
|
@@ -3463,6 +3441,21 @@ async function executeLifecycleResponse(
|
|
|
3463
3441
|
return (error as NodeJS.ErrnoException).code === "ENOENT";
|
|
3464
3442
|
}
|
|
3465
3443
|
};
|
|
3444
|
+
try {
|
|
3445
|
+
const currentTranscript = fsSync.lstatSync(cleanupTarget.transcriptPath, { bigint: true });
|
|
3446
|
+
if (
|
|
3447
|
+
currentTranscript.isFile() &&
|
|
3448
|
+
!currentTranscript.isSymbolicLink() &&
|
|
3449
|
+
currentTranscript.dev === cleanupTarget.transcriptIdentity.dev &&
|
|
3450
|
+
currentTranscript.ino === cleanupTarget.transcriptIdentity.ino
|
|
3451
|
+
)
|
|
3452
|
+
cleanupTarget.transcriptIdentity = {
|
|
3453
|
+
...cleanupTarget.transcriptIdentity,
|
|
3454
|
+
nlink: currentTranscript.nlink,
|
|
3455
|
+
};
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
3458
|
+
}
|
|
3466
3459
|
const transcriptParentIdentity = cleanup?.transcriptParentIdentity ?? validated.transcriptParentIdentity;
|
|
3467
3460
|
const durableArtifactsPlan =
|
|
3468
3461
|
cleanup?.artifactTree?.plannedPath ?? cleanup?.plannedArtifactsPath ?? cleanupTarget.plannedArtifactsPath;
|
|
@@ -3687,11 +3680,17 @@ async function executeLifecycleResponse(
|
|
|
3687
3680
|
const retainedTranscriptReplayHasNoSideAuthority = retainedTranscriptSidePaths.every(
|
|
3688
3681
|
candidate => candidate === undefined,
|
|
3689
3682
|
);
|
|
3690
|
-
const retainedTranscriptSideAuthorityIsProvenAbsent =
|
|
3691
|
-
retainedTranscriptSidePaths.every(pathIsAbsent) &&
|
|
3692
|
-
(cleanupTarget.detachedTranscriptPath ? true : retainedTranscriptIdentityIsAbsentFromParent());
|
|
3693
3683
|
if (cleanup && !retainedTranscriptReplayHasNoSideAuthority) {
|
|
3694
|
-
|
|
3684
|
+
const successorOrUnknownRemains = [
|
|
3685
|
+
cleanupTarget.retainedTranscriptSuccessorPath,
|
|
3686
|
+
cleanupTarget.retainedTranscriptUnknownPath,
|
|
3687
|
+
].some(candidate => candidate !== undefined && !pathIsAbsent(candidate));
|
|
3688
|
+
if (
|
|
3689
|
+
successorOrUnknownRemains ||
|
|
3690
|
+
!pathIsAbsent(cleanupTarget.retainedTranscriptPlaceholderPath) ||
|
|
3691
|
+
(!cleanupTarget.detachedTranscriptPath && !retainedTranscriptIdentityIsAbsentFromParent())
|
|
3692
|
+
)
|
|
3693
|
+
return await publishRetainedTranscriptSideAuthority();
|
|
3695
3694
|
cleanupTarget.retainedTranscriptSuccessorPath = undefined;
|
|
3696
3695
|
cleanupTarget.retainedTranscriptPlaceholderPath = undefined;
|
|
3697
3696
|
cleanupTarget.retainedTranscriptUnknownPath = undefined;
|
|
@@ -3861,8 +3860,6 @@ async function executeLifecycleResponse(
|
|
|
3861
3860
|
preauthorizedCleanup,
|
|
3862
3861
|
"Saved session cleanup is pending in artifacts: a planned quarantine alias remains before terminal completion.",
|
|
3863
3862
|
);
|
|
3864
|
-
if (deleted.kind === "deleted" && !retainedTranscriptIdentityIsAbsentFromParent())
|
|
3865
|
-
return await publishRetainedTranscriptSideAuthority();
|
|
3866
3863
|
const retainedRootArtifactsPlan = durableArtifactsPlan;
|
|
3867
3864
|
if (deleted.kind === "cleanup_pending")
|
|
3868
3865
|
return fail(
|