@gajae-code/coding-agent 0.10.0 → 0.10.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 +33 -0
- package/dist/types/cli.d.ts +1 -0
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/policy.d.ts +3 -0
- package/dist/types/coordinator-mcp/server.d.ts +17 -5
- package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -1
- package/dist/types/modes/components/gajae-pet-widget.d.ts +1 -0
- package/dist/types/modes/components/pet-capability.d.ts +37 -0
- package/dist/types/modes/components/pet-selector.d.ts +1 -1
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/types.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +0 -10
- package/dist/types/modes/interactive-mode.d.ts +2 -1
- package/dist/types/modes/print-mode.d.ts +3 -4
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
- package/dist/types/modes/types.d.ts +11 -1
- package/dist/types/modes/utils/context-usage.d.ts +4 -19
- package/dist/types/session/agent-session.d.ts +14 -1
- package/dist/types/session/context-estimation.d.ts +31 -0
- package/package.json +7 -7
- package/src/cli.ts +1 -0
- package/src/config/model-profiles.ts +19 -19
- package/src/config/settings.ts +96 -45
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/policy.ts +30 -0
- package/src/coordinator-mcp/server.ts +294 -66
- package/src/coordinator-mcp/session-reaper.ts +129 -0
- package/src/extensibility/extensions/runner.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -1
- package/src/extensibility/slash-commands.ts +3 -3
- package/src/gjc-runtime/tmux-owner-isolation.ts +4 -1
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/main.ts +3 -5
- package/src/modes/acp/acp-agent.ts +2 -2
- package/src/modes/components/custom-editor.ts +1 -0
- package/src/modes/components/footer.ts +3 -3
- package/src/modes/components/gajae-pet-widget.ts +169 -20
- package/src/modes/components/pet-capability.ts +115 -0
- package/src/modes/components/pet-selector.ts +4 -6
- package/src/modes/components/settings-selector.ts +28 -2
- package/src/modes/components/status-line/segments.ts +13 -6
- package/src/modes/components/status-line/types.ts +1 -1
- package/src/modes/components/status-line.ts +4 -171
- package/src/modes/controllers/extension-ui-controller.ts +10 -9
- package/src/modes/controllers/input-controller.ts +43 -13
- package/src/modes/controllers/selector-controller.ts +7 -18
- package/src/modes/interactive-mode.ts +65 -4
- package/src/modes/print-mode.ts +228 -92
- package/src/modes/rpc/rpc-client.ts +58 -1
- package/src/modes/rpc/rpc-mode.ts +286 -85
- package/src/modes/rpc/rpc-types.ts +15 -1
- package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
- package/src/modes/shared/agent-wire/command-validation.ts +10 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/types.ts +11 -1
- package/src/modes/utils/context-usage.ts +56 -92
- package/src/session/agent-session.ts +337 -90
- package/src/session/context-estimation.ts +91 -0
- package/src/session/session-manager.ts +9 -2
- package/src/slash-commands/builtin-registry.ts +31 -19
- package/src/slash-commands/helpers/context-report.ts +26 -4
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
type AgentTool,
|
|
29
29
|
assertImagePlaceholdersHavePayload,
|
|
30
30
|
canContinuePersistedHistory,
|
|
31
|
+
type ResolvedThinkingLevel,
|
|
31
32
|
resolveTelemetry,
|
|
32
33
|
type StablePrefixSnapshot,
|
|
33
34
|
ThinkingLevel,
|
|
@@ -279,6 +280,7 @@ import { buildWorkflowIntentDiff, WORKFLOW_INTENT_DIFF_CUSTOM_TYPE } from "../wo
|
|
|
279
280
|
import { buildWorkspaceTree, type WorkspaceTree } from "../workspace-tree";
|
|
280
281
|
import type { AuthStorage } from "./auth-storage";
|
|
281
282
|
import type { ClientBridge, ClientBridgePermissionOption, ClientBridgePermissionOutcome } from "./client-bridge";
|
|
283
|
+
import { computeNonMessageTokens } from "./context-estimation";
|
|
282
284
|
import {
|
|
283
285
|
type ContributionPrepOptions,
|
|
284
286
|
type ContributionPrepResult,
|
|
@@ -531,6 +533,12 @@ export interface RoleModelCycleResult {
|
|
|
531
533
|
role: string;
|
|
532
534
|
}
|
|
533
535
|
|
|
536
|
+
export interface DefaultModelSelectionResult {
|
|
537
|
+
readonly provider: string;
|
|
538
|
+
readonly modelId: string;
|
|
539
|
+
readonly thinkingLevel: ResolvedThinkingLevel;
|
|
540
|
+
}
|
|
541
|
+
|
|
534
542
|
/** Session statistics for /session command */
|
|
535
543
|
export interface SessionStats {
|
|
536
544
|
sessionFile: string | undefined;
|
|
@@ -1181,6 +1189,7 @@ export class AgentSession {
|
|
|
1181
1189
|
#scopedModels: ScopedModelSelection[];
|
|
1182
1190
|
#thinkingLevel: ThinkingLevel | undefined;
|
|
1183
1191
|
#activeModelProfile: string | undefined;
|
|
1192
|
+
#defaultModelSelectionTail: Promise<void> = Promise.resolve();
|
|
1184
1193
|
#promptTemplates: PromptTemplate[];
|
|
1185
1194
|
#slashCommands: FileSlashCommand[];
|
|
1186
1195
|
|
|
@@ -1233,6 +1242,11 @@ export class AgentSession {
|
|
|
1233
1242
|
#resourceSampler: () => EmergencyCompactionSample = () => this.#defaultResourceSample();
|
|
1234
1243
|
#retainedMemorySampler: (() => RetainedMemorySample) | undefined;
|
|
1235
1244
|
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
1245
|
+
/** Display-only context snapshot; pre-prompt compaction estimates deliberately remain uncached. */
|
|
1246
|
+
#contextUsageCache: { key: string; value: ContextUsage } | undefined;
|
|
1247
|
+
#contextUsageMessageIds = new WeakMap<AgentMessage, number>();
|
|
1248
|
+
#nextContextUsageMessageId = 0;
|
|
1249
|
+
#contextUsageEstimateCount = 0;
|
|
1236
1250
|
|
|
1237
1251
|
// Branch summarization state
|
|
1238
1252
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
@@ -2210,6 +2224,7 @@ export class AgentSession {
|
|
|
2210
2224
|
(this.#planCompactAbortPending || this.#silentAbortPending)
|
|
2211
2225
|
) {
|
|
2212
2226
|
(event.message as AssistantMessage).errorMessage = SILENT_ABORT_MARKER;
|
|
2227
|
+
this.agent.touchContext();
|
|
2213
2228
|
this.#planCompactAbortPending = false;
|
|
2214
2229
|
this.#silentAbortPending = false;
|
|
2215
2230
|
}
|
|
@@ -5921,7 +5936,7 @@ export class AgentSession {
|
|
|
5921
5936
|
reload: async () => {
|
|
5922
5937
|
await this.reload();
|
|
5923
5938
|
},
|
|
5924
|
-
getSystemPrompt: () => this.systemPrompt,
|
|
5939
|
+
getSystemPrompt: () => [...this.systemPrompt],
|
|
5925
5940
|
};
|
|
5926
5941
|
}
|
|
5927
5942
|
|
|
@@ -6986,6 +7001,94 @@ export class AgentSession {
|
|
|
6986
7001
|
await this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
6987
7002
|
}
|
|
6988
7003
|
|
|
7004
|
+
async setDefaultModelSelection(
|
|
7005
|
+
model: Model,
|
|
7006
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
7007
|
+
): Promise<DefaultModelSelectionResult> {
|
|
7008
|
+
const predecessor = this.#defaultModelSelectionTail;
|
|
7009
|
+
const transaction = Promise.withResolvers<void>();
|
|
7010
|
+
this.#defaultModelSelectionTail = transaction.promise;
|
|
7011
|
+
try {
|
|
7012
|
+
await predecessor;
|
|
7013
|
+
if (thinkingLevel === ThinkingLevel.Inherit) {
|
|
7014
|
+
throw new Error("Default model selection cannot inherit a thinking level");
|
|
7015
|
+
}
|
|
7016
|
+
const apiKey = await this.#modelRegistry.getApiKey(model, this.sessionId);
|
|
7017
|
+
if (!apiKey) {
|
|
7018
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
7019
|
+
}
|
|
7020
|
+
const resolvedLevel = resolveThinkingLevelForModel(model, thinkingLevel);
|
|
7021
|
+
const effectiveLevel =
|
|
7022
|
+
resolvedLevel ??
|
|
7023
|
+
resolveThinkingLevelForModel(model, model.thinking?.defaultLevel ?? this.thinkingLevel) ??
|
|
7024
|
+
ThinkingLevel.Off;
|
|
7025
|
+
await this.waitForIdle();
|
|
7026
|
+
const previousDefaultModelRole = this.settings.getGlobal("modelRoles")?.default;
|
|
7027
|
+
await this.sessionManager.flush();
|
|
7028
|
+
const previousSessionState = this.sessionManager.captureState();
|
|
7029
|
+
const previousModel = this.model;
|
|
7030
|
+
const previousThinkingLevel = this.thinkingLevel;
|
|
7031
|
+
const previousActiveRetryFallback = this.#activeRetryFallback ? { ...this.#activeRetryFallback } : undefined;
|
|
7032
|
+
await this.settings.setGlobalModelRoleAndFlush(
|
|
7033
|
+
"default",
|
|
7034
|
+
formatModelSelectorValue(`${model.provider}/${model.id}`, effectiveLevel),
|
|
7035
|
+
);
|
|
7036
|
+
try {
|
|
7037
|
+
await this.setModelTemporary(model, effectiveLevel);
|
|
7038
|
+
if (this.thinkingLevel === previousThinkingLevel) {
|
|
7039
|
+
this.sessionManager.appendThinkingLevelChange(this.thinkingLevel);
|
|
7040
|
+
}
|
|
7041
|
+
this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "default");
|
|
7042
|
+
} catch (error) {
|
|
7043
|
+
await this.settings.setGlobalModelRoleAndFlush("default", previousDefaultModelRole).catch(rollbackError => {
|
|
7044
|
+
logger.warn("Failed to restore durable default model selection after live apply failure", {
|
|
7045
|
+
error: String(rollbackError),
|
|
7046
|
+
});
|
|
7047
|
+
});
|
|
7048
|
+
let restoredLiveState = false;
|
|
7049
|
+
try {
|
|
7050
|
+
const failedEditMode = this.#resolveActiveEditMode();
|
|
7051
|
+
const currentModel = this.model;
|
|
7052
|
+
const hasCompatibleModelTransport =
|
|
7053
|
+
currentModel !== undefined &&
|
|
7054
|
+
previousModel !== undefined &&
|
|
7055
|
+
currentModel.provider === previousModel.provider &&
|
|
7056
|
+
currentModel.id === previousModel.id &&
|
|
7057
|
+
currentModel.api === previousModel.api;
|
|
7058
|
+
if (hasCompatibleModelTransport) {
|
|
7059
|
+
this.agent.setModel(previousModel);
|
|
7060
|
+
this.#syncAppendOnlyContext(previousModel);
|
|
7061
|
+
} else {
|
|
7062
|
+
this.#setModelWithProviderSessionReset(previousModel);
|
|
7063
|
+
}
|
|
7064
|
+
this.#thinkingLevel = previousThinkingLevel;
|
|
7065
|
+
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
7066
|
+
await this.#syncEditToolModeAfterModelChange(failedEditMode);
|
|
7067
|
+
restoredLiveState = true;
|
|
7068
|
+
} catch (rollbackError) {
|
|
7069
|
+
logger.warn("Failed to restore live default model selection after live apply failure", {
|
|
7070
|
+
error: String(rollbackError),
|
|
7071
|
+
});
|
|
7072
|
+
}
|
|
7073
|
+
if (restoredLiveState) {
|
|
7074
|
+
this.#activeRetryFallback = previousActiveRetryFallback;
|
|
7075
|
+
}
|
|
7076
|
+
try {
|
|
7077
|
+
this.sessionManager.restoreState(previousSessionState);
|
|
7078
|
+
await this.sessionManager.rewriteEntries();
|
|
7079
|
+
} catch (rollbackError) {
|
|
7080
|
+
logger.warn("Failed to restore session state after live default model selection failure", {
|
|
7081
|
+
error: String(rollbackError),
|
|
7082
|
+
});
|
|
7083
|
+
}
|
|
7084
|
+
throw error;
|
|
7085
|
+
}
|
|
7086
|
+
return { provider: model.provider, modelId: model.id, thinkingLevel: effectiveLevel };
|
|
7087
|
+
} finally {
|
|
7088
|
+
transaction.resolve();
|
|
7089
|
+
}
|
|
7090
|
+
}
|
|
7091
|
+
|
|
6989
7092
|
/**
|
|
6990
7093
|
* Cycle to next/previous model.
|
|
6991
7094
|
* Uses scoped models (from --models flag) if available, otherwise all available models.
|
|
@@ -8346,7 +8449,7 @@ export class AgentSession {
|
|
|
8346
8449
|
return candidate;
|
|
8347
8450
|
}
|
|
8348
8451
|
|
|
8349
|
-
#setModelWithProviderSessionReset(model: Model): void {
|
|
8452
|
+
#setModelWithProviderSessionReset(model: Model | undefined): void {
|
|
8350
8453
|
const currentModel = this.model;
|
|
8351
8454
|
if (currentModel) {
|
|
8352
8455
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
@@ -8386,15 +8489,15 @@ export class AgentSession {
|
|
|
8386
8489
|
}
|
|
8387
8490
|
}
|
|
8388
8491
|
|
|
8389
|
-
#closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model): void {
|
|
8492
|
+
#closeProviderSessionsForModelSwitch(currentModel: Model, nextModel: Model | undefined): void {
|
|
8390
8493
|
const providerKeys = new Set<string>();
|
|
8391
|
-
if (currentModel.api === "openai-codex-responses" || nextModel
|
|
8494
|
+
if (currentModel.api === "openai-codex-responses" || nextModel?.api === "openai-codex-responses") {
|
|
8392
8495
|
providerKeys.add("openai-codex-responses");
|
|
8393
8496
|
}
|
|
8394
8497
|
if (currentModel.api === "openai-responses") {
|
|
8395
8498
|
providerKeys.add(`openai-responses:${currentModel.provider}`);
|
|
8396
8499
|
}
|
|
8397
|
-
if (nextModel
|
|
8500
|
+
if (nextModel?.api === "openai-responses") {
|
|
8398
8501
|
providerKeys.add(`openai-responses:${nextModel.provider}`);
|
|
8399
8502
|
}
|
|
8400
8503
|
|
|
@@ -10369,18 +10472,24 @@ export class AgentSession {
|
|
|
10369
10472
|
attribution: "agent",
|
|
10370
10473
|
timestamp: incomingTimestamp,
|
|
10371
10474
|
};
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10475
|
+
const announceIncoming = () => {
|
|
10476
|
+
this.#emitIrcObservation(incomingRecord);
|
|
10477
|
+
this.#forwardIrcRelayToMain({
|
|
10478
|
+
observationId: incomingObservationId,
|
|
10479
|
+
from: args.from,
|
|
10480
|
+
to: this.#agentId ?? "?",
|
|
10481
|
+
body: args.message,
|
|
10482
|
+
kind: "message",
|
|
10483
|
+
timestamp: incomingTimestamp,
|
|
10484
|
+
});
|
|
10485
|
+
};
|
|
10381
10486
|
|
|
10382
10487
|
if (!awaitReply) {
|
|
10488
|
+
args.signal?.throwIfAborted();
|
|
10489
|
+
// Volatile session acceptance happens before any recipient or main-UI
|
|
10490
|
+
// observation, and before this delivery reports success to its sender.
|
|
10383
10491
|
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
|
10492
|
+
announceIncoming();
|
|
10384
10493
|
return { replyText: null };
|
|
10385
10494
|
}
|
|
10386
10495
|
|
|
@@ -10388,33 +10497,47 @@ export class AgentSession {
|
|
|
10388
10497
|
from: args.from,
|
|
10389
10498
|
message: args.message,
|
|
10390
10499
|
});
|
|
10391
|
-
|
|
10500
|
+
// Generate the reply before accepting or surfacing the exchange. Provider
|
|
10501
|
+
// failures and sender aborts therefore leave no accepted IRC batch or UI
|
|
10502
|
+
// observation. The deferred roster claim is committed only after the pair
|
|
10503
|
+
// below is accepted.
|
|
10504
|
+
const { replyText, commitRosterClaim, releaseRosterClaim } = await this.runEphemeralTurn({
|
|
10392
10505
|
promptText: incomingPrompt,
|
|
10393
10506
|
signal: args.signal,
|
|
10507
|
+
deferRosterCommit: true,
|
|
10394
10508
|
});
|
|
10509
|
+
try {
|
|
10510
|
+
const replyObservationId = crypto.randomUUID();
|
|
10511
|
+
const replyRecord: CustomMessage = {
|
|
10512
|
+
role: "custom",
|
|
10513
|
+
customType: "irc:autoreply",
|
|
10514
|
+
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
10515
|
+
display: true,
|
|
10516
|
+
details: { observationId: replyObservationId, to: args.from, reply: replyText },
|
|
10517
|
+
attribution: "agent",
|
|
10518
|
+
timestamp: Date.now(),
|
|
10519
|
+
};
|
|
10520
|
+
// Accept the ordered pair as one volatile batch before committing its
|
|
10521
|
+
// roster claim, notifying either UI, or resolving the sender delivery.
|
|
10522
|
+
args.signal?.throwIfAborted();
|
|
10523
|
+
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord], { deferFlush: true });
|
|
10524
|
+
commitRosterClaim?.();
|
|
10525
|
+
this.#flushOrSchedulePendingBackgroundExchanges();
|
|
10526
|
+
announceIncoming();
|
|
10527
|
+
this.#emitIrcObservation(replyRecord);
|
|
10528
|
+
this.#forwardIrcRelayToMain({
|
|
10529
|
+
observationId: replyObservationId,
|
|
10530
|
+
from: this.#agentId ?? "?",
|
|
10531
|
+
to: args.from,
|
|
10532
|
+
body: replyText,
|
|
10533
|
+
kind: "reply",
|
|
10534
|
+
timestamp: replyRecord.timestamp,
|
|
10535
|
+
});
|
|
10395
10536
|
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
content: `[IRC you → \`${args.from}\` (auto)]\n\n${replyText}`,
|
|
10401
|
-
display: true,
|
|
10402
|
-
details: { observationId: replyObservationId, to: args.from, reply: replyText },
|
|
10403
|
-
attribution: "agent",
|
|
10404
|
-
timestamp: Date.now(),
|
|
10405
|
-
};
|
|
10406
|
-
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
|
10407
|
-
this.#forwardIrcRelayToMain({
|
|
10408
|
-
observationId: replyObservationId,
|
|
10409
|
-
from: this.#agentId ?? "?",
|
|
10410
|
-
to: args.from,
|
|
10411
|
-
body: replyText,
|
|
10412
|
-
kind: "reply",
|
|
10413
|
-
timestamp: replyRecord.timestamp,
|
|
10414
|
-
});
|
|
10415
|
-
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
|
10416
|
-
|
|
10417
|
-
return { replyText };
|
|
10537
|
+
return { replyText };
|
|
10538
|
+
} finally {
|
|
10539
|
+
releaseRosterClaim?.();
|
|
10540
|
+
}
|
|
10418
10541
|
}
|
|
10419
10542
|
|
|
10420
10543
|
/**
|
|
@@ -10454,7 +10577,17 @@ export class AgentSession {
|
|
|
10454
10577
|
attribution: "agent",
|
|
10455
10578
|
timestamp: args.timestamp,
|
|
10456
10579
|
};
|
|
10457
|
-
|
|
10580
|
+
try {
|
|
10581
|
+
mainSession.emitIrcRelayObservation(relayRecord);
|
|
10582
|
+
} catch (error) {
|
|
10583
|
+
logger.warn("Failed to forward IRC relay observation", { error: String(error) });
|
|
10584
|
+
}
|
|
10585
|
+
}
|
|
10586
|
+
|
|
10587
|
+
#emitIrcObservation(message: CustomMessage): void {
|
|
10588
|
+
void this.#emitSessionEvent({ type: "irc_message", message }).catch(error => {
|
|
10589
|
+
logger.warn("Failed to emit IRC observation", { error: String(error) });
|
|
10590
|
+
});
|
|
10458
10591
|
}
|
|
10459
10592
|
|
|
10460
10593
|
/**
|
|
@@ -10462,7 +10595,7 @@ export class AgentSession {
|
|
|
10462
10595
|
* Does not persist the record to history. Public so other sessions can forward.
|
|
10463
10596
|
*/
|
|
10464
10597
|
emitIrcRelayObservation(record: CustomMessage): void {
|
|
10465
|
-
|
|
10598
|
+
this.#emitIrcObservation(record);
|
|
10466
10599
|
}
|
|
10467
10600
|
|
|
10468
10601
|
emitSubagentSteerObservation(args: { from: string; to: string; body: string; timestamp?: number }): void {
|
|
@@ -10592,8 +10725,16 @@ export class AgentSession {
|
|
|
10592
10725
|
promptText: string;
|
|
10593
10726
|
onTextDelta?: (delta: string) => void;
|
|
10594
10727
|
signal?: AbortSignal;
|
|
10595
|
-
|
|
10728
|
+
/** Defer the successful roster-claim commit until the caller accepts its exchange. */
|
|
10729
|
+
deferRosterCommit?: boolean;
|
|
10730
|
+
}): Promise<{
|
|
10731
|
+
replyText: string;
|
|
10732
|
+
assistantMessage: AssistantMessage;
|
|
10733
|
+
commitRosterClaim?: () => void;
|
|
10734
|
+
releaseRosterClaim?: () => void;
|
|
10735
|
+
}> {
|
|
10596
10736
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
10737
|
+
let rosterClaimDeferred = false;
|
|
10597
10738
|
try {
|
|
10598
10739
|
const model = this.model;
|
|
10599
10740
|
if (!model) {
|
|
@@ -10662,12 +10803,31 @@ export class AgentSession {
|
|
|
10662
10803
|
if (!assistantMessage) {
|
|
10663
10804
|
throw new Error("Ephemeral turn ended without a final message");
|
|
10664
10805
|
}
|
|
10806
|
+
if (rosterClaim && args.deferRosterCommit) {
|
|
10807
|
+
rosterClaimDeferred = true;
|
|
10808
|
+
let rosterClaimSettled = false;
|
|
10809
|
+
const settleRosterClaim = (commit: boolean) => {
|
|
10810
|
+
if (rosterClaimSettled) return;
|
|
10811
|
+
rosterClaimSettled = true;
|
|
10812
|
+
if (commit) {
|
|
10813
|
+
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10814
|
+
} else {
|
|
10815
|
+
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10816
|
+
}
|
|
10817
|
+
};
|
|
10818
|
+
return {
|
|
10819
|
+
replyText: dedupeIrcReply(replyText.trim()),
|
|
10820
|
+
assistantMessage,
|
|
10821
|
+
commitRosterClaim: () => settleRosterClaim(true),
|
|
10822
|
+
releaseRosterClaim: () => settleRosterClaim(false),
|
|
10823
|
+
};
|
|
10824
|
+
}
|
|
10665
10825
|
if (rosterClaim) {
|
|
10666
10826
|
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10667
10827
|
}
|
|
10668
10828
|
return { replyText: dedupeIrcReply(replyText.trim()), assistantMessage };
|
|
10669
10829
|
} finally {
|
|
10670
|
-
if (rosterClaim) {
|
|
10830
|
+
if (rosterClaim && !rosterClaimDeferred) {
|
|
10671
10831
|
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
10672
10832
|
}
|
|
10673
10833
|
}
|
|
@@ -10720,8 +10880,12 @@ export class AgentSession {
|
|
|
10720
10880
|
return messages;
|
|
10721
10881
|
}
|
|
10722
10882
|
|
|
10723
|
-
#queueBackgroundExchangeInjection(messages: CustomMessage[]): void {
|
|
10883
|
+
#queueBackgroundExchangeInjection(messages: CustomMessage[], options?: { deferFlush?: boolean }): void {
|
|
10724
10884
|
this.#pendingBackgroundExchanges.push(messages);
|
|
10885
|
+
if (!options?.deferFlush) this.#flushOrSchedulePendingBackgroundExchanges();
|
|
10886
|
+
}
|
|
10887
|
+
|
|
10888
|
+
#flushOrSchedulePendingBackgroundExchanges(): void {
|
|
10725
10889
|
if (!this.isStreaming) {
|
|
10726
10890
|
this.#flushPendingBackgroundExchanges();
|
|
10727
10891
|
return;
|
|
@@ -11408,43 +11572,92 @@ export class AgentSession {
|
|
|
11408
11572
|
const contextWindow = model.contextWindow ?? 0;
|
|
11409
11573
|
if (contextWindow <= 0) return undefined;
|
|
11410
11574
|
|
|
11575
|
+
const cacheKey = this.#contextUsageCacheKey(model, contextWindow);
|
|
11576
|
+
if (this.#contextUsageCache?.key === cacheKey) return { ...this.#contextUsageCache.value };
|
|
11577
|
+
this.#contextUsageEstimateCount++;
|
|
11578
|
+
|
|
11411
11579
|
// After compaction, the last assistant usage reflects pre-compaction context size.
|
|
11412
11580
|
// We can only trust usage from an assistant that responded after the latest compaction.
|
|
11413
11581
|
// If no such assistant exists, context token count is unknown until the next LLM response.
|
|
11414
11582
|
const branchEntries = this.sessionManager.getBranch();
|
|
11415
11583
|
const latestCompaction = getLatestCompactionEntry(branchEntries);
|
|
11584
|
+
const boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
|
|
11585
|
+
const anchor = this.#findAnchorableUsageIndex(this.messages, boundaryTs);
|
|
11586
|
+
let value: ContextUsage;
|
|
11587
|
+
if (latestCompaction && !anchor) {
|
|
11588
|
+
value = { tokens: null, contextWindow, percent: null, source: "unknown" };
|
|
11589
|
+
} else {
|
|
11590
|
+
const estimate = this.#estimateContextTokens(boundaryTs, anchor);
|
|
11591
|
+
value = {
|
|
11592
|
+
tokens: estimate.tokens,
|
|
11593
|
+
contextWindow,
|
|
11594
|
+
percent: (estimate.tokens / contextWindow) * 100,
|
|
11595
|
+
source: estimate.anchored ? "provider_anchor" : "heuristic",
|
|
11596
|
+
};
|
|
11597
|
+
}
|
|
11416
11598
|
|
|
11417
|
-
|
|
11418
|
-
|
|
11419
|
-
|
|
11420
|
-
|
|
11421
|
-
|
|
11422
|
-
|
|
11423
|
-
|
|
11424
|
-
|
|
11425
|
-
|
|
11426
|
-
|
|
11427
|
-
|
|
11428
|
-
|
|
11429
|
-
|
|
11430
|
-
|
|
11431
|
-
|
|
11432
|
-
|
|
11433
|
-
|
|
11599
|
+
this.#contextUsageCache = { key: cacheKey, value };
|
|
11600
|
+
return { ...value };
|
|
11601
|
+
}
|
|
11602
|
+
|
|
11603
|
+
getContextUsageObservabilityForTests(): { estimateCount: number } {
|
|
11604
|
+
return { estimateCount: this.#contextUsageEstimateCount };
|
|
11605
|
+
}
|
|
11606
|
+
|
|
11607
|
+
#contextUsageCacheKey(model: Model, contextWindow: number): string {
|
|
11608
|
+
const messages = this.messages;
|
|
11609
|
+
const lastMessage = messages[messages.length - 1];
|
|
11610
|
+
// Entry and leaf revisions change whenever the active branch changes, avoiding getBranch() on warm reads.
|
|
11611
|
+
const revision = this.sessionManager.revisionSnapshot();
|
|
11612
|
+
return `${this.agent.contextRevision}|${model.id}|${contextWindow}|${messages.length}|${this.#contextUsageMessageFingerprint(lastMessage)}|${revision.entry}:${revision.leaf}|${this.#computeContextUsageNonMessageInputsKey()}`;
|
|
11613
|
+
}
|
|
11614
|
+
|
|
11615
|
+
#contextUsageMessageFingerprint(message: AgentMessage | undefined): string {
|
|
11616
|
+
if (!message) return "";
|
|
11617
|
+
let messageId = this.#contextUsageMessageIds.get(message);
|
|
11618
|
+
if (messageId === undefined) {
|
|
11619
|
+
messageId = ++this.#nextContextUsageMessageId;
|
|
11620
|
+
this.#contextUsageMessageIds.set(message, messageId);
|
|
11621
|
+
}
|
|
11622
|
+
|
|
11623
|
+
const role = message.role;
|
|
11624
|
+
const timestamp = typeof message.timestamp === "number" ? message.timestamp : "";
|
|
11625
|
+
let contentLength = 0;
|
|
11626
|
+
let blockCount = 0;
|
|
11627
|
+
const record = message as {
|
|
11628
|
+
content?: unknown;
|
|
11629
|
+
command?: unknown;
|
|
11630
|
+
output?: unknown;
|
|
11631
|
+
summary?: unknown;
|
|
11632
|
+
stopReason?: unknown;
|
|
11633
|
+
usage?: Usage;
|
|
11634
|
+
};
|
|
11434
11635
|
|
|
11435
|
-
|
|
11436
|
-
|
|
11636
|
+
if (typeof record.command === "string") contentLength += record.command.length;
|
|
11637
|
+
if (typeof record.output === "string") contentLength += record.output.length;
|
|
11638
|
+
if (typeof record.summary === "string") contentLength += record.summary.length;
|
|
11639
|
+
if (typeof record.content === "string") {
|
|
11640
|
+
contentLength += record.content.length;
|
|
11641
|
+
} else if (Array.isArray(record.content)) {
|
|
11642
|
+
blockCount = record.content.length;
|
|
11643
|
+
for (const block of record.content) {
|
|
11644
|
+
if (!block || typeof block !== "object") continue;
|
|
11645
|
+
const content = block as { text?: unknown; thinking?: unknown; name?: unknown };
|
|
11646
|
+
if (typeof content.text === "string") contentLength += content.text.length;
|
|
11647
|
+
if (typeof content.thinking === "string") contentLength += content.thinking.length;
|
|
11648
|
+
if (typeof content.name === "string") contentLength += content.name.length;
|
|
11437
11649
|
}
|
|
11438
11650
|
}
|
|
11651
|
+
const stopReason = typeof record.stopReason === "string" ? record.stopReason : "";
|
|
11652
|
+
const usageTokens = record.usage ? calculateContextTokens(record.usage) : 0;
|
|
11653
|
+
return `${messageId}:${role}:${timestamp}:${contentLength}:${blockCount}:${stopReason}:${usageTokens}`;
|
|
11654
|
+
}
|
|
11439
11655
|
|
|
11440
|
-
|
|
11441
|
-
const
|
|
11442
|
-
|
|
11443
|
-
|
|
11444
|
-
|
|
11445
|
-
contextWindow,
|
|
11446
|
-
percent,
|
|
11447
|
-
};
|
|
11656
|
+
#computeContextUsageNonMessageInputsKey(): string {
|
|
11657
|
+
const systemPrompt = this.systemPrompt;
|
|
11658
|
+
let systemPromptLengths = "";
|
|
11659
|
+
for (const part of systemPrompt) systemPromptLengths += `${part.length},`;
|
|
11660
|
+
return `${systemPrompt.length}:${systemPromptLengths}|${this.agent.state.tools.length}|${this.skills.length}`;
|
|
11448
11661
|
}
|
|
11449
11662
|
|
|
11450
11663
|
async fetchUsageReports(signal?: AbortSignal): Promise<UsageReport[] | null> {
|
|
@@ -11459,10 +11672,19 @@ export class AgentSession {
|
|
|
11459
11672
|
/**
|
|
11460
11673
|
* Estimate context tokens from messages, using the last assistant usage when available.
|
|
11461
11674
|
*/
|
|
11462
|
-
#estimateContextTokens(
|
|
11675
|
+
#estimateContextTokens(
|
|
11676
|
+
boundaryTs: number,
|
|
11677
|
+
anchor: { index: number; usage: Usage } | undefined,
|
|
11678
|
+
): {
|
|
11463
11679
|
tokens: number;
|
|
11680
|
+
anchored: boolean;
|
|
11464
11681
|
} {
|
|
11465
|
-
return this.#estimateContextTokensWith(
|
|
11682
|
+
return this.#estimateContextTokensWith(
|
|
11683
|
+
message => this.#estimateMessageDisplayTokens(message),
|
|
11684
|
+
false,
|
|
11685
|
+
boundaryTs,
|
|
11686
|
+
anchor,
|
|
11687
|
+
);
|
|
11466
11688
|
}
|
|
11467
11689
|
|
|
11468
11690
|
/** Count inline image blocks in a message (for bucketing the fixed image token estimate). */
|
|
@@ -11493,6 +11715,24 @@ export class AgentSession {
|
|
|
11493
11715
|
return usage;
|
|
11494
11716
|
}
|
|
11495
11717
|
|
|
11718
|
+
/** Find the newest positive successful usage anchor after a compaction boundary. */
|
|
11719
|
+
#findAnchorableUsageIndex(
|
|
11720
|
+
messages: readonly AgentMessage[],
|
|
11721
|
+
boundaryTs: number,
|
|
11722
|
+
): { index: number; usage: Usage } | undefined {
|
|
11723
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11724
|
+
const message = messages[i];
|
|
11725
|
+
const usage = this.#anchorableAssistantUsage(message);
|
|
11726
|
+
if (!usage) continue;
|
|
11727
|
+
if (boundaryTs > 0) {
|
|
11728
|
+
const timestamp = message.timestamp;
|
|
11729
|
+
if (typeof timestamp !== "number" || !Number.isFinite(timestamp) || timestamp <= boundaryTs) continue;
|
|
11730
|
+
}
|
|
11731
|
+
return { index: i, usage };
|
|
11732
|
+
}
|
|
11733
|
+
return undefined;
|
|
11734
|
+
}
|
|
11735
|
+
|
|
11496
11736
|
/**
|
|
11497
11737
|
* Observed heuristic→actual token correction for the compaction keep window
|
|
11498
11738
|
* (Finding 7). Compares the provider's real prompt tokens against the chars/4
|
|
@@ -11537,39 +11777,45 @@ export class AgentSession {
|
|
|
11537
11777
|
|
|
11538
11778
|
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
11539
11779
|
tokens: number;
|
|
11780
|
+
anchored: boolean;
|
|
11540
11781
|
} {
|
|
11541
|
-
const estimate = this.#estimateContextTokensWith(
|
|
11782
|
+
const estimate = this.#estimateContextTokensWith(
|
|
11783
|
+
message => this.#estimateMessageCompactionDeltaTokens(message),
|
|
11784
|
+
true,
|
|
11785
|
+
);
|
|
11542
11786
|
return {
|
|
11543
11787
|
tokens: estimate.tokens + this.#estimateMessagesCompactionDeltaTokens(pendingMessages),
|
|
11788
|
+
anchored: estimate.anchored,
|
|
11544
11789
|
};
|
|
11545
11790
|
}
|
|
11546
11791
|
|
|
11547
|
-
#estimateContextTokensWith(
|
|
11792
|
+
#estimateContextTokensWith(
|
|
11793
|
+
estimateMessage: (message: AgentMessage) => number,
|
|
11794
|
+
inflateFixed = false,
|
|
11795
|
+
boundaryTs?: number,
|
|
11796
|
+
knownAnchor?: { index: number; usage: Usage } | undefined,
|
|
11797
|
+
): {
|
|
11548
11798
|
tokens: number;
|
|
11799
|
+
anchored: boolean;
|
|
11549
11800
|
} {
|
|
11550
11801
|
const messages = this.messages;
|
|
11551
|
-
|
|
11552
|
-
|
|
11553
|
-
|
|
11554
|
-
|
|
11555
|
-
|
|
11556
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11557
|
-
const usage = this.#anchorableAssistantUsage(messages[i]);
|
|
11558
|
-
if (usage) {
|
|
11559
|
-
lastUsage = usage;
|
|
11560
|
-
lastUsageIndex = i;
|
|
11561
|
-
break;
|
|
11562
|
-
}
|
|
11802
|
+
let anchor = knownAnchor;
|
|
11803
|
+
if (boundaryTs === undefined) {
|
|
11804
|
+
const latestCompaction = getLatestCompactionEntry(this.sessionManager.getBranch());
|
|
11805
|
+
boundaryTs = latestCompaction ? new Date(latestCompaction.timestamp).getTime() : 0;
|
|
11806
|
+
anchor = this.#findAnchorableUsageIndex(messages, boundaryTs);
|
|
11563
11807
|
}
|
|
11564
11808
|
|
|
11565
|
-
if (!
|
|
11566
|
-
// No usage data - estimate
|
|
11567
|
-
|
|
11809
|
+
if (!anchor) {
|
|
11810
|
+
// No usage data - estimate the full provider request.
|
|
11811
|
+
const fixedTokens = computeNonMessageTokens(this);
|
|
11812
|
+
let estimated = inflateFixed ? Math.ceil(fixedTokens * this.#compactionDeltaInflation) : fixedTokens;
|
|
11568
11813
|
for (const message of messages) {
|
|
11569
11814
|
estimated += estimateMessage(message);
|
|
11570
11815
|
}
|
|
11571
11816
|
return {
|
|
11572
11817
|
tokens: estimated,
|
|
11818
|
+
anchored: false,
|
|
11573
11819
|
};
|
|
11574
11820
|
}
|
|
11575
11821
|
|
|
@@ -11577,14 +11823,15 @@ export class AgentSession {
|
|
|
11577
11823
|
// tokens: the next request replays the anchor assistant's own output
|
|
11578
11824
|
// (text/reasoning/tool calls), so dropping it undercounts the very tokens
|
|
11579
11825
|
// a large-reasoning turn just added (Sol xhigh emits tens of thousands).
|
|
11580
|
-
const usageTokens = calculateContextTokens(
|
|
11826
|
+
const usageTokens = calculateContextTokens(anchor.usage);
|
|
11581
11827
|
let trailingTokens = 0;
|
|
11582
|
-
for (let i =
|
|
11828
|
+
for (let i = anchor.index + 1; i < messages.length; i++) {
|
|
11583
11829
|
trailingTokens += estimateMessage(messages[i]);
|
|
11584
11830
|
}
|
|
11585
11831
|
|
|
11586
11832
|
return {
|
|
11587
11833
|
tokens: usageTokens + trailingTokens,
|
|
11834
|
+
anchored: true,
|
|
11588
11835
|
};
|
|
11589
11836
|
}
|
|
11590
11837
|
|