jeopi-cli 16.4.1 → 16.4.3
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 +80 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +237 -50
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
|
@@ -1149,20 +1149,32 @@ export function resolveModelOverride(
|
|
|
1149
1149
|
modelPatterns: string[],
|
|
1150
1150
|
modelRegistry: ModelLookupRegistry,
|
|
1151
1151
|
settings?: Settings,
|
|
1152
|
-
): {
|
|
1152
|
+
): {
|
|
1153
|
+
model?: Model<Api>;
|
|
1154
|
+
thinkingLevel?: ConfiguredThinkingLevel;
|
|
1155
|
+
explicitThinkingLevel: boolean;
|
|
1156
|
+
warning?: string;
|
|
1157
|
+
} {
|
|
1153
1158
|
if (modelPatterns.length === 0) return { explicitThinkingLevel: false };
|
|
1154
1159
|
const availableModels = modelRegistry.getAvailable();
|
|
1155
1160
|
const matchPreferences = getModelMatchPreferences(settings);
|
|
1161
|
+
let warning: string | undefined;
|
|
1156
1162
|
for (const pattern of modelPatterns) {
|
|
1157
|
-
const {
|
|
1163
|
+
const {
|
|
1164
|
+
model,
|
|
1165
|
+
thinkingLevel,
|
|
1166
|
+
explicitThinkingLevel,
|
|
1167
|
+
warning: patternWarning,
|
|
1168
|
+
} = resolveModelRoleValue(pattern, availableModels, {
|
|
1158
1169
|
settings,
|
|
1159
1170
|
matchPreferences,
|
|
1160
1171
|
});
|
|
1161
1172
|
if (model) {
|
|
1162
|
-
return { model, thinkingLevel, explicitThinkingLevel };
|
|
1173
|
+
return { model, thinkingLevel, explicitThinkingLevel, warning: patternWarning };
|
|
1163
1174
|
}
|
|
1175
|
+
if (!warning && patternWarning) warning = patternWarning;
|
|
1164
1176
|
}
|
|
1165
|
-
return { explicitThinkingLevel: false };
|
|
1177
|
+
return { explicitThinkingLevel: false, warning };
|
|
1166
1178
|
}
|
|
1167
1179
|
|
|
1168
1180
|
/**
|
|
@@ -1176,6 +1188,10 @@ export function resolveModelOverride(
|
|
|
1176
1188
|
* `modelRoles.task` pointing at an unqualified id whose only available
|
|
1177
1189
|
* provider variant has no configured credentials — see #985).
|
|
1178
1190
|
*
|
|
1191
|
+
* `sessionId` is forwarded to `getApiKey` so that session-sticky OAuth
|
|
1192
|
+
* credentials resolve correctly during the pre-flight auth check. Without it,
|
|
1193
|
+
* providers with multiple OAuth accounts may return `undefined` even though
|
|
1194
|
+
* the credential is usable once the subagent session starts — see #5325.
|
|
1179
1195
|
* Keyless-by-design providers (llama.cpp, ollama, lm-studio) advertise the
|
|
1180
1196
|
* `kNoAuth` sentinel from `getApiKey` to signal that they do not require
|
|
1181
1197
|
* credentials. Those are treated as authenticated here so an explicitly
|
|
@@ -1191,18 +1207,20 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1191
1207
|
parentActiveModelPattern: string | undefined,
|
|
1192
1208
|
modelRegistry: ModelLookupRegistry & Pick<ModelRegistry, "getApiKey">,
|
|
1193
1209
|
settings?: Settings,
|
|
1210
|
+
sessionId?: string,
|
|
1194
1211
|
): Promise<{
|
|
1195
1212
|
model?: Model<Api>;
|
|
1196
1213
|
thinkingLevel?: ConfiguredThinkingLevel;
|
|
1197
1214
|
explicitThinkingLevel: boolean;
|
|
1198
1215
|
authFallbackUsed: boolean;
|
|
1216
|
+
warning?: string;
|
|
1199
1217
|
}> {
|
|
1200
1218
|
const primary = resolveModelOverride(modelPatterns, modelRegistry, settings);
|
|
1201
1219
|
if (!primary.model || !parentActiveModelPattern) {
|
|
1202
1220
|
return { ...primary, authFallbackUsed: false };
|
|
1203
1221
|
}
|
|
1204
1222
|
|
|
1205
|
-
const primaryKey = await modelRegistry.getApiKey(primary.model);
|
|
1223
|
+
const primaryKey = await modelRegistry.getApiKey(primary.model, sessionId);
|
|
1206
1224
|
if (primaryKey === kNoAuth || isAuthenticated(primaryKey)) {
|
|
1207
1225
|
return { ...primary, authFallbackUsed: false };
|
|
1208
1226
|
}
|
|
@@ -1214,7 +1232,7 @@ export async function resolveModelOverrideWithAuthFallback(
|
|
|
1214
1232
|
if (modelsAreEqual(fallback.model, primary.model)) {
|
|
1215
1233
|
return { ...primary, authFallbackUsed: false };
|
|
1216
1234
|
}
|
|
1217
|
-
const fallbackKey = await modelRegistry.getApiKey(fallback.model);
|
|
1235
|
+
const fallbackKey = await modelRegistry.getApiKey(fallback.model, sessionId);
|
|
1218
1236
|
if (!isAuthenticated(fallbackKey)) {
|
|
1219
1237
|
return { ...primary, authFallbackUsed: false };
|
|
1220
1238
|
}
|
|
@@ -1262,7 +1262,7 @@ export const SETTINGS_SCHEMA = {
|
|
|
1262
1262
|
textVerbosity: {
|
|
1263
1263
|
type: "enum",
|
|
1264
1264
|
values: ["low", "medium", "high"] as const,
|
|
1265
|
-
default: "
|
|
1265
|
+
default: "medium",
|
|
1266
1266
|
ui: {
|
|
1267
1267
|
tab: "model",
|
|
1268
1268
|
group: "Sampling",
|
|
@@ -1270,8 +1270,8 @@ export const SETTINGS_SCHEMA = {
|
|
|
1270
1270
|
description: "OpenAI Responses and Codex response verbosity (low, medium, or high)",
|
|
1271
1271
|
options: [
|
|
1272
1272
|
{ value: "low", label: "Low", description: "Prefer concise responses" },
|
|
1273
|
-
{ value: "medium", label: "Medium", description: "Balance brevity and detail" },
|
|
1274
|
-
{ value: "high", label: "High", description: "Prefer detailed responses
|
|
1273
|
+
{ value: "medium", label: "Medium", description: "Balance brevity and detail (default)" },
|
|
1274
|
+
{ value: "high", label: "High", description: "Prefer detailed responses" },
|
|
1275
1275
|
],
|
|
1276
1276
|
},
|
|
1277
1277
|
},
|
package/src/edit/index.ts
CHANGED
|
@@ -4,19 +4,13 @@ import { MismatchError as HashlineMismatchError } from "jeopi-hashline";
|
|
|
4
4
|
import hashlineGrammar from "jeopi-hashline/grammar.lark" with { type: "text" };
|
|
5
5
|
import hashlineDescription from "jeopi-hashline/prompt.md" with { type: "text" };
|
|
6
6
|
import { prompt } from "jeopi-utils";
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
type FileDiagnosticsResult,
|
|
10
|
-
flushLspWritethroughBatch,
|
|
11
|
-
type WritethroughCallback,
|
|
12
|
-
type WritethroughDeferredHandle,
|
|
13
|
-
writethroughNoop,
|
|
14
|
-
} from "../lsp";
|
|
7
|
+
import { createLspWritethrough, flushLspWritethroughBatch, type WritethroughCallback, writethroughNoop } from "../lsp";
|
|
8
|
+
import { DeferredDiagnostics } from "../lsp/deferred-diagnostics";
|
|
15
9
|
import { getDiagnosticsLedger } from "../lsp/diagnostics-ledger";
|
|
16
10
|
import applyPatchDescription from "../prompts/tools/apply-patch.md" with { type: "text" };
|
|
17
11
|
import patchDescription from "../prompts/tools/patch.md" with { type: "text" };
|
|
18
12
|
import replaceDescription from "../prompts/tools/replace.md" with { type: "text" };
|
|
19
|
-
import type {
|
|
13
|
+
import type { ToolSession } from "../tools";
|
|
20
14
|
import { truncateForPrompt } from "../tools/approval";
|
|
21
15
|
import { isInternalUrlPath } from "../tools/path-utils";
|
|
22
16
|
import { withPathWriteLock } from "../tools/path-write-lock";
|
|
@@ -385,12 +379,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
385
379
|
readonly #fuzzyThreshold: number;
|
|
386
380
|
readonly #writethrough: WritethroughCallback;
|
|
387
381
|
readonly #editMode?: EditMode;
|
|
388
|
-
readonly #
|
|
389
|
-
readonly #pendingDeferredFetches = new Map<string, AbortController>();
|
|
390
|
-
/** Fallback per-path mutation counter used only when the session does not expose
|
|
391
|
-
* a shared one. Prefer `session.bumpFileMutationVersion` so write (and any other
|
|
392
|
-
* tool) mutating the same file also invalidates pending late-diagnostics. */
|
|
393
|
-
readonly #editVersionByPath = new Map<string, number>();
|
|
382
|
+
readonly #deferredDiagnostics: DeferredDiagnostics;
|
|
394
383
|
|
|
395
384
|
constructor(private readonly session: ToolSession) {
|
|
396
385
|
const {
|
|
@@ -402,10 +391,11 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
402
391
|
this.#editMode = resolveConfiguredEditMode(envEditVariant);
|
|
403
392
|
this.#allowFuzzy = resolveAllowFuzzy(session, editFuzzy);
|
|
404
393
|
this.#fuzzyThreshold = resolveFuzzyThreshold(session, editFuzzyThreshold);
|
|
405
|
-
|
|
394
|
+
const deduplicateDiagnostics =
|
|
406
395
|
(session.enableLsp ?? true) &&
|
|
407
396
|
session.settings.get("lsp.diagnosticsOnEdit") &&
|
|
408
397
|
session.settings.get("lsp.diagnosticsDeduplicate");
|
|
398
|
+
this.#deferredDiagnostics = new DeferredDiagnostics(session, deduplicateDiagnostics);
|
|
409
399
|
this.#writethrough = withPathWriteLock(createEditWritethrough(session));
|
|
410
400
|
}
|
|
411
401
|
|
|
@@ -550,7 +540,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
550
540
|
// doubles as the documented full-file overwrite (patch.md <avoid>).
|
|
551
541
|
allowCreateOverwrite: true,
|
|
552
542
|
writethrough: tool.#writethrough,
|
|
553
|
-
beginDeferredDiagnosticsForPath: p => tool.#
|
|
543
|
+
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
554
544
|
}),
|
|
555
545
|
);
|
|
556
546
|
return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
@@ -589,7 +579,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
589
579
|
allowFuzzy: tool.#allowFuzzy,
|
|
590
580
|
fuzzyThreshold: tool.#fuzzyThreshold,
|
|
591
581
|
writethrough: tool.#writethrough,
|
|
592
|
-
beginDeferredDiagnosticsForPath: p => tool.#
|
|
582
|
+
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
593
583
|
}),
|
|
594
584
|
};
|
|
595
585
|
});
|
|
@@ -613,7 +603,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
613
603
|
signal,
|
|
614
604
|
batchRequest,
|
|
615
605
|
writethrough: tool.#writethrough,
|
|
616
|
-
beginDeferredDiagnosticsForPath: p => tool.#
|
|
606
|
+
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
617
607
|
});
|
|
618
608
|
},
|
|
619
609
|
},
|
|
@@ -639,7 +629,7 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
639
629
|
allowFuzzy: tool.#allowFuzzy,
|
|
640
630
|
fuzzyThreshold: tool.#fuzzyThreshold,
|
|
641
631
|
writethrough: tool.#writethrough,
|
|
642
|
-
beginDeferredDiagnosticsForPath: p => tool.#
|
|
632
|
+
beginDeferredDiagnosticsForPath: p => tool.#deferredDiagnostics.begin(p),
|
|
643
633
|
}),
|
|
644
634
|
);
|
|
645
635
|
return executeSinglePathEntries(path, runs, batchRequest, onUpdate, tool.session.cwd, signal);
|
|
@@ -647,60 +637,4 @@ export class EditTool implements AgentTool<TInput> {
|
|
|
647
637
|
},
|
|
648
638
|
}[this.mode];
|
|
649
639
|
}
|
|
650
|
-
|
|
651
|
-
#beginDeferredDiagnosticsForPath(path: string): WritethroughDeferredHandle {
|
|
652
|
-
const existingDeferred = this.#pendingDeferredFetches.get(path);
|
|
653
|
-
if (existingDeferred) {
|
|
654
|
-
existingDeferred.abort();
|
|
655
|
-
this.#pendingDeferredFetches.delete(path);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const deferredController = new AbortController();
|
|
659
|
-
const editVersion = this.#bumpFileVersion(path);
|
|
660
|
-
return {
|
|
661
|
-
onDeferredDiagnostics: (lateDiagnostics: FileDiagnosticsResult) => {
|
|
662
|
-
this.#pendingDeferredFetches.delete(path);
|
|
663
|
-
this.#injectLateDiagnostics(path, lateDiagnostics, editVersion);
|
|
664
|
-
},
|
|
665
|
-
signal: deferredController.signal,
|
|
666
|
-
finalize: (diagnostics: FileDiagnosticsResult | undefined) => {
|
|
667
|
-
if (!diagnostics) {
|
|
668
|
-
this.#pendingDeferredFetches.set(path, deferredController);
|
|
669
|
-
} else {
|
|
670
|
-
deferredController.abort();
|
|
671
|
-
}
|
|
672
|
-
},
|
|
673
|
-
};
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
#injectLateDiagnostics(path: string, diagnostics: FileDiagnosticsResult, editVersion: number): void {
|
|
677
|
-
const effective = this.#dedupDiagnostics
|
|
678
|
-
? getDiagnosticsLedger(this.session).reduce(path, diagnostics)
|
|
679
|
-
: diagnostics;
|
|
680
|
-
if (this.#dedupDiagnostics && effective.messages.length === 0) return;
|
|
681
|
-
|
|
682
|
-
const entry: DeferredDiagnosticsEntry = {
|
|
683
|
-
path,
|
|
684
|
-
summary: effective.summary ?? "",
|
|
685
|
-
messages: effective.messages ?? [],
|
|
686
|
-
errored: effective.errored,
|
|
687
|
-
// Drop at flush time if a later edit to the same file superseded this fetch.
|
|
688
|
-
isStale: () => this.#fileVersion(path) !== editVersion,
|
|
689
|
-
};
|
|
690
|
-
this.session.queueDeferredDiagnostics?.(entry);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
/** Bump the file's mutation counter (session-global when available). */
|
|
694
|
-
#bumpFileVersion(path: string): number {
|
|
695
|
-
if (this.session.bumpFileMutationVersion) return this.session.bumpFileMutationVersion(path);
|
|
696
|
-
const next = (this.#editVersionByPath.get(path) ?? 0) + 1;
|
|
697
|
-
this.#editVersionByPath.set(path, next);
|
|
698
|
-
return next;
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
/** Read the file's current mutation counter (session-global when available). */
|
|
702
|
-
#fileVersion(path: string): number {
|
|
703
|
-
if (this.session.getFileMutationVersion) return this.session.getFileMutationVersion(path);
|
|
704
|
-
return this.#editVersionByPath.get(path) ?? 0;
|
|
705
|
-
}
|
|
706
640
|
}
|
package/src/edit/renderer.ts
CHANGED
|
@@ -419,22 +419,25 @@ function formatStreamingDiff(
|
|
|
419
419
|
cache?: RenderedStringCache,
|
|
420
420
|
): string {
|
|
421
421
|
if (!diff) return "";
|
|
422
|
-
// Clamp the
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
422
|
+
// Clamp the tail to the viewport so a tall or fast-growing diff cannot
|
|
423
|
+
// outgrow the live window. Otherwise its mutating rows scroll above the
|
|
424
|
+
// native-scrollback commit boundary mid-stream and freeze into immutable
|
|
425
|
+
// history as a stale preview snapshot; the finalize repair then recommits
|
|
426
|
+
// the final render below it — a duplicated block on the tape. Collapsed
|
|
427
|
+
// gets a short fixed tail; expanded widens it to the viewport-sized window,
|
|
428
|
+
// never unbounded. The budget is VISUAL rows (a long wrapped line counts
|
|
427
429
|
// for more than one) at the framed block's inner width (border only —
|
|
428
|
-
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
|
|
429
|
-
// cheap raw-line wrap walk keeps the per-chunk cost bounded.
|
|
430
|
-
// are in the cache salt so a resize re-slices.
|
|
430
|
+
// contentPaddingLeft is 0); only the visible suffix is syntax-colored, so
|
|
431
|
+
// the cheap raw-line wrap walk keeps the per-chunk cost bounded.
|
|
432
|
+
// innerWidth/budget are in the cache salt so a resize re-slices.
|
|
431
433
|
const innerWidth = Math.max(1, width - 2);
|
|
432
|
-
const budget = expanded ?
|
|
434
|
+
const budget = expanded ? previewWindowRows() : Math.min(EDIT_STREAMING_PREVIEW_LINES, previewWindowRows());
|
|
433
435
|
let text = cachedRenderedString(cache, uiTheme, expanded, `${rawPath}:${innerWidth}:${budget}`, diff, () => {
|
|
434
436
|
// "Cursor" tail window: pin the last rows to the bottom so freshly streamed
|
|
435
437
|
// changes stay on screen. The whole-file diff is recomputed every chunk and
|
|
436
438
|
// its Myers alignment is not monotonic in payload length, so a hunk-aware
|
|
437
|
-
// window stutters as rows move between hunks. Expanded
|
|
439
|
+
// window stutters as rows move between hunks. Expanded widens the window
|
|
440
|
+
// to the viewport; the full diff appears once the result finalizes.
|
|
438
441
|
const allLines = diff.replace(/\n+$/u, "").split("\n");
|
|
439
442
|
let visualUsed = 0;
|
|
440
443
|
let cut = allLines.length;
|
|
@@ -603,21 +603,22 @@ describe("agent() through eval runtimes", () => {
|
|
|
603
603
|
});
|
|
604
604
|
const { session, sessionFile, sessionId } = makeEvalSession(tempDir, "py-agent-interrupt", settings);
|
|
605
605
|
mockAgents();
|
|
606
|
-
//
|
|
607
|
-
//
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
// unwind via KeyboardInterrupt instead of being hard-killed (which used to
|
|
611
|
-
// surface "[kernel] Python kernel shutdown" and lose all session state).
|
|
606
|
+
// Each kernel worker thread blocks in a synchronous `urllib` bridge call,
|
|
607
|
+
// joined by `parallel()`'s ThreadPoolExecutor exit. The host must keep
|
|
608
|
+
// those already-started calls attached until they settle, then interrupt
|
|
609
|
+
// the kernel before `parallel()` launches another wave.
|
|
612
610
|
let inFlight = 0;
|
|
611
|
+
let completed = 0;
|
|
613
612
|
let markSaturated: (() => void) | undefined;
|
|
614
613
|
const saturated = new Promise<void>(resolve => {
|
|
615
614
|
markSaturated = resolve;
|
|
616
615
|
});
|
|
617
|
-
|
|
616
|
+
const releaseAgents = Promise.withResolvers<void>();
|
|
617
|
+
const runSpy = vi.spyOn(taskExecutor, "runSubprocess").mockImplementation(async options => {
|
|
618
618
|
// task.maxConcurrency=6 → six bridge calls block at once; signal then.
|
|
619
619
|
if (++inFlight >= 6) markSaturated?.();
|
|
620
|
-
await
|
|
620
|
+
await releaseAgents.promise;
|
|
621
|
+
completed++;
|
|
621
622
|
return singleResult(options, { output: options.assignment ?? "" });
|
|
622
623
|
});
|
|
623
624
|
|
|
@@ -640,8 +641,7 @@ describe("agent() through eval runtimes", () => {
|
|
|
640
641
|
// bridge calls (condition-driven) instead of waiting a fixed wall second.
|
|
641
642
|
void saturated.then(() => ac.abort(new Error("external interrupt")));
|
|
642
643
|
|
|
643
|
-
const
|
|
644
|
-
const result = await executePython(
|
|
644
|
+
const resultPromise = executePython(
|
|
645
645
|
"import json\nprint(json.dumps(parallel([lambda n=n: agent(str(n)) for n in range(12)])))",
|
|
646
646
|
{
|
|
647
647
|
cwd: tempDir.path(),
|
|
@@ -653,13 +653,18 @@ describe("agent() through eval runtimes", () => {
|
|
|
653
653
|
signal: ac.signal,
|
|
654
654
|
},
|
|
655
655
|
);
|
|
656
|
-
|
|
656
|
+
await saturated;
|
|
657
|
+
await Promise.resolve();
|
|
658
|
+
expect(completed).toBe(0);
|
|
659
|
+
releaseAgents.resolve();
|
|
660
|
+
const result = await resultPromise;
|
|
657
661
|
|
|
658
|
-
// Cancelled, but cleanly: no hard-kill,
|
|
659
|
-
//
|
|
662
|
+
// Cancelled, but cleanly: no hard-kill, no orphaned bridge calls, and no
|
|
663
|
+
// second fan-out wave started after the deferred abort was delivered.
|
|
660
664
|
expect(result.cancelled).toBe(true);
|
|
661
665
|
expect(result.output).not.toContain("Python kernel shutdown");
|
|
662
|
-
expect(
|
|
666
|
+
expect(completed).toBe(6);
|
|
667
|
+
expect(runSpy).toHaveBeenCalledTimes(6);
|
|
663
668
|
|
|
664
669
|
// The persistent kernel survived the interrupt: prior state is intact.
|
|
665
670
|
const after = await executePython("print(PREP_MARKER)", {
|
|
@@ -5,7 +5,9 @@ import {
|
|
|
5
5
|
isEvalTimeoutControlEvent,
|
|
6
6
|
withBridgeTimeoutPause,
|
|
7
7
|
} from "../bridge-timeout";
|
|
8
|
+
import { executeWithKernelBase, type GenericKernel } from "../executor-base";
|
|
8
9
|
import type { JsStatusEvent } from "../js/shared/types";
|
|
10
|
+
import type { KernelDisplayOutput } from "../py/display";
|
|
9
11
|
|
|
10
12
|
describe("withBridgeTimeoutPause", () => {
|
|
11
13
|
it("emits one pause before the operation and one resume after it settles", async () => {
|
|
@@ -62,3 +64,61 @@ describe("withBridgeTimeoutPause", () => {
|
|
|
62
64
|
expect(isEvalTimeoutControlEvent({ op: "agent", id: "subagent-1" })).toBe(false);
|
|
63
65
|
});
|
|
64
66
|
});
|
|
67
|
+
|
|
68
|
+
class TestCancelledError extends Error {
|
|
69
|
+
readonly timedOut: boolean;
|
|
70
|
+
|
|
71
|
+
constructor(timedOut: boolean) {
|
|
72
|
+
super(timedOut ? "timed out" : "cancelled");
|
|
73
|
+
this.name = "TestCancelledError";
|
|
74
|
+
this.timedOut = timedOut;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
it("defers external aborts until an in-flight bridge call resumes", async () => {
|
|
79
|
+
const abortController = new AbortController();
|
|
80
|
+
const release = Promise.withResolvers<void>();
|
|
81
|
+
let completed = false;
|
|
82
|
+
let kernelSignal: AbortSignal | undefined;
|
|
83
|
+
const kernel: GenericKernel<Record<string, string | null>> = {
|
|
84
|
+
async execute(_code, options) {
|
|
85
|
+
kernelSignal = options.signal;
|
|
86
|
+
options.onDisplay({
|
|
87
|
+
type: "status",
|
|
88
|
+
event: { op: EVAL_TIMEOUT_PAUSE_OP },
|
|
89
|
+
} satisfies KernelDisplayOutput);
|
|
90
|
+
|
|
91
|
+
abortController.abort(new Error("external interrupt"));
|
|
92
|
+
expect(kernelSignal?.aborted).toBe(false);
|
|
93
|
+
|
|
94
|
+
await release.promise;
|
|
95
|
+
completed = true;
|
|
96
|
+
options.onDisplay({
|
|
97
|
+
type: "status",
|
|
98
|
+
event: { op: EVAL_TIMEOUT_RESUME_OP },
|
|
99
|
+
} satisfies KernelDisplayOutput);
|
|
100
|
+
return { status: "ok", cancelled: false, timedOut: false };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const resultPromise = executeWithKernelBase({
|
|
105
|
+
kernel,
|
|
106
|
+
code: "agent('slow')",
|
|
107
|
+
options: { signal: abortController.signal },
|
|
108
|
+
runIdPrefix: "test",
|
|
109
|
+
errorLogLabel: "test",
|
|
110
|
+
cancelledErrorClass: TestCancelledError,
|
|
111
|
+
buildKernelEnvPatch: () => ({}),
|
|
112
|
+
formatKernelTimeoutAnnotation: () => "kernel timed out",
|
|
113
|
+
formatTimeoutAnnotation: () => "timed out",
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await Promise.resolve();
|
|
117
|
+
expect(completed).toBe(false);
|
|
118
|
+
|
|
119
|
+
release.resolve();
|
|
120
|
+
const result = await resultPromise;
|
|
121
|
+
expect(completed).toBe(true);
|
|
122
|
+
expect(result.cancelled).toBe(true);
|
|
123
|
+
expect(result.exitCode).toBeUndefined();
|
|
124
|
+
});
|
|
@@ -3,7 +3,7 @@ import { Settings } from "../config/settings";
|
|
|
3
3
|
import { OutputSink } from "../session/streaming-output";
|
|
4
4
|
import type { ToolSession } from "../tools";
|
|
5
5
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
6
|
-
import { isEvalTimeoutControlEvent } from "./bridge-timeout";
|
|
6
|
+
import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP, isEvalTimeoutControlEvent } from "./bridge-timeout";
|
|
7
7
|
import type { JsStatusEvent } from "./js/shared/types";
|
|
8
8
|
import type { KernelDisplayOutput } from "./py/display";
|
|
9
9
|
import { registerPyToolBridge } from "./py/tool-bridge";
|
|
@@ -158,6 +158,76 @@ export async function waitForPromiseWithCancellation<T>(
|
|
|
158
158
|
return await resultPromise;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
interface BridgeAbortShield {
|
|
162
|
+
signal: AbortSignal | undefined;
|
|
163
|
+
abortRequested: boolean;
|
|
164
|
+
timedOut: boolean;
|
|
165
|
+
handleStatus?: (event: JsStatusEvent) => void;
|
|
166
|
+
dispose?: () => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createBridgeAbortShield(source: AbortSignal | undefined): BridgeAbortShield {
|
|
170
|
+
const shield: BridgeAbortShield = {
|
|
171
|
+
signal: undefined,
|
|
172
|
+
abortRequested: false,
|
|
173
|
+
timedOut: false,
|
|
174
|
+
};
|
|
175
|
+
if (!source) return shield;
|
|
176
|
+
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
let pauseDepth = 0;
|
|
179
|
+
let abortReason: unknown;
|
|
180
|
+
let removeAbortListener: (() => void) | undefined;
|
|
181
|
+
|
|
182
|
+
const requestAbort = (reason: unknown): void => {
|
|
183
|
+
shield.abortRequested = true;
|
|
184
|
+
shield.timedOut =
|
|
185
|
+
shield.timedOut ||
|
|
186
|
+
(typeof DOMException !== "undefined" && reason instanceof DOMException
|
|
187
|
+
? reason.name === "TimeoutError"
|
|
188
|
+
: reason instanceof Error && reason.name === "TimeoutError");
|
|
189
|
+
abortReason = reason;
|
|
190
|
+
if (pauseDepth > 0 || controller.signal.aborted) return;
|
|
191
|
+
controller.abort(reason);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onAbort = (): void => {
|
|
195
|
+
const reason = source.reason;
|
|
196
|
+
requestAbort(reason);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
shield.signal = controller.signal;
|
|
200
|
+
shield.handleStatus = (event: JsStatusEvent): void => {
|
|
201
|
+
if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
|
|
202
|
+
pauseDepth++;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (event.op !== EVAL_TIMEOUT_RESUME_OP || pauseDepth === 0) return;
|
|
206
|
+
pauseDepth--;
|
|
207
|
+
if (shield.abortRequested && !controller.signal.aborted) controller.abort(abortReason);
|
|
208
|
+
};
|
|
209
|
+
shield.dispose = (): void => {
|
|
210
|
+
removeAbortListener?.();
|
|
211
|
+
removeAbortListener = undefined;
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
if (source.aborted) {
|
|
215
|
+
requestAbort(source.reason);
|
|
216
|
+
} else {
|
|
217
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
218
|
+
removeAbortListener = () => {
|
|
219
|
+
source.removeEventListener("abort", onAbort);
|
|
220
|
+
};
|
|
221
|
+
if (source.aborted) {
|
|
222
|
+
source.removeEventListener("abort", onAbort);
|
|
223
|
+
removeAbortListener = undefined;
|
|
224
|
+
requestAbort(source.reason);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return shield;
|
|
229
|
+
}
|
|
230
|
+
|
|
161
231
|
export function createCancelledKernelResult(output: string): KernelExecutionResult {
|
|
162
232
|
const outputBytes = Buffer.byteLength(output, "utf-8");
|
|
163
233
|
const outputLines = output.length > 0 ? 1 : 0;
|
|
@@ -304,9 +374,11 @@ export async function executeWithKernelBase<
|
|
|
304
374
|
const displayOutputs: KernelDisplayOutput[] = [];
|
|
305
375
|
const deadlineMs = (resolveDeadlineMs ?? getExecutionDeadlineMs)(options);
|
|
306
376
|
let executionTimeoutMs: number | undefined;
|
|
377
|
+
const abortShield = createBridgeAbortShield(options?.signal);
|
|
307
378
|
|
|
308
379
|
const collectDisplay = (output: KernelDisplayOutput): void => {
|
|
309
380
|
if (output.type === "status") {
|
|
381
|
+
abortShield.handleStatus?.(output.event);
|
|
310
382
|
options?.onStatus?.(output.event);
|
|
311
383
|
if (!isJulia && isEvalTimeoutControlEvent(output.event)) return;
|
|
312
384
|
}
|
|
@@ -320,8 +392,11 @@ export async function executeWithKernelBase<
|
|
|
320
392
|
options?.toolSession && options?.bridgeSessionId
|
|
321
393
|
? registerPyToolBridge(options.bridgeSessionId, runId, {
|
|
322
394
|
toolSession: options.toolSession,
|
|
323
|
-
signal:
|
|
395
|
+
signal: abortShield.signal,
|
|
324
396
|
emitStatus,
|
|
397
|
+
abortRequested: () => {
|
|
398
|
+
return abortShield.abortRequested;
|
|
399
|
+
},
|
|
325
400
|
})
|
|
326
401
|
: null;
|
|
327
402
|
|
|
@@ -338,14 +413,15 @@ export async function executeWithKernelBase<
|
|
|
338
413
|
cwd: options?.cwd,
|
|
339
414
|
env: buildKernelEnvPatch(options ?? ({} as TOptions)),
|
|
340
415
|
id: runId,
|
|
341
|
-
signal:
|
|
416
|
+
signal: abortShield.signal,
|
|
342
417
|
timeoutMs: executionTimeoutMs,
|
|
343
418
|
onChunk: text => sink.push(text),
|
|
344
419
|
onDisplay: output => collectDisplay(output),
|
|
345
420
|
});
|
|
346
421
|
|
|
347
|
-
if (result.cancelled) {
|
|
348
|
-
const
|
|
422
|
+
if (result.cancelled || abortShield.abortRequested) {
|
|
423
|
+
const timedOut = result.timedOut || abortShield.timedOut;
|
|
424
|
+
const annotation = timedOut
|
|
349
425
|
? formatKernelTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, result.kernelKilled ?? false)
|
|
350
426
|
: undefined;
|
|
351
427
|
const dumped = await sink.dump(annotation);
|
|
@@ -397,8 +473,8 @@ export async function executeWithKernelBase<
|
|
|
397
473
|
stdinRequested: false,
|
|
398
474
|
};
|
|
399
475
|
} catch (err) {
|
|
400
|
-
if (isCancellationError(err, cancelledErrorClass) ||
|
|
401
|
-
const timedOut = isTimedOutCancellation(err, cancelledErrorClass,
|
|
476
|
+
if (isCancellationError(err, cancelledErrorClass) || abortShield.abortRequested || abortShield.signal?.aborted) {
|
|
477
|
+
const timedOut = abortShield.timedOut || isTimedOutCancellation(err, cancelledErrorClass, abortShield.signal);
|
|
402
478
|
const dumped = await sink.dump(
|
|
403
479
|
timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs) : undefined,
|
|
404
480
|
);
|
|
@@ -421,5 +497,6 @@ export async function executeWithKernelBase<
|
|
|
421
497
|
throw error;
|
|
422
498
|
} finally {
|
|
423
499
|
unregisterBridge?.();
|
|
500
|
+
abortShield.dispose?.();
|
|
424
501
|
}
|
|
425
502
|
}
|
|
@@ -48,6 +48,15 @@ type BabelClassDeclaration = {
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
type BabelLexicalDecl = BabelVariableDeclaration | BabelClassDeclaration;
|
|
51
|
+
type BabelFunctionDeclaration = {
|
|
52
|
+
type: "FunctionDeclaration";
|
|
53
|
+
start: number;
|
|
54
|
+
end: number;
|
|
55
|
+
id: { start: number; end: number; name: string } | null;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Top-level declarations whose bindings must survive the cell (demoted and/or published). */
|
|
59
|
+
type BabelPublishableDecl = BabelLexicalDecl | BabelFunctionDeclaration;
|
|
51
60
|
|
|
52
61
|
type BabelExpressionStatement = {
|
|
53
62
|
type: "ExpressionStatement";
|
|
@@ -322,7 +331,7 @@ function collectBindingNames(pattern: unknown, names: string[]): void {
|
|
|
322
331
|
}
|
|
323
332
|
}
|
|
324
333
|
|
|
325
|
-
function getLexicalBindingNames(node:
|
|
334
|
+
function getLexicalBindingNames(node: BabelPublishableDecl): string[] {
|
|
326
335
|
const names: string[] = [];
|
|
327
336
|
if (node.type === "VariableDeclaration") {
|
|
328
337
|
for (const declaration of node.declarations ?? []) collectBindingNames(declaration.id, names);
|
|
@@ -348,40 +357,49 @@ function appendGlobalBindingPublish(source: string, names: readonly string[]): s
|
|
|
348
357
|
* let { a, b } = obj; -> var { a, b } = obj;
|
|
349
358
|
* class Foo extends Bar {} -> var Foo = class extends Bar {};
|
|
350
359
|
*
|
|
351
|
-
* When the source must run inside the async wrapper, demoted `var`s
|
|
352
|
-
*
|
|
353
|
-
*
|
|
360
|
+
* When the source must run inside the async wrapper (top-level `await`), demoted `var`s —
|
|
361
|
+
* and the user's own top-level `var` and `function` declarations — would be scoped to the
|
|
362
|
+
* wrapper function and die with the cell. In that mode we publish every top-level binding
|
|
363
|
+
* back to the wrapper's lexical `this`, which is the worker global object.
|
|
354
364
|
*
|
|
355
|
-
* Nested declarations (inside functions, blocks, classes) are left alone
|
|
365
|
+
* Nested declarations (inside functions, blocks, classes) are left alone — they're
|
|
356
366
|
* scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
|
|
357
367
|
*/
|
|
358
368
|
async function demoteTopLevelLexicals(code: string, options: { publishGlobals?: boolean } = {}): Promise<string> {
|
|
359
|
-
|
|
369
|
+
const publishGlobals = options.publishGlobals === true;
|
|
370
|
+
const fastPath = publishGlobals ? /\b(?:const|let|class|var|function)\b/ : /\b(?:const|let|class)\b/;
|
|
371
|
+
if (!fastPath.test(code)) return code;
|
|
360
372
|
|
|
361
373
|
const ast = await parseProgram(code);
|
|
362
374
|
if (!ast) {
|
|
363
375
|
return code;
|
|
364
376
|
}
|
|
365
377
|
|
|
366
|
-
const targets:
|
|
378
|
+
const targets: Array<{ node: BabelPublishableDecl; demote: boolean }> = [];
|
|
367
379
|
for (const node of ast.program.body) {
|
|
368
380
|
if (node.type === "VariableDeclaration") {
|
|
369
381
|
const decl = node as unknown as BabelVariableDeclaration;
|
|
370
|
-
if (decl.kind === "const" || decl.kind === "let") targets.push(decl);
|
|
382
|
+
if (decl.kind === "const" || decl.kind === "let") targets.push({ node: decl, demote: true });
|
|
383
|
+
else if (publishGlobals) targets.push({ node: decl, demote: false });
|
|
371
384
|
} else if (node.type === "ClassDeclaration") {
|
|
372
385
|
const decl = node as unknown as BabelClassDeclaration;
|
|
373
|
-
if (decl.id) targets.push(decl);
|
|
386
|
+
if (decl.id) targets.push({ node: decl, demote: true });
|
|
387
|
+
} else if (publishGlobals && node.type === "FunctionDeclaration") {
|
|
388
|
+
const decl = node as unknown as BabelFunctionDeclaration;
|
|
389
|
+
if (decl.id) targets.push({ node: decl, demote: false });
|
|
374
390
|
}
|
|
375
391
|
}
|
|
376
392
|
if (targets.length === 0) return code;
|
|
377
393
|
|
|
378
|
-
targets.sort((a, b) => b.start - a.start);
|
|
394
|
+
targets.sort((a, b) => b.node.start - a.node.start);
|
|
379
395
|
let result = code;
|
|
380
|
-
for (const node of targets) {
|
|
396
|
+
for (const { node, demote } of targets) {
|
|
381
397
|
const segment = result.slice(node.start, node.end);
|
|
382
|
-
const bindingNames =
|
|
398
|
+
const bindingNames = publishGlobals ? getLexicalBindingNames(node) : [];
|
|
383
399
|
let replacement: string;
|
|
384
|
-
if (
|
|
400
|
+
if (!demote) {
|
|
401
|
+
replacement = segment;
|
|
402
|
+
} else if (node.type === "VariableDeclaration") {
|
|
385
403
|
replacement = `var${segment.slice(node.kind.length)}`;
|
|
386
404
|
} else {
|
|
387
405
|
const id = node.id;
|