@gajae-code/coding-agent 0.12.11 → 0.12.12
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 +38 -0
- package/dist/types/cli/notify-cli.d.ts +42 -1
- package/dist/types/cli/update-cli.d.ts +9 -3
- package/dist/types/commands/notify.d.ts +6 -0
- package/dist/types/config/file-lock.d.ts +3 -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/main.d.ts +6 -2
- 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/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 +24 -7
- package/dist/types/sdk/bus/telegram-daemon.d.ts +104 -16
- package/dist/types/sdk/bus/telegram-reference.d.ts +10 -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 -0
- package/dist/types/session/artifacts.d.ts +2 -2
- package/dist/types/session/session-manager.d.ts +23 -5
- package/dist/types/tools/index.d.ts +8 -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/notify-cli.ts +226 -2
- package/src/cli/update-cli.ts +90 -23
- package/src/commands/notify.ts +27 -3
- package/src/commands/sdk.ts +1 -0
- package/src/config/file-lock-gc.ts +18 -0
- package/src/config/file-lock.ts +61 -16
- package/src/config/model-profiles.ts +31 -15
- 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/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/launch-tmux.ts +53 -1
- package/src/internal-urls/docs-index.generated.ts +11 -10
- package/src/main.ts +203 -67
- package/src/modes/DESIGN.md +8 -5
- package/src/modes/components/footer.ts +10 -18
- 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/utils/host-status.ts +38 -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/index.ts +70 -0
- 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 +24 -7
- package/src/sdk/bus/telegram-daemon.ts +1961 -497
- package/src/sdk/bus/telegram-reference.ts +42 -6
- 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 +139 -28
- package/src/sdk/startup-capability.ts +10 -1
- package/src/session/agent-session.ts +220 -9
- package/src/session/artifacts.ts +56 -13
- package/src/session/internal/managed-session-storage.ts +206 -7
- package/src/session/session-manager.ts +148 -38
- package/src/setup/provider-presets.json +1 -1
- package/src/task/index.ts +92 -44
- package/src/tools/index.ts +8 -0
- package/src/utils/npm-registry.ts +660 -0
|
@@ -184,7 +184,7 @@ import {
|
|
|
184
184
|
} from "@gajae-code/utils";
|
|
185
185
|
|
|
186
186
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
187
|
-
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
187
|
+
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager, type OwnerSubagentShutdownLease } from "../async";
|
|
188
188
|
import { reset as resetCapabilities } from "../capability";
|
|
189
189
|
import type { Rule } from "../capability/rule";
|
|
190
190
|
import type { CasReceipt } from "../config/atomic-yaml-patch";
|
|
@@ -610,6 +610,8 @@ export interface AgentSessionConfig {
|
|
|
610
610
|
discoveryMode?: "off" | "mcp-only" | "all";
|
|
611
611
|
/** MCP tool names to activate for the current session when discovery mode is enabled. */
|
|
612
612
|
initialSelectedMCPToolNames?: string[];
|
|
613
|
+
/** Keep persisted MCP names until a deferred exact catalog becomes available. */
|
|
614
|
+
preserveUnavailableInitialMCPToolSelection?: boolean;
|
|
613
615
|
/** Built-in discoverable tool names restored for the current all-discovery session. */
|
|
614
616
|
initialSelectedDiscoveredBuiltinToolNames?: string[];
|
|
615
617
|
/** Discoverable built-ins active for configured or explicit reasons independently of persisted discovery selection. */
|
|
@@ -656,6 +658,8 @@ export interface AgentSessionConfig {
|
|
|
656
658
|
* process-global manager **MUST NOT** dispose it on their own teardown.
|
|
657
659
|
*/
|
|
658
660
|
ownedMcpManager?: MCPManager;
|
|
661
|
+
/** Optional startup dependency that must settle before the first provider turn. */
|
|
662
|
+
startupTurnBarrier?: Promise<void>;
|
|
659
663
|
/** Optional fork-context seed used to initialize a child session before its first prompt. */
|
|
660
664
|
forkContextSeed?: ForkContextSeed;
|
|
661
665
|
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
@@ -1947,6 +1951,7 @@ export class AgentSession {
|
|
|
1947
1951
|
*/
|
|
1948
1952
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
1949
1953
|
readonly #ownedMcpManager: MCPManager | undefined;
|
|
1954
|
+
#startupTurnBarrier: Promise<void> | undefined;
|
|
1950
1955
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
1951
1956
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
1952
1957
|
#evalExecutionDisposing = false;
|
|
@@ -1966,6 +1971,8 @@ export class AgentSession {
|
|
|
1966
1971
|
#isDisposed = false;
|
|
1967
1972
|
#disposePromise: Promise<void> | undefined;
|
|
1968
1973
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
1974
|
+
readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
|
|
1975
|
+
readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
|
|
1969
1976
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
1970
1977
|
// Extension system
|
|
1971
1978
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
@@ -2269,10 +2276,23 @@ export class AgentSession {
|
|
|
2269
2276
|
next.ready.resolve();
|
|
2270
2277
|
}
|
|
2271
2278
|
|
|
2279
|
+
async #awaitStartupTurnBarrier(): Promise<void> {
|
|
2280
|
+
const barrier = this.#startupTurnBarrier;
|
|
2281
|
+
if (!barrier) return;
|
|
2282
|
+
await barrier;
|
|
2283
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrier = undefined;
|
|
2284
|
+
}
|
|
2285
|
+
extendStartupTurnBarrier(barrier: Promise<void>): void {
|
|
2286
|
+
const current = this.#startupTurnBarrier;
|
|
2287
|
+
this.#startupTurnBarrier = current ? Promise.all([current, barrier]).then(() => {}) : barrier;
|
|
2288
|
+
void this.#startupTurnBarrier.catch(() => {});
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2272
2291
|
async #withSessionAdmission<T>(
|
|
2273
2292
|
kind: SessionAdmissionKind,
|
|
2274
2293
|
body: (lease: SessionAdmissionLease) => Promise<T>,
|
|
2275
2294
|
): Promise<T> {
|
|
2295
|
+
if (kind === "prompt") await this.#awaitStartupTurnBarrier();
|
|
2276
2296
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
2277
2297
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
2278
2298
|
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
@@ -2601,6 +2621,7 @@ export class AgentSession {
|
|
|
2601
2621
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
2602
2622
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
2603
2623
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
2624
|
+
this.#startupTurnBarrier = config.startupTurnBarrier;
|
|
2604
2625
|
this.#scopedModels = config.scopedModels ?? [];
|
|
2605
2626
|
this.#thinkingLevel = config.thinkingLevel;
|
|
2606
2627
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -2661,6 +2682,8 @@ export class AgentSession {
|
|
|
2661
2682
|
injectIdle: async messages => {
|
|
2662
2683
|
const first = messages[0];
|
|
2663
2684
|
if (!first) return;
|
|
2685
|
+
await this.#awaitStartupTurnBarrier();
|
|
2686
|
+
if (this.#isDisposed) return;
|
|
2664
2687
|
if (messages.length === 1) {
|
|
2665
2688
|
await this.agent.prompt(first, this.#managedFallbackPromptOptions());
|
|
2666
2689
|
} else {
|
|
@@ -2723,7 +2746,7 @@ export class AgentSession {
|
|
|
2723
2746
|
config.initialDiscoveredBuiltinToolSelectionIsExplicit === true
|
|
2724
2747
|
? this.#selectRestorableDiscoveredBuiltinToolNames(config.initialPersistedDiscoveredBuiltinToolNames ?? [])
|
|
2725
2748
|
: undefined;
|
|
2726
|
-
this.#pruneSelectedMCPToolNames();
|
|
2749
|
+
if (!config.preserveUnavailableInitialMCPToolSelection) this.#pruneSelectedMCPToolNames();
|
|
2727
2750
|
const persistInitialMCPToolSelection =
|
|
2728
2751
|
config.persistInitialMCPToolSelection === true && config.initialMCPToolSelectionIsExplicit !== false;
|
|
2729
2752
|
const persistInitialDiscoveredBuiltinToolSelection =
|
|
@@ -3377,6 +3400,9 @@ export class AgentSession {
|
|
|
3377
3400
|
getAgentId(): string | undefined {
|
|
3378
3401
|
return this.#agentId;
|
|
3379
3402
|
}
|
|
3403
|
+
get isDisposed(): boolean {
|
|
3404
|
+
return this.#isDisposed;
|
|
3405
|
+
}
|
|
3380
3406
|
|
|
3381
3407
|
registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void {
|
|
3382
3408
|
if (this.#isDisposed) throw new Error("Cannot register tool cleanup after session disposal has started.");
|
|
@@ -3384,6 +3410,23 @@ export class AgentSession {
|
|
|
3384
3410
|
return () => this.#toolSessionCleanups.delete(cleanup);
|
|
3385
3411
|
}
|
|
3386
3412
|
|
|
3413
|
+
registerToolSessionTransitionCleanup(cleanup: () => Promise<void> | void): () => void {
|
|
3414
|
+
if (this.#isDisposed)
|
|
3415
|
+
throw new Error("Cannot register tool transition cleanup after session disposal has started.");
|
|
3416
|
+
this.#toolSessionTransitionCleanups.add(cleanup);
|
|
3417
|
+
return () => this.#toolSessionTransitionCleanups.delete(cleanup);
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
async #runToolSessionTransitionCleanups(): Promise<void> {
|
|
3421
|
+
const cleanups = Array.from(this.#toolSessionTransitionCleanups);
|
|
3422
|
+
this.#toolSessionTransitionCleanups.clear();
|
|
3423
|
+
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
3424
|
+
for (const result of results) {
|
|
3425
|
+
if (result.status === "rejected")
|
|
3426
|
+
logger.warn("Tool session transition cleanup failed", { error: String(result.reason) });
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
|
|
3387
3430
|
async #runToolSessionCleanups(): Promise<void> {
|
|
3388
3431
|
const cleanups = Array.from(this.#toolSessionCleanups);
|
|
3389
3432
|
this.#toolSessionCleanups.clear();
|
|
@@ -3436,6 +3479,61 @@ export class AgentSession {
|
|
|
3436
3479
|
manager.cancelAll({ ownerId: this.#agentId });
|
|
3437
3480
|
}
|
|
3438
3481
|
|
|
3482
|
+
async #settleOwnAsyncJobsBeforeArtifactRetirement(): Promise<void> {
|
|
3483
|
+
const ownerId = this.#agentId;
|
|
3484
|
+
const manager = AsyncJobManager.instance();
|
|
3485
|
+
if (!ownerId || !manager) return;
|
|
3486
|
+
const lease = manager.beginOwnerSubagentShutdown(ownerId);
|
|
3487
|
+
if (!lease) throw new Error("Owned async cleanup is already in progress before artifact retirement.");
|
|
3488
|
+
let committed = false;
|
|
3489
|
+
try {
|
|
3490
|
+
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
3491
|
+
const proof = await manager.cancelAndProveOwnerSubagents(lease);
|
|
3492
|
+
if (!proof.confirmed)
|
|
3493
|
+
throw new Error("Owned subagent cleanup could not be confirmed before artifact retirement.");
|
|
3494
|
+
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3495
|
+
throw new Error("Owned async deliveries did not settle before artifact retirement.");
|
|
3496
|
+
if (!(await manager.cancelAndSettleOwnerJobs(ownerId)))
|
|
3497
|
+
throw new Error("Owned async jobs did not settle before artifact retirement.");
|
|
3498
|
+
committed = true;
|
|
3499
|
+
} finally {
|
|
3500
|
+
manager.finishOwnerSubagentShutdown(lease, committed ? "commit" : "release");
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
#scheduleDeferredOwnerShutdownFinalization(
|
|
3505
|
+
manager: AsyncJobManager,
|
|
3506
|
+
lease: OwnerSubagentShutdownLease,
|
|
3507
|
+
ownerId: string,
|
|
3508
|
+
): void {
|
|
3509
|
+
const finalization = (async () => {
|
|
3510
|
+
while (!this.#isDisposed) {
|
|
3511
|
+
try {
|
|
3512
|
+
manager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
3513
|
+
const proof = await manager.cancelAndProveOwnerSubagents(lease);
|
|
3514
|
+
if (!proof.confirmed) throw new Error(`owner_subagent_${proof.reason}`);
|
|
3515
|
+
if (!(await manager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
3516
|
+
throw new Error("owner_delivery_settlement_timeout");
|
|
3517
|
+
if (!(await manager.cancelAndSettleOwnerJobs(ownerId))) throw new Error("owner_job_settlement_timeout");
|
|
3518
|
+
if (this.#isDisposed) break;
|
|
3519
|
+
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
3520
|
+
await this.#runToolSessionTransitionCleanups();
|
|
3521
|
+
manager.finishOwnerSubagentShutdown(lease, "commit");
|
|
3522
|
+
return;
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
logger.warn("Deferred owner shutdown finalization retry failed", {
|
|
3525
|
+
ownerId,
|
|
3526
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3527
|
+
});
|
|
3528
|
+
await Bun.sleep(25);
|
|
3529
|
+
}
|
|
3530
|
+
}
|
|
3531
|
+
manager.finishOwnerSubagentShutdown(lease, "release");
|
|
3532
|
+
})();
|
|
3533
|
+
this.#deferredOwnerShutdownFinalizations.add(finalization);
|
|
3534
|
+
void finalization.finally(() => this.#deferredOwnerShutdownFinalizations.delete(finalization));
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3439
3537
|
#suppressOwnAsyncJobDeliveries(): void {
|
|
3440
3538
|
if (!this.#agentId) return;
|
|
3441
3539
|
const manager = AsyncJobManager.instance();
|
|
@@ -4647,6 +4745,11 @@ export class AgentSession {
|
|
|
4647
4745
|
settleLease();
|
|
4648
4746
|
return;
|
|
4649
4747
|
}
|
|
4748
|
+
await this.#awaitStartupTurnBarrier();
|
|
4749
|
+
if (!canContinue()) {
|
|
4750
|
+
settleLease();
|
|
4751
|
+
return;
|
|
4752
|
+
}
|
|
4650
4753
|
try {
|
|
4651
4754
|
if (!options?.skipCompactionCheck) {
|
|
4652
4755
|
await this.#checkEstimatedContextBeforePrompt();
|
|
@@ -5996,6 +6099,7 @@ export class AgentSession {
|
|
|
5996
6099
|
// the session that owns the manager goes on to dispose it (which itself
|
|
5997
6100
|
// nukes any leftover jobs and pending deliveries).
|
|
5998
6101
|
this.#cancelOwnAsyncJobs();
|
|
6102
|
+
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
5999
6103
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
6000
6104
|
if (ownedAsyncManager) {
|
|
6001
6105
|
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
@@ -6007,6 +6111,7 @@ export class AgentSession {
|
|
|
6007
6111
|
AsyncJobManager.setInstance(undefined);
|
|
6008
6112
|
}
|
|
6009
6113
|
}
|
|
6114
|
+
await this.#runToolSessionTransitionCleanups();
|
|
6010
6115
|
await this.#runToolSessionCleanups();
|
|
6011
6116
|
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
6012
6117
|
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
@@ -6834,6 +6939,12 @@ export class AgentSession {
|
|
|
6834
6939
|
const built = await this.#runAdmittedBaseSystemPromptRebuild(() =>
|
|
6835
6940
|
this.#rebuildSystemPrompt!(validToolNames, this.#toolRegistry),
|
|
6836
6941
|
);
|
|
6942
|
+
if (this.#isDisposed) {
|
|
6943
|
+
if (generation === this.#baseSystemPromptGeneration) {
|
|
6944
|
+
this.#pendingAppliedToolSignature = undefined;
|
|
6945
|
+
}
|
|
6946
|
+
return;
|
|
6947
|
+
}
|
|
6837
6948
|
if (generation === this.#baseSystemPromptGeneration) {
|
|
6838
6949
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
6839
6950
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
@@ -7319,6 +7430,8 @@ export class AgentSession {
|
|
|
7319
7430
|
if (!canContinuePersistedHistory(this.agent.state.messages)) {
|
|
7320
7431
|
throw new Error("Cannot continue from persisted message history");
|
|
7321
7432
|
}
|
|
7433
|
+
await this.#awaitStartupTurnBarrier();
|
|
7434
|
+
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
7322
7435
|
this.#beginInFlight();
|
|
7323
7436
|
let hindsightRecall: string | undefined;
|
|
7324
7437
|
try {
|
|
@@ -10183,6 +10296,7 @@ export class AgentSession {
|
|
|
10183
10296
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10184
10297
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10185
10298
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10299
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10186
10300
|
} catch (error) {
|
|
10187
10301
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10188
10302
|
}
|
|
@@ -10252,6 +10366,7 @@ export class AgentSession {
|
|
|
10252
10366
|
// Last fallible gate while public getters still show the predecessor (#3138).
|
|
10253
10367
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10254
10368
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10369
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10255
10370
|
} catch (error) {
|
|
10256
10371
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10257
10372
|
}
|
|
@@ -10418,7 +10533,9 @@ export class AgentSession {
|
|
|
10418
10533
|
}
|
|
10419
10534
|
try {
|
|
10420
10535
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
10536
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10421
10537
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
10538
|
+
await this.#runToolSessionTransitionCleanups();
|
|
10422
10539
|
} catch (error) {
|
|
10423
10540
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
10424
10541
|
}
|
|
@@ -11535,6 +11652,7 @@ export class AgentSession {
|
|
|
11535
11652
|
const artifactManager = this.sessionManager.getArtifactManager();
|
|
11536
11653
|
const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
|
|
11537
11654
|
let reservedArtifactId: string | undefined;
|
|
11655
|
+
let artifactAllocationAvailable = artifactManager !== null;
|
|
11538
11656
|
if (artifactManager) {
|
|
11539
11657
|
try {
|
|
11540
11658
|
reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
|
|
@@ -11542,14 +11660,24 @@ export class AgentSession {
|
|
|
11542
11660
|
logger.warn("Failed to reserve artifact ID for pruned tool output", {
|
|
11543
11661
|
error: error instanceof Error ? error.message : String(error),
|
|
11544
11662
|
});
|
|
11663
|
+
artifactAllocationAvailable = false;
|
|
11545
11664
|
}
|
|
11546
11665
|
}
|
|
11547
11666
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
|
|
11548
11667
|
relaxedMinimum: overThreshold ? 0 : undefined,
|
|
11549
11668
|
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11550
11669
|
artifactRef: candidate => {
|
|
11551
|
-
if (!artifactManager) return undefined;
|
|
11552
|
-
|
|
11670
|
+
if (!artifactManager || !artifactAllocationAvailable) return undefined;
|
|
11671
|
+
let id: string;
|
|
11672
|
+
try {
|
|
11673
|
+
id = reservedArtifactId ?? String(artifactManager.allocateId());
|
|
11674
|
+
} catch (error) {
|
|
11675
|
+
artifactAllocationAvailable = false;
|
|
11676
|
+
logger.warn("Failed to allocate artifact ID for pruned tool output", {
|
|
11677
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11678
|
+
});
|
|
11679
|
+
return undefined;
|
|
11680
|
+
}
|
|
11553
11681
|
reservedArtifactId = undefined;
|
|
11554
11682
|
const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
|
|
11555
11683
|
prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
|
|
@@ -12161,10 +12289,12 @@ export class AgentSession {
|
|
|
12161
12289
|
const successorGateEmitter = this.#constructWorkflowGateEmitter(prepared.sessionId);
|
|
12162
12290
|
// Last fallible action: verified local:// readiness from immutable staged options.
|
|
12163
12291
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
12292
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
12164
12293
|
|
|
12165
12294
|
// --- Commit boundary: synchronous adoption is the sole identity publication.
|
|
12166
12295
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
12167
12296
|
committed = true;
|
|
12297
|
+
await this.#runToolSessionTransitionCleanups();
|
|
12168
12298
|
this.agent.reset();
|
|
12169
12299
|
this.#syncAgentSessionId();
|
|
12170
12300
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -12195,7 +12325,6 @@ export class AgentSession {
|
|
|
12195
12325
|
this.#resetIrcRosterDeliveryState();
|
|
12196
12326
|
this.#planReferenceSent = false;
|
|
12197
12327
|
this.#planReferencePath = "local://PLAN.md";
|
|
12198
|
-
this.#cancelOwnAsyncJobs();
|
|
12199
12328
|
// The predecessor's fenced async-job results (queued while the
|
|
12200
12329
|
// transition held the delivery fence) belong to the handed-off session,
|
|
12201
12330
|
// not the successor. Suppress and drop ONLY the async-result kind so they
|
|
@@ -14937,6 +15066,17 @@ export class AgentSession {
|
|
|
14937
15066
|
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14938
15067
|
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14939
15068
|
const canReplayRotatedCredential = credentialRotated;
|
|
15069
|
+
// Universal automatic replay-safety gate (#3791): once the failed attempt
|
|
15070
|
+
// carries observable assistant text, thinking, or tool-call content,
|
|
15071
|
+
// automatic session retry must not re-issue the request. This covers
|
|
15072
|
+
// configured legacy retry (which previously only gated first-event
|
|
15073
|
+
// timeouts) as well as bare defaults. Managed fallback keeps its
|
|
15074
|
+
// provisional discard path (events never published to the session).
|
|
15075
|
+
// Content-free credential rotation remains eligible above; first-event
|
|
15076
|
+
// timeout keeps its additional typed/scope checks earlier in this method.
|
|
15077
|
+
if (!managedFallback && assistantMessageHasVisibleOrToolContent(message)) {
|
|
15078
|
+
return false;
|
|
15079
|
+
}
|
|
14940
15080
|
// Bare defaults retain their narrow watchdog and Codex admissions. A
|
|
14941
15081
|
// first-event timeout adds the typed, content-free, current-clean-scope
|
|
14942
15082
|
// requirement above; other transient watchdogs preserve legacy behavior.
|
|
@@ -16300,6 +16440,10 @@ export class AgentSession {
|
|
|
16300
16440
|
onTransitionMutationStarted?: () => void;
|
|
16301
16441
|
},
|
|
16302
16442
|
): Promise<boolean> {
|
|
16443
|
+
let ownerShutdownManager: AsyncJobManager | undefined;
|
|
16444
|
+
let ownerShutdownLease: OwnerSubagentShutdownLease | undefined;
|
|
16445
|
+
let ownerShutdownTransitionCommitted = false;
|
|
16446
|
+
let ownerShutdownFinalizationDeferred = false;
|
|
16303
16447
|
this.#beginSessionTransition("switch-session");
|
|
16304
16448
|
try {
|
|
16305
16449
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
@@ -16320,9 +16464,31 @@ export class AgentSession {
|
|
|
16320
16464
|
}
|
|
16321
16465
|
options?.onTransitionMutationStarted?.();
|
|
16322
16466
|
|
|
16323
|
-
|
|
16467
|
+
const asyncManager = AsyncJobManager.instance();
|
|
16468
|
+
const ownerId = this.#agentId;
|
|
16469
|
+
const lease =
|
|
16470
|
+
switchingToDifferentSession && asyncManager && ownerId
|
|
16471
|
+
? asyncManager.beginOwnerSubagentShutdown(ownerId)
|
|
16472
|
+
: undefined;
|
|
16473
|
+
if (switchingToDifferentSession && asyncManager && ownerId && !lease) {
|
|
16474
|
+
this.emitNotice(
|
|
16475
|
+
"error",
|
|
16476
|
+
"Cannot switch sessions while owned subagent cleanup is already in progress.",
|
|
16477
|
+
"switch-session-subagent-cleanup",
|
|
16478
|
+
);
|
|
16479
|
+
return false;
|
|
16480
|
+
}
|
|
16481
|
+
if (lease && asyncManager) {
|
|
16482
|
+
ownerShutdownManager = asyncManager;
|
|
16483
|
+
ownerShutdownLease = lease;
|
|
16484
|
+
}
|
|
16324
16485
|
await this.abort();
|
|
16486
|
+
if (this.isCompacting) {
|
|
16487
|
+
this.abortCompaction();
|
|
16488
|
+
while (this.isCompacting) await Bun.sleep(10);
|
|
16489
|
+
}
|
|
16325
16490
|
|
|
16491
|
+
this.#disconnectFromAgent();
|
|
16326
16492
|
// Flush pending writes before switching so restore snapshots reflect committed state.
|
|
16327
16493
|
await this.sessionManager.flush();
|
|
16328
16494
|
const previousSessionState = this.sessionManager.captureState();
|
|
@@ -16353,9 +16519,13 @@ export class AgentSession {
|
|
|
16353
16519
|
? this.#suspendWorkflowGateEmitter(previousSessionState.sessionId)
|
|
16354
16520
|
: undefined;
|
|
16355
16521
|
let unavailableDefaultChainMessage: string | undefined;
|
|
16522
|
+
let transitionCleanupCommitted = false;
|
|
16356
16523
|
|
|
16357
16524
|
try {
|
|
16358
|
-
await this.sessionManager.setSessionFile(sessionPath
|
|
16525
|
+
await this.sessionManager.setSessionFile(sessionPath, {
|
|
16526
|
+
deferEphemeralArtifactRetirement: switchingToDifferentSession,
|
|
16527
|
+
});
|
|
16528
|
+
if (switchingToDifferentSession) this.sessionManager.stageAdoptedArtifactManagerForTransition();
|
|
16359
16529
|
// The successor identity is already rotated in the manager but not yet
|
|
16360
16530
|
// published; gate its local:// root before publication so the agent,
|
|
16361
16531
|
// workflow-gate emitter, and hooks cannot resolve against an ungated
|
|
@@ -16455,6 +16625,38 @@ export class AgentSession {
|
|
|
16455
16625
|
this.#resetIrcRosterDeliveryState();
|
|
16456
16626
|
}
|
|
16457
16627
|
|
|
16628
|
+
if (switchingToDifferentSession) {
|
|
16629
|
+
let ownerShutdownSettled = true;
|
|
16630
|
+
if (ownerShutdownManager && ownerShutdownLease && ownerId) {
|
|
16631
|
+
try {
|
|
16632
|
+
ownerShutdownManager.runOwnerProducerCleanupsStrict({ ownerId });
|
|
16633
|
+
const proof = await ownerShutdownManager.cancelAndProveOwnerSubagents(ownerShutdownLease);
|
|
16634
|
+
if (!proof.confirmed)
|
|
16635
|
+
throw new Error("Owned subagent cleanup could not be confirmed after successor validation.");
|
|
16636
|
+
if (!(await ownerShutdownManager.waitForOwnerInFlightDeliveries(ownerId)))
|
|
16637
|
+
throw new Error("Owned async deliveries did not settle after successor validation.");
|
|
16638
|
+
if (!(await ownerShutdownManager.cancelAndSettleOwnerJobs(ownerId)))
|
|
16639
|
+
throw new Error("Owned async jobs did not settle after successor validation.");
|
|
16640
|
+
} catch (error) {
|
|
16641
|
+
ownerShutdownSettled = false;
|
|
16642
|
+
ownerShutdownFinalizationDeferred = true;
|
|
16643
|
+
this.#scheduleDeferredOwnerShutdownFinalization(ownerShutdownManager, ownerShutdownLease, ownerId);
|
|
16644
|
+
this.#suppressOwnAsyncJobDeliveries();
|
|
16645
|
+
this.emitNotice(
|
|
16646
|
+
"error",
|
|
16647
|
+
`Successor session is active, but predecessor async cleanup did not settle: ${error instanceof Error ? error.message : String(error)}`,
|
|
16648
|
+
"switch-session-subagent-cleanup",
|
|
16649
|
+
);
|
|
16650
|
+
}
|
|
16651
|
+
}
|
|
16652
|
+
transitionCleanupCommitted = true;
|
|
16653
|
+
// Different files may intentionally carry the same copied session id; pathname transition is the commit signal.
|
|
16654
|
+
ownerShutdownTransitionCommitted = true;
|
|
16655
|
+
if (ownerShutdownSettled) {
|
|
16656
|
+
this.sessionManager.retireEphemeralArtifactsAfterTransition();
|
|
16657
|
+
await this.#runToolSessionTransitionCleanups();
|
|
16658
|
+
}
|
|
16659
|
+
}
|
|
16458
16660
|
this.#reconnectToAgent();
|
|
16459
16661
|
// Fence predecessor continuations before session_switch starts SDK runtime
|
|
16460
16662
|
// teardown. The previous runtime waits for those continuations to settle;
|
|
@@ -16463,7 +16665,8 @@ export class AgentSession {
|
|
|
16463
16665
|
this.#bindWorkflowGateEmitter(previousSessionState.sessionId, suspendedWorkflowGateEmitter);
|
|
16464
16666
|
// session_switch is the post-commit identity signal. SDK authority and
|
|
16465
16667
|
// other identity-bound integrations must not observe the successor until
|
|
16466
|
-
// messages, model state, MCP selections,
|
|
16668
|
+
// messages, model state, MCP selections, the agent subscription, and
|
|
16669
|
+
// session-scoped tool cleanup are complete.
|
|
16467
16670
|
if (this.#extensionRunner) {
|
|
16468
16671
|
await this.#extensionRunner.emit({
|
|
16469
16672
|
type: "session_switch",
|
|
@@ -16474,6 +16677,7 @@ export class AgentSession {
|
|
|
16474
16677
|
}
|
|
16475
16678
|
return true;
|
|
16476
16679
|
} catch (error) {
|
|
16680
|
+
if (transitionCleanupCommitted) throw error;
|
|
16477
16681
|
this.sessionManager.restoreState(previousSessionState);
|
|
16478
16682
|
this.#defaultFallbackController = undefined;
|
|
16479
16683
|
this.#syncAgentSessionId(previousSessionState.sessionId);
|
|
@@ -16532,6 +16736,12 @@ export class AgentSession {
|
|
|
16532
16736
|
throw error;
|
|
16533
16737
|
}
|
|
16534
16738
|
} finally {
|
|
16739
|
+
if (ownerShutdownManager && ownerShutdownLease && !ownerShutdownFinalizationDeferred) {
|
|
16740
|
+
ownerShutdownManager.finishOwnerSubagentShutdown(
|
|
16741
|
+
ownerShutdownLease,
|
|
16742
|
+
ownerShutdownTransitionCommitted ? "commit" : "release",
|
|
16743
|
+
);
|
|
16744
|
+
}
|
|
16535
16745
|
this.#endSessionTransition();
|
|
16536
16746
|
}
|
|
16537
16747
|
}
|
|
@@ -16583,13 +16793,14 @@ export class AgentSession {
|
|
|
16583
16793
|
: await this.sessionManager.prepareNewSession({ parentSession: previousSessionFile });
|
|
16584
16794
|
try {
|
|
16585
16795
|
await initializeLocalRoot(this.#localProtocolOptions(prepared));
|
|
16796
|
+
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
16586
16797
|
this.sessionManager.commitPreparedNewSession(prepared);
|
|
16798
|
+
await this.#runToolSessionTransitionCleanups();
|
|
16587
16799
|
} catch (error) {
|
|
16588
16800
|
throw await discardPreparedNewSessionAfterFailure(this.sessionManager, prepared, error);
|
|
16589
16801
|
}
|
|
16590
16802
|
this.#pendingNextTurnMessages = [];
|
|
16591
16803
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
16592
|
-
this.#cancelOwnAsyncJobs();
|
|
16593
16804
|
|
|
16594
16805
|
this.#syncTodoPhasesFromBranch();
|
|
16595
16806
|
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 =
|