@gajae-code/coding-agent 0.7.3 → 0.7.5
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 +58 -0
- package/bin/gjc.js +4 -0
- package/dist/types/cli/plugin-cli.d.ts +2 -0
- package/dist/types/commands/plugin.d.ts +6 -0
- package/dist/types/commands/session.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +8 -1
- package/dist/types/extensibility/gjc-plugins/compiler.d.ts +19 -0
- package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +29 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +13 -0
- package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +26 -0
- package/dist/types/extensibility/gjc-plugins/observability.d.ts +27 -0
- package/dist/types/extensibility/gjc-plugins/prompt-appendix.d.ts +16 -0
- package/dist/types/extensibility/gjc-plugins/registry.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +42 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +158 -2
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +8 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/psmux-detect.d.ts +78 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +30 -2
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +18 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/modes/components/model-selector.d.ts +6 -0
- package/dist/types/notifications/html-format.d.ts +11 -0
- package/dist/types/notifications/index.d.ts +149 -1
- package/dist/types/notifications/lifecycle-commands.d.ts +72 -0
- package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
- package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
- package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +35 -0
- package/dist/types/notifications/telegram-daemon.d.ts +60 -0
- package/dist/types/notifications/telegram-reference.d.ts +3 -1
- package/dist/types/notifications/topic-registry.d.ts +10 -9
- package/dist/types/runtime-mcp/types.d.ts +7 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +14 -4
- package/dist/types/session/blob-store.d.ts +25 -0
- package/dist/types/session/session-manager.d.ts +57 -0
- package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +9 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/utils/changelog.d.ts +1 -0
- package/package.json +11 -9
- package/scripts/g004-tmux-smoke.ts +100 -0
- package/scripts/g005-daemon-smoke.ts +181 -0
- package/scripts/g011-daemon-path-smoke.ts +153 -0
- package/src/cli/plugin-cli.ts +66 -3
- package/src/cli.ts +21 -4
- package/src/commands/plugin.ts +4 -0
- package/src/commands/session.ts +18 -0
- package/src/config/model-profile-activation.ts +55 -7
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +1 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +3 -3
- package/src/defaults/gjc/skills/team/SKILL.md +5 -4
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +41 -13
- package/src/export/html/index.ts +2 -2
- package/src/extensibility/gjc-plugins/compiler.ts +351 -0
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +170 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +109 -0
- package/src/extensibility/gjc-plugins/installer.ts +434 -0
- package/src/extensibility/gjc-plugins/loader.ts +3 -1
- package/src/extensibility/gjc-plugins/mcp-policy.ts +239 -0
- package/src/extensibility/gjc-plugins/observability.ts +84 -0
- package/src/extensibility/gjc-plugins/paths.ts +1 -1
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +109 -0
- package/src/extensibility/gjc-plugins/registry.ts +180 -0
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +234 -0
- package/src/extensibility/gjc-plugins/schema.ts +250 -20
- package/src/extensibility/gjc-plugins/session-validation.ts +147 -0
- package/src/extensibility/gjc-plugins/types.ts +199 -3
- package/src/extensibility/gjc-plugins/validation.ts +80 -0
- package/src/extensibility/skills.ts +15 -0
- package/src/gjc-runtime/launch-tmux.ts +58 -15
- package/src/gjc-runtime/psmux-detect.ts +239 -0
- package/src/gjc-runtime/team-runtime.ts +56 -23
- package/src/gjc-runtime/tmux-common.ts +85 -3
- package/src/gjc-runtime/tmux-sessions.ts +111 -9
- package/src/gjc-runtime/ultragoal-runtime.ts +75 -15
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/main.ts +14 -3
- package/src/modes/components/assistant-message.ts +49 -1
- package/src/modes/components/hook-editor.ts +1 -1
- package/src/modes/components/hook-selector.ts +67 -43
- package/src/modes/components/model-selector.ts +44 -11
- package/src/modes/controllers/extension-ui-controller.ts +0 -27
- package/src/modes/controllers/selector-controller.ts +50 -11
- package/src/modes/interactive-mode.ts +2 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/notifications/html-format.ts +38 -0
- package/src/notifications/index.ts +242 -12
- package/src/notifications/lifecycle-commands.ts +228 -0
- package/src/notifications/lifecycle-control-runtime.ts +400 -0
- package/src/notifications/lifecycle-orchestrator.ts +358 -0
- package/src/notifications/rate-limit-pool.ts +19 -0
- package/src/notifications/recent-activity.ts +132 -0
- package/src/notifications/telegram-daemon.ts +433 -8
- package/src/notifications/telegram-reference.ts +25 -7
- package/src/notifications/topic-registry.ts +18 -9
- package/src/prompts/agents/executor.md +2 -2
- package/src/runtime-mcp/transports/stdio.ts +38 -4
- package/src/runtime-mcp/types.ts +7 -0
- package/src/sdk.ts +157 -10
- package/src/session/agent-session.ts +166 -74
- package/src/session/blob-store.ts +196 -8
- package/src/session/session-manager.ts +739 -12
- package/src/slash-commands/builtin-registry.ts +23 -3
- package/src/slash-commands/helpers/fast-status-report.ts +13 -3
- package/src/system-prompt.ts +9 -0
- package/src/task/executor.ts +31 -7
- package/src/task/index.ts +2 -0
- package/src/tools/ask.ts +5 -1
- package/src/tools/index.ts +3 -1
- package/src/utils/changelog.ts +8 -0
|
@@ -50,7 +50,11 @@ import {
|
|
|
50
50
|
type SummaryOptions,
|
|
51
51
|
shouldCompact,
|
|
52
52
|
} from "@gajae-code/agent-core/compaction";
|
|
53
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
DEFAULT_PRUNE_CONFIG,
|
|
55
|
+
pruneAssistantToolArguments,
|
|
56
|
+
pruneToolOutputs,
|
|
57
|
+
} from "@gajae-code/agent-core/compaction/pruning";
|
|
54
58
|
import type {
|
|
55
59
|
AssistantMessage,
|
|
56
60
|
Context,
|
|
@@ -127,7 +131,7 @@ import {
|
|
|
127
131
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
128
132
|
import { reset as resetCapabilities } from "../capability";
|
|
129
133
|
import type { Rule } from "../capability/rule";
|
|
130
|
-
import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
134
|
+
import { GJC_MODEL_ASSIGNMENT_TARGETS, MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
|
|
131
135
|
import {
|
|
132
136
|
extractExplicitThinkingSelector,
|
|
133
137
|
formatModelSelectorValue,
|
|
@@ -421,6 +425,13 @@ export interface AgentSessionConfig {
|
|
|
421
425
|
* **MUST NOT** dispose it on their own teardown.
|
|
422
426
|
*/
|
|
423
427
|
ownedAsyncJobManager?: AsyncJobManager;
|
|
428
|
+
/**
|
|
429
|
+
* MCPManager whose lifecycle this session owns (top-level sessions that
|
|
430
|
+
* connected plugin-bundle MCP servers). Only the owned manager is
|
|
431
|
+
* disconnected on dispose; subagents and callers that merely observe the
|
|
432
|
+
* process-global manager **MUST NOT** dispose it on their own teardown.
|
|
433
|
+
*/
|
|
434
|
+
ownedMcpManager?: MCPManager;
|
|
424
435
|
/** Optional fork-context seed used to initialize a child session before its first prompt. */
|
|
425
436
|
forkContextSeed?: ForkContextSeed;
|
|
426
437
|
/** Optional provider state override. Fork-context children should omit this by default. */
|
|
@@ -958,6 +969,7 @@ export class AgentSession {
|
|
|
958
969
|
* this undefined and **MUST NOT** dispose the global instance on teardown.
|
|
959
970
|
*/
|
|
960
971
|
readonly #ownedAsyncJobManager: AsyncJobManager | undefined;
|
|
972
|
+
readonly #ownedMcpManager: MCPManager | undefined;
|
|
961
973
|
#pendingPythonMessages: PythonExecutionMessage[] = [];
|
|
962
974
|
#activeEvalExecutions = new Set<Promise<unknown>>();
|
|
963
975
|
#evalExecutionDisposing = false;
|
|
@@ -1087,6 +1099,16 @@ export class AgentSession {
|
|
|
1087
1099
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1088
1100
|
#promptGeneration = 0;
|
|
1089
1101
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
1102
|
+
/**
|
|
1103
|
+
* Provider keys for which the Anthropic fast-mode auto-fallback fired this
|
|
1104
|
+
* session (the provider rejected `speed:"fast"` and we retried without it).
|
|
1105
|
+
* Provider/API-session scoped — matching the provider's own per-session
|
|
1106
|
+
* `fastModeDisabled` flag — NOT model-keyed. Transient (never persisted): it
|
|
1107
|
+
* suppresses the current-model fast indicator and dedups the one-time warning
|
|
1108
|
+
* WITHOUT mutating the user's intended `serviceTier`, so task subagents still
|
|
1109
|
+
* inherit the intended tier and a different provider still shows fast.
|
|
1110
|
+
*/
|
|
1111
|
+
#fastModeAutoDisabledProviderKeys = new Set<string>();
|
|
1090
1112
|
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
1091
1113
|
readonly rawSseDebugBuffer: RawSseDebugBuffer;
|
|
1092
1114
|
|
|
@@ -1161,6 +1183,7 @@ export class AgentSession {
|
|
|
1161
1183
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
1162
1184
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
1163
1185
|
this.#ownedAsyncJobManager = config.ownedAsyncJobManager;
|
|
1186
|
+
this.#ownedMcpManager = config.ownedMcpManager;
|
|
1164
1187
|
this.#scopedModels = config.scopedModels ?? [];
|
|
1165
1188
|
this.#thinkingLevel = config.thinkingLevel;
|
|
1166
1189
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -1976,12 +1999,18 @@ export class AgentSession {
|
|
|
1976
1999
|
const currentGrantsAnthropicPriority =
|
|
1977
2000
|
this.serviceTier === "priority" || this.serviceTier === "claude-only";
|
|
1978
2001
|
if (assistantMsg.disabledFeatures?.includes("priority") && currentGrantsAnthropicPriority) {
|
|
1979
|
-
|
|
1980
|
-
this.
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
)
|
|
2002
|
+
// The provider auto-dropped `speed:"fast"` for the current model's
|
|
2003
|
+
// provider this turn. Record a transient, provider-scoped marker
|
|
2004
|
+
// instead of clearing the user's intended tier, so task subagents
|
|
2005
|
+
// still inherit it and a different provider still gets fast mode.
|
|
2006
|
+
// Warn once per provider until the user re-arms with `/fast on`.
|
|
2007
|
+
if (this.#markFastModeAutoDisabledForCurrentModel()) {
|
|
2008
|
+
this.emitNotice(
|
|
2009
|
+
"warning",
|
|
2010
|
+
"Priority/fast mode rejected for this model; retried without it. Fast mode is off for this model until you re-enable it with /fast on.",
|
|
2011
|
+
"priority",
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
1985
2014
|
}
|
|
1986
2015
|
// Resolve TTSR resume gate before checking for new deferred injections.
|
|
1987
2016
|
// Gate on #ttsrAbortPending, not stopReason: a non-TTSR abort (e.g. streaming
|
|
@@ -3240,10 +3269,14 @@ export class AgentSession {
|
|
|
3240
3269
|
AsyncJobManager.setInstance(undefined);
|
|
3241
3270
|
}
|
|
3242
3271
|
}
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3272
|
+
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
3273
|
+
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
3274
|
+
// observe the process-global manager must never tear down a manager they do
|
|
3275
|
+
// not own. Mirrors the ownedAsyncJobManager rule above.
|
|
3276
|
+
const ownedMcpManager = this.#ownedMcpManager;
|
|
3277
|
+
if (ownedMcpManager) {
|
|
3278
|
+
await ownedMcpManager.disconnectAll();
|
|
3279
|
+
if (MCPManager.instance() === ownedMcpManager) {
|
|
3247
3280
|
MCPManager.setInstance(undefined);
|
|
3248
3281
|
}
|
|
3249
3282
|
}
|
|
@@ -5604,7 +5637,7 @@ export class AgentSession {
|
|
|
5604
5637
|
}
|
|
5605
5638
|
|
|
5606
5639
|
#syncTodoPhasesFromBranch(): void {
|
|
5607
|
-
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.
|
|
5640
|
+
const phases = getLatestTodoPhasesFromEntries(this.sessionManager.getActivePathEntriesCanonical());
|
|
5608
5641
|
// Strip completed/abandoned tasks — they were done in a previous run,
|
|
5609
5642
|
// so they have no bearing on progress tracking for the new turn.
|
|
5610
5643
|
for (const phase of phases) {
|
|
@@ -5613,6 +5646,42 @@ export class AgentSession {
|
|
|
5613
5646
|
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
5614
5647
|
}
|
|
5615
5648
|
|
|
5649
|
+
async #applyCompactionPostAppend(
|
|
5650
|
+
compactionEntryId: string,
|
|
5651
|
+
firstKeptEntryId: string,
|
|
5652
|
+
fromExtension?: boolean,
|
|
5653
|
+
): Promise<CompactionEntry | undefined> {
|
|
5654
|
+
const eviction = this.sessionManager.evictCompactedContent(firstKeptEntryId, compactionEntryId);
|
|
5655
|
+
if (eviction.evictedEntries > 0) await this.sessionManager.rewriteEntries();
|
|
5656
|
+
const sessionContext = this.buildDisplaySessionContext();
|
|
5657
|
+
this.agent.replaceMessages(sessionContext.messages);
|
|
5658
|
+
this.#syncTodoPhasesFromBranch();
|
|
5659
|
+
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
5660
|
+
|
|
5661
|
+
// Get the saved compaction entry for the hook without materializing all entries.
|
|
5662
|
+
const savedCompactionEntry = this.sessionManager.getEntryForFidelity(compactionEntryId) as
|
|
5663
|
+
| CompactionEntry
|
|
5664
|
+
| undefined;
|
|
5665
|
+
|
|
5666
|
+
if (this.#extensionRunner && savedCompactionEntry) {
|
|
5667
|
+
await this.#extensionRunner.emit({
|
|
5668
|
+
type: "session_compact",
|
|
5669
|
+
compactionEntry: savedCompactionEntry,
|
|
5670
|
+
fromExtension: fromExtension ?? false,
|
|
5671
|
+
});
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
return savedCompactionEntry;
|
|
5675
|
+
}
|
|
5676
|
+
|
|
5677
|
+
async applyCompactionPostAppendForTests(
|
|
5678
|
+
compactionEntryId: string,
|
|
5679
|
+
firstKeptEntryId: string,
|
|
5680
|
+
fromExtension?: boolean,
|
|
5681
|
+
): Promise<CompactionEntry | undefined> {
|
|
5682
|
+
return this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
5683
|
+
}
|
|
5684
|
+
|
|
5616
5685
|
#cloneTodoPhases(phases: TodoPhase[]): TodoPhase[] {
|
|
5617
5686
|
return phases.map(phase => ({
|
|
5618
5687
|
name: phase.name,
|
|
@@ -6242,33 +6311,81 @@ export class AgentSession {
|
|
|
6242
6311
|
return resolveServiceTier(this.#subagentServiceTier(), provider) === "priority";
|
|
6243
6312
|
}
|
|
6244
6313
|
|
|
6314
|
+
/**
|
|
6315
|
+
* Provider/API-session key used to scope the fast-mode auto-disable marker.
|
|
6316
|
+
* Mirrors the provider's own per-session `fastModeDisabled` scope (the key in
|
|
6317
|
+
* {@link #providerSessionState} cleared by `clearAnthropicFastModeFallback`),
|
|
6318
|
+
* so the marker is provider-scoped, never model-keyed. Returns `undefined`
|
|
6319
|
+
* when no model/provider is selected.
|
|
6320
|
+
*/
|
|
6321
|
+
#fastModeProviderKey(provider: string | undefined = this.model?.provider): string | undefined {
|
|
6322
|
+
return provider;
|
|
6323
|
+
}
|
|
6324
|
+
|
|
6325
|
+
/**
|
|
6326
|
+
* Record that the current model's provider had fast mode auto-dropped this
|
|
6327
|
+
* session. Returns `true` only when the provider key is newly marked, so the
|
|
6328
|
+
* caller emits the one-time warning exactly once per provider until re-arm.
|
|
6329
|
+
*/
|
|
6330
|
+
#markFastModeAutoDisabledForCurrentModel(): boolean {
|
|
6331
|
+
const key = this.#fastModeProviderKey();
|
|
6332
|
+
if (key === undefined) return false;
|
|
6333
|
+
if (this.#fastModeAutoDisabledProviderKeys.has(key)) return false;
|
|
6334
|
+
this.#fastModeAutoDisabledProviderKeys.add(key);
|
|
6335
|
+
return true;
|
|
6336
|
+
}
|
|
6337
|
+
|
|
6338
|
+
/** True when `provider`'s fast mode was auto-disabled this session. */
|
|
6339
|
+
#isFastModeAutoDisabledForProvider(provider?: string): boolean {
|
|
6340
|
+
const key = this.#fastModeProviderKey(provider);
|
|
6341
|
+
return key !== undefined && this.#fastModeAutoDisabledProviderKeys.has(key);
|
|
6342
|
+
}
|
|
6343
|
+
|
|
6344
|
+
/**
|
|
6345
|
+
* Re-arm fast mode after an auto-disable: clear the provider's sticky
|
|
6346
|
+
* `fastModeDisabled` fallback flag and the session auto-disable markers so the
|
|
6347
|
+
* next request carries `speed:"fast"` again and a future rejection can warn
|
|
6348
|
+
* once more. Called on explicit re-enable (`/fast on`, re-arming tier change),
|
|
6349
|
+
* never by the transient Q1 auto-disable path.
|
|
6350
|
+
*/
|
|
6351
|
+
#rearmFastMode(): void {
|
|
6352
|
+
clearAnthropicFastModeFallback(this.#providerSessionState);
|
|
6353
|
+
this.#fastModeAutoDisabledProviderKeys.clear();
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6245
6356
|
/**
|
|
6246
6357
|
* True when the configured `serviceTier` resolves to `"priority"` for the
|
|
6247
|
-
* *currently selected model's provider
|
|
6248
|
-
* that
|
|
6249
|
-
*
|
|
6358
|
+
* *currently selected model's provider* AND fast mode was not auto-disabled
|
|
6359
|
+
* for that provider this session. This is the current-model EFFECTIVE
|
|
6360
|
+
* predicate (what the next request actually does); use {@link isFastForProvider}
|
|
6361
|
+
* for pure configured intent (e.g. subagent/`modelRoles` display rows).
|
|
6250
6362
|
*/
|
|
6251
6363
|
isFastModeActive(): boolean {
|
|
6252
|
-
|
|
6364
|
+
const provider = this.model?.provider;
|
|
6365
|
+
return this.isFastForProvider(provider) && !this.#isFastModeAutoDisabledForProvider(provider);
|
|
6253
6366
|
}
|
|
6254
6367
|
|
|
6255
6368
|
setServiceTier(serviceTier: ServiceTier | undefined): void {
|
|
6256
|
-
|
|
6257
|
-
//
|
|
6258
|
-
//
|
|
6259
|
-
//
|
|
6260
|
-
//
|
|
6261
|
-
// and the warning notice fires every turn.
|
|
6369
|
+
// Re-arming a priority-granting tier always clears the per-session
|
|
6370
|
+
// auto-fallback sticky disable AND the auto-disable markers so the next
|
|
6371
|
+
// request carries `speed: "fast"` again — even when the tier is unchanged
|
|
6372
|
+
// (re-selecting the same tier is a deliberate re-arm), and before the
|
|
6373
|
+
// no-op early-return below.
|
|
6262
6374
|
if (serviceTier === "priority" || serviceTier === "claude-only") {
|
|
6263
|
-
|
|
6375
|
+
this.#rearmFastMode();
|
|
6264
6376
|
}
|
|
6377
|
+
if (this.serviceTier === serviceTier) return;
|
|
6265
6378
|
this.agent.serviceTier = serviceTier;
|
|
6266
6379
|
this.sessionManager.appendServiceTierChange(serviceTier ?? null);
|
|
6267
6380
|
}
|
|
6268
6381
|
|
|
6269
6382
|
setFastMode(enabled: boolean): void {
|
|
6270
6383
|
if (enabled && this.isFastModeEnabled()) {
|
|
6271
|
-
//
|
|
6384
|
+
// Intent already grants fast mode under some scope — keep the user's
|
|
6385
|
+
// scoped value but still re-arm, so an explicit `/fast on` after a
|
|
6386
|
+
// provider auto-disable actually clears the sticky fallback + markers
|
|
6387
|
+
// (otherwise it is a silent no-op). No history append: intent is unchanged.
|
|
6388
|
+
this.#rearmFastMode();
|
|
6272
6389
|
return;
|
|
6273
6390
|
}
|
|
6274
6391
|
this.setServiceTier(enabled ? "priority" : undefined);
|
|
@@ -6326,19 +6443,23 @@ export class AgentSession {
|
|
|
6326
6443
|
async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
|
|
6327
6444
|
const branchEntries = this.sessionManager.getBranch();
|
|
6328
6445
|
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6329
|
-
|
|
6446
|
+
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
6447
|
+
const tokensSaved = result.tokensSaved + argumentResult.argumentTokensSaved;
|
|
6448
|
+
const prunedCount = result.prunedCount + argumentResult.argumentPrunedCount;
|
|
6449
|
+
if (prunedCount === 0) {
|
|
6330
6450
|
return undefined;
|
|
6331
6451
|
}
|
|
6332
6452
|
|
|
6333
6453
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
6334
6454
|
// the pruning mutations must be written back into the canonical store.
|
|
6335
|
-
|
|
6455
|
+
const combined = [...result.prunedEntries, ...argumentResult.prunedEntries];
|
|
6456
|
+
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
6336
6457
|
await this.sessionManager.rewriteEntries();
|
|
6337
6458
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6338
6459
|
this.agent.replaceMessages(sessionContext.messages);
|
|
6339
6460
|
this.#syncTodoPhasesFromBranch();
|
|
6340
6461
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6341
|
-
return
|
|
6462
|
+
return { prunedCount, tokensSaved };
|
|
6342
6463
|
}
|
|
6343
6464
|
|
|
6344
6465
|
/**
|
|
@@ -6456,7 +6577,7 @@ export class AgentSession {
|
|
|
6456
6577
|
throw new CompactionCancelledError();
|
|
6457
6578
|
}
|
|
6458
6579
|
|
|
6459
|
-
this.sessionManager.appendCompaction(
|
|
6580
|
+
const compactionEntryId = this.sessionManager.appendCompaction(
|
|
6460
6581
|
summary,
|
|
6461
6582
|
shortSummary,
|
|
6462
6583
|
firstKeptEntryId,
|
|
@@ -6465,24 +6586,7 @@ export class AgentSession {
|
|
|
6465
6586
|
fromExtension,
|
|
6466
6587
|
preserveData,
|
|
6467
6588
|
);
|
|
6468
|
-
|
|
6469
|
-
const sessionContext = this.buildDisplaySessionContext();
|
|
6470
|
-
this.agent.replaceMessages(sessionContext.messages);
|
|
6471
|
-
this.#syncTodoPhasesFromBranch();
|
|
6472
|
-
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
6473
|
-
|
|
6474
|
-
// Get the saved compaction entry for the hook
|
|
6475
|
-
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
6476
|
-
| CompactionEntry
|
|
6477
|
-
| undefined;
|
|
6478
|
-
|
|
6479
|
-
if (this.#extensionRunner && savedCompactionEntry) {
|
|
6480
|
-
await this.#extensionRunner.emit({
|
|
6481
|
-
type: "session_compact",
|
|
6482
|
-
compactionEntry: savedCompactionEntry,
|
|
6483
|
-
fromExtension,
|
|
6484
|
-
});
|
|
6485
|
-
}
|
|
6589
|
+
await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
6486
6590
|
|
|
6487
6591
|
const compactionResult: CompactionResult = {
|
|
6488
6592
|
summary,
|
|
@@ -7480,11 +7584,14 @@ export class AgentSession {
|
|
|
7480
7584
|
availableModels: Model[],
|
|
7481
7585
|
currentModel: Model | undefined,
|
|
7482
7586
|
): ResolvedModelRoleValue {
|
|
7587
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role as keyof typeof GJC_MODEL_ASSIGNMENT_TARGETS];
|
|
7483
7588
|
const roleModelStr =
|
|
7484
|
-
|
|
7485
|
-
?
|
|
7486
|
-
|
|
7487
|
-
|
|
7589
|
+
target?.settingsPath === "task.agentModelOverrides"
|
|
7590
|
+
? this.settings.get("task.agentModelOverrides")[role]
|
|
7591
|
+
: role === "default"
|
|
7592
|
+
? (this.settings.getModelRole("default") ??
|
|
7593
|
+
(currentModel ? `${currentModel.provider}/${currentModel.id}` : undefined))
|
|
7594
|
+
: this.settings.getModelRole(role);
|
|
7488
7595
|
|
|
7489
7596
|
if (!roleModelStr) {
|
|
7490
7597
|
return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
|
|
@@ -7969,7 +8076,7 @@ export class AgentSession {
|
|
|
7969
8076
|
return;
|
|
7970
8077
|
}
|
|
7971
8078
|
|
|
7972
|
-
this.sessionManager.appendCompaction(
|
|
8079
|
+
const compactionEntryId = this.sessionManager.appendCompaction(
|
|
7973
8080
|
summary,
|
|
7974
8081
|
shortSummary,
|
|
7975
8082
|
firstKeptEntryId,
|
|
@@ -7978,24 +8085,7 @@ export class AgentSession {
|
|
|
7978
8085
|
fromExtension,
|
|
7979
8086
|
preserveData,
|
|
7980
8087
|
);
|
|
7981
|
-
|
|
7982
|
-
const sessionContext = this.buildDisplaySessionContext();
|
|
7983
|
-
this.agent.replaceMessages(sessionContext.messages);
|
|
7984
|
-
this.#syncTodoPhasesFromBranch();
|
|
7985
|
-
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
7986
|
-
|
|
7987
|
-
// Get the saved compaction entry for the hook
|
|
7988
|
-
const savedCompactionEntry = newEntries.find(e => e.type === "compaction" && e.summary === summary) as
|
|
7989
|
-
| CompactionEntry
|
|
7990
|
-
| undefined;
|
|
7991
|
-
|
|
7992
|
-
if (this.#extensionRunner && savedCompactionEntry) {
|
|
7993
|
-
await this.#extensionRunner.emit({
|
|
7994
|
-
type: "session_compact",
|
|
7995
|
-
compactionEntry: savedCompactionEntry,
|
|
7996
|
-
fromExtension,
|
|
7997
|
-
});
|
|
7998
|
-
}
|
|
8088
|
+
await this.#applyCompactionPostAppend(compactionEntryId, firstKeptEntryId, fromExtension);
|
|
7999
8089
|
|
|
8000
8090
|
const result: CompactionResult = {
|
|
8001
8091
|
summary,
|
|
@@ -9613,7 +9703,7 @@ export class AgentSession {
|
|
|
9613
9703
|
cancelled: boolean;
|
|
9614
9704
|
}> {
|
|
9615
9705
|
const previousSessionFile = this.sessionFile;
|
|
9616
|
-
const selectedEntry = this.sessionManager.
|
|
9706
|
+
const selectedEntry = this.sessionManager.getEntryForFidelity(entryId);
|
|
9617
9707
|
|
|
9618
9708
|
if (selectedEntry?.type !== "message" || selectedEntry.message.role !== "user") {
|
|
9619
9709
|
throw new Error("Invalid entry ID for branching");
|
|
@@ -9711,7 +9801,7 @@ export class AgentSession {
|
|
|
9711
9801
|
throw new Error("No model available for summarization");
|
|
9712
9802
|
}
|
|
9713
9803
|
|
|
9714
|
-
const targetEntry = this.sessionManager.
|
|
9804
|
+
const targetEntry = this.sessionManager.getEntryForFidelity(targetId);
|
|
9715
9805
|
if (!targetEntry) {
|
|
9716
9806
|
throw new Error(`Entry ${targetId} not found`);
|
|
9717
9807
|
}
|
|
@@ -9867,9 +9957,11 @@ export class AgentSession {
|
|
|
9867
9957
|
|
|
9868
9958
|
for (const entry of entries) {
|
|
9869
9959
|
if (entry.type !== "message") continue;
|
|
9870
|
-
|
|
9960
|
+
const fidelityEntry = this.sessionManager.getEntryForFidelity(entry.id);
|
|
9961
|
+
if (fidelityEntry?.type !== "message") continue;
|
|
9962
|
+
if (fidelityEntry.message.role !== "user") continue;
|
|
9871
9963
|
|
|
9872
|
-
const text = this.#extractUserMessageText(
|
|
9964
|
+
const text = this.#extractUserMessageText(fidelityEntry.message.content);
|
|
9873
9965
|
if (text) {
|
|
9874
9966
|
result.push({ entryId: entry.id, text });
|
|
9875
9967
|
}
|
|
@@ -11,6 +11,82 @@ export interface BlobPutResult {
|
|
|
11
11
|
get ref(): string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export interface CheckedBlobPutResult extends BlobPutResult {
|
|
15
|
+
bytes: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class BlobCorruptError extends Error {
|
|
19
|
+
constructor(
|
|
20
|
+
readonly hash: string,
|
|
21
|
+
readonly path: string,
|
|
22
|
+
) {
|
|
23
|
+
super(`Blob ${hash} at ${path} failed SHA-256 verification`);
|
|
24
|
+
this.name = "BlobCorruptError";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sha256Hex(data: Buffer): string {
|
|
29
|
+
return new Bun.SHA256().update(data).digest("hex");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeBlobPutResult(hash: string, blobPath: string): BlobPutResult;
|
|
33
|
+
function makeBlobPutResult(hash: string, blobPath: string, bytes: number): CheckedBlobPutResult;
|
|
34
|
+
function makeBlobPutResult(hash: string, blobPath: string, bytes?: number): BlobPutResult | CheckedBlobPutResult {
|
|
35
|
+
const result = {
|
|
36
|
+
hash,
|
|
37
|
+
path: blobPath,
|
|
38
|
+
get ref() {
|
|
39
|
+
return `${BLOB_PREFIX}${hash}`;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
if (bytes === undefined) return result;
|
|
43
|
+
return { ...result, bytes };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function fsyncDirBestEffortSync(dir: string): void {
|
|
47
|
+
let fd: number | null = null;
|
|
48
|
+
try {
|
|
49
|
+
fd = fs.openSync(dir, "r");
|
|
50
|
+
fs.fsyncSync(fd);
|
|
51
|
+
} catch {
|
|
52
|
+
// Best-effort only: some platforms/filesystems do not support fsync on directories.
|
|
53
|
+
} finally {
|
|
54
|
+
if (fd !== null) fs.closeSync(fd);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Best-effort fsync of an installed blob file. Used on install paths that
|
|
60
|
+
* create the destination by copy (not hard-link from an already-fsynced temp),
|
|
61
|
+
* so the durable-install contract holds there too. Failures are best-effort:
|
|
62
|
+
* some platforms/filesystems reject fsync on read-only handles.
|
|
63
|
+
*/
|
|
64
|
+
function fsyncFileBestEffortSync(filePath: string): void {
|
|
65
|
+
let fd: number | null = null;
|
|
66
|
+
try {
|
|
67
|
+
fd = fs.openSync(filePath, "r");
|
|
68
|
+
fs.fsyncSync(fd);
|
|
69
|
+
} catch {
|
|
70
|
+
// Best-effort only.
|
|
71
|
+
} finally {
|
|
72
|
+
if (fd !== null) fs.closeSync(fd);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function uniqueTempBlobPath(dir: string, hash: string): string {
|
|
77
|
+
return path.join(dir, `${hash}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function verifyBlobBytesSync(hash: string, blobPath: string, data: Buffer): void {
|
|
81
|
+
if (sha256Hex(data) !== hash) throw new BlobCorruptError(hash, blobPath);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function verifyBlobFileSync(hash: string, blobPath: string): Buffer {
|
|
85
|
+
const data = fs.readFileSync(blobPath);
|
|
86
|
+
verifyBlobBytesSync(hash, blobPath, data);
|
|
87
|
+
return data;
|
|
88
|
+
}
|
|
89
|
+
|
|
14
90
|
/**
|
|
15
91
|
* Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
|
|
16
92
|
*
|
|
@@ -60,6 +136,79 @@ export class BlobStore {
|
|
|
60
136
|
return result;
|
|
61
137
|
}
|
|
62
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Durably install binary data as an immutable content-addressed blob.
|
|
141
|
+
*
|
|
142
|
+
* Callers that persist references to this blob must mutate canonical session entries only
|
|
143
|
+
* after this method returns successfully. A corrupt pre-existing target is reported with
|
|
144
|
+
* {@link BlobCorruptError}; it is never silently overwritten or trusted.
|
|
145
|
+
*/
|
|
146
|
+
putImmutableSync(data: Buffer): CheckedBlobPutResult {
|
|
147
|
+
const hash = sha256Hex(data);
|
|
148
|
+
const blobPath = path.join(this.dir, hash);
|
|
149
|
+
const result = makeBlobPutResult(hash, blobPath, data.byteLength);
|
|
150
|
+
fs.mkdirSync(this.dir, { recursive: true });
|
|
151
|
+
|
|
152
|
+
if (fs.existsSync(blobPath)) {
|
|
153
|
+
verifyBlobFileSync(hash, blobPath);
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const tempPath = uniqueTempBlobPath(this.dir, hash);
|
|
158
|
+
try {
|
|
159
|
+
const fd = fs.openSync(tempPath, "wx");
|
|
160
|
+
try {
|
|
161
|
+
fs.writeFileSync(fd, data);
|
|
162
|
+
fs.fsyncSync(fd);
|
|
163
|
+
} finally {
|
|
164
|
+
fs.closeSync(fd);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
fs.linkSync(tempPath, blobPath);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (isEnoent(err)) throw err;
|
|
171
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : undefined;
|
|
172
|
+
if (code === "EEXIST") {
|
|
173
|
+
verifyBlobFileSync(hash, blobPath);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
if (code === "EPERM" || code === "ENOTSUP" || code === "EOPNOTSUPP") {
|
|
177
|
+
// Hard links unsupported (e.g. cross-device / some network FS). Use an
|
|
178
|
+
// EXCLUSIVE copy so a concurrently installed winner is never overwritten;
|
|
179
|
+
// verify it by hash on EEXIST instead of clobbering it.
|
|
180
|
+
try {
|
|
181
|
+
fs.copyFileSync(tempPath, blobPath, fs.constants.COPYFILE_EXCL);
|
|
182
|
+
// The temp was fsync'd before install, but copyFileSync creates a
|
|
183
|
+
// fresh destination whose bytes are not yet durable — fsync it so the
|
|
184
|
+
// durable-install contract holds on hard-link-unsupported paths too.
|
|
185
|
+
fsyncFileBestEffortSync(blobPath);
|
|
186
|
+
} catch (copyErr) {
|
|
187
|
+
const copyCode =
|
|
188
|
+
typeof copyErr === "object" && copyErr !== null && "code" in copyErr ? copyErr.code : undefined;
|
|
189
|
+
if (copyCode === "EEXIST") {
|
|
190
|
+
verifyBlobFileSync(hash, blobPath);
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
throw copyErr;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
throw err;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
verifyBlobFileSync(hash, blobPath);
|
|
201
|
+
fsyncDirBestEffortSync(this.dir);
|
|
202
|
+
return result;
|
|
203
|
+
} finally {
|
|
204
|
+
try {
|
|
205
|
+
fs.unlinkSync(tempPath);
|
|
206
|
+
} catch {
|
|
207
|
+
// Best-effort temp cleanup: never mask the primary result or throw.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
63
212
|
/** Read blob by hash, returns Buffer or null if not found. */
|
|
64
213
|
async get(hash: string): Promise<Buffer | null> {
|
|
65
214
|
const blobPath = path.join(this.dir, hash);
|
|
@@ -84,6 +233,22 @@ export class BlobStore {
|
|
|
84
233
|
}
|
|
85
234
|
}
|
|
86
235
|
|
|
236
|
+
/** Read blob by hash and verify its content hash; returns null if not found. */
|
|
237
|
+
async getChecked(hash: string): Promise<Buffer | null> {
|
|
238
|
+
return this.getCheckedSync(hash);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Synchronously read blob by hash and verify its content hash; returns null if not found. */
|
|
242
|
+
getCheckedSync(hash: string): Buffer | null {
|
|
243
|
+
const blobPath = path.join(this.dir, hash);
|
|
244
|
+
try {
|
|
245
|
+
return verifyBlobFileSync(hash, blobPath);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
if (isEnoent(err)) return null;
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
87
252
|
/** Check if a blob exists. */
|
|
88
253
|
async has(hash: string): Promise<boolean> {
|
|
89
254
|
try {
|
|
@@ -134,6 +299,12 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
134
299
|
return result;
|
|
135
300
|
}
|
|
136
301
|
|
|
302
|
+
putImmutableSync(data: Buffer): CheckedBlobPutResult {
|
|
303
|
+
const result = super.putImmutableSync(data);
|
|
304
|
+
this.#cachePut(result.hash, Buffer.from(data));
|
|
305
|
+
return result;
|
|
306
|
+
}
|
|
307
|
+
|
|
137
308
|
getSync(hash: string): Buffer | null {
|
|
138
309
|
const cached = this.#bufferCache.get(hash);
|
|
139
310
|
if (cached) {
|
|
@@ -152,6 +323,12 @@ export class EphemeralBlobStore extends BlobStore {
|
|
|
152
323
|
return data;
|
|
153
324
|
}
|
|
154
325
|
|
|
326
|
+
getCheckedSync(hash: string): Buffer | null {
|
|
327
|
+
const data = super.getCheckedSync(hash);
|
|
328
|
+
if (data) this.#cachePut(hash, Buffer.from(data));
|
|
329
|
+
return data;
|
|
330
|
+
}
|
|
331
|
+
|
|
155
332
|
clear(): void {
|
|
156
333
|
this.#bufferCache.clear();
|
|
157
334
|
this.#bufferCacheBytes = 0;
|
|
@@ -208,15 +385,15 @@ export class MemoryBlobStore extends BlobStore {
|
|
|
208
385
|
}
|
|
209
386
|
|
|
210
387
|
putSync(data: Buffer): BlobPutResult {
|
|
211
|
-
const hash =
|
|
388
|
+
const hash = sha256Hex(data);
|
|
212
389
|
this.#store(hash, Buffer.from(data));
|
|
213
|
-
return {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
390
|
+
return makeBlobPutResult(hash, `memory:${hash}`);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
putImmutableSync(data: Buffer): CheckedBlobPutResult {
|
|
394
|
+
const hash = sha256Hex(data);
|
|
395
|
+
this.#store(hash, Buffer.from(data));
|
|
396
|
+
return makeBlobPutResult(hash, `memory:${hash}`, data.byteLength);
|
|
220
397
|
}
|
|
221
398
|
|
|
222
399
|
async get(hash: string): Promise<Buffer | null> {
|
|
@@ -232,6 +409,17 @@ export class MemoryBlobStore extends BlobStore {
|
|
|
232
409
|
return Buffer.from(data);
|
|
233
410
|
}
|
|
234
411
|
|
|
412
|
+
async getChecked(hash: string): Promise<Buffer | null> {
|
|
413
|
+
return this.getCheckedSync(hash);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
getCheckedSync(hash: string): Buffer | null {
|
|
417
|
+
const data = this.getSync(hash);
|
|
418
|
+
if (!data) return null;
|
|
419
|
+
verifyBlobBytesSync(hash, `memory:${hash}`, data);
|
|
420
|
+
return data;
|
|
421
|
+
}
|
|
422
|
+
|
|
235
423
|
async has(hash: string): Promise<boolean> {
|
|
236
424
|
return this.#blobs.has(hash);
|
|
237
425
|
}
|