@gajae-code/coding-agent 0.8.0 → 0.8.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 +48 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/notifications/config.d.ts +8 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +18 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/tools/skill.d.ts +1 -1
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +37 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli.ts +3 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +67 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
- package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/extensibility/skills.ts +30 -2
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +59 -11
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/team-runtime.ts +23 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
- package/src/hooks/native-skill-hook.ts +29 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +52 -41
- package/src/modes/acp/acp-agent.ts +7 -6
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +4 -4
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +186 -25
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +43 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +24 -13
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/notifications/config.ts +9 -1
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +39 -9
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +3 -1
- package/src/notifications/telegram-daemon.ts +231 -43
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/computer.md +2 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +20 -6
- package/src/session/agent-session.ts +225 -31
- package/src/session/agent-storage.ts +4 -0
- package/src/session/session-manager.ts +44 -2
- package/src/slash-commands/builtin-registry.ts +153 -71
- package/src/task/executor.ts +7 -4
- package/src/task/index.ts +9 -3
- package/src/task/output-manager.ts +16 -6
- package/src/task/receipt.ts +26 -7
- package/src/tools/computer.ts +246 -43
- package/src/tools/skill.ts +1 -1
- package/src/tools/telegram-send.ts +6 -0
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- Disabled means disabled: when the tool is disabled (`computer.alwaysOn=false` with `computer.enabled` unset/false) or the platform is unsupported, every action including `screenshot` fails with `COMPUTER_DISABLED` and captures nothing.
|
|
8
8
|
- Callable only on Apple Silicon macOS (`arm64` darwin); available by default there, with `computer.alwaysOn=false` as the off-switch and `computer.enabled=true` as the manual enable path.
|
|
9
|
-
- Native execution remains supervisor-gated. If the stop/suspend supervisor is unavailable, stale, suspended, permissioned off, display-stale, or cancelled, the action fails closed with a `COMPUTER_*` code.
|
|
9
|
+
- Native execution remains supervisor-gated. If the stop/suspend supervisor is unavailable, stale, suspended, permissioned off, display-stale, or cancelled, the action fails closed with a `COMPUTER_*` code. Coordinate actions carry the latest known screenshot display epoch when one is available so display-topology changes fail with `COMPUTER_DISPLAY_STALE`.
|
|
10
10
|
- Respect the user's stop/suspend request immediately. Do not loop desktop actions after a stop/suspend/error.
|
|
11
11
|
- The user can stop or suspend the session at any time with the configured kill-switch hotkey (default `Control+Option+Command+Escape`). If you see `COMPUTER_CANCELLED` or `COMPUTER_SUPERVISOR_NOT_LIVE`, stop and wait for the user.
|
|
12
12
|
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
Coordinates are screenshot pixels, not CSS pixels and not normalized fractions. Use the latest successful `screenshot` dimensions and origin/scale metadata as the coordinate frame. Do not guess coordinates outside the screenshot bounds.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
For stale-display protection to apply, derive pointer coordinates from a successful screenshot in the same tool session; a screenshot-first `batch` is preferred because later coordinate steps are validated against that screenshot and carry its display epoch into native execution. If a coordinate is out of bounds, the batch stops and reports `COMPUTER_COORD_INVALID`. Always capture a fresh screenshot before acting if the display may have changed.
|
|
18
18
|
|
|
19
19
|
## Actions
|
|
20
20
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface RuntimeApiKeyTarget {
|
|
2
|
+
setRuntimeApiKey(provider: string, apiKey: string): void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface RuntimeApiKeyModel {
|
|
6
|
+
provider: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function applyCliRuntimeApiKeyOverride(
|
|
10
|
+
target: RuntimeApiKeyTarget,
|
|
11
|
+
apiKey: string | undefined,
|
|
12
|
+
model: RuntimeApiKeyModel | undefined,
|
|
13
|
+
): void {
|
|
14
|
+
if (!apiKey || !model) return;
|
|
15
|
+
target.setRuntimeApiKey(model.provider, apiKey);
|
|
16
|
+
}
|
package/src/sdk.ts
CHANGED
|
@@ -1214,8 +1214,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1214
1214
|
|
|
1215
1215
|
const agentRegistry = options.agentRegistry ?? AgentRegistry.global();
|
|
1216
1216
|
const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1217
|
+
const isSubSession = taskDepth > 0 || Boolean(options.parentTaskPrefix) || Boolean(options.currentAgentType);
|
|
1218
|
+
const resolvedAgentDisplayName = options.agentDisplayName ?? (isSubSession ? "sub" : "main");
|
|
1219
1219
|
const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
|
|
1220
1220
|
|
|
1221
1221
|
try {
|
|
@@ -1520,12 +1520,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1520
1520
|
}
|
|
1521
1521
|
let notificationCfg: NotificationConfig | undefined;
|
|
1522
1522
|
try {
|
|
1523
|
-
notificationCfg = getNotificationConfig(
|
|
1523
|
+
notificationCfg = getNotificationConfig(settings);
|
|
1524
1524
|
} catch {
|
|
1525
1525
|
notificationCfg = undefined;
|
|
1526
1526
|
}
|
|
1527
|
-
if (
|
|
1528
|
-
|
|
1527
|
+
if (
|
|
1528
|
+
shouldRegisterNotificationsExtension({
|
|
1529
|
+
env: process.env,
|
|
1530
|
+
cfg: notificationCfg,
|
|
1531
|
+
taskDepth,
|
|
1532
|
+
parentTaskPrefix: options.parentTaskPrefix,
|
|
1533
|
+
currentAgentType: options.currentAgentType,
|
|
1534
|
+
})
|
|
1535
|
+
) {
|
|
1536
|
+
inlineExtensions.push(api => createNotificationsExtension(api, { settings }));
|
|
1529
1537
|
}
|
|
1530
1538
|
|
|
1531
1539
|
// Extension/module discovery is quarantined; retain only the private
|
|
@@ -1632,6 +1640,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1632
1640
|
cwd,
|
|
1633
1641
|
sessionManager,
|
|
1634
1642
|
modelRegistry,
|
|
1643
|
+
{
|
|
1644
|
+
kind: isSubSession ? "sub" : "main",
|
|
1645
|
+
taskDepth,
|
|
1646
|
+
...(options.parentTaskPrefix ? { parentTaskPrefix: options.parentTaskPrefix } : {}),
|
|
1647
|
+
...(options.currentAgentType ? { currentAgentType: options.currentAgentType } : {}),
|
|
1648
|
+
},
|
|
1635
1649
|
);
|
|
1636
1650
|
}
|
|
1637
1651
|
|
|
@@ -1924,7 +1938,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1924
1938
|
agentRegistry.register({
|
|
1925
1939
|
id: resolvedAgentId,
|
|
1926
1940
|
displayName: resolvedAgentDisplayName,
|
|
1927
|
-
kind:
|
|
1941
|
+
kind: isSubSession ? "sub" : "main",
|
|
1928
1942
|
parentId: options.parentTaskPrefix,
|
|
1929
1943
|
session: null,
|
|
1930
1944
|
sessionFile: sessionManager.getSessionFile() ?? null,
|
|
@@ -905,9 +905,17 @@ function extractPermissionLocations(
|
|
|
905
905
|
* `tag` is set only by `enqueueCustomMessageDisplay` (used for skill-prompt
|
|
906
906
|
* custom messages queued during streaming) and is matched by the custom-role
|
|
907
907
|
* `message_start` dequeue branch; user-message pushes leave it undefined and
|
|
908
|
-
* rely on the existing text-equality match. `sequence`
|
|
909
|
-
*
|
|
908
|
+
* rely on the existing text-equality match. `sequence` gives each queued chip a
|
|
909
|
+
* stable edit id while the display arrays preserve delivery order. */
|
|
910
910
|
type QueuedDisplayEntry = { text: string; tag?: string; sequence: number };
|
|
911
|
+
export type QueuedMessageEditMode = "steer" | "followUp";
|
|
912
|
+
|
|
913
|
+
export interface QueuedMessageEditEntry {
|
|
914
|
+
id: string;
|
|
915
|
+
text: string;
|
|
916
|
+
mode: QueuedMessageEditMode;
|
|
917
|
+
label: string;
|
|
918
|
+
}
|
|
911
919
|
|
|
912
920
|
/** A custom message contributed at the before-agent-start point. */
|
|
913
921
|
export type BeforeAgentStartInternalMessage = Pick<
|
|
@@ -1134,17 +1142,19 @@ export class AgentSession {
|
|
|
1134
1142
|
#streamingEditFileCache = new Map<string, string>();
|
|
1135
1143
|
#promptInFlightCount = 0;
|
|
1136
1144
|
// Wire-level agent_end emission deferred until #promptInFlightCount drops to 0.
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1140
|
-
//
|
|
1141
|
-
// on `agent_end` can fire its next `prompt`
|
|
1142
|
-
//
|
|
1143
|
-
//
|
|
1145
|
+
// Local subscribers and extension hooks both receive the deferred event from
|
|
1146
|
+
// #flushPendingAgentEnd(), preserving the local-before-extension ordering used
|
|
1147
|
+
// by #emitSessionEvent while still preventing external clients from resuming
|
|
1148
|
+
// before #promptWithMessage's finally decrements #promptInFlightCount. Without
|
|
1149
|
+
// this, a client that resumes on `agent_end` can fire its next `prompt` while
|
|
1150
|
+
// the session still reports busy and hit AgentBusyError. Flushed from both
|
|
1151
|
+
// #endInFlight (normal) and #resetInFlight (abort).
|
|
1144
1152
|
#pendingAgentEndEmit: AgentSessionEvent | undefined;
|
|
1145
1153
|
#obfuscator: SecretObfuscator | undefined;
|
|
1146
1154
|
#checkpointState: CheckpointState | undefined = undefined;
|
|
1147
1155
|
#providerReplaySourceCache = new WeakMap<AgentMessage, ProviderReplaySourceCacheEntry>();
|
|
1156
|
+
#lastOversizedAutoMaintenanceAttemptSignature: string | undefined = undefined;
|
|
1157
|
+
|
|
1148
1158
|
#pendingRewindReport: string | undefined = undefined;
|
|
1149
1159
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
1150
1160
|
#promptGeneration = 0;
|
|
@@ -1223,6 +1233,7 @@ export class AgentSession {
|
|
|
1223
1233
|
if (!pending) return;
|
|
1224
1234
|
this.#pendingAgentEndEmit = undefined;
|
|
1225
1235
|
this.#emit(pending);
|
|
1236
|
+
void this.#queueExtensionEvent(pending);
|
|
1226
1237
|
}
|
|
1227
1238
|
|
|
1228
1239
|
constructor(config: AgentSessionConfig) {
|
|
@@ -1533,10 +1544,9 @@ export class AgentSession {
|
|
|
1533
1544
|
recordSkip("unsupported-role");
|
|
1534
1545
|
return undefined;
|
|
1535
1546
|
}
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1547
|
+
const messageWithoutProviderPayload = { ...message } as Message & { providerPayload?: unknown };
|
|
1548
|
+
delete messageWithoutProviderPayload.providerPayload;
|
|
1549
|
+
const cloned = cloneJsonValueForForkSeed(messageWithoutProviderPayload) as Message;
|
|
1540
1550
|
if (Array.isArray(cloned.content)) {
|
|
1541
1551
|
const sanitizedContent: TextContent[] = [];
|
|
1542
1552
|
for (const block of cloned.content) {
|
|
@@ -1548,6 +1558,10 @@ export class AgentSession {
|
|
|
1548
1558
|
recordSkip(`unsupported-content-${block.type}`);
|
|
1549
1559
|
}
|
|
1550
1560
|
}
|
|
1561
|
+
if (sanitizedContent.length === 0) {
|
|
1562
|
+
recordSkip("empty-content");
|
|
1563
|
+
return undefined;
|
|
1564
|
+
}
|
|
1551
1565
|
return { ...cloned, content: sanitizedContent } as Message;
|
|
1552
1566
|
}
|
|
1553
1567
|
return cloned;
|
|
@@ -1561,9 +1575,14 @@ export class AgentSession {
|
|
|
1561
1575
|
const sanitized = sanitizeMessage(providerMessages[i]!);
|
|
1562
1576
|
if (!sanitized) continue;
|
|
1563
1577
|
const messageTokens = estimateMessageTokensHeuristic(sanitized);
|
|
1578
|
+
// Stop at the first message that overflows the token budget. The loop walks
|
|
1579
|
+
// newest→oldest, so `break` keeps the seed a CONTIGUOUS run of the most recent
|
|
1580
|
+
// messages. `continue` here would skip the oversized recent message and scavenge
|
|
1581
|
+
// smaller OLDER ones, yielding a non-contiguous seed that misrepresents the
|
|
1582
|
+
// conversation and violates the recency contract of the receipt/last-turn/bounded modes.
|
|
1564
1583
|
if (maxTokens > 0 && approximateTokens + messageTokens > maxTokens) {
|
|
1565
1584
|
recordSkip("token-limit");
|
|
1566
|
-
|
|
1585
|
+
break;
|
|
1567
1586
|
}
|
|
1568
1587
|
selected.unshift(sanitized);
|
|
1569
1588
|
approximateTokens += messageTokens;
|
|
@@ -1645,6 +1664,9 @@ export class AgentSession {
|
|
|
1645
1664
|
return entry;
|
|
1646
1665
|
}
|
|
1647
1666
|
|
|
1667
|
+
#queuedMessageEditId(mode: QueuedMessageEditMode, sequence: number): string {
|
|
1668
|
+
return `${mode}:${sequence}`;
|
|
1669
|
+
}
|
|
1648
1670
|
/** Register a compact display string for a custom message that the caller is
|
|
1649
1671
|
* about to dispatch via `promptCustomMessage` / `sendCustomMessage`.
|
|
1650
1672
|
* Returns a stable tag the caller MUST embed in
|
|
@@ -1752,17 +1774,19 @@ export class AgentSession {
|
|
|
1752
1774
|
}
|
|
1753
1775
|
|
|
1754
1776
|
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1777
|
+
const persistRuntimeState = () =>
|
|
1778
|
+
persistCoordinatorRuntimeStateFromEvent(event, {
|
|
1779
|
+
sessionId: this.sessionId,
|
|
1780
|
+
cwd: this.sessionManager.getCwd(),
|
|
1781
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1760
1784
|
if (event.type === "message_update") {
|
|
1761
1785
|
this.#emit(event);
|
|
1786
|
+
void persistRuntimeState();
|
|
1762
1787
|
void this.#queueExtensionEvent(event);
|
|
1763
1788
|
return;
|
|
1764
1789
|
}
|
|
1765
|
-
await this.#emitExtensionEvent(event);
|
|
1766
1790
|
// Hold the wire-level agent_end until in-flight prompts unwind. Subscribers
|
|
1767
1791
|
// (rpc-mode, ACP, Cursor) treat agent_end as the "session is idle" signal;
|
|
1768
1792
|
// emitting while #promptInFlightCount > 0 lets a client fire its next
|
|
@@ -1772,10 +1796,18 @@ export class AgentSession {
|
|
|
1772
1796
|
// supersedes the pending one, which is what subscribers want — they only
|
|
1773
1797
|
// care about the final settle.
|
|
1774
1798
|
if (event.type === "agent_end" && this.#promptInFlightCount > 0) {
|
|
1799
|
+
void persistRuntimeState();
|
|
1775
1800
|
this.#pendingAgentEndEmit = event;
|
|
1776
1801
|
return;
|
|
1777
1802
|
}
|
|
1803
|
+
|
|
1804
|
+
// Local subscribers are part of the AgentSession control path: retryNow(),
|
|
1805
|
+
// auto-continuation gates, goal reminders, and tests all observe these events
|
|
1806
|
+
// synchronously. Coordinator sidecar writes and extension hooks are secondary
|
|
1807
|
+
// sinks, so they must not delay or suppress local delivery.
|
|
1778
1808
|
this.#emit(event);
|
|
1809
|
+
void persistRuntimeState();
|
|
1810
|
+
await this.#emitExtensionEvent(event);
|
|
1779
1811
|
}
|
|
1780
1812
|
|
|
1781
1813
|
// Track last assistant message for auto-compaction check
|
|
@@ -3227,11 +3259,15 @@ export class AgentSession {
|
|
|
3227
3259
|
maxAttempts: event.maxAttempts,
|
|
3228
3260
|
});
|
|
3229
3261
|
} else if (event.type === "goal_updated") {
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3262
|
+
try {
|
|
3263
|
+
await this.#extensionRunner.emit({
|
|
3264
|
+
type: "goal_updated",
|
|
3265
|
+
goal: event.goal,
|
|
3266
|
+
state: event.state,
|
|
3267
|
+
});
|
|
3268
|
+
} catch (error) {
|
|
3269
|
+
logger.warn("Goal updated extension hook failed", { error: String(error) });
|
|
3270
|
+
}
|
|
3235
3271
|
}
|
|
3236
3272
|
}
|
|
3237
3273
|
|
|
@@ -5715,6 +5751,72 @@ export class AgentSession {
|
|
|
5715
5751
|
};
|
|
5716
5752
|
}
|
|
5717
5753
|
|
|
5754
|
+
getQueuedMessageEntries(): QueuedMessageEditEntry[] {
|
|
5755
|
+
const entries: QueuedMessageEditEntry[] = [];
|
|
5756
|
+
for (const entry of this.#steeringMessages) {
|
|
5757
|
+
entries.push({
|
|
5758
|
+
id: this.#queuedMessageEditId("steer", entry.sequence),
|
|
5759
|
+
text: entry.text,
|
|
5760
|
+
mode: "steer",
|
|
5761
|
+
label: "Steer",
|
|
5762
|
+
});
|
|
5763
|
+
}
|
|
5764
|
+
for (const entry of this.#followUpMessages) {
|
|
5765
|
+
entries.push({
|
|
5766
|
+
id: this.#queuedMessageEditId("followUp", entry.sequence),
|
|
5767
|
+
text: entry.text,
|
|
5768
|
+
mode: "followUp",
|
|
5769
|
+
label: "Queued",
|
|
5770
|
+
});
|
|
5771
|
+
}
|
|
5772
|
+
return entries;
|
|
5773
|
+
}
|
|
5774
|
+
|
|
5775
|
+
removeQueuedMessageForEditing(id: string): string | undefined {
|
|
5776
|
+
const [mode, sequenceText] = id.split(":");
|
|
5777
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return undefined;
|
|
5778
|
+
const sequence = Number(sequenceText);
|
|
5779
|
+
if (!Number.isInteger(sequence)) return undefined;
|
|
5780
|
+
|
|
5781
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
5782
|
+
const index = queue.findIndex(entry => entry.sequence === sequence);
|
|
5783
|
+
if (index === -1) return undefined;
|
|
5784
|
+
|
|
5785
|
+
const [entry] = queue.splice(index, 1);
|
|
5786
|
+
if (mode === "steer") {
|
|
5787
|
+
this.agent.removeSteerAt(index);
|
|
5788
|
+
} else {
|
|
5789
|
+
this.agent.removeFollowUpAt(index);
|
|
5790
|
+
}
|
|
5791
|
+
return entry?.text;
|
|
5792
|
+
}
|
|
5793
|
+
|
|
5794
|
+
moveQueuedMessageForEditing(id: string, direction: "up" | "down"): boolean {
|
|
5795
|
+
const [mode, sequenceText] = id.split(":");
|
|
5796
|
+
if ((mode !== "steer" && mode !== "followUp") || sequenceText === undefined) return false;
|
|
5797
|
+
const sequence = Number(sequenceText);
|
|
5798
|
+
if (!Number.isInteger(sequence)) return false;
|
|
5799
|
+
|
|
5800
|
+
const queue = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
5801
|
+
const fromIndex = queue.findIndex(entry => entry.sequence === sequence);
|
|
5802
|
+
if (fromIndex === -1) return false;
|
|
5803
|
+
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
|
5804
|
+
const agentMoved =
|
|
5805
|
+
mode === "steer" ? this.agent.moveSteer(fromIndex, toIndex) : this.agent.moveFollowUp(fromIndex, toIndex);
|
|
5806
|
+
if (!agentMoved) return false;
|
|
5807
|
+
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
5808
|
+
}
|
|
5809
|
+
|
|
5810
|
+
#moveQueuedDisplayEntry(queue: QueuedDisplayEntry[], fromIndex: number, toIndex: number): boolean {
|
|
5811
|
+
if (fromIndex < 0 || fromIndex >= queue.length) return false;
|
|
5812
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
5813
|
+
if (fromIndex === toIndex) return true;
|
|
5814
|
+
const [entry] = queue.splice(fromIndex, 1);
|
|
5815
|
+
if (!entry) return false;
|
|
5816
|
+
queue.splice(toIndex, 0, entry);
|
|
5817
|
+
return true;
|
|
5818
|
+
}
|
|
5819
|
+
|
|
5718
5820
|
/**
|
|
5719
5821
|
* Pop the newest queued message across steering and follow-up queues.
|
|
5720
5822
|
* Used by dequeue keybinding to restore messages to editor one at a time.
|
|
@@ -5726,15 +5828,11 @@ export class AgentSession {
|
|
|
5726
5828
|
const followUpEntry = this.#followUpMessages.at(-1);
|
|
5727
5829
|
|
|
5728
5830
|
if (steeringEntry && (!followUpEntry || steeringEntry.sequence > followUpEntry.sequence)) {
|
|
5729
|
-
|
|
5730
|
-
this.agent.popLastSteer();
|
|
5731
|
-
return entry?.text;
|
|
5831
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("steer", steeringEntry.sequence));
|
|
5732
5832
|
}
|
|
5733
5833
|
|
|
5734
5834
|
if (followUpEntry) {
|
|
5735
|
-
|
|
5736
|
-
this.agent.popLastFollowUp();
|
|
5737
|
-
return entry?.text;
|
|
5835
|
+
return this.removeQueuedMessageForEditing(this.#queuedMessageEditId("followUp", followUpEntry.sequence));
|
|
5738
5836
|
}
|
|
5739
5837
|
|
|
5740
5838
|
return undefined;
|
|
@@ -7672,6 +7770,59 @@ export class AgentSession {
|
|
|
7672
7770
|
return false;
|
|
7673
7771
|
}
|
|
7674
7772
|
|
|
7773
|
+
#buildAutoMaintenanceAttemptSignature(
|
|
7774
|
+
action: "context-full" | "handoff",
|
|
7775
|
+
preparation: CompactionPreparation,
|
|
7776
|
+
hookPrompt: string | undefined,
|
|
7777
|
+
hookContext: string[] | undefined,
|
|
7778
|
+
candidateModels: readonly Model[],
|
|
7779
|
+
): string {
|
|
7780
|
+
const source = JSON.stringify({
|
|
7781
|
+
action,
|
|
7782
|
+
model: this.model ? this.#getModelKey(this.model) : undefined,
|
|
7783
|
+
candidates: candidateModels.map(model => this.#getModelKey(model)),
|
|
7784
|
+
isSplitTurn: preparation.isSplitTurn,
|
|
7785
|
+
previousSummary: preparation.previousSummary,
|
|
7786
|
+
previousPreserveData: preparation.previousPreserveData,
|
|
7787
|
+
hookPrompt,
|
|
7788
|
+
hookContext,
|
|
7789
|
+
messagesToSummarize: preparation.messagesToSummarize.map(message =>
|
|
7790
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
7791
|
+
),
|
|
7792
|
+
turnPrefixMessages: preparation.turnPrefixMessages.map(message =>
|
|
7793
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
7794
|
+
),
|
|
7795
|
+
recentMessages: preparation.recentMessages.map(message =>
|
|
7796
|
+
this.#normalizeSessionMessageForProviderReplay(message),
|
|
7797
|
+
),
|
|
7798
|
+
});
|
|
7799
|
+
return `${this.#hashProviderReplaySource(source).toString(16)}:${source.length}`;
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
#isOversizedMaintenanceError(errorMessage: string): boolean {
|
|
7803
|
+
return isContextOverflow(
|
|
7804
|
+
{
|
|
7805
|
+
role: "assistant",
|
|
7806
|
+
content: [],
|
|
7807
|
+
api: this.model?.api ?? "anthropic-messages",
|
|
7808
|
+
provider: this.model?.provider ?? "unknown",
|
|
7809
|
+
model: this.model?.id ?? "unknown",
|
|
7810
|
+
stopReason: "error",
|
|
7811
|
+
errorMessage,
|
|
7812
|
+
usage: {
|
|
7813
|
+
input: 0,
|
|
7814
|
+
output: 0,
|
|
7815
|
+
cacheRead: 0,
|
|
7816
|
+
cacheWrite: 0,
|
|
7817
|
+
totalTokens: 0,
|
|
7818
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
7819
|
+
},
|
|
7820
|
+
timestamp: Date.now(),
|
|
7821
|
+
} as AssistantMessage,
|
|
7822
|
+
this.model?.contextWindow,
|
|
7823
|
+
);
|
|
7824
|
+
}
|
|
7825
|
+
|
|
7675
7826
|
#getModelKey(model: Model): string {
|
|
7676
7827
|
return `${model.provider}/${model.id}`;
|
|
7677
7828
|
}
|
|
@@ -7942,6 +8093,7 @@ export class AgentSession {
|
|
|
7942
8093
|
const autoCompactionAbortController = new AbortController();
|
|
7943
8094
|
this.#autoCompactionAbortController = autoCompactionAbortController;
|
|
7944
8095
|
const autoCompactionSignal = autoCompactionAbortController.signal;
|
|
8096
|
+
let maintenanceAttemptSignature: string | undefined;
|
|
7945
8097
|
|
|
7946
8098
|
try {
|
|
7947
8099
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
@@ -8089,6 +8241,26 @@ export class AgentSession {
|
|
|
8089
8241
|
preserveData = compactionPrep.preserveData;
|
|
8090
8242
|
} else {
|
|
8091
8243
|
const candidates = this.#getCompactionModelCandidates(availableModels);
|
|
8244
|
+
maintenanceAttemptSignature = this.#buildAutoMaintenanceAttemptSignature(
|
|
8245
|
+
action,
|
|
8246
|
+
preparation,
|
|
8247
|
+
compactionPrep.hookPrompt,
|
|
8248
|
+
compactionPrep.hookContext,
|
|
8249
|
+
candidates,
|
|
8250
|
+
);
|
|
8251
|
+
if (this.#lastOversizedAutoMaintenanceAttemptSignature === maintenanceAttemptSignature) {
|
|
8252
|
+
await this.#emitSessionEvent({
|
|
8253
|
+
type: "auto_compaction_end",
|
|
8254
|
+
action,
|
|
8255
|
+
result: undefined,
|
|
8256
|
+
aborted: false,
|
|
8257
|
+
willRetry: false,
|
|
8258
|
+
skipped: true,
|
|
8259
|
+
errorMessage:
|
|
8260
|
+
"Auto-compaction skipped: previous unchanged maintenance request exceeded the model context window; change or reduce the conversation before retrying maintenance.",
|
|
8261
|
+
});
|
|
8262
|
+
return;
|
|
8263
|
+
}
|
|
8092
8264
|
const retrySettings = this.settings.getGroup("retry");
|
|
8093
8265
|
const telemetry = resolveTelemetry(this.agent.telemetry, this.sessionId);
|
|
8094
8266
|
let compactResult: CompactionResult | undefined;
|
|
@@ -8221,6 +8393,8 @@ export class AgentSession {
|
|
|
8221
8393
|
details,
|
|
8222
8394
|
preserveData,
|
|
8223
8395
|
};
|
|
8396
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
8397
|
+
|
|
8224
8398
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
8225
8399
|
await this.#emitSessionEvent({
|
|
8226
8400
|
type: "auto_compaction_end",
|
|
@@ -8258,6 +8432,9 @@ export class AgentSession {
|
|
|
8258
8432
|
return;
|
|
8259
8433
|
}
|
|
8260
8434
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
8435
|
+
if (maintenanceAttemptSignature && this.#isOversizedMaintenanceError(errorMessage)) {
|
|
8436
|
+
this.#lastOversizedAutoMaintenanceAttemptSignature = maintenanceAttemptSignature;
|
|
8437
|
+
}
|
|
8261
8438
|
await this.#emitSessionEvent({
|
|
8262
8439
|
type: "auto_compaction_end",
|
|
8263
8440
|
action,
|
|
@@ -8382,6 +8559,16 @@ export class AgentSession {
|
|
|
8382
8559
|
);
|
|
8383
8560
|
}
|
|
8384
8561
|
|
|
8562
|
+
#isRefusalErrorMessage(errorMessage: string): boolean {
|
|
8563
|
+
// Provider safety-refusal stops. Matches the engine-generated Anthropic
|
|
8564
|
+
// labels from packages/ai ("Refusal (<category>): …", "Refusal (no
|
|
8565
|
+
// details provided)", "Content flagged by safety filters") plus generic
|
|
8566
|
+
// usage-policy refusal copy.
|
|
8567
|
+
return /^refusal(\s*\(|:|$)|content flagged by safety filters|blocked under .{0,40}usage policy/i.test(
|
|
8568
|
+
errorMessage,
|
|
8569
|
+
);
|
|
8570
|
+
}
|
|
8571
|
+
|
|
8385
8572
|
#extractExplicitHttpStatusFromErrorMessage(errorMessage: string): number | undefined {
|
|
8386
8573
|
// Parse only explicit HTTP/status wording. Do not treat generic
|
|
8387
8574
|
// `error: 400` as an HTTP status because rate-limit copy can say
|
|
@@ -8402,6 +8589,13 @@ export class AgentSession {
|
|
|
8402
8589
|
const contextWindow = this.model?.contextWindow ?? 0;
|
|
8403
8590
|
if (isContextOverflow(message, contextWindow)) return "overflow";
|
|
8404
8591
|
const err = message.errorMessage;
|
|
8592
|
+
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
8593
|
+
// "sensitive") are deterministic for the submitted context: replaying
|
|
8594
|
+
// the identical conversation re-triggers the identical refusal, so an
|
|
8595
|
+
// auto-retry loop can never succeed and only re-bills the full context
|
|
8596
|
+
// on every attempt (#1655). Surface immediately instead of joining the
|
|
8597
|
+
// unbounded "unknown" retry class.
|
|
8598
|
+
if (this.#isRefusalErrorMessage(err)) return "terminal";
|
|
8405
8599
|
if (isLocalModelEndpoint(this.model) && this.#isLocalProviderAvailabilityErrorMessage(err)) {
|
|
8406
8600
|
return "local_unavailable";
|
|
8407
8601
|
}
|
|
@@ -420,6 +420,10 @@ FROM model_usage_legacy
|
|
|
420
420
|
this.#authStore.setCache(key, value, expiresAtSec);
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
deleteCachePrefix(prefix: string): void {
|
|
424
|
+
this.#authStore.deleteCachePrefix?.(prefix);
|
|
425
|
+
}
|
|
426
|
+
|
|
423
427
|
/**
|
|
424
428
|
* Deletes expired cache entries. Call periodically for cleanup.
|
|
425
429
|
*/
|
|
@@ -1308,6 +1308,45 @@ function isImageBlock(value: unknown): value is { type: "image"; data: string; m
|
|
|
1308
1308
|
);
|
|
1309
1309
|
}
|
|
1310
1310
|
|
|
1311
|
+
function stripUndefinedPlainObjectFields(value: unknown, path = "entry"): unknown {
|
|
1312
|
+
if (value === undefined) return undefined;
|
|
1313
|
+
if (value === null || typeof value !== "object") return value;
|
|
1314
|
+
if (Array.isArray(value)) {
|
|
1315
|
+
let changed = false;
|
|
1316
|
+
const result: unknown[] = new Array(value.length);
|
|
1317
|
+
for (let index = 0; index < value.length; index++) {
|
|
1318
|
+
const item = value[index];
|
|
1319
|
+
if (item === undefined) {
|
|
1320
|
+
throw new Error(`Session entry contains undefined array item at ${path}[${index}]`);
|
|
1321
|
+
}
|
|
1322
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}[${index}]`);
|
|
1323
|
+
if (next !== item) changed = true;
|
|
1324
|
+
result[index] = next;
|
|
1325
|
+
}
|
|
1326
|
+
return changed ? result : value;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1330
|
+
if (prototype !== Object.prototype && prototype !== null) return value;
|
|
1331
|
+
|
|
1332
|
+
let changed = false;
|
|
1333
|
+
const entries: Array<readonly [string, unknown]> = [];
|
|
1334
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1335
|
+
if (item === undefined) {
|
|
1336
|
+
changed = true;
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
const next = stripUndefinedPlainObjectFields(item, `${path}.${key}`);
|
|
1340
|
+
if (next !== item) changed = true;
|
|
1341
|
+
entries.push([key, next]);
|
|
1342
|
+
}
|
|
1343
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function normalizeSessionEntryForStorage(entry: SessionEntry): SessionEntry {
|
|
1347
|
+
return stripUndefinedPlainObjectFields(entry) as SessionEntry;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1311
1350
|
const RESIDENT_EXTERNALIZE_STRING_EXCLUDED_KEYS = new Set([
|
|
1312
1351
|
"id",
|
|
1313
1352
|
"type",
|
|
@@ -2254,10 +2293,12 @@ class NdjsonFileWriter {
|
|
|
2254
2293
|
}
|
|
2255
2294
|
}
|
|
2256
2295
|
|
|
2296
|
+
const DEFAULT_WELCOME_RECENT_SESSION_LIMIT = 20;
|
|
2297
|
+
|
|
2257
2298
|
/** Get recent sessions for display in welcome screen */
|
|
2258
2299
|
export async function getRecentSessions(
|
|
2259
2300
|
sessionDir: string,
|
|
2260
|
-
limit =
|
|
2301
|
+
limit = DEFAULT_WELCOME_RECENT_SESSION_LIMIT,
|
|
2261
2302
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2262
2303
|
): Promise<RecentSessionInfo[]> {
|
|
2263
2304
|
const sessions = await getSortedSessions(sessionDir, storage);
|
|
@@ -3734,7 +3775,8 @@ export class SessionManager {
|
|
|
3734
3775
|
}
|
|
3735
3776
|
|
|
3736
3777
|
#appendEntry(entry: SessionEntry): void {
|
|
3737
|
-
const
|
|
3778
|
+
const normalizedEntry = normalizeSessionEntryForStorage(entry);
|
|
3779
|
+
const residentEntry = prepareEntryForResidentSync(normalizedEntry, this.#residentBlobStores()) as SessionEntry;
|
|
3738
3780
|
this.#fileEntries.push(residentEntry);
|
|
3739
3781
|
this.#byId.set(residentEntry.id, residentEntry);
|
|
3740
3782
|
this.#leafId = residentEntry.id;
|