@gajae-code/coding-agent 0.12.11 → 0.12.13
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 +68 -0
- package/dist/types/cli/marketplace-hint.d.ts +28 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/cli.d.ts +10 -0
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock-gc.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +3 -0
- package/dist/types/config/model-registry.d.ts +6 -0
- package/dist/types/config/models-config-schema.d.ts +99 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +3 -1
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/ouroboros-ooo-bridge.d.ts +13 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +0 -1
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +14 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +11 -0
- package/dist/types/main.d.ts +6 -2
- package/dist/types/modes/acp/acp-agent.d.ts +24 -1
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/utils/host-status.d.ts +23 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +12 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -0
- package/dist/types/sdk/bus/chat-daemon-command-channel.d.ts +244 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +11 -1
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +7 -0
- package/dist/types/sdk/bus/control-plane-frames.d.ts +43 -0
- package/dist/types/sdk/bus/conversation-store.d.ts +18 -0
- package/dist/types/sdk/bus/existing-thread-readiness.d.ts +71 -0
- package/dist/types/sdk/bus/html-format.d.ts +10 -0
- package/dist/types/sdk/bus/notification-service.d.ts +10 -0
- package/dist/types/sdk/bus/slack-daemon.d.ts +27 -0
- package/dist/types/sdk/bus/slack-live-provider.d.ts +11 -0
- package/dist/types/sdk/bus/slack-provider.d.ts +14 -0
- package/dist/types/sdk/bus/slack-thread-binding.d.ts +140 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +28 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -17
- package/dist/types/sdk/bus/telegram-reference.d.ts +20 -1
- package/dist/types/sdk/bus/topic-registry.d.ts +130 -85
- package/dist/types/sdk/host/host.d.ts +52 -0
- package/dist/types/sdk/lifecycle-session.d.ts +6 -0
- package/dist/types/sdk/session-activation.d.ts +41 -0
- package/dist/types/sdk/session.d.ts +18 -0
- package/dist/types/sdk/startup-capability.d.ts +8 -1
- package/dist/types/session/agent-session.d.ts +7 -2
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +40 -6
- package/dist/types/tools/index.d.ts +15 -0
- package/dist/types/utils/npm-registry.d.ts +71 -0
- package/examples/extensions/README.md +38 -4
- package/examples/extensions/ooo-bridge.ts +15 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/src/async/job-manager.ts +9 -2
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/marketplace-hint.ts +39 -0
- package/src/cli/notify-cli.ts +226 -2
- package/src/cli/plugin-cli.ts +10 -0
- package/src/cli/update-cli.ts +90 -23
- package/src/cli.ts +23 -0
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +65 -26
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +46 -30
- package/src/config/model-registry.ts +17 -5
- package/src/config/models-config-schema.ts +9 -4
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +2 -0
- package/src/coordinator-mcp/server.ts +358 -7
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/ouroboros-ooo-bridge.ts +235 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +4 -9
- package/src/extensibility/extensions/runner.ts +15 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +9 -5
- package/src/gjc-runtime/deep-interview-state.ts +27 -3
- package/src/gjc-runtime/gc-render.ts +8 -1
- package/src/gjc-runtime/gc-runtime.ts +15 -1
- package/src/gjc-runtime/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/main.ts +214 -70
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/acp/acp-agent.ts +123 -7
- package/src/modes/components/footer.ts +10 -18
- package/src/modes/components/model-selector.ts +52 -5
- package/src/modes/components/session-selector.ts +26 -3
- package/src/modes/components/status-line/gh.ts +23 -2
- package/src/modes/controllers/event-controller.ts +3 -0
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +11 -0
- package/src/modes/controllers/selector-controller.ts +8 -2
- package/src/modes/interactive-mode.ts +7 -1
- package/src/modes/print-mode.ts +10 -0
- package/src/modes/utils/host-status.ts +38 -0
- package/src/prompts/system/project-prompt.md +19 -0
- package/src/runtime-mcp/types.ts +2 -0
- package/src/sdk/broker/lifecycle.ts +60 -3
- package/src/sdk/bus/chat-daemon-cli.ts +42 -0
- package/src/sdk/bus/chat-daemon-command-channel.ts +921 -0
- package/src/sdk/bus/chat-daemon-control.ts +16 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +257 -21
- package/src/sdk/bus/control-plane-frames.ts +51 -0
- package/src/sdk/bus/conversation-store.ts +29 -1
- package/src/sdk/bus/existing-thread-readiness.ts +155 -0
- package/src/sdk/bus/html-format.ts +19 -3
- package/src/sdk/bus/index.ts +91 -12
- package/src/sdk/bus/notification-service.ts +28 -1
- package/src/sdk/bus/rich-render.ts +3 -2
- package/src/sdk/bus/slack-daemon.ts +99 -4
- package/src/sdk/bus/slack-live-provider.ts +32 -2
- package/src/sdk/bus/slack-provider.ts +11 -0
- package/src/sdk/bus/slack-thread-binding.ts +641 -0
- package/src/sdk/bus/telegram-daemon-cli.ts +11 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +28 -7
- package/src/sdk/bus/telegram-daemon.ts +2007 -509
- package/src/sdk/bus/telegram-reference.ts +114 -15
- package/src/sdk/bus/topic-registry.ts +674 -181
- package/src/sdk/host/host.ts +180 -1
- package/src/sdk/lifecycle-session.ts +8 -2
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session-activation.ts +127 -0
- package/src/sdk/session.ts +144 -29
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -13
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +228 -7
- package/src/session/session-manager.ts +225 -60
- package/src/setup/provider-presets.json +3 -3
- package/src/task/index.ts +92 -44
- package/src/thinking-metadata.ts +2 -1
- package/src/tools/ask.ts +10 -0
- package/src/tools/index.ts +15 -0
- package/src/tools/todo-write.ts +4 -4
- package/src/utils/npm-registry.ts +660 -0
|
@@ -159,7 +159,6 @@ export interface ForkContextSeed {
|
|
|
159
159
|
messages: Message[];
|
|
160
160
|
agentMessages: AgentMessage[];
|
|
161
161
|
metadata: ForkContextSeedMetadata;
|
|
162
|
-
cacheIdentity?: string;
|
|
163
162
|
appendOnlyPrefixSnapshot?: StablePrefixSnapshot;
|
|
164
163
|
}
|
|
165
164
|
|
|
@@ -167,7 +166,6 @@ export interface ForkContextSeedOptions {
|
|
|
167
166
|
maxMessages: number;
|
|
168
167
|
maxTokens: number;
|
|
169
168
|
preserveLatestUser?: boolean;
|
|
170
|
-
cacheIdentity?: string;
|
|
171
169
|
signal?: AbortSignal;
|
|
172
170
|
}
|
|
173
171
|
|
|
@@ -184,7 +182,7 @@ import {
|
|
|
184
182
|
} from "@gajae-code/utils";
|
|
185
183
|
|
|
186
184
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
187
|
-
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
185
|
+
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager, type OwnerSubagentShutdownLease } from "../async";
|
|
188
186
|
import { reset as resetCapabilities } from "../capability";
|
|
189
187
|
import type { Rule } from "../capability/rule";
|
|
190
188
|
import type { CasReceipt } from "../config/atomic-yaml-patch";
|
|
@@ -610,6 +608,8 @@ export interface AgentSessionConfig {
|
|
|
610
608
|
discoveryMode?: "off" | "mcp-only" | "all";
|
|
611
609
|
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
612
610
|
initialSelectedMCPToolNames?: string[];
|
|
611
|
+
/** Keep persisted MCP names until a deferred exact catalog becomes available. */
|
|
612
|
+
preserveUnavailableInitialMCPToolSelection?: boolean;
|
|
613
613
|
/** Built-in discoverable tool names restored for the current all-discovery session. */
|
|
614
614
|
initialSelectedDiscoveredBuiltinToolNames?: string[];
|
|
615
615
|
/** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
|
|
@@ -656,6 +656,8 @@ export interface AgentSessionConfig {
|
|
|
656
656
|
* process-global manager **MUST NOT** dispose it on their own teardown.
|
|
657
657
|
*/
|
|
658
658
|
ownedMcpManager?: MCPManager;
|
|
659
|
+
/** Optional startup dependency that must settle before the first provider turn. */
|
|
660
|
+
startupTurnBarrier?: Promise<void>;
|
|
659
661
|
/** Optional fork-context seed used to initialize a child session before its first prompt. */
|
|
660
662
|
forkContextSeed?: ForkContextSeed;
|
|
661
663
|
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
@@ -1947,6 +1949,7 @@ export class AgentSession {
|
|
|
1947
1949
|
*/
|
|
1948
1950
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
1949
1951
|
readonly #ownedMcpManager: MCPManager | undefined;
|
|
1952
|
+
#startupTurnBarrier: Promise<void> | undefined;
|
|
1950
1953
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
1951
1954
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
1952
1955
|
#evalExecutionDisposing = false;
|
|
@@ -1966,6 +1969,8 @@ export class AgentSession {
|
|
|
1966
1969
|
#isDisposed = false;
|
|
1967
1970
|
#disposePromise: Promise<void> | undefined;
|
|
1968
1971
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
1972
|
+
readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
|
|
1973
|
+
readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
|
|
1969
1974
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
1970
1975
|
// Extension system
|
|
1971
1976
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
@@ -2269,10 +2274,23 @@ export class AgentSession {
|
|
|
2269
2274
|
next.ready.resolve();
|
|
2270
2275
|
}
|
|
2271
2276
|
|
|
2277
|
+
async #awaitStartupTurnBarrier(): Promise<void> {
|
|
2278
|
+
const barrier = this.#startupTurnBarrier;
|
|
2279
|
+
if (!barrier) return;
|
|
2280
|
+
await barrier;
|
|
2281
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
|
|
2282
|
+
}
|
|
2283
|
+
extendStartupTurnBarrier(barrier: Promise<void>): void {
|
|
2284
|
+
const current = this.#startupTurnBarrier;
|
|
2285
|
+
this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
|
|
2286
|
+
void this.#startupTurnBarrier.catch(() => {});
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2272
2289
|
async #withSessionAdmission<T>(
|
|
2273
2290
|
kind: SessionAdmissionKind,
|
|
2274
2291
|
body: (lease: SessionAdmissionLease) => Promise<T>,
|
|
2275
2292
|
): Promise<T> {
|
|
2293
|
+
if (kind === "prompt") await this.#awaitStartupTurnBarrier();
|
|
2276
2294
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
2277
2295
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
2278
2296
|
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
@@ -2601,6 +2619,7 @@ export class AgentSession {
|
|
|
2601
2619
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
2602
2620
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
2603
2621
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
2622
|
+
this.#startupTurnBarrier = config.startupTurnBarrier;
|
|
2604
2623
|
this.#scopedModels = config.scopedModels ?? [];
|
|
2605
2624
|
this.#thinkingLevel = config.thinkingLevel;
|
|
2606
2625
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -2661,6 +2680,8 @@ export class AgentSession {
|
|
|
2661
2680
|
injectIdle: async messages => {
|
|
2662
2681
|
const first = messages[0];
|
|
2663
2682
|
if (!first) return;
|
|
2683
|
+
await this.#awaitStartupTurnBarrier();
|
|
2684
|
+
if (this.#isDisposed) return;
|
|
2664
2685
|
if (messages.length === 1) {
|
|
2665
2686
|
await this.agent.prompt(first, this.#managedFallbackPromptOptions());
|
|
2666
2687
|
} else {
|
|
@@ -2723,7 +2744,7 @@ export class AgentSession {
|
|
|
2723
2744
|
config.initialDiscoveredBuiltinToolSelectionIsExplicit === true
|
|
2724
2745
|
? this.#selectRestorableDiscoveredBuiltinToolNames(config.initialPersistedDiscoveredBuiltinToolNames ?? [])
|
|
2725
2746
|
: undefined;
|
|
2726
|
-
this.#pruneSelectedMCPToolNames();
|
|
2747
|
+
if (!config.preserveUnavailableInitialMCPToolSelection) this.#pruneSelectedMCPToolNames();
|
|
2727
2748
|
const persistInitialMCPToolSelection =
|
|
2728
2749
|
config.persistInitialMCPToolSelection === true && config.initialMCPToolSelectionIsExplicit !== false;
|
|
2729
2750
|
const persistInitialDiscoveredBuiltinToolSelection =
|
|
@@ -3095,7 +3116,6 @@ export class AgentSession {
|
|
|
3095
3116
|
maxTokens,
|
|
3096
3117
|
skippedReasons: {},
|
|
3097
3118
|
},
|
|
3098
|
-
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
3099
3119
|
};
|
|
3100
3120
|
}
|
|
3101
3121
|
const transformedMessages = await this.#transformContext([...this.messages], options.signal);
|
|
@@ -3295,7 +3315,6 @@ export class AgentSession {
|
|
|
3295
3315
|
maxTokens,
|
|
3296
3316
|
skippedReasons,
|
|
3297
3317
|
},
|
|
3298
|
-
cacheIdentity: options.cacheIdentity ?? this.sessionId,
|
|
3299
3318
|
appendOnlyPrefixSnapshot,
|
|
3300
3319
|
};
|
|
3301
3320
|
}
|
|
@@ -3377,6 +3396,9 @@ export class AgentSession {
|
|
|
3377
3396
|
getAgentId(): string | undefined {
|
|
3378
3397
|
return this.#agentId;
|
|
3379
3398
|
}
|
|
3399
|
+
get isDisposed(): boolean {
|
|
3400
|
+
return this.#isDisposed;
|
|
3401
|
+
}
|
|
3380
3402
|
|
|
3381
3403
|
registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void {
|
|
3382
3404
|
if (this.#isDisposed) throw new Error("Cannot register tool cleanup after session disposal has started.");
|
|
@@ -3384,6 +3406,23 @@ export class AgentSession {
|
|
|
3384
3406
|
return () => this.#toolSessionCleanups.delete(cleanup);
|
|
3385
3407
|
}
|
|
3386
3408
|
|
|
3409
|
+
registerToolSessionTransitionCleanup(cleanup: () => Promise<void> | void): () => void {
|
|
3410
|
+
if (this.#isDisposed)
|
|
3411
|
+
throw new Error("Cannot register tool transition cleanup after session disposal has started.");
|
|
3412
|
+
this.#toolSessionTransitionCleanups.add(cleanup);
|
|
3413
|
+
return () => this.#toolSessionTransitionCleanups.delete(cleanup);
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
async #runToolSessionTransitionCleanups(): Promise<void> {
|
|
3417
|
+
const cleanups = Array.from(this.#toolSessionTransitionCleanups);
|
|
3418
|
+
this.#toolSessionTransitionCleanups.clear();
|
|
3419
|
+
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
3420
|
+
for (const result of results) {
|
|
3421
|
+
if (result.status === "rejected")
|
|
3422
|
+
logger.warn("Tool session transition cleanup failed", { error: String(result.reason) });
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
|
|
3387
3426
|
async #runToolSessionCleanups(): Promise<void> {
|
|
3388
3427
|
const cleanups = Array.from(this.#toolSessionCleanups);
|
|
3389
3428
|
this.#toolSessionCleanups.clear();
|
|
@@ -3436,6 +3475,61 @@ export class AgentSession {
|
|
|
3436
3475
|
manager.cancelAll({ ownerId: this.#agentId });
|
|
3437
3476
|
}
|
|
3438
3477
|
|
|
3478
|
+
async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
|
|
3479
|
+
const ownerId = this.#agentId;
|
|
3480
|
+
const manager = AsyncJobManager.instance();
|
|
3481
|
+
if (!ownerId || !manager) return;
|
|
3482
|
+
const lease = manager.beginOwnerSubagentShutdown(ownerId);
|
|
3483
|
+
if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
|
|
3484
|
+
let committed = false;
|
|
3485
|
+
try {
|
|
3486
|
+
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
3487
|
+
const proof = await manager.cancelAndProveOwnerSubagents(lease);
|
|
3488
|
+
if (!proof.confirmed)
|
|
3489
|
+
throw new Error("Owned subagent cleanup could not be confirmed before artifact retirement.");
|
|
3490
|
+
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3491
|
+
throw new Error("Owned async deliveries did not settle before artifact retirement.");
|
|
3492
|
+
if (!(await manager.cancelAndSettleOwnerJobs(ownerId)))
|
|
3493
|
+
throw new Error("Owned async jobs did not settle before artifact retirement.");
|
|
3494
|
+
committed = true;
|
|
3495
|
+
} finally {
|
|
3496
|
+
manager.finishOwnerSubagentShutdown(lease, committed ? "commit" : "release");
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
#scheduleDeferredOwnerShutdownFinalization(
|
|
3501
|
+
manager: AsyncJobManager,
|
|
3502
|
+
lease: OwnerSubagentShutdownLease,
|
|
3503
|
+
ownerId: string,
|
|
3504
|
+
): void {
|
|
3505
|
+
const finalization = (async () => {
|
|
3506
|
+
while (!this.#isDisposed) {
|
|
3507
|
+
try {
|
|
3508
|
+
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
3509
|
+
const proof = await manager.cancelAndProveOwnerSubagents(lease);
|
|
3510
|
+
if (!proof.confirmed) throw new Error(`owner_subagent_${proof.reason}`);
|
|
3511
|
+
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3512
|
+
throw new Error("owner_delivery_settlement_timeout");
|
|
3513
|
+
if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
|
|
3514
|
+
if (this.#isDisposed) break;
|
|
3515
|
+
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
3516
|
+
await this.#runToolSessionTransitionCleanups();
|
|
3517
|
+
manager.finishOwnerSubagentShutdown(lease, "commit");
|
|
3518
|
+
return;
|
|
3519
|
+
} catch (error) {
|
|
3520
|
+
logger.warn("Deferred owner shutdown finalization retry failed", {
|
|
3521
|
+
ownerId,
|
|
3522
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3523
|
+
});
|
|
3524
|
+
await Bun.sleep(25);
|
|
3525
|
+
}
|
|
3526
|
+
}
|
|
3527
|
+
manager.finishOwnerSubagentShutdown(lease, "release");
|
|
3528
|
+
})();
|
|
3529
|
+
this.#deferredOwnerShutdownFinalizations.add(finalization);
|
|
3530
|
+
void finalization.finally(() => this.#deferredOwnerShutdownFinalizations.delete(finalization));
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3439
3533
|
#suppressOwnAsyncJobDeliveries(): void {
|
|
3440
3534
|
if (!this.#agentId) return;
|
|
3441
3535
|
const manager = AsyncJobManager.instance();
|
|
@@ -4647,6 +4741,11 @@ export class AgentSession {
|
|
|
4647
4741
|
settleLease();
|
|
4648
4742
|
return;
|
|
4649
4743
|
}
|
|
4744
|
+
await this.#awaitStartupTurnBarrier();
|
|
4745
|
+
if (!canContinue()) {
|
|
4746
|
+
settleLease();
|
|
4747
|
+
return;
|
|
4748
|
+
}
|
|
4650
4749
|
try {
|
|
4651
4750
|
if (!options?.skipCompactionCheck) {
|
|
4652
4751
|
await this.#checkEstimatedContextBeforePrompt();
|
|
@@ -5996,6 +6095,7 @@ export class AgentSession {
|
|
|
5996
6095
|
// the session that owns the manager goes on to dispose it (which itself
|
|
5997
6096
|
// nukes any leftover jobs and pending deliveries).
|
|
5998
6097
|
this.#cancelOwnAsyncJobs();
|
|
6098
|
+
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
5999
6099
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
6000
6100
|
if (ownedAsyncManager) {
|
|
6001
6101
|
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
@@ -6007,6 +6107,7 @@ export class AgentSession {
|
|
|
6007
6107
|
AsyncJobManager.setInstance(undefined);
|
|
6008
6108
|
}
|
|
6009
6109
|
}
|
|
6110
|
+
await this.#runToolSessionTransitionCleanups();
|
|
6010
6111
|
await this.#runToolSessionCleanups();
|
|
6011
6112
|
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
6012
6113
|
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
@@ -6834,6 +6935,12 @@ export class AgentSession {
|
|
|
6834
6935
|
const built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
|
|
6835
6936
|
this.#rebuildSystemPrompt!(validToolNames, this.#toolRegistry),
|
|
6836
6937
|
);
|
|
6938
|
+
if (this.#isDisposed) {
|
|
6939
|
+
if (generation === this.#baseSystemPromptGeneration) {
|
|
6940
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
6941
|
+
}
|
|
6942
|
+
return;
|
|
6943
|
+
}
|
|
6837
6944
|
if (generation === this.#baseSystemPromptGeneration) {
|
|
6838
6945
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
6839
6946
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
@@ -7319,6 +7426,8 @@ export class AgentSession {
|
|
|
7319
7426
|
if (!canContinuePersistedHistory(this.agent.state.messages)) {
|
|
7320
7427
|
throw new Error("Cannot continue from persisted message history");
|
|
7321
7428
|
}
|
|
7429
|
+
await this.#awaitStartupTurnBarrier();
|
|
7430
|
+
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
7322
7431
|
this.#beginInFlight();
|
|
7323
7432
|
let hindsightRecall: string | undefined;
|
|
7324
7433
|
try {
|
|
@@ -10183,6 +10292,7 @@ export class AgentSession {
|
|
|
10183
10292
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10184
10293
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10185
10294
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10295
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10186
10296
|
} catch (error) {
|
|
10187
10297
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10188
10298
|
}
|
|
@@ -10252,6 +10362,7 @@ export class AgentSession {
|
|
|
10252
10362
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10253
10363
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10254
10364
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10365
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10255
10366
|
} catch (error) {
|
|
10256
10367
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10257
10368
|
}
|
|
@@ -10418,7 +10529,9 @@ export class AgentSession {
|
|
|
10418
10529
|
}
|
|
10419
10530
|
try {
|
|
10420
10531
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10532
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10421
10533
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10534
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10422
10535
|
} catch (error) {
|
|
10423
10536
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10424
10537
|
}
|
|
@@ -11535,6 +11648,7 @@ export class AgentSession {
|
|
|
11535
11648
|
const artifactManager = this.sessionManager.getArtifactManager();
|
|
11536
11649
|
const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
|
|
11537
11650
|
let reservedArtifactId: string | undefined;
|
|
11651
|
+
let artifactAllocationAvailable = artifactManager !== null;
|
|
11538
11652
|
if (artifactManager) {
|
|
11539
11653
|
try {
|
|
11540
11654
|
reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
|
|
@@ -11542,14 +11656,24 @@ export class AgentSession {
|
|
|
11542
11656
|
logger.warn("Failed to reserve artifact ID for pruned tool output", {
|
|
11543
11657
|
error: error instanceof Error ? error.message : String(error),
|
|
11544
11658
|
});
|
|
11659
|
+
artifactAllocationAvailable = false;
|
|
11545
11660
|
}
|
|
11546
11661
|
}
|
|
11547
11662
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
|
|
11548
11663
|
relaxedMinimum: overThreshold ? 0 : undefined,
|
|
11549
11664
|
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11550
11665
|
artifactRef: candidate => {
|
|
11551
|
-
if (!artifactManager) return undefined;
|
|
11552
|
-
|
|
11666
|
+
if (!artifactManager || !artifactAllocationAvailable) return undefined;
|
|
11667
|
+
let id: string;
|
|
11668
|
+
try {
|
|
11669
|
+
id = reservedArtifactId ?? String(artifactManager.allocateId());
|
|
11670
|
+
} catch (error) {
|
|
11671
|
+
artifactAllocationAvailable = false;
|
|
11672
|
+
logger.warn("Failed to allocate artifact ID for pruned tool output", {
|
|
11673
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11674
|
+
});
|
|
11675
|
+
return undefined;
|
|
11676
|
+
}
|
|
11553
11677
|
reservedArtifactId = undefined;
|
|
11554
11678
|
const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
|
|
11555
11679
|
prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
|
|
@@ -12161,10 +12285,12 @@ export class AgentSession {
|
|
|
12161
12285
|
const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
|
|
12162
12286
|
// Last fallible action: verified local:// readiness from immutable staged options.
|
|
12163
12287
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12288
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
12164
12289
|
|
|
12165
12290
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
12166
12291
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
12167
12292
|
committed = true;
|
|
12293
|
+
await this.#runToolSessionTransitionCleanups();
|
|
12168
12294
|
this.agent.reset();
|
|
12169
12295
|
this.#syncAgentSessionId();
|
|
12170
12296
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -12195,7 +12321,6 @@ export class AgentSession {
|
|
|
12195
12321
|
this.#resetIrcRosterDeliveryState();
|
|
12196
12322
|
this.#planReferenceSent = false;
|
|
12197
12323
|
this.#planReferencePath = "local://PLAN.md";
|
|
12198
|
-
this.#cancelOwnAsyncJobs();
|
|
12199
12324
|
// The predecessor's fenced async-job results (queued while the
|
|
12200
12325
|
// transition held the delivery fence) belong to the handed-off session,
|
|
12201
12326
|
// not the successor. Suppress and drop ONLY the async-result kind so they
|
|
@@ -14937,6 +15062,17 @@ export class AgentSession {
|
|
|
14937
15062
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14938
15063
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14939
15064
|
const canReplayRotatedCredential = credentialRotated;
|
|
15065
|
+
// Universal automatic replay-safety gate (#3791): once the failed attempt
|
|
15066
|
+
// carries observable assistant text, thinking, or tool-call content,
|
|
15067
|
+
// automatic session retry must not re-issue the request. This covers
|
|
15068
|
+
// configured legacy retry (which previously only gated first-event
|
|
15069
|
+
// timeouts) as well as bare defaults. Managed fallback keeps its
|
|
15070
|
+
// provisional discard path (events never published to the session).
|
|
15071
|
+
// Content-free credential rotation remains eligible above; first-event
|
|
15072
|
+
// timeout keeps its additional typed/scope checks earlier in this method.
|
|
15073
|
+
if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
|
|
15074
|
+
return false;
|
|
15075
|
+
}
|
|
14940
15076
|
// Bare defaults retain their narrow watchdog and Codex admissions. A
|
|
14941
15077
|
// first-event timeout adds the typed, content-free, current-clean-scope
|
|
14942
15078
|
// requirement above; other transient watchdogs preserve legacy behavior.
|
|
@@ -16300,6 +16436,10 @@ export class AgentSession {
|
|
|
16300
16436
|
onTransitionMutationStarted?: () => void;
|
|
16301
16437
|
},
|
|
16302
16438
|
): Promise<boolean> {
|
|
16439
|
+
let ownerShutdownManager: AsyncJobManager | undefined;
|
|
16440
|
+
let ownerShutdownLease: OwnerSubagentShutdownLease | undefined;
|
|
16441
|
+
let ownerShutdownTransitionCommitted = false;
|
|
16442
|
+
let ownerShutdownFinalizationDeferred = false;
|
|
16303
16443
|
this.#beginSessionTransition("switch-session");
|
|
16304
16444
|
try {
|
|
16305
16445
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
@@ -16320,9 +16460,31 @@ export class AgentSession {
|
|
|
16320
16460
|
}
|
|
16321
16461
|
options?.onTransitionMutationStarted?.();
|
|
16322
16462
|
|
|
16323
|
-
|
|
16463
|
+
const asyncManager = AsyncJobManager.instance();
|
|
16464
|
+
const ownerId = this.#agentId;
|
|
16465
|
+
const lease =
|
|
16466
|
+
switchingToDifferentSession && asyncManager && ownerId
|
|
16467
|
+
? asyncManager.beginOwnerSubagentShutdown(ownerId)
|
|
16468
|
+
: undefined;
|
|
16469
|
+
if (switchingToDifferentSession && asyncManager && ownerId && !lease) {
|
|
16470
|
+
this.emitNotice(
|
|
16471
|
+
"error",
|
|
16472
|
+
"Cannot switch sessions while owned subagent cleanup is already in progress.",
|
|
16473
|
+
"switch-session-subagent-cleanup",
|
|
16474
|
+
);
|
|
16475
|
+
return false;
|
|
16476
|
+
}
|
|
16477
|
+
if (lease && asyncManager) {
|
|
16478
|
+
ownerShutdownManager = asyncManager;
|
|
16479
|
+
ownerShutdownLease = lease;
|
|
16480
|
+
}
|
|
16324
16481
|
await this.abort();
|
|
16482
|
+
if (this.isCompacting) {
|
|
16483
|
+
this.abortCompaction();
|
|
16484
|
+
while (this.isCompacting) await Bun.sleep(10);
|
|
16485
|
+
}
|
|
16325
16486
|
|
|
16487
|
+
this.#disconnectFromAgent();
|
|
16326
16488
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
16327
16489
|
await this.sessionManager.flush();
|
|
16328
16490
|
const previousSessionState = this.sessionManager.captureState();
|
|
@@ -16353,9 +16515,13 @@ export class AgentSession {
|
|
|
16353
16515
|
? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
|
|
16354
16516
|
: undefined;
|
|
16355
16517
|
let unavailableDefaultChainMessage: string | undefined;
|
|
16518
|
+
let transitionCleanupCommitted = false;
|
|
16356
16519
|
|
|
16357
16520
|
try {
|
|
16358
|
-
await this.sessionManager.setSessionFile(sessionPath
|
|
16521
|
+
await this.sessionManager.setSessionFile(sessionPath, {
|
|
16522
|
+
deferEphemeralArtifactRetirement: switchingToDifferentSession,
|
|
16523
|
+
});
|
|
16524
|
+
if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
|
|
16359
16525
|
// The successor identity is already rotated in the manager but not yet
|
|
16360
16526
|
// published; gate its local:// root before publication so the agent,
|
|
16361
16527
|
// workflow-gate emitter, and hooks cannot resolve against an ungated
|
|
@@ -16455,6 +16621,38 @@ export class AgentSession {
|
|
|
16455
16621
|
this.#resetIrcRosterDeliveryState();
|
|
16456
16622
|
}
|
|
16457
16623
|
|
|
16624
|
+
if (switchingToDifferentSession) {
|
|
16625
|
+
let ownerShutdownSettled = true;
|
|
16626
|
+
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
16627
|
+
try {
|
|
16628
|
+
ownerShutdownManager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
16629
|
+
const proof = await ownerShutdownManager.cancelAndProveOwnerSubagents(ownerShutdownLease);
|
|
16630
|
+
if (!proof.confirmed)
|
|
16631
|
+
throw new Error("Owned subagent cleanup could not be confirmed after successor validation.");
|
|
16632
|
+
if (!(await ownerShutdownManager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
16633
|
+
throw new Error("Owned async deliveries did not settle after successor validation.");
|
|
16634
|
+
if (!(await ownerShutdownManager.cancelAndSettleOwnerJobs(ownerId)))
|
|
16635
|
+
throw new Error("Owned async jobs did not settle after successor validation.");
|
|
16636
|
+
} catch (error) {
|
|
16637
|
+
ownerShutdownSettled = false;
|
|
16638
|
+
ownerShutdownFinalizationDeferred = true;
|
|
16639
|
+
this.#scheduleDeferredOwnerShutdownFinalization(ownerShutdownManager, ownerShutdownLease, ownerId);
|
|
16640
|
+
this.#suppressOwnAsyncJobDeliveries();
|
|
16641
|
+
this.emitNotice(
|
|
16642
|
+
"error",
|
|
16643
|
+
`Successor session is active, but predecessor async cleanup did not settle: ${error instanceof Error ? error.message : String(error)}`,
|
|
16644
|
+
"switch-session-subagent-cleanup",
|
|
16645
|
+
);
|
|
16646
|
+
}
|
|
16647
|
+
}
|
|
16648
|
+
transitionCleanupCommitted = true;
|
|
16649
|
+
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
16650
|
+
ownerShutdownTransitionCommitted = true;
|
|
16651
|
+
if (ownerShutdownSettled) {
|
|
16652
|
+
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
16653
|
+
await this.#runToolSessionTransitionCleanups();
|
|
16654
|
+
}
|
|
16655
|
+
}
|
|
16458
16656
|
this.#reconnectToAgent();
|
|
16459
16657
|
// Fence predecessor continuations before session_switch starts SDK runtime
|
|
16460
16658
|
// teardown. The previous runtime waits for those continuations to settle;
|
|
@@ -16463,7 +16661,8 @@ export class AgentSession {
|
|
|
16463
16661
|
this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
|
|
16464
16662
|
// session_switch is the post-commit identity signal. SDK authority and
|
|
16465
16663
|
// other identity-bound integrations must not observe the successor until
|
|
16466
|
-
// messages, model state, MCP selections,
|
|
16664
|
+
// messages, model state, MCP selections, the agent subscription, and
|
|
16665
|
+
// session-scoped tool cleanup are complete.
|
|
16467
16666
|
if (this.#extensionRunner) {
|
|
16468
16667
|
await this.#extensionRunner.emit({
|
|
16469
16668
|
type: "session_switch",
|
|
@@ -16474,6 +16673,7 @@ export class AgentSession {
|
|
|
16474
16673
|
}
|
|
16475
16674
|
return true;
|
|
16476
16675
|
} catch (error) {
|
|
16676
|
+
if (transitionCleanupCommitted) throw error;
|
|
16477
16677
|
this.sessionManager.restoreState(previousSessionState);
|
|
16478
16678
|
this.#defaultFallbackController = undefined;
|
|
16479
16679
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
@@ -16532,6 +16732,12 @@ export class AgentSession {
|
|
|
16532
16732
|
throw error;
|
|
16533
16733
|
}
|
|
16534
16734
|
} finally {
|
|
16735
|
+
if (ownerShutdownManager && ownerShutdownLease && !ownerShutdownFinalizationDeferred) {
|
|
16736
|
+
ownerShutdownManager.finishOwnerSubagentShutdown(
|
|
16737
|
+
ownerShutdownLease,
|
|
16738
|
+
ownerShutdownTransitionCommitted ? "commit" : "release",
|
|
16739
|
+
);
|
|
16740
|
+
}
|
|
16535
16741
|
this.#endSessionTransition();
|
|
16536
16742
|
}
|
|
16537
16743
|
}
|
|
@@ -16583,13 +16789,14 @@ export class AgentSession {
|
|
|
16583
16789
|
: await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
|
|
16584
16790
|
try {
|
|
16585
16791
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
16792
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
16586
16793
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
16794
|
+
await this.#runToolSessionTransitionCleanups();
|
|
16587
16795
|
} catch (error) {
|
|
16588
16796
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
16589
16797
|
}
|
|
16590
16798
|
this.#pendingNextTurnMessages = [];
|
|
16591
16799
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
16592
|
-
this.#cancelOwnAsyncJobs();
|
|
16593
16800
|
|
|
16594
16801
|
this.#syncTodoPhasesFromBranch();
|
|
16595
16802
|
this.#syncAgentSessionId();
|
package/src/session/artifacts.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ensureManagedDirectory,
|
|
14
14
|
type ManagedSessionDescendantStore,
|
|
15
15
|
publishManagedFileNoReplace,
|
|
16
|
+
publishManagedFileNoReplaceSync,
|
|
16
17
|
} from "./internal/managed-session-storage";
|
|
17
18
|
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
18
19
|
|
|
@@ -78,7 +79,8 @@ export class ArtifactManager {
|
|
|
78
79
|
readonly #dir: string;
|
|
79
80
|
readonly #store: ManagedSessionDescendantStore | undefined;
|
|
80
81
|
#dirCreated = false;
|
|
81
|
-
#initialized
|
|
82
|
+
#initialized: Promise<void> | undefined;
|
|
83
|
+
#initializedComplete = false;
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
86
|
* @param dir Directory that will hold artifact files. Created lazily on first save.
|
|
@@ -118,10 +120,10 @@ export class ArtifactManager {
|
|
|
118
120
|
else ensureManagedDirectory(this.#dir);
|
|
119
121
|
this.#dirCreated = true;
|
|
120
122
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// Memoized: concurrent first writers must not each rescan and restart the
|
|
124
|
+
// ID counter at the same value, which would collide on publish.
|
|
125
|
+
this.#initialized ??= this.#scanExistingIds();
|
|
126
|
+
await this.#initialized;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
#filename(id: string, toolType: string): string {
|
|
@@ -129,6 +131,44 @@ export class ArtifactManager {
|
|
|
129
131
|
return `${id}.${toolType}.log`;
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
#claimFilename(id: number): string {
|
|
135
|
+
return `.artifact-id-${id}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#nextCandidateId(): number {
|
|
139
|
+
const id = this.#nextId++;
|
|
140
|
+
if (!Number.isSafeInteger(id) || id < 0) throw new Error("artifact_id_out_of_range");
|
|
141
|
+
return id;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async #claimNextId(): Promise<number> {
|
|
145
|
+
while (true) {
|
|
146
|
+
const id = this.#nextCandidateId();
|
|
147
|
+
try {
|
|
148
|
+
await this.#publish("", this.#claimFilename(id));
|
|
149
|
+
return id;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#claimNextIdSync(): number {
|
|
157
|
+
if (!this.#initializedComplete)
|
|
158
|
+
throw new Error("ArtifactManager must be initialized before synchronous allocation");
|
|
159
|
+
while (true) {
|
|
160
|
+
const id = this.#nextCandidateId();
|
|
161
|
+
try {
|
|
162
|
+
const filename = this.#claimFilename(id);
|
|
163
|
+
if (this.#store) this.#store.publishNoReplaceSync(filename, new Uint8Array());
|
|
164
|
+
else publishManagedFileNoReplaceSync(path.join(this.#dir, filename), new Uint8Array());
|
|
165
|
+
return id;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (!(error instanceof Error) || error.message !== "destination_conflict") throw error;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
132
172
|
async #publish(content: string, filename: string): Promise<void> {
|
|
133
173
|
if (this.#store) await this.#store.publishNoReplace(filename, Buffer.from(content, "utf8"));
|
|
134
174
|
else await publishManagedFileNoReplace(path.join(this.#dir, filename), Buffer.from(content, "utf8"));
|
|
@@ -250,22 +290,25 @@ export class ArtifactManager {
|
|
|
250
290
|
const files = await this.listFiles();
|
|
251
291
|
let maxId = -1;
|
|
252
292
|
for (const file of files) {
|
|
253
|
-
//
|
|
254
|
-
|
|
293
|
+
// Published artifacts are `{id}.{toolType}.log`; hidden claim files reserve
|
|
294
|
+
// the numeric namespace across independent managers and processes.
|
|
295
|
+
const match = file.match(/^(\d+)\..*\.log$/) ?? file.match(/^\.artifact-id-(\d+)$/);
|
|
255
296
|
if (match) {
|
|
256
|
-
const id =
|
|
297
|
+
const id = Number(match[1]);
|
|
298
|
+
if (!Number.isSafeInteger(id) || id < 0) throw new Error("artifact_id_out_of_range");
|
|
257
299
|
if (id > maxId) maxId = id;
|
|
258
300
|
}
|
|
259
301
|
}
|
|
260
302
|
this.#nextId = maxId + 1;
|
|
303
|
+
this.#initializedComplete = true;
|
|
261
304
|
}
|
|
262
305
|
|
|
263
306
|
/**
|
|
264
|
-
* Atomically
|
|
265
|
-
*
|
|
307
|
+
* Atomically claim the next artifact ID after this manager has been initialized.
|
|
308
|
+
* Prefer `allocatePath` or `save`; this synchronous seam exists for pruning callbacks.
|
|
266
309
|
*/
|
|
267
310
|
allocateId(): number {
|
|
268
|
-
return this.#
|
|
311
|
+
return this.#claimNextIdSync();
|
|
269
312
|
}
|
|
270
313
|
|
|
271
314
|
/**
|
|
@@ -276,7 +319,7 @@ export class ArtifactManager {
|
|
|
276
319
|
*/
|
|
277
320
|
async allocatePath(toolType: string): Promise<{ id: string; path?: string }> {
|
|
278
321
|
await this.#ensureDir();
|
|
279
|
-
const id = String(this
|
|
322
|
+
const id = String(await this.#claimNextId());
|
|
280
323
|
if (this.#store) return { id };
|
|
281
324
|
return { id, path: path.join(this.#dir, this.#filename(id, toolType)) };
|
|
282
325
|
}
|
|
@@ -288,7 +331,7 @@ export class ArtifactManager {
|
|
|
288
331
|
*/
|
|
289
332
|
async save(content: string, toolType: string, options: ArtifactSaveOptions = {}): Promise<string> {
|
|
290
333
|
await this.#ensureDir();
|
|
291
|
-
const id = String(this
|
|
334
|
+
const id = String(await this.#claimNextId());
|
|
292
335
|
const maxBytes = Math.max(0, options.maxBytes ?? DEFAULT_ARTIFACT_MAX_BYTES);
|
|
293
336
|
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
294
337
|
const published =
|