@gajae-code/coding-agent 0.13.0 → 0.13.2
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 +34 -0
- package/dist/types/config/file-lock.d.ts +2 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/models-config-schema.d.ts +6 -0
- package/dist/types/config/settings-schema.d.ts +17 -5
- package/dist/types/config/settings.d.ts +10 -1
- package/dist/types/edit/streaming.d.ts +5 -5
- package/dist/types/eval/py/kernel.d.ts +2 -2
- package/dist/types/eval/py/runtime.d.ts +5 -1
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +3 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +3 -1
- package/dist/types/modes/components/model-selector.d.ts +3 -2
- package/dist/types/runtime/process-lifecycle.d.ts +12 -31
- package/dist/types/sdk/broker/identity.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +17 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +11 -6
- package/dist/types/sdk/broker/startup-failure.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/conversation-store.d.ts +1 -0
- package/dist/types/sdk/bus/index.d.ts +4 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +10 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -2
- package/dist/types/sdk/bus/telegram-daemon.d.ts +17 -1
- package/dist/types/sdk/host/query/cursor.d.ts +4 -0
- package/dist/types/sdk/host/query/handlers.d.ts +6 -0
- package/dist/types/sdk/host/session-runtime.d.ts +7 -6
- package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
- package/dist/types/sdk/index.d.ts +1 -0
- package/dist/types/sdk/turn-result.d.ts +37 -0
- package/dist/types/session/agent-session.d.ts +8 -11
- package/dist/types/session/blob-store.d.ts +6 -3
- package/dist/types/session/internal/managed-session-storage.d.ts +13 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +12 -3
- package/dist/types/task/executor.d.ts +4 -7
- package/dist/types/tools/index.d.ts +1 -1
- package/dist/types/tools/read.d.ts +1 -1
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/utils/edit-mode.d.ts +69 -0
- package/package.json +8 -8
- package/scripts/generate-hotkeys-docs.ts +8 -6
- package/src/commands/sdk.ts +23 -2
- package/src/config/file-lock.ts +25 -5
- package/src/config/keybindings.ts +4 -2
- package/src/config/model-registry.ts +20 -0
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +29 -22
- package/src/config/settings.ts +42 -10
- package/src/coordinator-mcp/server.ts +87 -106
- package/src/edit/index.ts +5 -19
- package/src/edit/renderer.ts +3 -2
- package/src/edit/streaming.ts +28 -15
- package/src/eval/py/executor.ts +108 -10
- package/src/eval/py/kernel.ts +86 -12
- package/src/eval/py/runtime.ts +200 -16
- package/src/exec/bash-executor.ts +36 -37
- package/src/gjc-runtime/gc-runtime.ts +25 -3
- package/src/gjc-runtime/managed-owner-supervisor.ts +27 -1
- package/src/gjc-runtime/session-state-sidecar.ts +110 -4
- package/src/gjc-runtime/tmux-sessions.ts +34 -5
- package/src/internal-urls/docs-index.generated.ts +9 -9
- package/src/modes/acp/acp-agent.ts +10 -2
- package/src/modes/components/model-selector.ts +28 -8
- package/src/modes/controllers/plan-mode-controller.ts +3 -3
- package/src/modes/controllers/selector-controller.ts +4 -2
- package/src/modes/interactive-mode.ts +4 -0
- package/src/runtime/process-lifecycle.ts +126 -27
- package/src/runtime-mcp/transports/stdio.ts +11 -6
- package/src/sdk/acp/adapter.ts +18 -10
- package/src/sdk/broker/broker.ts +55 -4
- package/src/sdk/broker/ensure.ts +10 -7
- package/src/sdk/broker/identity.ts +10 -0
- package/src/sdk/broker/lifecycle-ledger.ts +56 -6
- package/src/sdk/broker/lifecycle.ts +53 -10
- package/src/sdk/broker/process-incarnation.ts +6 -1
- package/src/sdk/broker/session-index.ts +28 -11
- package/src/sdk/broker/startup-failure.ts +101 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/chat-daemon-runtime.ts +8 -1
- package/src/sdk/bus/conversation-store.ts +171 -50
- package/src/sdk/bus/index.ts +101 -47
- package/src/sdk/bus/kind-aware-reconciliation.ts +52 -2
- package/src/sdk/bus/notification-service.ts +3 -3
- package/src/sdk/bus/reconciliation-store.ts +16 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -2
- package/src/sdk/bus/telegram-daemon.ts +151 -41
- package/src/sdk/bus/topic-registry.ts +38 -9
- package/src/sdk/cli/session-cli.ts +20 -7
- package/src/sdk/client/discovery.ts +23 -2
- package/src/sdk/host/query/cursor.ts +4 -0
- package/src/sdk/host/query/handlers.ts +99 -100
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
- package/src/sdk/host/session-runtime.test.ts +98 -1
- package/src/sdk/host/session-runtime.ts +57 -15
- package/src/sdk/host/websocket-transport.lifecycle.test.ts +192 -2
- package/src/sdk/host/websocket-transport.ts +54 -30
- package/src/sdk/index.ts +1 -0
- package/src/sdk/mcp/server.ts +19 -6
- package/src/sdk/protocol/operation-inventory.generated.json +1 -1
- package/src/sdk/protocol/operation-registry.ts +8 -12
- package/src/sdk/session.ts +5 -14
- package/src/sdk/turn-result.ts +53 -0
- package/src/session/agent-session.ts +112 -69
- package/src/session/blob-store.ts +91 -21
- package/src/session/internal/managed-session-storage.ts +136 -21
- package/src/session/session-manager.ts +408 -81
- package/src/session/session-storage.ts +68 -10
- package/src/slash-commands/builtin-registry.ts +43 -5
- package/src/slash-commands/helpers/fast-status-report.ts +24 -8
- package/src/task/executor.ts +25 -8
- package/src/task/index.ts +5 -4
- package/src/tools/bash.ts +11 -9
- package/src/tools/index.ts +1 -1
- package/src/tools/read.ts +12 -11
- package/src/tools/search.ts +7 -7
- package/src/utils/edit-mode.ts +180 -9
- package/src/web/search/providers/insane.ts +55 -1
|
@@ -107,9 +107,10 @@ import {
|
|
|
107
107
|
classifyContextOverflow,
|
|
108
108
|
getSupportedEfforts,
|
|
109
109
|
isContextOverflow,
|
|
110
|
+
isFastModeEffectiveForProvider,
|
|
110
111
|
isUsageLimitError,
|
|
112
|
+
modelSupportsServiceTier,
|
|
111
113
|
modelsAreEqual,
|
|
112
|
-
resolveServiceTier,
|
|
113
114
|
streamSimple,
|
|
114
115
|
} from "@gajae-code/ai/core";
|
|
115
116
|
import {
|
|
@@ -752,6 +753,13 @@ type AutoCompactionTerminalStatus =
|
|
|
752
753
|
| { kind: "skipped"; continuationScheduled?: boolean }
|
|
753
754
|
| { kind: "failed" };
|
|
754
755
|
|
|
756
|
+
type ToolOutputPruneResult = {
|
|
757
|
+
prunedCount: number;
|
|
758
|
+
tokensSaved: number;
|
|
759
|
+
committed: boolean;
|
|
760
|
+
failure?: "artifact_persistence";
|
|
761
|
+
};
|
|
762
|
+
|
|
755
763
|
/**
|
|
756
764
|
* R3.2 pre-submit seam: `build` assembles the attempt messages (Phase A once,
|
|
757
765
|
* Phase B per attempt) and may throw the typed overflow error; `reset` drops the
|
|
@@ -3233,7 +3241,7 @@ export class AgentSession {
|
|
|
3233
3241
|
}
|
|
3234
3242
|
|
|
3235
3243
|
/** Restore a temporary scope, unwinding any auto-owned scopes above it. */
|
|
3236
|
-
restoreTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): boolean {
|
|
3244
|
+
async restoreTemporaryProviderSessionScope(token: TemporaryProviderSessionScope): Promise<boolean> {
|
|
3237
3245
|
const scopeIndex = this.#temporaryProviderSessionScopes.findLastIndex(scope => scope.token === token);
|
|
3238
3246
|
if (
|
|
3239
3247
|
scopeIndex < 0 ||
|
|
@@ -3242,12 +3250,12 @@ export class AgentSession {
|
|
|
3242
3250
|
return false;
|
|
3243
3251
|
}
|
|
3244
3252
|
while (this.#temporaryProviderSessionScopes.length > scopeIndex) {
|
|
3245
|
-
this.#restoreTopTemporaryProviderSessionScope();
|
|
3253
|
+
await this.#restoreTopTemporaryProviderSessionScope();
|
|
3246
3254
|
}
|
|
3247
3255
|
return true;
|
|
3248
3256
|
}
|
|
3249
3257
|
|
|
3250
|
-
#restoreTopTemporaryProviderSessionScope(): void {
|
|
3258
|
+
async #restoreTopTemporaryProviderSessionScope(): Promise<void> {
|
|
3251
3259
|
const scope = this.#temporaryProviderSessionScopes.pop();
|
|
3252
3260
|
if (!scope) return;
|
|
3253
3261
|
this.#closeProviderSessionMap(this.#providerSessionState, "temporary scope restore");
|
|
@@ -3266,7 +3274,7 @@ export class AgentSession {
|
|
|
3266
3274
|
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
3267
3275
|
this.#thinkingLevel = scope.thinkingLevel;
|
|
3268
3276
|
this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
|
|
3269
|
-
|
|
3277
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
3270
3278
|
}
|
|
3271
3279
|
|
|
3272
3280
|
/** Promote a temporary scope. The suspended provider state is permanently closed. */
|
|
@@ -6787,17 +6795,9 @@ export class AgentSession {
|
|
|
6787
6795
|
return resolveEditMode(this.#getEditModeSession());
|
|
6788
6796
|
}
|
|
6789
6797
|
|
|
6790
|
-
#resolveEditModeForModel(model: Model): EditMode {
|
|
6791
|
-
return resolveEditMode({
|
|
6792
|
-
settings: this.settings,
|
|
6793
|
-
getActiveModelString: () => formatModelString(model),
|
|
6794
|
-
});
|
|
6795
|
-
}
|
|
6796
|
-
|
|
6797
6798
|
async #prepareDefaultModelSelectionPrompt(model: Model): Promise<string[] | undefined> {
|
|
6798
6799
|
if (!this.#rebuildSystemPrompt) return undefined;
|
|
6799
6800
|
if (!this.getActiveToolNames().includes("edit")) return undefined;
|
|
6800
|
-
if (this.#resolveActiveEditMode() === this.#resolveEditModeForModel(model)) return undefined;
|
|
6801
6801
|
const built = await this.#rebuildSystemPrompt(this.getActiveToolNames(), this.#toolRegistry, model);
|
|
6802
6802
|
return built.systemPrompt;
|
|
6803
6803
|
}
|
|
@@ -6836,11 +6836,8 @@ export class AgentSession {
|
|
|
6836
6836
|
this.#lastAppliedToolSignature = this.#computeAppliedToolSignature(activeToolNames, activeTools);
|
|
6837
6837
|
}
|
|
6838
6838
|
|
|
6839
|
-
async #syncEditToolModeAfterModelChange(
|
|
6840
|
-
|
|
6841
|
-
if (previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit")) {
|
|
6842
|
-
await this.refreshBaseSystemPrompt();
|
|
6843
|
-
}
|
|
6839
|
+
async #syncEditToolModeAfterModelChange(_previousEditMode: EditMode): Promise<void> {
|
|
6840
|
+
if (this.getActiveToolNames().includes("edit")) await this.refreshBaseSystemPrompt();
|
|
6844
6841
|
}
|
|
6845
6842
|
|
|
6846
6843
|
getSelectedMCPToolNames(): string[] {
|
|
@@ -10912,8 +10909,31 @@ export class AgentSession {
|
|
|
10912
10909
|
await this.#settleOwnAsyncJobsBeforeArtifactRetirement();
|
|
10913
10910
|
} catch (error) {
|
|
10914
10911
|
const forkedFile = forkedManager.getSessionFile();
|
|
10915
|
-
|
|
10916
|
-
|
|
10912
|
+
const cleanupErrors: unknown[] = [];
|
|
10913
|
+
try {
|
|
10914
|
+
await forkedManager.close();
|
|
10915
|
+
} catch (closeError) {
|
|
10916
|
+
cleanupErrors.push(closeError);
|
|
10917
|
+
logger.warn("Bounded fork successor close failed after initialization failure", {
|
|
10918
|
+
error: closeError instanceof Error ? closeError.message : String(closeError),
|
|
10919
|
+
});
|
|
10920
|
+
}
|
|
10921
|
+
if (forkedFile) {
|
|
10922
|
+
try {
|
|
10923
|
+
await previousManager.discardUncommittedSession(forkedFile);
|
|
10924
|
+
} catch (cleanupError) {
|
|
10925
|
+
cleanupErrors.push(cleanupError);
|
|
10926
|
+
logger.warn("Bounded fork successor cleanup failed after initialization failure", {
|
|
10927
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
10928
|
+
});
|
|
10929
|
+
}
|
|
10930
|
+
}
|
|
10931
|
+
if (cleanupErrors.length > 0) {
|
|
10932
|
+
throw new AggregateError(
|
|
10933
|
+
[error, ...cleanupErrors],
|
|
10934
|
+
"Bounded fork initialization and successor cleanup both failed.",
|
|
10935
|
+
);
|
|
10936
|
+
}
|
|
10917
10937
|
throw error;
|
|
10918
10938
|
}
|
|
10919
10939
|
this.sessionManager = forkedManager;
|
|
@@ -11450,7 +11470,7 @@ export class AgentSession {
|
|
|
11450
11470
|
currentAutoScope !== undefined &&
|
|
11451
11471
|
this.#temporaryProviderSessionScopes.at(-1) === currentAutoScope;
|
|
11452
11472
|
if (replaceAutoScope && currentAutoScope) {
|
|
11453
|
-
this.#restoreTopTemporaryProviderSessionScope();
|
|
11473
|
+
await this.#restoreTopTemporaryProviderSessionScope();
|
|
11454
11474
|
}
|
|
11455
11475
|
const scope = isTemporaryOperation
|
|
11456
11476
|
? (suppliedScope ??
|
|
@@ -11481,7 +11501,7 @@ export class AgentSession {
|
|
|
11481
11501
|
if (options?.persistAsSessionDefault === true) this.#clearActiveModelProfileForConcreteDefault(options?.cause);
|
|
11482
11502
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
11483
11503
|
} catch (error) {
|
|
11484
|
-
if (ownsScope) this.restoreTemporaryProviderSessionScope(scope);
|
|
11504
|
+
if (ownsScope) await this.restoreTemporaryProviderSessionScope(scope);
|
|
11485
11505
|
throw error;
|
|
11486
11506
|
}
|
|
11487
11507
|
return scope;
|
|
@@ -12196,17 +12216,12 @@ export class AgentSession {
|
|
|
12196
12216
|
}
|
|
12197
12217
|
|
|
12198
12218
|
/**
|
|
12199
|
-
* True when the configured
|
|
12200
|
-
*
|
|
12201
|
-
*
|
|
12202
|
-
* undefined. This is the canonical provider-aware fast-mode predicate.
|
|
12219
|
+
* True when the configured tier is realized as a fast-mode field on the
|
|
12220
|
+
* provider's wire protocol. Providers that silently drop unscoped priority
|
|
12221
|
+
* intent return false so UI indicators match the request that is sent.
|
|
12203
12222
|
*/
|
|
12204
|
-
isFastForProvider(provider?: string): boolean {
|
|
12205
|
-
|
|
12206
|
-
// (no model selected) it cannot apply, even under an unscoped `priority`
|
|
12207
|
-
// tier that `resolveServiceTier` would otherwise pass through.
|
|
12208
|
-
if (provider === undefined) return false;
|
|
12209
|
-
return resolveServiceTier(this.serviceTier, provider) === "priority";
|
|
12223
|
+
isFastForProvider(provider?: string, supportsServiceTier = false): boolean {
|
|
12224
|
+
return isFastModeEffectiveForProvider(this.serviceTier, provider, supportsServiceTier);
|
|
12210
12225
|
}
|
|
12211
12226
|
|
|
12212
12227
|
/**
|
|
@@ -12223,14 +12238,11 @@ export class AgentSession {
|
|
|
12223
12238
|
}
|
|
12224
12239
|
|
|
12225
12240
|
/**
|
|
12226
|
-
*
|
|
12227
|
-
* against
|
|
12228
|
-
* main session tier. Use this for `task.agentModelOverrides` role rows so the
|
|
12229
|
-
* ⚡ glyph reflects the tier the subagent actually runs under.
|
|
12241
|
+
* Wire-effective fast-mode predicate for task-tool subagent roles, evaluated
|
|
12242
|
+
* against `task.serviceTier` rather than the main session tier.
|
|
12230
12243
|
*/
|
|
12231
|
-
isFastForSubagentProvider(provider?: string): boolean {
|
|
12232
|
-
|
|
12233
|
-
return resolveServiceTier(this.#subagentServiceTier(), provider) === "priority";
|
|
12244
|
+
isFastForSubagentProvider(provider?: string, supportsServiceTier = false): boolean {
|
|
12245
|
+
return isFastModeEffectiveForProvider(this.#subagentServiceTier(), provider, supportsServiceTier);
|
|
12234
12246
|
}
|
|
12235
12247
|
|
|
12236
12248
|
/**
|
|
@@ -12286,7 +12298,10 @@ export class AgentSession {
|
|
|
12286
12298
|
*/
|
|
12287
12299
|
isFastModeActive(): boolean {
|
|
12288
12300
|
const provider = this.model?.provider;
|
|
12289
|
-
|
|
12301
|
+
const supportsServiceTier = modelSupportsServiceTier(this.model);
|
|
12302
|
+
return (
|
|
12303
|
+
this.isFastForProvider(provider, supportsServiceTier) && !this.#isFastModeAutoDisabledForProvider(provider)
|
|
12304
|
+
);
|
|
12290
12305
|
}
|
|
12291
12306
|
|
|
12292
12307
|
setServiceTier(serviceTier: ServiceTier | undefined): void {
|
|
@@ -12381,19 +12396,34 @@ export class AgentSession {
|
|
|
12381
12396
|
signal?: AbortSignal,
|
|
12382
12397
|
overThreshold = false,
|
|
12383
12398
|
options?: { commitGate?: (actual: { prunedCount: number; tokensSaved: number }) => boolean },
|
|
12384
|
-
): Promise<
|
|
12399
|
+
): Promise<ToolOutputPruneResult | undefined> {
|
|
12385
12400
|
const branchEntries = this.sessionManager.getBranch();
|
|
12401
|
+
const artifactManager = await this.sessionManager.ensureArtifactManager();
|
|
12402
|
+
// Over-threshold callers have already proven tool-output savings before entering
|
|
12403
|
+
// this path. If exact persistence is unavailable, fail closed before hashing and
|
|
12404
|
+
// planning large outputs so maintenance cannot drift into timeout-driven abort.
|
|
12405
|
+
if (overThreshold && !artifactManager)
|
|
12406
|
+
return { prunedCount: 0, tokensSaved: 0, committed: false, failure: "artifact_persistence" };
|
|
12386
12407
|
const plan = planToolOutputPrune(branchEntries, {
|
|
12387
12408
|
...DEFAULT_PRUNE_CONFIG,
|
|
12388
12409
|
minimumSavings: overThreshold ? 0 : DEFAULT_PRUNE_CONFIG.minimumSavings,
|
|
12389
12410
|
});
|
|
12390
|
-
const artifactManager = await this.sessionManager.ensureArtifactManager();
|
|
12391
12411
|
const published = new Map<string, ToolOutputPruneEvictionHandle>();
|
|
12392
12412
|
// Fail closed when tool-output eviction is planned but no artifact store can be
|
|
12393
12413
|
// established: do not report a successful prune that skipped durable eviction.
|
|
12394
12414
|
if (!artifactManager && plan.digests.length > 0) {
|
|
12395
|
-
return
|
|
12415
|
+
return { prunedCount: 0, tokensSaved: 0, committed: false, failure: "artifact_persistence" };
|
|
12396
12416
|
}
|
|
12417
|
+
const removePublishedArtifacts = async (): Promise<void> => {
|
|
12418
|
+
for (const handle of published.values()) {
|
|
12419
|
+
const removed = await artifactManager?.removeNamedBestEffort(`${handle.artifactId}.evicted.log`);
|
|
12420
|
+
if (removed === false) {
|
|
12421
|
+
logger.warn("Failed to remove unpublished tool-output eviction artifact", {
|
|
12422
|
+
artifactId: handle.artifactId,
|
|
12423
|
+
});
|
|
12424
|
+
}
|
|
12425
|
+
}
|
|
12426
|
+
};
|
|
12397
12427
|
|
|
12398
12428
|
// Publish exact text one candidate at a time. The plan carries only digests and
|
|
12399
12429
|
// replacement proposals; original output bytes exist only in this iteration.
|
|
@@ -12416,22 +12446,19 @@ export class AgentSession {
|
|
|
12416
12446
|
outcome: outcome.outcome,
|
|
12417
12447
|
diagnostic: "diagnostic" in outcome ? outcome.diagnostic : undefined,
|
|
12418
12448
|
});
|
|
12419
|
-
if (outcome.outcome === "failed")
|
|
12449
|
+
if (outcome.outcome === "failed") {
|
|
12450
|
+
await removePublishedArtifacts();
|
|
12451
|
+
return {
|
|
12452
|
+
prunedCount: published.size,
|
|
12453
|
+
tokensSaved: 0,
|
|
12454
|
+
committed: false,
|
|
12455
|
+
failure: "artifact_persistence",
|
|
12456
|
+
};
|
|
12457
|
+
}
|
|
12420
12458
|
}
|
|
12421
12459
|
}
|
|
12422
12460
|
}
|
|
12423
12461
|
|
|
12424
|
-
const removePublishedArtifacts = async (): Promise<void> => {
|
|
12425
|
-
for (const handle of published.values()) {
|
|
12426
|
-
const removed = await artifactManager?.removeNamedBestEffort(`${handle.artifactId}.evicted.log`);
|
|
12427
|
-
if (removed === false) {
|
|
12428
|
-
logger.warn("Failed to remove unpublished tool-output eviction artifact", {
|
|
12429
|
-
artifactId: handle.artifactId,
|
|
12430
|
-
});
|
|
12431
|
-
}
|
|
12432
|
-
}
|
|
12433
|
-
};
|
|
12434
|
-
|
|
12435
12462
|
// Evaluate non-tool pruning on a disposable copy so the gate runs before any
|
|
12436
12463
|
// live entry is mutated. This avoids retaining originals for rollback.
|
|
12437
12464
|
const estimateEntries = structuredClone(branchEntries) as typeof branchEntries;
|
|
@@ -13513,7 +13540,7 @@ export class AgentSession {
|
|
|
13513
13540
|
relaxedMinimum: 0,
|
|
13514
13541
|
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
13515
13542
|
});
|
|
13516
|
-
let pruneResult:
|
|
13543
|
+
let pruneResult: ToolOutputPruneResult | undefined;
|
|
13517
13544
|
if (
|
|
13518
13545
|
pruneEstimate.tokensSaved > 0 &&
|
|
13519
13546
|
!shouldCompact(
|
|
@@ -13525,6 +13552,7 @@ export class AgentSession {
|
|
|
13525
13552
|
) {
|
|
13526
13553
|
pruneResult = await this.#pruneToolOutputs(maintenanceSignal, true);
|
|
13527
13554
|
if (isAborted()) return result("aborted");
|
|
13555
|
+
if (pruneResult?.failure === "artifact_persistence") return result("failed");
|
|
13528
13556
|
if (pruneResult) contextTokens = Math.max(0, contextTokens - pruneResult.tokensSaved);
|
|
13529
13557
|
}
|
|
13530
13558
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, autoCompactionOutputReserveTokens)) {
|
|
@@ -14132,7 +14160,7 @@ export class AgentSession {
|
|
|
14132
14160
|
signal,
|
|
14133
14161
|
});
|
|
14134
14162
|
if (signal?.aborted) {
|
|
14135
|
-
if (scope) this.restoreTemporaryProviderSessionScope(scope);
|
|
14163
|
+
if (scope) await this.restoreTemporaryProviderSessionScope(scope);
|
|
14136
14164
|
return false;
|
|
14137
14165
|
}
|
|
14138
14166
|
logger.debug("Context promotion switched model on overflow", {
|
|
@@ -15641,8 +15669,10 @@ export class AgentSession {
|
|
|
15641
15669
|
controller.seedResolution(activeIndex, [...controller.skips, ...resolution.skips]);
|
|
15642
15670
|
}
|
|
15643
15671
|
if (!resolution.model) throw new Error(this.#fallbackExhaustionError(controller));
|
|
15672
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
15644
15673
|
this.#setModelAuthoritatively(resolution.model, "restore");
|
|
15645
15674
|
this.setThinkingLevel(resolution.explicitThinkingLevel ? resolution.thinkingLevel : this.thinkingLevel);
|
|
15675
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
15646
15676
|
}
|
|
15647
15677
|
|
|
15648
15678
|
/**
|
|
@@ -15839,8 +15869,10 @@ export class AgentSession {
|
|
|
15839
15869
|
const from =
|
|
15840
15870
|
controller.tried.at(-1)?.selector ?? controller.chain.entries[controller.activeIndex - 1] ?? selector;
|
|
15841
15871
|
const to = selector;
|
|
15872
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
15842
15873
|
this.#setModelAuthoritatively(resolved.model, "fallback-switch");
|
|
15843
15874
|
this.setThinkingLevel(resolved.explicitThinkingLevel ? resolved.thinkingLevel : this.thinkingLevel);
|
|
15875
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
15844
15876
|
if (from !== to) {
|
|
15845
15877
|
this.#emit({
|
|
15846
15878
|
type: "model_fallback_switched",
|
|
@@ -16158,18 +16190,27 @@ export class AgentSession {
|
|
|
16158
16190
|
this.#retryAbortController?.abort();
|
|
16159
16191
|
this.#retryAbortController = retryAbortController;
|
|
16160
16192
|
this.#retryNowRequested = false;
|
|
16161
|
-
|
|
16162
|
-
|
|
16163
|
-
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16193
|
+
try {
|
|
16194
|
+
await this.#emitSessionEvent({
|
|
16195
|
+
type: "auto_retry_start",
|
|
16196
|
+
attempt: this.#retryAttempt,
|
|
16197
|
+
maxAttempts: managedFallback
|
|
16198
|
+
? controller.maxAttempts
|
|
16199
|
+
: firstEventTimeout
|
|
16200
|
+
? retrySettings.maxRetries + 1
|
|
16201
|
+
: retrySettings.maxRetries,
|
|
16202
|
+
delayMs,
|
|
16203
|
+
errorMessage,
|
|
16204
|
+
unbounded: managedFallback ? false : legacyUnbounded,
|
|
16205
|
+
});
|
|
16206
|
+
} catch (error) {
|
|
16207
|
+
if (this.#retryAbortController === retryAbortController) this.#retryAbortController = undefined;
|
|
16208
|
+
retryAbortController.abort();
|
|
16209
|
+
this.#failRetryRecovery(
|
|
16210
|
+
`Retry start delivery failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
16211
|
+
);
|
|
16212
|
+
throw error;
|
|
16213
|
+
}
|
|
16173
16214
|
|
|
16174
16215
|
const messages = this.agent.state.messages;
|
|
16175
16216
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
@@ -17658,6 +17699,7 @@ export class AgentSession {
|
|
|
17658
17699
|
: didReloadConversationChange
|
|
17659
17700
|
? "conversation-reload"
|
|
17660
17701
|
: undefined;
|
|
17702
|
+
const previousEditMode = this.#resolveActiveEditMode();
|
|
17661
17703
|
await this.#restoreMCPSelectionsForSessionContext(sessionContext);
|
|
17662
17704
|
|
|
17663
17705
|
// The target session is loaded and MCP selections are restored: discard
|
|
@@ -17731,6 +17773,7 @@ export class AgentSession {
|
|
|
17731
17773
|
if (!this.model || !modelsAreEqual(this.model, resolution.model)) {
|
|
17732
17774
|
this.#setModelAuthoritatively(resolution.model, "restore");
|
|
17733
17775
|
}
|
|
17776
|
+
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
17734
17777
|
if (resolution.explicitThinkingLevel && resolution.thinkingLevel !== undefined) {
|
|
17735
17778
|
this.setThinkingLevel(resolution.thinkingLevel);
|
|
17736
17779
|
}
|
|
@@ -2,6 +2,7 @@ import * as crypto from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as fsp from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { exactUnlink } from "@gajae-code/natives";
|
|
5
6
|
import { isEnoent, logger, postmortem } from "@gajae-code/utils";
|
|
6
7
|
|
|
7
8
|
const BLOB_PREFIX = "blob:sha256:";
|
|
@@ -387,9 +388,35 @@ function removeResidentCacheTreeNoFollow(pathname: string): void {
|
|
|
387
388
|
fs.unlinkSync(pathname);
|
|
388
389
|
}
|
|
389
390
|
|
|
390
|
-
|
|
391
|
+
function assertResidentCacheDisposalParent(instanceDir: string, parentDescriptor: number): void {
|
|
392
|
+
const parentDir = path.dirname(instanceDir);
|
|
393
|
+
try {
|
|
394
|
+
assertResidentCacheDirectoryPathMatchesDescriptor(parentDir, parentDescriptor, residentCacheOwnerUid(parentDir));
|
|
395
|
+
} catch (error) {
|
|
396
|
+
throw residentCacheTrustError("parent_authority_changed", instanceDir, error);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function disposeVerifiedResidentCacheInstanceDir(instanceDir: string, parentDescriptor?: number): void {
|
|
391
401
|
const instanceKey = path.resolve(instanceDir);
|
|
392
402
|
const uid = residentCacheOwnerUid(instanceDir);
|
|
403
|
+
let parentVerified = false;
|
|
404
|
+
if (parentDescriptor !== undefined) {
|
|
405
|
+
assertResidentCacheDisposalParent(instanceDir, parentDescriptor);
|
|
406
|
+
parentVerified = true;
|
|
407
|
+
}
|
|
408
|
+
// ENOENT is authoritative only while the retained parent still names the
|
|
409
|
+
// directory or after this process has already retired its ownership record.
|
|
410
|
+
try {
|
|
411
|
+
fs.lstatSync(instanceDir);
|
|
412
|
+
} catch (error) {
|
|
413
|
+
if (errorCode(error) !== "ENOENT") throw residentCacheTrustError("directory_unverifiable", instanceDir, error);
|
|
414
|
+
if (parentDescriptor !== undefined) assertResidentCacheDisposalParent(instanceDir, parentDescriptor);
|
|
415
|
+
if (!parentVerified && ownedResidentCacheInstanceDirs.has(instanceKey))
|
|
416
|
+
throw residentCacheTrustError("directory_absence_unverified", instanceDir, error);
|
|
417
|
+
ownedResidentCacheInstanceDirs.delete(instanceKey);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
393
420
|
const quarantineDir = path.join(
|
|
394
421
|
path.dirname(instanceDir),
|
|
395
422
|
`${path.basename(instanceDir)}.dispose.${crypto.randomUUID()}`,
|
|
@@ -408,7 +435,9 @@ export function disposeVerifiedResidentCacheInstanceDir(instanceDir: string): vo
|
|
|
408
435
|
descriptor = openVerifiedResidentCacheDirectory(instanceDir, uid);
|
|
409
436
|
assertResidentCacheDirectoryPathMatchesDescriptor(instanceDir, descriptor, uid);
|
|
410
437
|
const expected = fs.fstatSync(descriptor);
|
|
438
|
+
if (parentDescriptor !== undefined) assertResidentCacheDisposalParent(instanceDir, parentDescriptor);
|
|
411
439
|
fs.renameSync(instanceDir, quarantineDir);
|
|
440
|
+
if (parentDescriptor !== undefined) assertResidentCacheDisposalParent(instanceDir, parentDescriptor);
|
|
412
441
|
const moved = fs.lstatSync(quarantineDir);
|
|
413
442
|
if (
|
|
414
443
|
!moved.isDirectory() ||
|
|
@@ -1051,6 +1080,7 @@ export class BlobStore {
|
|
|
1051
1080
|
|
|
1052
1081
|
interface EphemeralBlobStoreOptions {
|
|
1053
1082
|
readonly adoptVerifiedDir?: boolean;
|
|
1083
|
+
readonly disposalParentDescriptor?: number;
|
|
1054
1084
|
}
|
|
1055
1085
|
|
|
1056
1086
|
/**
|
|
@@ -1097,10 +1127,12 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1097
1127
|
#bufferCacheBytes = 0;
|
|
1098
1128
|
#adoptedVerifiedDir = false;
|
|
1099
1129
|
#disposed = false;
|
|
1130
|
+
#disposalParentDescriptor: number | null = null;
|
|
1100
1131
|
|
|
1101
1132
|
constructor(dir: string, options: EphemeralBlobStoreOptions = {}) {
|
|
1102
1133
|
super(dir);
|
|
1103
1134
|
this.#adoptedVerifiedDir = options.adoptVerifiedDir === true;
|
|
1135
|
+
this.#disposalParentDescriptor = options.disposalParentDescriptor ?? null;
|
|
1104
1136
|
if (this.#adoptedVerifiedDir) return;
|
|
1105
1137
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
1106
1138
|
fs.mkdirSync(dir, { recursive: true, mode: BLOB_DIR_MODE });
|
|
@@ -1114,10 +1146,15 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1114
1146
|
static adoptVerifiedDir(dir: string): EphemeralBlobStore {
|
|
1115
1147
|
const uid = residentCacheOwnerUid(dir);
|
|
1116
1148
|
let descriptor: number | null = null;
|
|
1149
|
+
let parentDescriptor: number | null = null;
|
|
1117
1150
|
let failure: unknown;
|
|
1118
1151
|
try {
|
|
1119
1152
|
descriptor = openVerifiedResidentCacheDirectory(dir, uid);
|
|
1120
1153
|
assertResidentCacheDirectoryPathMatchesDescriptor(dir, descriptor, uid);
|
|
1154
|
+
const parentDir = path.dirname(dir);
|
|
1155
|
+
const parentUid = residentCacheOwnerUid(parentDir);
|
|
1156
|
+
parentDescriptor = openVerifiedResidentCacheDirectory(parentDir, parentUid);
|
|
1157
|
+
assertResidentCacheDirectoryPathMatchesDescriptor(parentDir, parentDescriptor, parentUid);
|
|
1121
1158
|
} catch (error) {
|
|
1122
1159
|
failure = error;
|
|
1123
1160
|
}
|
|
@@ -1129,10 +1166,18 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1129
1166
|
}
|
|
1130
1167
|
}
|
|
1131
1168
|
if (failure !== undefined) {
|
|
1169
|
+
if (parentDescriptor !== null) {
|
|
1170
|
+
try {
|
|
1171
|
+
fs.closeSync(parentDescriptor);
|
|
1172
|
+
} catch {
|
|
1173
|
+
// Preserve the adoption failure.
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1132
1176
|
removeEmptyResidentCacheInstanceDir(dir);
|
|
1133
1177
|
throw residentCacheTrustError("instance_adoption_failed", dir, failure);
|
|
1134
1178
|
}
|
|
1135
|
-
|
|
1179
|
+
if (parentDescriptor === null) throw new ResidentCacheTrustError("parent_authority_unavailable", dir);
|
|
1180
|
+
return new EphemeralBlobStore(dir, { adoptVerifiedDir: true, disposalParentDescriptor: parentDescriptor });
|
|
1136
1181
|
}
|
|
1137
1182
|
|
|
1138
1183
|
#cachePut(hash: string, data: Buffer): void {
|
|
@@ -1258,7 +1303,15 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
1258
1303
|
this.#bufferCache.clear();
|
|
1259
1304
|
this.#bufferCacheBytes = 0;
|
|
1260
1305
|
if (this.#adoptedVerifiedDir) {
|
|
1261
|
-
disposeVerifiedResidentCacheInstanceDir(this.dir);
|
|
1306
|
+
disposeVerifiedResidentCacheInstanceDir(this.dir, this.#disposalParentDescriptor ?? undefined);
|
|
1307
|
+
if (this.#disposalParentDescriptor !== null) {
|
|
1308
|
+
try {
|
|
1309
|
+
fs.closeSync(this.#disposalParentDescriptor);
|
|
1310
|
+
} catch {
|
|
1311
|
+
// Disposal is already complete; descriptor cleanup cannot restore the tree.
|
|
1312
|
+
}
|
|
1313
|
+
this.#disposalParentDescriptor = null;
|
|
1314
|
+
}
|
|
1262
1315
|
this.#disposed = true;
|
|
1263
1316
|
return;
|
|
1264
1317
|
}
|
|
@@ -1566,8 +1619,10 @@ export interface CanonicalBlobEntry {
|
|
|
1566
1619
|
readonly path: string;
|
|
1567
1620
|
readonly bytes: number;
|
|
1568
1621
|
readonly mtimeMs: number;
|
|
1569
|
-
readonly
|
|
1570
|
-
readonly
|
|
1622
|
+
readonly mtimeNs: bigint;
|
|
1623
|
+
readonly dev: bigint;
|
|
1624
|
+
readonly ino: bigint;
|
|
1625
|
+
readonly nlink: bigint;
|
|
1571
1626
|
}
|
|
1572
1627
|
|
|
1573
1628
|
const CANONICAL_BLOB_NAME = /^[0-9a-f]{64}$/;
|
|
@@ -1599,20 +1654,22 @@ export async function listCanonicalBlobs(dir: string): Promise<CanonicalBlobEntr
|
|
|
1599
1654
|
for (const name of names) {
|
|
1600
1655
|
if (!CANONICAL_BLOB_NAME.test(name)) continue;
|
|
1601
1656
|
const blobPath = path.join(dir, name);
|
|
1602
|
-
let stat: fs.
|
|
1657
|
+
let stat: fs.BigIntStats;
|
|
1603
1658
|
try {
|
|
1604
|
-
stat = await fsp.lstat(blobPath);
|
|
1659
|
+
stat = await fsp.lstat(blobPath, { bigint: true });
|
|
1605
1660
|
} catch {
|
|
1606
1661
|
continue;
|
|
1607
1662
|
}
|
|
1608
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !==
|
|
1663
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) continue;
|
|
1609
1664
|
entries.push({
|
|
1610
1665
|
hash: name,
|
|
1611
1666
|
path: blobPath,
|
|
1612
|
-
bytes: stat.size,
|
|
1613
|
-
mtimeMs: stat.mtimeMs,
|
|
1667
|
+
bytes: Number(stat.size),
|
|
1668
|
+
mtimeMs: Number(stat.mtimeMs),
|
|
1669
|
+
mtimeNs: stat.mtimeNs,
|
|
1614
1670
|
dev: stat.dev,
|
|
1615
1671
|
ino: stat.ino,
|
|
1672
|
+
nlink: stat.nlink,
|
|
1616
1673
|
});
|
|
1617
1674
|
}
|
|
1618
1675
|
return entries;
|
|
@@ -1635,32 +1692,45 @@ export async function removeCanonicalBlob(
|
|
|
1635
1692
|
entry: CanonicalBlobEntry,
|
|
1636
1693
|
options: { beforeUnlink?: () => Promise<boolean> } = {},
|
|
1637
1694
|
): Promise<{ removed: true } | { removed: false; reason: string; failed?: true }> {
|
|
1638
|
-
let stat: fs.
|
|
1695
|
+
let stat: fs.BigIntStats;
|
|
1639
1696
|
try {
|
|
1640
|
-
stat = await fsp.lstat(entry.path);
|
|
1697
|
+
stat = await fsp.lstat(entry.path, { bigint: true });
|
|
1641
1698
|
} catch (error) {
|
|
1642
1699
|
if (isEnoent(error)) return { removed: false, reason: "blob_disappeared" };
|
|
1643
1700
|
return { removed: false, reason: `blob_unverifiable: ${String(error)}`, failed: true };
|
|
1644
1701
|
}
|
|
1645
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !==
|
|
1702
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1n) {
|
|
1646
1703
|
return { removed: false, reason: "blob_not_plain_file" };
|
|
1647
1704
|
}
|
|
1648
1705
|
if (
|
|
1649
1706
|
stat.dev !== entry.dev ||
|
|
1650
1707
|
stat.ino !== entry.ino ||
|
|
1651
|
-
stat.size !== entry.bytes ||
|
|
1652
|
-
stat.
|
|
1708
|
+
stat.size !== BigInt(entry.bytes) ||
|
|
1709
|
+
stat.mtimeNs !== entry.mtimeNs
|
|
1653
1710
|
) {
|
|
1654
1711
|
return { removed: false, reason: "blob_changed" };
|
|
1655
1712
|
}
|
|
1656
1713
|
if (options.beforeUnlink && !(await options.beforeUnlink())) {
|
|
1657
1714
|
return { removed: false, reason: "blob_reference_evidence_changed" };
|
|
1658
1715
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1716
|
+
const removed = exactUnlink(entry.path, {
|
|
1717
|
+
dev: entry.dev,
|
|
1718
|
+
ino: entry.ino,
|
|
1719
|
+
nlink: entry.nlink,
|
|
1720
|
+
size: BigInt(entry.bytes),
|
|
1721
|
+
mtimeNs: entry.mtimeNs,
|
|
1722
|
+
sha256: entry.hash,
|
|
1723
|
+
quarantineName: `.gjc-gc-blob-${entry.hash}`,
|
|
1724
|
+
});
|
|
1725
|
+
if (removed.ok) return { removed: true };
|
|
1726
|
+
if (removed.code === "cleanup_pending" && removed.detachedPath) {
|
|
1727
|
+
try {
|
|
1728
|
+
await fsp.unlink(removed.detachedPath);
|
|
1729
|
+
return { removed: true };
|
|
1730
|
+
} catch (error) {
|
|
1731
|
+
return { removed: false, reason: `blob_cleanup_failed: ${String(error)}`, failed: true };
|
|
1732
|
+
}
|
|
1665
1733
|
}
|
|
1734
|
+
if (removed.code === "not_found") return { removed: false, reason: "blob_disappeared" };
|
|
1735
|
+
return { removed: false, reason: `blob_unlink_failed: ${removed.code ?? "unknown"}`, failed: true };
|
|
1666
1736
|
}
|