@ordewell/core 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{IFileSystem-DCd-rkeH.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
- package/dist/{IFileSystem-B8Nkgg4e.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
- package/dist/{IRunner-CXdYyIZo.d.mts → IRunner-Crkt3tHD.d.ts} +64 -3
- package/dist/{IRunner-QBdYcatb.d.ts → IRunner-lMQIa5Wc.d.mts} +64 -3
- package/dist/{ModeResolver-geqjKkLY.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
- package/dist/{ModeResolver-BOU90Nn-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
- package/dist/{Task-CRVIqSfy.d.mts → Task-D4QHjdGv.d.mts} +93 -11
- package/dist/{Task-CRVIqSfy.d.ts → Task-D4QHjdGv.d.ts} +93 -11
- package/dist/{chunk-A57ITQE7.mjs → chunk-4QLFM73A.mjs} +34 -5
- package/dist/chunk-4QLFM73A.mjs.map +1 -0
- package/dist/{chunk-FH6M5QZG.mjs → chunk-6LEA6WAM.mjs} +88 -7
- package/dist/chunk-6LEA6WAM.mjs.map +1 -0
- package/dist/{chunk-645FALZX.mjs → chunk-F3KIX7WR.mjs} +3 -3
- package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
- package/dist/chunk-NJGID6BG.mjs.map +1 -0
- package/dist/{chunk-SM6BV6XS.mjs → chunk-RT4NQ2BZ.mjs} +3 -1
- package/dist/chunk-RT4NQ2BZ.mjs.map +1 -0
- package/dist/index.d.mts +307 -66
- package/dist/index.d.ts +307 -66
- package/dist/index.js +1688 -402
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1567 -388
- package/dist/index.mjs.map +1 -1
- package/dist/parsing.d.mts +2 -2
- package/dist/parsing.d.ts +2 -2
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/{plan-utils-oC8bO4pN.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
- package/dist/{plan-utils-KU1Rse82.d.mts → plan-utils-wI8MuDBC.d.mts} +204 -19
- package/dist/plan-utils.d.mts +4 -4
- package/dist/plan-utils.d.ts +4 -4
- package/dist/plan-utils.js +94 -5
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +13 -3
- package/dist/testing.d.mts +6 -3
- package/dist/testing.d.ts +6 -3
- package/dist/testing.js +7 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +7 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-A57ITQE7.mjs.map +0 -1
- package/dist/chunk-FH6M5QZG.mjs.map +0 -1
- package/dist/chunk-OYUSADFM.mjs.map +0 -1
- package/dist/chunk-SM6BV6XS.mjs.map +0 -1
- /package/dist/{chunk-645FALZX.mjs.map → chunk-F3KIX7WR.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -114,6 +114,7 @@ __export(index_exports, {
|
|
|
114
114
|
PlannerNotReadyError: () => PlannerNotReadyError,
|
|
115
115
|
PlannerTurnDiscardedError: () => PlannerTurnDiscardedError,
|
|
116
116
|
PlannerTurnStoppedError: () => PlannerTurnStoppedError,
|
|
117
|
+
PlannerUsageLimitError: () => PlannerUsageLimitError,
|
|
117
118
|
REFUSED_COMMANDS: () => REFUSED_COMMANDS,
|
|
118
119
|
RUN_SETTINGS_DEFAULTS: () => RUN_SETTINGS_DEFAULTS,
|
|
119
120
|
RunSettingsError: () => RunSettingsError,
|
|
@@ -170,6 +171,8 @@ __export(index_exports, {
|
|
|
170
171
|
approvalScopes: () => approvalScopes,
|
|
171
172
|
assertWorkspaceExists: () => assertWorkspaceExists,
|
|
172
173
|
assertWorkspaceIsProject: () => assertWorkspaceIsProject,
|
|
174
|
+
assignmentIssue: () => assignmentIssue,
|
|
175
|
+
assignmentIssueText: () => assignmentIssueText,
|
|
173
176
|
augmentedPath: () => augmentedPath,
|
|
174
177
|
automergeSummary: () => automergeSummary,
|
|
175
178
|
autonomyLevelLabel: () => autonomyLevelLabel,
|
|
@@ -186,6 +189,7 @@ __export(index_exports, {
|
|
|
186
189
|
canSetDependencies: () => canSetDependencies,
|
|
187
190
|
canSplitTask: () => canSplitTask,
|
|
188
191
|
capConflictFiles: () => capConflictFiles,
|
|
192
|
+
checkoutRule: () => checkoutRule,
|
|
189
193
|
classifyCommand: () => classifyCommand,
|
|
190
194
|
classifyOutcome: () => classifyOutcome,
|
|
191
195
|
clearAugmentedPathCache: () => clearAugmentedPathCache,
|
|
@@ -199,6 +203,7 @@ __export(index_exports, {
|
|
|
199
203
|
commandAllowedBy: () => commandAllowedBy,
|
|
200
204
|
configuredProviders: () => configuredProviders,
|
|
201
205
|
continuability: () => continuability,
|
|
206
|
+
countedSessionCost: () => countedSessionCost,
|
|
202
207
|
createAiService: () => createAiService,
|
|
203
208
|
createEmptyPlan: () => createEmptyPlan,
|
|
204
209
|
createSession: () => createSession,
|
|
@@ -262,6 +267,7 @@ __export(index_exports, {
|
|
|
262
267
|
isMeasured: () => isMeasured,
|
|
263
268
|
isOpenAiProvider: () => isOpenAiProvider,
|
|
264
269
|
isPlannerBackend: () => isPlannerBackend,
|
|
270
|
+
isRepoGroup: () => isRepoGroup,
|
|
265
271
|
isRunnerApproval: () => isRunnerApproval,
|
|
266
272
|
isSupervisorLevel: () => isSupervisorLevel,
|
|
267
273
|
isTaskRunning: () => isTaskRunning,
|
|
@@ -284,6 +290,7 @@ __export(index_exports, {
|
|
|
284
290
|
markdownHtml: () => markdownHtml,
|
|
285
291
|
markdownTokens: () => markdownTokens,
|
|
286
292
|
mcpClientConfig: () => mcpClientConfig,
|
|
293
|
+
mergeTarget: () => mergeTarget,
|
|
287
294
|
migrateOldConfigDir: () => migrateOldConfigDir,
|
|
288
295
|
migratePlanIsolation: () => migratePlanIsolation,
|
|
289
296
|
migratePlanState: () => migratePlanState,
|
|
@@ -333,6 +340,7 @@ __export(index_exports, {
|
|
|
333
340
|
removedPluginNotice: () => removedPluginNotice,
|
|
334
341
|
renderTaskQueryAnswer: () => renderTaskQueryAnswer,
|
|
335
342
|
replayTaskLog: () => replayTaskLog,
|
|
343
|
+
reposWithWork: () => reposWithWork,
|
|
336
344
|
researchShellWarning: () => researchShellWarning,
|
|
337
345
|
researchToolsPath: () => researchToolsPath,
|
|
338
346
|
resetInstalledPlanner: () => resetInstalledPlanner,
|
|
@@ -346,6 +354,7 @@ __export(index_exports, {
|
|
|
346
354
|
resolveWithin: () => resolveWithin,
|
|
347
355
|
resumeLabel: () => resumeLabel,
|
|
348
356
|
resumeTime: () => resumeTime,
|
|
357
|
+
resumedSessionWindow: () => resumedSessionWindow,
|
|
349
358
|
runSettingsOf: () => runSettingsOf,
|
|
350
359
|
runnerForProvider: () => runnerForProvider,
|
|
351
360
|
runnerModesFrom: () => runnerModesFrom,
|
|
@@ -386,6 +395,7 @@ __export(index_exports, {
|
|
|
386
395
|
taskRowView: () => taskRowView,
|
|
387
396
|
taskStartedNotice: () => taskStartedNotice,
|
|
388
397
|
taskStatusKind: () => taskStatusKind,
|
|
398
|
+
taskUsageSummary: () => taskUsageSummary,
|
|
389
399
|
textHasTaskOps: () => textHasTaskOps,
|
|
390
400
|
textHasTaskQuery: () => textHasTaskQuery,
|
|
391
401
|
titledRefs: () => titledRefs,
|
|
@@ -488,6 +498,7 @@ function migrateLoadedTasks(tasks) {
|
|
|
488
498
|
for (const task of tasks) {
|
|
489
499
|
if (typeof task.runnerSessionId !== "string") delete task.runnerSessionId;
|
|
490
500
|
if (typeof task.carriedFrom !== "string") delete task.carriedFrom;
|
|
501
|
+
if (typeof task.runnerSessionOwner !== "string") delete task.runnerSessionOwner;
|
|
491
502
|
const legacy = task;
|
|
492
503
|
if (!task.runnerSessionId && legacy.transport?.kind === "structured" && typeof legacy.transport.nativeSessionId === "string") {
|
|
493
504
|
task.runnerSessionId = legacy.transport.nativeSessionId;
|
|
@@ -747,6 +758,7 @@ function keepExecutionState(current, rewrite) {
|
|
|
747
758
|
verdict: prior?.verdict,
|
|
748
759
|
outputSummary: prior?.outputSummary,
|
|
749
760
|
runnerSessionId: prior?.runnerSessionId,
|
|
761
|
+
runnerSessionOwner: prior?.runnerSessionOwner,
|
|
750
762
|
carriedFrom: prior?.carriedFrom,
|
|
751
763
|
attemptSkills: prior?.attemptSkills,
|
|
752
764
|
attemptUsage: prior?.attemptUsage,
|
|
@@ -835,8 +847,21 @@ function addAttemptUsage(usage, record, now) {
|
|
|
835
847
|
const folded = addPlannerUsage(usage, record);
|
|
836
848
|
return { ...usage, ...folded, lastPromptTokens: folded.lastPromptTokens, contextWindow: folded.contextWindow, wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)) };
|
|
837
849
|
}
|
|
838
|
-
function endAttemptUsage(usage, now) {
|
|
839
|
-
return {
|
|
850
|
+
function endAttemptUsage(usage, now, runnerSessionCost, sessionId) {
|
|
851
|
+
return {
|
|
852
|
+
...usage,
|
|
853
|
+
endedAt: now,
|
|
854
|
+
wallTimeMs: Math.max(0, Date.parse(now) - Date.parse(usage.startedAt)),
|
|
855
|
+
...sessionId ? { sessionId } : {},
|
|
856
|
+
...runnerSessionCost ? { runnerSessionCost } : {}
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function countedSessionCost(attempts, sessionId) {
|
|
860
|
+
return [...attempts ?? []].reverse().find((usage) => usage.runnerSessionCost?.sessionId === sessionId)?.runnerSessionCost;
|
|
861
|
+
}
|
|
862
|
+
function resumedSessionWindow(attempts, runner, attempt, sessionId) {
|
|
863
|
+
const last = [...attempts ?? []].reverse().find((usage) => usage.attempt !== attempt && usage.runner === runner && usage.sessionId === sessionId && usage.contextWindow !== void 0);
|
|
864
|
+
return last ? { source: last.source, model: last.model, contextWindow: last.contextWindow } : void 0;
|
|
840
865
|
}
|
|
841
866
|
function addUsageTotals(totals, addition) {
|
|
842
867
|
let next = addUsage(totals, { source: "", inputTokens: addition.inputTokens, outputTokens: addition.outputTokens, cachedInputTokens: addition.cachedInputTokens });
|
|
@@ -865,6 +890,16 @@ function planUsage(tasks) {
|
|
|
865
890
|
}
|
|
866
891
|
return result;
|
|
867
892
|
}
|
|
893
|
+
function taskUsageSummary(task) {
|
|
894
|
+
const attempts = task.attemptUsage ?? [];
|
|
895
|
+
const latest = attempts.at(-1);
|
|
896
|
+
const contextFill = latest ? plannerContextFill(latest) : void 0;
|
|
897
|
+
return {
|
|
898
|
+
totals: attempts.reduce((sum, attempt) => addUsageTotals(sum, attempt.totals), {}),
|
|
899
|
+
attempts: attempts.length,
|
|
900
|
+
...contextFill ? { contextFill } : {}
|
|
901
|
+
};
|
|
902
|
+
}
|
|
868
903
|
|
|
869
904
|
// src/models/TaskLog.ts
|
|
870
905
|
var HEAD_LINES = 60;
|
|
@@ -966,6 +1001,9 @@ function toTaskLogEvent(event) {
|
|
|
966
1001
|
};
|
|
967
1002
|
case "command_output":
|
|
968
1003
|
return { type: "command_output", text: event.text };
|
|
1004
|
+
// Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
|
|
1005
|
+
case "model_switched":
|
|
1006
|
+
case "model_drifted":
|
|
969
1007
|
case "background_agent":
|
|
970
1008
|
return null;
|
|
971
1009
|
}
|
|
@@ -5417,8 +5455,8 @@ function forms(text3) {
|
|
|
5417
5455
|
function sensitiveEnvValues(env) {
|
|
5418
5456
|
return Object.entries(env).filter(([name, value]) => SENSITIVE_ENV_NAME.test(name) && !/_(?:URL|URI|PATH|FILE|DIR)$/i.test(name) && value !== void 0).map(([, value]) => (value ?? "").trim());
|
|
5419
5457
|
}
|
|
5420
|
-
function carriesSecret(texts,
|
|
5421
|
-
const secrets = [...
|
|
5458
|
+
function carriesSecret(texts, held2) {
|
|
5459
|
+
const secrets = [...held2].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
|
|
5422
5460
|
return texts.flatMap(forms).some((text3) => secrets.some((secret) => text3.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text3)) || [...text3.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
|
|
5423
5461
|
}
|
|
5424
5462
|
|
|
@@ -7239,6 +7277,12 @@ function commandOutput(view, { text: text3 }) {
|
|
|
7239
7277
|
return { ...view, blocks: replaceAt(view.blocks, i, { ...block, output: block.output ? `${block.output}
|
|
7240
7278
|
${text3}` : text3 }) };
|
|
7241
7279
|
}
|
|
7280
|
+
function modelSwitchNotice({ from, to, plan, kept }) {
|
|
7281
|
+
if (!plan) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}${kept ? ` (${kept})` : ""}`;
|
|
7282
|
+
const planned = plan.model ? `${plan.model.label ?? plan.model.model}${plan.effort ? `, ${plan.effort}` : ""}` : void 0;
|
|
7283
|
+
if (!kept) return `Model changed in the runner: ${from.label ?? from.model} \u2192 ${to.label ?? to.model}; the plan now says ${planned ?? to.label ?? to.model}`;
|
|
7284
|
+
return `The runner now answers on ${to.label ?? to.model}; ${planned ? `the plan still says ${planned}` : "the plan names no model"} (${kept})`;
|
|
7285
|
+
}
|
|
7242
7286
|
function appendStep(view, make) {
|
|
7243
7287
|
const next = append(view, make);
|
|
7244
7288
|
return sealLane(next, null, findLastIndex(next.blocks, (b) => b.type !== "usage"));
|
|
@@ -7300,6 +7344,8 @@ function reduceTaskLog(view, event) {
|
|
|
7300
7344
|
return startCommand(view, event);
|
|
7301
7345
|
case "command_output":
|
|
7302
7346
|
return commandOutput(view, event);
|
|
7347
|
+
case "model_switched":
|
|
7348
|
+
return append(view, (id) => settledMessage(id, "system", modelSwitchNotice(event)));
|
|
7303
7349
|
case "compaction":
|
|
7304
7350
|
return appendStep(view, (id) => ({
|
|
7305
7351
|
type: "compaction",
|
|
@@ -7780,7 +7826,7 @@ var CODEX_MANIFEST = {
|
|
|
7780
7826
|
}
|
|
7781
7827
|
},
|
|
7782
7828
|
modelDiscovery: {
|
|
7783
|
-
// Codex has no `codex models` subcommand (see
|
|
7829
|
+
// Codex has no `codex models` subcommand (see ADR-0004). Discovery
|
|
7784
7830
|
// tries three sources in order, each falling through to the next:
|
|
7785
7831
|
//
|
|
7786
7832
|
// 1. `codex app-server` stdio JSON-RPC (`initialize` → `model/list`) —
|
|
@@ -7851,6 +7897,10 @@ function parseOpenCodeModels(stdout, manifest) {
|
|
|
7851
7897
|
}
|
|
7852
7898
|
return models;
|
|
7853
7899
|
}
|
|
7900
|
+
function contextLimit(limit) {
|
|
7901
|
+
const context = limit?.context;
|
|
7902
|
+
return typeof context === "number" && Number.isFinite(context) && context > 0 ? { contextWindow: context } : {};
|
|
7903
|
+
}
|
|
7854
7904
|
function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
7855
7905
|
const normalized2 = stdout.endsWith("\n") ? stdout : stdout + "\n";
|
|
7856
7906
|
const parts = normalized2.split(/^([A-Za-z0-9._-]+\/[^\s"{}]+)\n/m);
|
|
@@ -7866,12 +7916,14 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
7866
7916
|
const jsonText = parts[i + 1].trim();
|
|
7867
7917
|
if (seen.has(fullId)) continue;
|
|
7868
7918
|
let variantIds = [];
|
|
7919
|
+
let window = {};
|
|
7869
7920
|
let modelLabel = prefLabels.get(fullId) || fullId.split("/").pop() || fullId;
|
|
7870
7921
|
const runnerProvider = fullId.split("/")[0];
|
|
7871
7922
|
try {
|
|
7872
7923
|
const data = JSON.parse(jsonText);
|
|
7873
7924
|
if (data.name) modelLabel = prefLabels.get(fullId) || data.name;
|
|
7874
7925
|
variantIds = Object.keys(data.variants ?? {});
|
|
7926
|
+
window = contextLimit(data.limit);
|
|
7875
7927
|
} catch {
|
|
7876
7928
|
}
|
|
7877
7929
|
seen.add(fullId);
|
|
@@ -7879,12 +7931,15 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
7879
7931
|
modelId: fullId,
|
|
7880
7932
|
modelLabel,
|
|
7881
7933
|
runnerProvider,
|
|
7882
|
-
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
|
|
7934
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
7935
|
+
...window
|
|
7883
7936
|
});
|
|
7884
7937
|
}
|
|
7885
|
-
|
|
7886
|
-
|
|
7887
|
-
|
|
7938
|
+
return preferredFirst(models, manifest);
|
|
7939
|
+
}
|
|
7940
|
+
function preferredFirst(models, manifest) {
|
|
7941
|
+
const prefOrder = manifest.modelDiscovery.preferredPatterns?.map((p) => p.id) ?? [];
|
|
7942
|
+
return [...models].sort((a, b) => {
|
|
7888
7943
|
const ai = prefOrder.indexOf(a.modelId);
|
|
7889
7944
|
const bi = prefOrder.indexOf(b.modelId);
|
|
7890
7945
|
if (ai >= 0 && bi >= 0) return ai - bi;
|
|
@@ -7892,7 +7947,37 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
7892
7947
|
if (bi >= 0) return 1;
|
|
7893
7948
|
return a.modelId.localeCompare(b.modelId);
|
|
7894
7949
|
});
|
|
7895
|
-
|
|
7950
|
+
}
|
|
7951
|
+
function parseOpenCodeModelsApi(stdout, manifest) {
|
|
7952
|
+
let data;
|
|
7953
|
+
try {
|
|
7954
|
+
data = JSON.parse(stdout)?.data;
|
|
7955
|
+
} catch {
|
|
7956
|
+
return [];
|
|
7957
|
+
}
|
|
7958
|
+
if (!Array.isArray(data)) return [];
|
|
7959
|
+
const prefLabels = new Map((manifest.modelDiscovery.preferredPatterns ?? []).map((p) => [p.id, p.label]));
|
|
7960
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7961
|
+
const models = [];
|
|
7962
|
+
for (const entry of data) {
|
|
7963
|
+
if (!entry || typeof entry.providerID !== "string" || typeof entry.modelID !== "string") continue;
|
|
7964
|
+
if (entry.enabled === false) continue;
|
|
7965
|
+
const fullId = `${entry.providerID}/${entry.modelID}`;
|
|
7966
|
+
if (seen.has(fullId)) continue;
|
|
7967
|
+
seen.add(fullId);
|
|
7968
|
+
const variantIds = Array.isArray(entry.variants) ? entry.variants.flatMap((v) => {
|
|
7969
|
+
const id = v?.id;
|
|
7970
|
+
return typeof id === "string" && id ? [id] : [];
|
|
7971
|
+
}) : [];
|
|
7972
|
+
models.push({
|
|
7973
|
+
modelId: fullId,
|
|
7974
|
+
modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
|
|
7975
|
+
runnerProvider: entry.providerID,
|
|
7976
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
7977
|
+
...contextLimit(entry.limit)
|
|
7978
|
+
});
|
|
7979
|
+
}
|
|
7980
|
+
return preferredFirst(models, manifest);
|
|
7896
7981
|
}
|
|
7897
7982
|
|
|
7898
7983
|
// src/plugins/builtin/opencode.manifest.ts
|
|
@@ -7908,13 +7993,17 @@ var OPENCODE_MANIFEST = {
|
|
|
7908
7993
|
modelDiscovery: {
|
|
7909
7994
|
method: "command",
|
|
7910
7995
|
discoveryCommands: [
|
|
7996
|
+
// v1 prints per-model variants under --verbose; v2 rejects the flag (a
|
|
7997
|
+
// fast non-zero exit) and serves the same catalog, variants included, over
|
|
7998
|
+
// `api model.list`. v1 is asked first so it never reaches the `api` form.
|
|
7911
7999
|
{ command: "opencode", args: ["models", "--verbose"], parser: parseOpenCodeModelsVerbose },
|
|
8000
|
+
{ command: "opencode", args: ["api", "model.list"], parser: parseOpenCodeModelsApi },
|
|
7912
8001
|
{ command: "opencode", args: ["models"], parser: parseOpenCodeModels }
|
|
7913
8002
|
],
|
|
7914
8003
|
// No fallbackModels: the list must always reflect what `opencode models`
|
|
7915
8004
|
// actually reports for THIS user (their configured providers only). A
|
|
7916
8005
|
// hardcoded fallback previously leaked models the user didn't have.
|
|
7917
|
-
// No static variants — per-model variants
|
|
8006
|
+
// No static variants — per-model variants come from the first two commands above
|
|
7918
8007
|
providerQualifiedIds: true
|
|
7919
8008
|
},
|
|
7920
8009
|
contextFile: "AGENTS.md",
|
|
@@ -9769,6 +9858,7 @@ var LOG_KINDS = {
|
|
|
9769
9858
|
turn_start: true,
|
|
9770
9859
|
command: true,
|
|
9771
9860
|
command_output: true,
|
|
9861
|
+
model_switched: true,
|
|
9772
9862
|
compaction: true,
|
|
9773
9863
|
runner_loaded: true,
|
|
9774
9864
|
turn_end: true,
|
|
@@ -9804,6 +9894,11 @@ ${s.content}` : ""}`).join("\n");
|
|
|
9804
9894
|
return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
|
|
9805
9895
|
case "command_output":
|
|
9806
9896
|
return event.text;
|
|
9897
|
+
case "model_switched": {
|
|
9898
|
+
const { plan } = event;
|
|
9899
|
+
const planned = plan ? `; plan: ${plan.model?.model ?? "no model"}${plan.effort ? ` (${plan.effort})` : ""}` : "";
|
|
9900
|
+
return `${event.from.model} \u2192 ${event.to.model}${planned}${event.kept ? `, kept: ${event.kept}` : ""}`;
|
|
9901
|
+
}
|
|
9807
9902
|
case "compaction":
|
|
9808
9903
|
return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
|
|
9809
9904
|
case "runner_loaded":
|
|
@@ -11363,6 +11458,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
|
|
|
11363
11458
|
...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
|
|
11364
11459
|
};
|
|
11365
11460
|
}
|
|
11461
|
+
var MODEL_COMMAND = "model";
|
|
11366
11462
|
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
11367
11463
|
function usageRecord(usage, model, subagentId) {
|
|
11368
11464
|
const record = {
|
|
@@ -11424,9 +11520,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11424
11520
|
/** The model answering the planner's current message, as its `message_start` named it. */
|
|
11425
11521
|
plannerModel;
|
|
11426
11522
|
/**
|
|
11427
|
-
* The session's `total_cost_usd` as last
|
|
11428
|
-
*
|
|
11429
|
-
*
|
|
11523
|
+
* The session's `total_cost_usd` as last counted. A resume starts from what
|
|
11524
|
+
* was counted of that session before, and is undefined without it: the CLI
|
|
11525
|
+
* restores the session's running total, which holds turns counted earlier.
|
|
11430
11526
|
*/
|
|
11431
11527
|
reportedCostUsd = 0;
|
|
11432
11528
|
reportedModelCostUsd = /* @__PURE__ */ new Map();
|
|
@@ -11467,6 +11563,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11467
11563
|
terminalOnly = /* @__PURE__ */ new Set();
|
|
11468
11564
|
/** Whether this process's `runner_loaded` went out: its first `init` sends it. */
|
|
11469
11565
|
loadedReported = false;
|
|
11566
|
+
/** The models `initialize` listed, which turn the ids the CLI reports back into the ones a plan holds. */
|
|
11567
|
+
listedModels = [];
|
|
11568
|
+
/** The id the task's main thread last reported answering on. */
|
|
11569
|
+
mainModel;
|
|
11570
|
+
/** What the plan should say the task runs: its spawn's model, then each switch followed. */
|
|
11571
|
+
followedModel;
|
|
11572
|
+
/**
|
|
11573
|
+
* Model commands sent through the chat whose switch has not been seen yet,
|
|
11574
|
+
* oldest first, each with the turns still to end before its own has. Several
|
|
11575
|
+
* can be pending: a command's switch is first reported by the next turn's
|
|
11576
|
+
* `init`, which can be another `/model`'s.
|
|
11577
|
+
*/
|
|
11578
|
+
modelCommands = [];
|
|
11470
11579
|
spawnSpec(opts, workspace) {
|
|
11471
11580
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
11472
11581
|
const args = [
|
|
@@ -11492,12 +11601,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11492
11601
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
11493
11602
|
if (opts.resumeSessionId) {
|
|
11494
11603
|
args.push("--resume", opts.resumeSessionId);
|
|
11495
|
-
this.
|
|
11604
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
11496
11605
|
this.pendingResume = opts.resumeSessionId;
|
|
11497
11606
|
}
|
|
11498
11607
|
this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
|
|
11499
11608
|
return { command: "claude", args, env: NO_AUTO_MEMORY };
|
|
11500
11609
|
}
|
|
11610
|
+
sessionCost() {
|
|
11611
|
+
const sessionId = this.sessionId ?? this.pendingResume;
|
|
11612
|
+
if (!sessionId || this.reportedCostUsd === void 0) return null;
|
|
11613
|
+
return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
|
|
11614
|
+
}
|
|
11615
|
+
/** A count kept for another session says nothing of this one's. */
|
|
11616
|
+
countFrom(sessionId, counted) {
|
|
11617
|
+
const known = counted?.sessionId === sessionId ? counted : void 0;
|
|
11618
|
+
this.reportedCostUsd = known?.usd;
|
|
11619
|
+
this.reportedModelCostUsd = new Map(Object.entries(known?.modelUsd ?? {}));
|
|
11620
|
+
}
|
|
11501
11621
|
savedOutput(abs) {
|
|
11502
11622
|
return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
|
|
11503
11623
|
}
|
|
@@ -11512,6 +11632,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11512
11632
|
*/
|
|
11513
11633
|
taskSpawnSpec(opts) {
|
|
11514
11634
|
this.requestedMode = opts.flags.permissionMode;
|
|
11635
|
+
this.followedModel = opts.model;
|
|
11515
11636
|
const args = [
|
|
11516
11637
|
...TASK_PROTOCOL_ARGS,
|
|
11517
11638
|
"--permission-prompt-tool",
|
|
@@ -11525,7 +11646,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11525
11646
|
if (opts.model) args.push("--model", opts.model);
|
|
11526
11647
|
if (opts.resumeSessionId) {
|
|
11527
11648
|
args.push("--resume", opts.resumeSessionId);
|
|
11528
|
-
this.
|
|
11649
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
11529
11650
|
this.pendingResume = opts.resumeSessionId;
|
|
11530
11651
|
}
|
|
11531
11652
|
if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
|
|
@@ -11567,6 +11688,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11567
11688
|
if (opts.kind !== "task") return;
|
|
11568
11689
|
const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
|
|
11569
11690
|
this.listedCommands = initialized?.commands ?? [];
|
|
11691
|
+
this.listedModels = initialized?.models ?? [];
|
|
11570
11692
|
if (opts.flags.permissionMode !== "plan" || !opts.model) return;
|
|
11571
11693
|
const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
|
|
11572
11694
|
if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
|
|
@@ -11620,6 +11742,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11620
11742
|
if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
|
|
11621
11743
|
const uuid = (0, import_crypto4.randomUUID)();
|
|
11622
11744
|
this.steers.set(uuid, id);
|
|
11745
|
+
this.noteModelCommand(text3, 2);
|
|
11623
11746
|
this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text: text3 }] } });
|
|
11624
11747
|
return true;
|
|
11625
11748
|
}
|
|
@@ -11671,12 +11794,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11671
11794
|
}
|
|
11672
11795
|
turnPayload(message) {
|
|
11673
11796
|
this.openTurnState();
|
|
11797
|
+
this.noteModelCommand(message, 1);
|
|
11674
11798
|
return `${JSON.stringify({
|
|
11675
11799
|
type: "user",
|
|
11676
11800
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
11677
11801
|
})}
|
|
11678
11802
|
`;
|
|
11679
11803
|
}
|
|
11804
|
+
/**
|
|
11805
|
+
* The CLI runs a message that starts with a slash command as that command,
|
|
11806
|
+
* whether the surface sent it as one or as plain text, so the message
|
|
11807
|
+
* itself says a model switch was asked for: every way into the chat counts.
|
|
11808
|
+
*/
|
|
11809
|
+
noteModelCommand(text3, turnsLeft) {
|
|
11810
|
+
const [, name, args = ""] = /^\/(\S+)\s*([\s\S]*)$/.exec(text3) ?? [];
|
|
11811
|
+
if (this.role !== "task" || !name || (findRunnerCommand(this.commands(), name)?.name ?? name) !== MODEL_COMMAND) return;
|
|
11812
|
+
this.modelCommands.push({ args: args.trim(), turnsLeft });
|
|
11813
|
+
}
|
|
11680
11814
|
openTurnState() {
|
|
11681
11815
|
this.turnHasText = false;
|
|
11682
11816
|
this.interruptRequested = false;
|
|
@@ -11788,6 +11922,7 @@ ${block.text}` : block.text });
|
|
|
11788
11922
|
this.clearFollowOnTimer();
|
|
11789
11923
|
if (this.refuseOtherMode(msg, emit)) return;
|
|
11790
11924
|
this.readLoaded(msg, emit);
|
|
11925
|
+
this.followModel(msg.model, emit);
|
|
11791
11926
|
}
|
|
11792
11927
|
if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
|
|
11793
11928
|
if (msg.subtype === "compact_boundary") {
|
|
@@ -11810,6 +11945,7 @@ ${block.text}` : block.text });
|
|
|
11810
11945
|
return;
|
|
11811
11946
|
case "result":
|
|
11812
11947
|
this.reportSessionCost(msg, emit);
|
|
11948
|
+
for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
|
|
11813
11949
|
if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
|
|
11814
11950
|
this.interruptRequested = false;
|
|
11815
11951
|
this.closeTurn({ type: "turn_end", interrupted: true }, emit);
|
|
@@ -11829,6 +11965,55 @@ ${block.text}` : block.text });
|
|
|
11829
11965
|
return;
|
|
11830
11966
|
}
|
|
11831
11967
|
}
|
|
11968
|
+
/**
|
|
11969
|
+
* A task's main thread reported the model it answers on. A change is the
|
|
11970
|
+
* user's own switch when a model command went out before it: Ordewell never
|
|
11971
|
+
* passes `--fallback-model`, so nothing else should move it. A change with
|
|
11972
|
+
* no command before it is reported as drift, and the plan does not follow
|
|
11973
|
+
* it. A command whose turn ended with the model unchanged switched nothing,
|
|
11974
|
+
* and is let go.
|
|
11975
|
+
*
|
|
11976
|
+
* Effort is never reported (recorded on 2.1.302, `task-effort-command`):
|
|
11977
|
+
* `init` names the model alone, its `per_turn_effort_active` says effort is
|
|
11978
|
+
* on, not which; `/effort low` only prints that it set it; and `/model`
|
|
11979
|
+
* takes no effort (`/model haiku high` is "not found"). So a switch carries
|
|
11980
|
+
* no effort and the plan's stays as planned.
|
|
11981
|
+
*/
|
|
11982
|
+
followModel(reported, emit) {
|
|
11983
|
+
if (this.role !== "task" || !reported || reported === "<synthetic>") return;
|
|
11984
|
+
const previous = this.mainModel;
|
|
11985
|
+
this.mainModel = reported;
|
|
11986
|
+
if (previous === void 0 || previous === reported) {
|
|
11987
|
+
this.modelCommands = this.modelCommands.filter((c) => c.turnsLeft > 0);
|
|
11988
|
+
return;
|
|
11989
|
+
}
|
|
11990
|
+
const matched = this.modelCommands.findIndex((c) => this.runsAs(c.args, reported));
|
|
11991
|
+
const command = this.modelCommands[Math.max(matched, 0)];
|
|
11992
|
+
this.modelCommands = this.modelCommands.slice(Math.max(matched, 0) + 1);
|
|
11993
|
+
const from = this.listedModel(previous, this.followedModel);
|
|
11994
|
+
if (!command) {
|
|
11995
|
+
emit({ type: "model_drifted", from, to: this.listedModel(reported, void 0) });
|
|
11996
|
+
return;
|
|
11997
|
+
}
|
|
11998
|
+
const to = this.listedModel(reported, command.args);
|
|
11999
|
+
this.followedModel = to.model;
|
|
12000
|
+
emit({ type: "model_switched", from, to });
|
|
12001
|
+
}
|
|
12002
|
+
/** Whether the model value `asked` runs as the id `resolved`, by what `initialize` lists. */
|
|
12003
|
+
runsAs(asked, resolved) {
|
|
12004
|
+
return asked === resolved || this.listedModels.some((m) => m.value === asked && m.resolvedModel === resolved);
|
|
12005
|
+
}
|
|
12006
|
+
/**
|
|
12007
|
+
* The value `initialize` lists for the id a model runs as. Several can share
|
|
12008
|
+
* one (`sonnet`, `default`); the one asked for wins, and `default` only as a
|
|
12009
|
+
* last resort, since it names whatever the CLI's default becomes.
|
|
12010
|
+
*/
|
|
12011
|
+
listedModel(resolved, asked) {
|
|
12012
|
+
const listed3 = this.listedModels.filter((m) => m.value && (m.resolvedModel === resolved || m.value === resolved));
|
|
12013
|
+
const pick = listed3.find((m) => m.value === asked && asked !== "default") ?? listed3.find((m) => m.value !== "default") ?? listed3[0];
|
|
12014
|
+
if (!pick?.value) return { model: resolved };
|
|
12015
|
+
return { model: pick.value, ...pick.displayName ? { label: pick.displayName } : {} };
|
|
12016
|
+
}
|
|
11832
12017
|
/**
|
|
11833
12018
|
* `--permission-mode auto` on a model or account without auto mode is not
|
|
11834
12019
|
* refused: the CLI says nothing and starts in `default`, then asks about every
|
|
@@ -11912,8 +12097,10 @@ ${block.text}` : block.text });
|
|
|
11912
12097
|
* {@link AgentEvent}), so nothing here has to reconcile with it.
|
|
11913
12098
|
*/
|
|
11914
12099
|
handleStreamEvent(event, emit) {
|
|
11915
|
-
if (event.type === "message_start")
|
|
11916
|
-
|
|
12100
|
+
if (event.type === "message_start") {
|
|
12101
|
+
this.plannerModel = event.message?.model;
|
|
12102
|
+
this.followModel(this.plannerModel, emit);
|
|
12103
|
+
} else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
|
|
11917
12104
|
else if (event.type === "content_block_start" && event.content_block?.type === "text") {
|
|
11918
12105
|
this.pendingBreak = this.turnHasText;
|
|
11919
12106
|
} else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
|
@@ -11928,7 +12115,8 @@ ${block.text}` : block.text });
|
|
|
11928
12115
|
* What only the result line knows: the cost, which covers every call the
|
|
11929
12116
|
* session made — subagents included, since none of their lines carries one —
|
|
11930
12117
|
* and the planner model's window. `total_cost_usd` is a running total, so a
|
|
11931
|
-
* turn reports its growth.
|
|
12118
|
+
* turn reports its growth. After a resume it grows from what was counted of
|
|
12119
|
+
* the session before; with no count kept, the first turn only sets the
|
|
11932
12120
|
* baseline: its total includes turns counted before, and a turn's own share
|
|
11933
12121
|
* cannot be told apart from them.
|
|
11934
12122
|
*/
|
|
@@ -12508,6 +12696,97 @@ var CodexRpc = class {
|
|
|
12508
12696
|
}
|
|
12509
12697
|
};
|
|
12510
12698
|
|
|
12699
|
+
// src/utils/wallTime.ts
|
|
12700
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
12701
|
+
function lastMatch(re, text3) {
|
|
12702
|
+
let last = null;
|
|
12703
|
+
for (const m of text3.matchAll(re)) last = m;
|
|
12704
|
+
return last;
|
|
12705
|
+
}
|
|
12706
|
+
function hour24(hour, meridiem) {
|
|
12707
|
+
const h = Number(hour) % 12;
|
|
12708
|
+
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
12709
|
+
}
|
|
12710
|
+
function month(name) {
|
|
12711
|
+
if (!name) return void 0;
|
|
12712
|
+
const at = MONTHS.indexOf(name.toLowerCase());
|
|
12713
|
+
return at < 0 ? void 0 : at;
|
|
12714
|
+
}
|
|
12715
|
+
function placeWallTime(wall, zone, now) {
|
|
12716
|
+
const today = wallFields(now, zone);
|
|
12717
|
+
const year = wall.year ?? today.year;
|
|
12718
|
+
const monthIndex = wall.month ?? today.month;
|
|
12719
|
+
const day = wall.day ?? today.day;
|
|
12720
|
+
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
12721
|
+
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
12722
|
+
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
12723
|
+
}
|
|
12724
|
+
var HOUR_MS = 60 * 6e4;
|
|
12725
|
+
function wallFields(instant, zone) {
|
|
12726
|
+
if (!zone) {
|
|
12727
|
+
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
12728
|
+
}
|
|
12729
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
12730
|
+
timeZone: zone,
|
|
12731
|
+
hourCycle: "h23",
|
|
12732
|
+
year: "numeric",
|
|
12733
|
+
month: "numeric",
|
|
12734
|
+
day: "numeric",
|
|
12735
|
+
hour: "numeric",
|
|
12736
|
+
minute: "numeric",
|
|
12737
|
+
second: "numeric"
|
|
12738
|
+
}).formatToParts(instant);
|
|
12739
|
+
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
12740
|
+
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
12741
|
+
}
|
|
12742
|
+
function zoneOffsetMs(instant, zone) {
|
|
12743
|
+
const f = wallFields(new Date(instant), zone);
|
|
12744
|
+
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
12745
|
+
}
|
|
12746
|
+
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
12747
|
+
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
12748
|
+
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
12749
|
+
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
12750
|
+
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
12751
|
+
return new Date(instant);
|
|
12752
|
+
}
|
|
12753
|
+
|
|
12754
|
+
// src/services/harness/codexUsageLimit.ts
|
|
12755
|
+
var CODEX_RESET_RE = /[Tt]ry again at (?:([A-Z][a-z]{2}) (\d{1,2})(?:st|nd|rd|th)?, (\d{4}) )?(\d{1,2}):(\d{2}) ([AP]M)\./g;
|
|
12756
|
+
function codexUsageLimitReset(text3, now) {
|
|
12757
|
+
const m = lastMatch(CODEX_RESET_RE, text3);
|
|
12758
|
+
if (!m) return null;
|
|
12759
|
+
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
12760
|
+
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
12761
|
+
if (mon) {
|
|
12762
|
+
const at = month(mon);
|
|
12763
|
+
if (at === void 0) return null;
|
|
12764
|
+
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
12765
|
+
}
|
|
12766
|
+
return placeWallTime(wall, void 0, now);
|
|
12767
|
+
}
|
|
12768
|
+
function mergeCodexRateLimits(known, params) {
|
|
12769
|
+
const snapshot = params?.rateLimits;
|
|
12770
|
+
if (!snapshot || typeof snapshot !== "object") return known;
|
|
12771
|
+
const id = snapshot.limitId ?? "";
|
|
12772
|
+
const before = known.get(id);
|
|
12773
|
+
const next = new Map(known);
|
|
12774
|
+
next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
|
|
12775
|
+
return next;
|
|
12776
|
+
}
|
|
12777
|
+
function isCodexUsageLimit(error) {
|
|
12778
|
+
const info = error?.codexErrorInfo;
|
|
12779
|
+
if (info === void 0 || info === null || info === "other") return void 0;
|
|
12780
|
+
if (info === "usageLimitExceeded") return true;
|
|
12781
|
+
return typeof info === "object" && "usageLimitExceeded" in info;
|
|
12782
|
+
}
|
|
12783
|
+
function codexUsageLimitSignal(error, limits, now) {
|
|
12784
|
+
const limited = isCodexUsageLimit(error);
|
|
12785
|
+
if (limited !== true) return limited;
|
|
12786
|
+
const resets = [...limits.values()].flatMap((snapshot) => [snapshot.primary, snapshot.secondary]).flatMap((window) => window && (window.usedPercent ?? 0) >= 100 && typeof window.resetsAt === "number" ? [window.resetsAt * 1e3] : []).filter((at) => at > now.getTime());
|
|
12787
|
+
return resets.length > 0 ? { resetsAt: new Date(Math.max(...resets)).toISOString() } : {};
|
|
12788
|
+
}
|
|
12789
|
+
|
|
12511
12790
|
// src/services/harness/CodexAdapter.ts
|
|
12512
12791
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
12513
12792
|
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
@@ -12651,6 +12930,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
12651
12930
|
turn = null;
|
|
12652
12931
|
/** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
|
|
12653
12932
|
endedTurnId = null;
|
|
12933
|
+
/**
|
|
12934
|
+
* The account's limit windows as Codex last reported them. Only read when a
|
|
12935
|
+
* turn fails on the usage limit; an update on its own acts on nothing.
|
|
12936
|
+
*/
|
|
12937
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
12654
12938
|
/** An interrupt was asked for during this turn, so however it ends, it was cut short. */
|
|
12655
12939
|
interruptRequested = false;
|
|
12656
12940
|
/**
|
|
@@ -13046,6 +13330,9 @@ ${this.exitMessage()}`);
|
|
|
13046
13330
|
case "thread/tokenUsage/updated":
|
|
13047
13331
|
this.emitUsage(msg.params, emit);
|
|
13048
13332
|
return;
|
|
13333
|
+
case "account/rateLimits/updated":
|
|
13334
|
+
this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
|
|
13335
|
+
return;
|
|
13049
13336
|
case "turn/started": {
|
|
13050
13337
|
if (this.subagentOf(msg.params?.threadId)) return;
|
|
13051
13338
|
this.turnHasText = false;
|
|
@@ -13086,7 +13373,7 @@ ${this.exitMessage()}`);
|
|
|
13086
13373
|
const failure = msg.params;
|
|
13087
13374
|
if (failure?.willRetry) return;
|
|
13088
13375
|
this.closeTurn(emit);
|
|
13089
|
-
emit(
|
|
13376
|
+
emit(this.turnFailed(failure?.error));
|
|
13090
13377
|
return;
|
|
13091
13378
|
}
|
|
13092
13379
|
case "turn/completed": {
|
|
@@ -13097,7 +13384,7 @@ ${this.exitMessage()}`);
|
|
|
13097
13384
|
if (this.interruptRequested || turn?.status === "interrupted") {
|
|
13098
13385
|
emit({ type: "turn_end", interrupted: true });
|
|
13099
13386
|
} else if (turn?.status === "failed") {
|
|
13100
|
-
emit(
|
|
13387
|
+
emit(this.turnFailed(turn.error));
|
|
13101
13388
|
} else {
|
|
13102
13389
|
emit({ type: "turn_end" });
|
|
13103
13390
|
}
|
|
@@ -13109,6 +13396,11 @@ ${this.exitMessage()}`);
|
|
|
13109
13396
|
return;
|
|
13110
13397
|
}
|
|
13111
13398
|
}
|
|
13399
|
+
turnFailed(error) {
|
|
13400
|
+
const message = error?.message || "Codex ended the turn with an error.";
|
|
13401
|
+
const usageLimit = codexUsageLimitSignal(error, this.rateLimits, /* @__PURE__ */ new Date());
|
|
13402
|
+
return usageLimit === void 0 ? { type: "error", message } : { type: "error", message, usageLimit };
|
|
13403
|
+
}
|
|
13112
13404
|
turnStarted(id) {
|
|
13113
13405
|
const turn = this.turn;
|
|
13114
13406
|
if (!turn || turn.id || typeof id !== "string") return;
|
|
@@ -13566,9 +13858,9 @@ function recordUnusable(servers, approval) {
|
|
|
13566
13858
|
for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
|
|
13567
13859
|
}
|
|
13568
13860
|
var NOT_APPROVED = "not approved for planning";
|
|
13569
|
-
function heldServersNote(
|
|
13861
|
+
function heldServersNote(held2, unreadable = []) {
|
|
13570
13862
|
const lines2 = [
|
|
13571
|
-
...
|
|
13863
|
+
...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
|
|
13572
13864
|
...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
|
|
13573
13865
|
];
|
|
13574
13866
|
if (lines2.length === 0) return "";
|
|
@@ -13705,8 +13997,8 @@ var PendingSteers = class {
|
|
|
13705
13997
|
* False when it was already delivered, so a late failure does not recall it.
|
|
13706
13998
|
*/
|
|
13707
13999
|
forget(id) {
|
|
13708
|
-
for (const [messageId,
|
|
13709
|
-
if (
|
|
14000
|
+
for (const [messageId, held2] of this.pending) {
|
|
14001
|
+
if (held2 !== id) continue;
|
|
13710
14002
|
this.pending.delete(messageId);
|
|
13711
14003
|
return true;
|
|
13712
14004
|
}
|
|
@@ -13784,9 +14076,9 @@ var ChildSessions = class {
|
|
|
13784
14076
|
adopt(child, callId) {
|
|
13785
14077
|
if (this.owners.get(child)) return null;
|
|
13786
14078
|
this.owners.set(child, callId);
|
|
13787
|
-
const
|
|
14079
|
+
const held2 = this.held.get(child) ?? [];
|
|
13788
14080
|
this.held.delete(child);
|
|
13789
|
-
return
|
|
14081
|
+
return held2;
|
|
13790
14082
|
}
|
|
13791
14083
|
owns(callId) {
|
|
13792
14084
|
return [...this.owners.values()].includes(callId);
|
|
@@ -14310,11 +14602,11 @@ var OpenCodeV2 = class {
|
|
|
14310
14602
|
adoptChild(data, state, onEvent, turn) {
|
|
14311
14603
|
const child = data.metadata?.sessionID;
|
|
14312
14604
|
const callId = data.id;
|
|
14313
|
-
const
|
|
14314
|
-
if (!
|
|
14605
|
+
const held2 = child && callId ? state.children.adopt(child, callId) : null;
|
|
14606
|
+
if (!held2 || !callId) return;
|
|
14315
14607
|
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
14316
14608
|
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
14317
|
-
for (const frame of
|
|
14609
|
+
for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
|
|
14318
14610
|
}
|
|
14319
14611
|
finishChild(callId, success, output, state, onEvent) {
|
|
14320
14612
|
if (!state.children.owns(callId)) return;
|
|
@@ -15026,13 +15318,13 @@ var OpenCodeAdapter = class {
|
|
|
15026
15318
|
trackSubagent(part, callId, turn, onEvent) {
|
|
15027
15319
|
const state = part.state;
|
|
15028
15320
|
const child = state?.metadata?.sessionId;
|
|
15029
|
-
const
|
|
15030
|
-
if (
|
|
15321
|
+
const held2 = child ? turn.children.adopt(child, callId) : null;
|
|
15322
|
+
if (held2) {
|
|
15031
15323
|
const input = state?.input ?? {};
|
|
15032
15324
|
const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
15033
15325
|
const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
|
|
15034
15326
|
onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
|
|
15035
|
-
for (const frame of
|
|
15327
|
+
for (const frame of held2) this.onFrame(frame, turn, onEvent);
|
|
15036
15328
|
}
|
|
15037
15329
|
const status = state?.status;
|
|
15038
15330
|
if (status === "completed" || status === "error") {
|
|
@@ -15262,61 +15554,6 @@ var OPENCODE_REPO_CONFIG = {
|
|
|
15262
15554
|
}
|
|
15263
15555
|
};
|
|
15264
15556
|
|
|
15265
|
-
// src/utils/wallTime.ts
|
|
15266
|
-
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
15267
|
-
function lastMatch(re, text3) {
|
|
15268
|
-
let last = null;
|
|
15269
|
-
for (const m of text3.matchAll(re)) last = m;
|
|
15270
|
-
return last;
|
|
15271
|
-
}
|
|
15272
|
-
function hour24(hour, meridiem) {
|
|
15273
|
-
const h = Number(hour) % 12;
|
|
15274
|
-
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
15275
|
-
}
|
|
15276
|
-
function month(name) {
|
|
15277
|
-
if (!name) return void 0;
|
|
15278
|
-
const at = MONTHS.indexOf(name.toLowerCase());
|
|
15279
|
-
return at < 0 ? void 0 : at;
|
|
15280
|
-
}
|
|
15281
|
-
function placeWallTime(wall, zone, now) {
|
|
15282
|
-
const today = wallFields(now, zone);
|
|
15283
|
-
const year = wall.year ?? today.year;
|
|
15284
|
-
const monthIndex = wall.month ?? today.month;
|
|
15285
|
-
const day = wall.day ?? today.day;
|
|
15286
|
-
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
15287
|
-
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
15288
|
-
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
15289
|
-
}
|
|
15290
|
-
var HOUR_MS = 60 * 6e4;
|
|
15291
|
-
function wallFields(instant, zone) {
|
|
15292
|
-
if (!zone) {
|
|
15293
|
-
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
15294
|
-
}
|
|
15295
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
15296
|
-
timeZone: zone,
|
|
15297
|
-
hourCycle: "h23",
|
|
15298
|
-
year: "numeric",
|
|
15299
|
-
month: "numeric",
|
|
15300
|
-
day: "numeric",
|
|
15301
|
-
hour: "numeric",
|
|
15302
|
-
minute: "numeric",
|
|
15303
|
-
second: "numeric"
|
|
15304
|
-
}).formatToParts(instant);
|
|
15305
|
-
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
15306
|
-
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
15307
|
-
}
|
|
15308
|
-
function zoneOffsetMs(instant, zone) {
|
|
15309
|
-
const f = wallFields(new Date(instant), zone);
|
|
15310
|
-
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
15311
|
-
}
|
|
15312
|
-
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
15313
|
-
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
15314
|
-
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
15315
|
-
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
15316
|
-
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
15317
|
-
return new Date(instant);
|
|
15318
|
-
}
|
|
15319
|
-
|
|
15320
15557
|
// src/services/harness/claudeUsageLimit.ts
|
|
15321
15558
|
var CLAUDE_RESET_RE = /You['’]ve hit your [^\n]*?· (?:your [^·\n]*? )?resets (?:([A-Z][a-z]{2}) (\d{1,2}), (?:(\d{4}), )?)?(\d{1,2})(?::(\d{2}))?([ap]m) \(([A-Za-z0-9_+\-/]+)\)/g;
|
|
15322
15559
|
function claudeUsageLimitReset(text3, now) {
|
|
@@ -15332,21 +15569,6 @@ function claudeUsageLimitReset(text3, now) {
|
|
|
15332
15569
|
return placeWallTime(wall, zone, now);
|
|
15333
15570
|
}
|
|
15334
15571
|
|
|
15335
|
-
// src/services/harness/codexUsageLimit.ts
|
|
15336
|
-
var CODEX_RESET_RE = /[Tt]ry again at (?:([A-Z][a-z]{2}) (\d{1,2})(?:st|nd|rd|th)?, (\d{4}) )?(\d{1,2}):(\d{2}) ([AP]M)\./g;
|
|
15337
|
-
function codexUsageLimitReset(text3, now) {
|
|
15338
|
-
const m = lastMatch(CODEX_RESET_RE, text3);
|
|
15339
|
-
if (!m) return null;
|
|
15340
|
-
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
15341
|
-
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
15342
|
-
if (mon) {
|
|
15343
|
-
const at = month(mon);
|
|
15344
|
-
if (at === void 0) return null;
|
|
15345
|
-
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
15346
|
-
}
|
|
15347
|
-
return placeWallTime(wall, void 0, now);
|
|
15348
|
-
}
|
|
15349
|
-
|
|
15350
15572
|
// src/services/harness/connectors.ts
|
|
15351
15573
|
var CONNECTORS = {
|
|
15352
15574
|
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
|
|
@@ -15677,6 +15899,7 @@ function parseCodexModelsCache(content) {
|
|
|
15677
15899
|
}
|
|
15678
15900
|
}
|
|
15679
15901
|
var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
15902
|
+
var NEGATIVE_TTL_MS = 6e4;
|
|
15680
15903
|
function geminiDiscoveryCacheKey(apiKey) {
|
|
15681
15904
|
const dir = globalDataDir();
|
|
15682
15905
|
const hash = apiKey.slice(-8);
|
|
@@ -15744,7 +15967,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
|
|
|
15744
15967
|
modelId,
|
|
15745
15968
|
modelLabel: m.displayName || modelId,
|
|
15746
15969
|
runnerProvider: "gemini",
|
|
15747
|
-
variants: []
|
|
15970
|
+
variants: [],
|
|
15971
|
+
...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
|
|
15748
15972
|
});
|
|
15749
15973
|
}
|
|
15750
15974
|
}
|
|
@@ -15806,26 +16030,33 @@ async function discoverFromApi(manifest, config, fetchImpl) {
|
|
|
15806
16030
|
return null;
|
|
15807
16031
|
}
|
|
15808
16032
|
}
|
|
16033
|
+
function wasKilled(err) {
|
|
16034
|
+
return !!err && typeof err === "object" && err.killed === true;
|
|
16035
|
+
}
|
|
15809
16036
|
async function discoverFromCommand(manifest, execImpl) {
|
|
15810
16037
|
const discovery = manifest.modelDiscovery;
|
|
15811
16038
|
const commands = discovery.discoveryCommands ?? [
|
|
15812
16039
|
{ command: discovery.command, args: discovery.args || [], parser: discovery.parser }
|
|
15813
16040
|
];
|
|
15814
16041
|
const runCommands = async () => {
|
|
16042
|
+
let timedOut2 = false;
|
|
15815
16043
|
for (const cmd of commands) {
|
|
15816
16044
|
try {
|
|
15817
16045
|
const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
|
|
15818
16046
|
const models = parseModelOutput(stdout, cmd.parser, manifest);
|
|
15819
|
-
if (models.length > 0) return applyVariants(mergeCanonicalAliases(models, manifest), manifest);
|
|
15820
|
-
} catch {
|
|
16047
|
+
if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
|
|
16048
|
+
} catch (err) {
|
|
16049
|
+
if (wasKilled(err)) timedOut2 = true;
|
|
15821
16050
|
}
|
|
15822
16051
|
}
|
|
15823
|
-
return null;
|
|
16052
|
+
return { models: null, timedOut: timedOut2 };
|
|
15824
16053
|
};
|
|
15825
16054
|
const first = await runCommands();
|
|
15826
|
-
if (first) return { models: first, fromFallback: false };
|
|
15827
|
-
|
|
15828
|
-
|
|
16055
|
+
if (first.models) return { models: first.models, fromFallback: false };
|
|
16056
|
+
if (first.timedOut) {
|
|
16057
|
+
const second = await runCommands();
|
|
16058
|
+
if (second.models) return { models: second.models, fromFallback: false };
|
|
16059
|
+
}
|
|
15829
16060
|
return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
|
|
15830
16061
|
}
|
|
15831
16062
|
function mergeCanonicalAliases(discovered, manifest) {
|
|
@@ -15862,16 +16093,33 @@ function parseLineByLine(stdout) {
|
|
|
15862
16093
|
}
|
|
15863
16094
|
var ModelDiscovery = class {
|
|
15864
16095
|
cache = /* @__PURE__ */ new Map();
|
|
16096
|
+
/**
|
|
16097
|
+
* Degraded results (a fallback list, or empty) remembered for
|
|
16098
|
+
* {@link NEGATIVE_TTL_MS}. They are deliberately not cached — a transient
|
|
16099
|
+
* failure must heal — but re-running a failing CLI on every lookup is the
|
|
16100
|
+
* other extreme.
|
|
16101
|
+
*/
|
|
16102
|
+
misses = /* @__PURE__ */ new Map();
|
|
16103
|
+
/**
|
|
16104
|
+
* Discoveries still running, so a second lookup of the same runner joins the
|
|
16105
|
+
* first instead of spawning the CLI again. A slow CLI is the case this
|
|
16106
|
+
* exists for: a planner switch and a runner-list refresh land inside one
|
|
16107
|
+
* another's wait, and two `opencode models` calls race each other for the
|
|
16108
|
+
* same background service.
|
|
16109
|
+
*/
|
|
16110
|
+
inflight = /* @__PURE__ */ new Map();
|
|
15865
16111
|
registry;
|
|
15866
16112
|
execImpl;
|
|
15867
16113
|
fetchImpl;
|
|
15868
16114
|
appServerClient;
|
|
15869
16115
|
readFileImpl;
|
|
15870
|
-
|
|
16116
|
+
now;
|
|
16117
|
+
constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
|
|
15871
16118
|
this.registry = registry;
|
|
15872
16119
|
this.execImpl = execImpl;
|
|
15873
16120
|
this.fetchImpl = fetchImpl;
|
|
15874
16121
|
this.appServerClient = appServerClient;
|
|
16122
|
+
this.now = now;
|
|
15875
16123
|
this.readFileImpl = readFileImpl ?? ((filePath) => {
|
|
15876
16124
|
try {
|
|
15877
16125
|
return fsSync3.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
|
|
@@ -15915,10 +16163,20 @@ var ModelDiscovery = class {
|
|
|
15915
16163
|
const runnerLabel = manifest.displayName || runner;
|
|
15916
16164
|
return models.map((m) => ({ ...m, runnerId: runner, runnerLabel }));
|
|
15917
16165
|
}
|
|
15918
|
-
|
|
15919
|
-
|
|
15920
|
-
|
|
15921
|
-
|
|
16166
|
+
discoverRaw(runner) {
|
|
16167
|
+
const cached2 = this.cache.get(runner);
|
|
16168
|
+
if (cached2) return Promise.resolve(cached2);
|
|
16169
|
+
const remembered = this.misses.get(runner);
|
|
16170
|
+
if (remembered && this.now() - remembered.at < NEGATIVE_TTL_MS) return Promise.resolve(remembered.models);
|
|
16171
|
+
const running2 = this.inflight.get(runner);
|
|
16172
|
+
if (running2) return running2;
|
|
16173
|
+
const started = this.discoverUncached(runner).finally(() => {
|
|
16174
|
+
if (this.inflight.get(runner) === started) this.inflight.delete(runner);
|
|
16175
|
+
});
|
|
16176
|
+
this.inflight.set(runner, started);
|
|
16177
|
+
return started;
|
|
16178
|
+
}
|
|
16179
|
+
async discoverUncached(runner) {
|
|
15922
16180
|
const manifest = this.registry.getManifest(runner);
|
|
15923
16181
|
if (!manifest) {
|
|
15924
16182
|
return [];
|
|
@@ -15950,10 +16208,13 @@ var ModelDiscovery = class {
|
|
|
15950
16208
|
models = buildFallbackModels(manifest);
|
|
15951
16209
|
}
|
|
15952
16210
|
if (cacheable && models.length > 0) this.cache.set(runner, models);
|
|
16211
|
+
else this.misses.set(runner, { models, at: this.now() });
|
|
15953
16212
|
return models;
|
|
15954
16213
|
}
|
|
15955
16214
|
clear() {
|
|
15956
16215
|
this.cache.clear();
|
|
16216
|
+
this.misses.clear();
|
|
16217
|
+
this.inflight.clear();
|
|
15957
16218
|
}
|
|
15958
16219
|
/** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
|
|
15959
16220
|
getCached(runner) {
|
|
@@ -16115,6 +16376,7 @@ function parseMaxParallel(value) {
|
|
|
16115
16376
|
// src/services/SettingsService.ts
|
|
16116
16377
|
var fs10 = __toESM(require("fs"));
|
|
16117
16378
|
var path26 = __toESM(require("path"));
|
|
16379
|
+
var import_crypto10 = require("crypto");
|
|
16118
16380
|
|
|
16119
16381
|
// src/services/TaskOps.ts
|
|
16120
16382
|
var import_uuid3 = require("uuid");
|
|
@@ -16168,7 +16430,14 @@ function clampThinkingEffort(effort, variants) {
|
|
|
16168
16430
|
}
|
|
16169
16431
|
return void 0;
|
|
16170
16432
|
}
|
|
16171
|
-
function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
|
|
16433
|
+
function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner, planned = []) {
|
|
16434
|
+
const prior = new Map(flattenTasks(planned).map((t) => [t.id, t]));
|
|
16435
|
+
const unwritten = (task) => {
|
|
16436
|
+
const was = prior.get(task.id);
|
|
16437
|
+
if (!was?.assignedModel || !task.assignedModel || was.assignedRunner !== task.assignedRunner) return void 0;
|
|
16438
|
+
if (was.assignedModel.modelId !== task.assignedModel.modelId || was.assignedModel.thinkingEffort !== task.assignedModel.thinkingEffort) return void 0;
|
|
16439
|
+
return was;
|
|
16440
|
+
};
|
|
16172
16441
|
const clampVariant = (task) => {
|
|
16173
16442
|
if (!task.assignedModel || !modelsByRunner) return task;
|
|
16174
16443
|
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
@@ -16197,6 +16466,11 @@ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
|
|
|
16197
16466
|
};
|
|
16198
16467
|
const coerceOne = (task) => {
|
|
16199
16468
|
if (task.type === "user") return task;
|
|
16469
|
+
const kept = unwritten(task);
|
|
16470
|
+
if (kept) {
|
|
16471
|
+
const left = { ...task, assignedModel: kept.assignedModel };
|
|
16472
|
+
return kept.status === "in_progress" || kept.status === "awaiting_user" ? left : clampVariant(left);
|
|
16473
|
+
}
|
|
16200
16474
|
const allowlist = allowedFor(task.assignedRunner);
|
|
16201
16475
|
if (!task.assignedModel) {
|
|
16202
16476
|
if (allowlist && allowlist.length > 0) {
|
|
@@ -16259,7 +16533,7 @@ function blockedByLabel(refs) {
|
|
|
16259
16533
|
function skippedLabel(reason) {
|
|
16260
16534
|
return reason ? `Skipped: ${reason}` : "Skipped";
|
|
16261
16535
|
}
|
|
16262
|
-
var WAITS_FOR_APPROVAL_LABEL = "
|
|
16536
|
+
var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
|
|
16263
16537
|
function approvalLabel(count) {
|
|
16264
16538
|
if (!count) return null;
|
|
16265
16539
|
return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
|
|
@@ -16444,6 +16718,15 @@ function lockedTaskRefusal(task, action = "changed") {
|
|
|
16444
16718
|
if (task.status === "completed") return `"${task.title}" is completed and cannot be ${action}: rearm it to run it again, or add a new task for further work`;
|
|
16445
16719
|
return null;
|
|
16446
16720
|
}
|
|
16721
|
+
function liveModelRefusal(task) {
|
|
16722
|
+
return `"${task.title}" has a live runner, so its model and effort cannot change now: stop the task first, or use the runner's own /model`;
|
|
16723
|
+
}
|
|
16724
|
+
function liveRunnerRefusal(task) {
|
|
16725
|
+
return `"${task.title}" has a live runner, so its runner cannot change now: stop the task first`;
|
|
16726
|
+
}
|
|
16727
|
+
function sameAssignment(a, b) {
|
|
16728
|
+
return a?.modelId === b?.modelId && a?.thinkingEffort === b?.thinkingEffort;
|
|
16729
|
+
}
|
|
16447
16730
|
var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
|
|
16448
16731
|
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
16449
16732
|
if (!catalog) return { ok: true };
|
|
@@ -16503,11 +16786,18 @@ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
|
16503
16786
|
const { task: target, parent } = found;
|
|
16504
16787
|
const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
|
|
16505
16788
|
if (locked) return { ok: false, error: locked };
|
|
16789
|
+
const coherence = typeCoherenceCheck(target, changes);
|
|
16790
|
+
const landing = coherence?.clear?.includes("assignedModel") ? { ...changes, assignedModel: void 0 } : changes;
|
|
16791
|
+
if ("assignedModel" in landing && !sameAssignment(landing.assignedModel, target.assignedModel) && catalog?.liveRunner?.(taskId)) {
|
|
16792
|
+
return { ok: false, error: liveModelRefusal(target) };
|
|
16793
|
+
}
|
|
16794
|
+
if ("assignedRunner" in changes && changes.assignedRunner !== target.assignedRunner && catalog?.liveRunner?.(taskId)) {
|
|
16795
|
+
return { ok: false, error: liveRunnerRefusal(target) };
|
|
16796
|
+
}
|
|
16506
16797
|
if (changes.dependencies) {
|
|
16507
16798
|
const depCheck = canSetDependencies(parent?.subtasks ?? tasks, taskId, changes.dependencies);
|
|
16508
16799
|
if (!depCheck.ok) return depCheck;
|
|
16509
16800
|
}
|
|
16510
|
-
const coherence = typeCoherenceCheck(target, changes);
|
|
16511
16801
|
if (coherence && !coherence.ok) return coherence;
|
|
16512
16802
|
const ops = opsCheck(target, parent !== null, changes);
|
|
16513
16803
|
if (ops) return ops;
|
|
@@ -16745,9 +17035,9 @@ function idsOf(op) {
|
|
|
16745
17035
|
return ids[next++];
|
|
16746
17036
|
};
|
|
16747
17037
|
}
|
|
16748
|
-
function applyTaskOps(currentTasks, newOps, runners, catalog,
|
|
16749
|
-
const ops = [...
|
|
16750
|
-
const opLabel = (i) => i <
|
|
17038
|
+
function applyTaskOps(currentTasks, newOps, runners, catalog, held2 = []) {
|
|
17039
|
+
const ops = [...held2, ...newOps];
|
|
17040
|
+
const opLabel = (i) => i < held2.length ? `earlier op ${i + 1} (${ops[i].op})` : `op ${i - held2.length + 1} (${ops[i].op})`;
|
|
16751
17041
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
16752
17042
|
const errors = [];
|
|
16753
17043
|
const summary = [];
|
|
@@ -17439,23 +17729,34 @@ function createWakePolicy(options) {
|
|
|
17439
17729
|
// src/services/runnerExit.ts
|
|
17440
17730
|
var USAGE_LIMIT_RE = /\b(?:usage|session|weekly|daily|monthly) limit\b|\brate limit (?:exceeded|reached)\b|\brate[- ]limited\b|\blimit (?:will )?reset\b|\bquota (?:exceeded|reached)\b|\btoo many requests\b/i;
|
|
17441
17731
|
var USAGE_LIMIT_TAIL = 4096;
|
|
17442
|
-
function classifyRunnerStop(
|
|
17443
|
-
|
|
17732
|
+
function classifyRunnerStop(said2) {
|
|
17733
|
+
if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
|
|
17734
|
+
return USAGE_LIMIT_RE.test(wordsOf(said2).slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
|
|
17735
|
+
}
|
|
17736
|
+
function wordsOf(said2) {
|
|
17737
|
+
return typeof said2 === "string" ? said2 : said2.message;
|
|
17444
17738
|
}
|
|
17445
17739
|
function stopsRunner(reason) {
|
|
17446
17740
|
return reason !== "stop" && reason !== "load";
|
|
17447
17741
|
}
|
|
17448
|
-
function readUsageLimitReset(runner,
|
|
17742
|
+
function readUsageLimitReset(runner, said2, now = /* @__PURE__ */ new Date()) {
|
|
17743
|
+
const reset = (typeof said2 === "string" ? null : reportedReset(said2)) ?? readReset(runner, wordsOf(said2), now);
|
|
17744
|
+
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
17745
|
+
return reset;
|
|
17746
|
+
}
|
|
17747
|
+
function reportedReset(failure) {
|
|
17748
|
+
const at = failure.usageLimit ? failure.usageLimit.resetsAt : void 0;
|
|
17749
|
+
const reset = at === void 0 ? null : new Date(at);
|
|
17750
|
+
return reset && !Number.isNaN(reset.getTime()) ? reset : null;
|
|
17751
|
+
}
|
|
17752
|
+
function readReset(runner, output, now) {
|
|
17449
17753
|
const read2 = connectorFor(runner)?.usageLimitReset;
|
|
17450
17754
|
if (!read2) return null;
|
|
17451
|
-
let reset;
|
|
17452
17755
|
try {
|
|
17453
|
-
|
|
17756
|
+
return read2(output.slice(-USAGE_LIMIT_TAIL), now);
|
|
17454
17757
|
} catch {
|
|
17455
17758
|
return null;
|
|
17456
17759
|
}
|
|
17457
|
-
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
17458
|
-
return reset;
|
|
17459
17760
|
}
|
|
17460
17761
|
var DAY_MS = 24 * 60 * 6e4;
|
|
17461
17762
|
|
|
@@ -17636,15 +17937,15 @@ var ReplySplitter = class {
|
|
|
17636
17937
|
push(segmentId, text3) {
|
|
17637
17938
|
const state = this.segments.get(segmentId);
|
|
17638
17939
|
if (state === "text" || state === "plan") return { route: state, text: text3 };
|
|
17639
|
-
const
|
|
17640
|
-
const opensWithObject = opensWithJsonObject(
|
|
17940
|
+
const held2 = (state?.held ?? "") + text3;
|
|
17941
|
+
const opensWithObject = opensWithJsonObject(held2);
|
|
17641
17942
|
if (opensWithObject === void 0) {
|
|
17642
|
-
this.segments.set(segmentId, { held });
|
|
17943
|
+
this.segments.set(segmentId, { held: held2 });
|
|
17643
17944
|
return null;
|
|
17644
17945
|
}
|
|
17645
17946
|
const route = opensWithObject ? "plan" : "text";
|
|
17646
17947
|
this.segments.set(segmentId, route);
|
|
17647
|
-
return { route, text:
|
|
17948
|
+
return { route, text: held2 };
|
|
17648
17949
|
}
|
|
17649
17950
|
};
|
|
17650
17951
|
var TurnStream = class {
|
|
@@ -17799,6 +18100,7 @@ function plannerTranscript(history) {
|
|
|
17799
18100
|
const loadsOf = /* @__PURE__ */ new Map();
|
|
17800
18101
|
let owner = -1;
|
|
17801
18102
|
for (const entry of history) {
|
|
18103
|
+
if (entry.kind === "error") continue;
|
|
17802
18104
|
if (entry.kind === "skill_load" && entry.skill) {
|
|
17803
18105
|
const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
|
|
17804
18106
|
if (loads) loads.push(entry.skill);
|
|
@@ -17887,6 +18189,8 @@ function handoffOf(run) {
|
|
|
17887
18189
|
path: repo.path,
|
|
17888
18190
|
integrationBranch: repo.integrationBranch,
|
|
17889
18191
|
baseRef: repo.baseRef,
|
|
18192
|
+
...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
|
|
18193
|
+
...repo.startedDetached ? { startedDetached: true } : {},
|
|
17890
18194
|
landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
|
|
17891
18195
|
})),
|
|
17892
18196
|
landed: merged.map(entry)
|
|
@@ -19216,6 +19520,9 @@ ${task.prompt ?? task.description}`;
|
|
|
19216
19520
|
}
|
|
19217
19521
|
|
|
19218
19522
|
// src/services/continuation.ts
|
|
19523
|
+
function sessionOwner(task) {
|
|
19524
|
+
return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
|
|
19525
|
+
}
|
|
19219
19526
|
function continuability(task) {
|
|
19220
19527
|
const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
|
|
19221
19528
|
if (task.type !== "ai") return refuse("it is not run by an agent.");
|
|
@@ -19225,6 +19532,10 @@ function continuability(task) {
|
|
|
19225
19532
|
const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
|
|
19226
19533
|
if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
|
|
19227
19534
|
if (!task.runnerSessionId) return refuse("its runner left no saved session to resume. Use Retry instead.");
|
|
19535
|
+
const owner = sessionOwner(task);
|
|
19536
|
+
if (owner && owner !== task.assignedRunner) {
|
|
19537
|
+
return refuse(`its saved session belongs to ${owner}; Retry starts fresh on ${task.assignedRunner} and carries over its files.`);
|
|
19538
|
+
}
|
|
19228
19539
|
return { ok: true, sessionId: task.runnerSessionId };
|
|
19229
19540
|
}
|
|
19230
19541
|
function canContinue(task) {
|
|
@@ -19348,6 +19659,7 @@ function serializePlan(plan) {
|
|
|
19348
19659
|
runners: plan.runners,
|
|
19349
19660
|
generatedAt: plan.generatedAt,
|
|
19350
19661
|
runSettings: plan.runSettings,
|
|
19662
|
+
...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
|
|
19351
19663
|
conversationHistory: surfaceConversation(plan.conversationHistory),
|
|
19352
19664
|
prdMarkdown: plan.prdMarkdown
|
|
19353
19665
|
};
|
|
@@ -19388,6 +19700,70 @@ function markOf(was, now) {
|
|
|
19388
19700
|
return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
|
|
19389
19701
|
}
|
|
19390
19702
|
|
|
19703
|
+
// src/services/AssignmentScope.ts
|
|
19704
|
+
var OVER = /* @__PURE__ */ new Set(["completed", "skipped"]);
|
|
19705
|
+
function assignmentIssue(task, scope) {
|
|
19706
|
+
if (task.type !== "ai" || OVER.has(task.status)) return null;
|
|
19707
|
+
if (!scope.enabledRunners.includes(task.assignedRunner)) return { kind: "runner-off", runner: task.assignedRunner };
|
|
19708
|
+
const model = task.assignedModel;
|
|
19709
|
+
const allowed = effectiveAllowlist(scope.allowlist[task.assignedRunner], task.assignedRunner, scope.models);
|
|
19710
|
+
if (model && allowed && !allowed.includes(model.modelId)) {
|
|
19711
|
+
return { kind: "model-off-list", runner: task.assignedRunner, modelId: model.modelId, modelLabel: model.modelLabel || model.modelId };
|
|
19712
|
+
}
|
|
19713
|
+
return null;
|
|
19714
|
+
}
|
|
19715
|
+
function assignmentIssueText(issue) {
|
|
19716
|
+
return issue.kind === "runner-off" ? `${issue.runner} is switched off` : `${issue.modelLabel} is not in the allowlist`;
|
|
19717
|
+
}
|
|
19718
|
+
function assignmentConsequence({ status, awaitingReason, resumeAt }) {
|
|
19719
|
+
if (status === "failed") return "failed; a retry would run it there unless moved";
|
|
19720
|
+
if (status === "awaiting_user" && awaitingReason === "usage-limit") {
|
|
19721
|
+
return resumeAt ? "paused on a usage limit; resumes there at the reset unless moved" : "paused on a usage limit; a retry would run it there unless moved";
|
|
19722
|
+
}
|
|
19723
|
+
if (status === "awaiting_user" && (awaitingReason === "conflict" || awaitingReason === "files-changed")) return "waiting for the user; what runs next runs there unless moved";
|
|
19724
|
+
if (status === "in_progress" || status === "awaiting_user") return "already running as assigned";
|
|
19725
|
+
return "runs as assigned unless moved";
|
|
19726
|
+
}
|
|
19727
|
+
|
|
19728
|
+
// src/services/RunnerChange.ts
|
|
19729
|
+
var sameSet = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((id) => b.includes(id));
|
|
19730
|
+
function scopeChange(told, now) {
|
|
19731
|
+
const switchedOn = now.enabledRunners.filter((r) => !told.enabledRunners.includes(r));
|
|
19732
|
+
const switchedOff = told.enabledRunners.filter((r) => !now.enabledRunners.includes(r));
|
|
19733
|
+
const models = now.models;
|
|
19734
|
+
const allowlists = [.../* @__PURE__ */ new Set([...Object.keys(told.allowlist), ...Object.keys(now.allowlist)])].map((runner) => ({
|
|
19735
|
+
runner,
|
|
19736
|
+
before: effectiveAllowlist(told.allowlist[runner], runner, models),
|
|
19737
|
+
after: effectiveAllowlist(now.allowlist[runner], runner, models)
|
|
19738
|
+
})).filter((c) => !sameSet(c.before, c.after));
|
|
19739
|
+
return switchedOn.length || switchedOff.length || allowlists.length ? { switchedOn, switchedOff, allowlists } : null;
|
|
19740
|
+
}
|
|
19741
|
+
function flaggedTasks(rows, scope) {
|
|
19742
|
+
return rows.flatMap(({ ref, task }) => {
|
|
19743
|
+
const issue = assignmentIssue(task, scope);
|
|
19744
|
+
return issue ? [{ ref, title: task.title, task, text: assignmentIssueText(issue), status: task.status, awaitingReason: task.awaitingReason, resumeAt: task.resumeAt }] : [];
|
|
19745
|
+
});
|
|
19746
|
+
}
|
|
19747
|
+
function allowlistLine({ runner, before, after }) {
|
|
19748
|
+
if (!after) return `${runner} is no longer limited: any of its models may be assigned.`;
|
|
19749
|
+
if (!before) return `${runner} is now limited to: ${after.join(", ")}.`;
|
|
19750
|
+
const added = after.filter((id) => !before.includes(id));
|
|
19751
|
+
const removed = before.filter((id) => !after.includes(id));
|
|
19752
|
+
return `${runner} models: ${[added.length ? `now allowed ${added.join(", ")}` : "", removed.length ? `no longer allowed ${removed.join(", ")}` : ""].filter(Boolean).join("; ")}.`;
|
|
19753
|
+
}
|
|
19754
|
+
function runnersChangedBlock(change, flagged, tools) {
|
|
19755
|
+
return [
|
|
19756
|
+
"<runners_changed>",
|
|
19757
|
+
"The user changed which runners and models you may assign since your last turn.",
|
|
19758
|
+
change.switchedOn.length ? `Runners switched on: ${change.switchedOn.join(", ")}.` : "",
|
|
19759
|
+
change.switchedOff.length ? `Runners switched off: ${change.switchedOff.join(", ")}.` : "",
|
|
19760
|
+
...change.allowlists.map(allowlistLine),
|
|
19761
|
+
flagged.length ? ["These tasks are now outside that:", ...flagged.map((f) => `- ${f.ref} "${f.title}" (${f.text}) \u2014 ${assignmentConsequence(f)}`)].join("\n") : "",
|
|
19762
|
+
`${tools ? "list_runners and list_models give" : "<available_models> shows"} what you may assign now.${flagged.length ? " Consider moving the tasks above to an enabled runner and an allowed model, or tell the user why you left them." : ""}`,
|
|
19763
|
+
"</runners_changed>"
|
|
19764
|
+
].filter(Boolean).join("\n");
|
|
19765
|
+
}
|
|
19766
|
+
|
|
19391
19767
|
// src/services/PlannerConversation.ts
|
|
19392
19768
|
var CATALOG_MODEL_CAP = 100;
|
|
19393
19769
|
var PLANNER_TOOLS_REMINDER = "Runners, models and modes may have changed since you last read them: call list_runners and list_models just before submit_plan.";
|
|
@@ -19452,6 +19828,17 @@ var PlannerTurnFailedError = class extends Error {
|
|
|
19452
19828
|
this.name = "PlannerTurnFailedError";
|
|
19453
19829
|
}
|
|
19454
19830
|
};
|
|
19831
|
+
var PlannerUsageLimitError = class extends PlannerTurnFailedError {
|
|
19832
|
+
resendAt;
|
|
19833
|
+
/** Why nothing is sent again on its own, when nothing is. */
|
|
19834
|
+
held;
|
|
19835
|
+
constructor(limit) {
|
|
19836
|
+
super(limit.text);
|
|
19837
|
+
this.name = "PlannerUsageLimitError";
|
|
19838
|
+
this.resendAt = limit.resendAt;
|
|
19839
|
+
if (limit.held) this.held = limit.held;
|
|
19840
|
+
}
|
|
19841
|
+
};
|
|
19455
19842
|
var PlannerTurnStoppedError = class extends Error {
|
|
19456
19843
|
constructor(options) {
|
|
19457
19844
|
super("The planner turn was stopped.", options);
|
|
@@ -19477,6 +19864,8 @@ var PlannerConversation = class {
|
|
|
19477
19864
|
savedInBackground = 0;
|
|
19478
19865
|
/** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
|
|
19479
19866
|
toldAutorun = false;
|
|
19867
|
+
/** The runners and allowlist the planner was last told of: its opening prompt, or a turn's block since. Null until a conversation opens. */
|
|
19868
|
+
toldScope = null;
|
|
19480
19869
|
live = null;
|
|
19481
19870
|
openTurn = null;
|
|
19482
19871
|
get transcript() {
|
|
@@ -19592,8 +19981,8 @@ var PlannerConversation = class {
|
|
|
19592
19981
|
* from the moment it is handed in until it lands, is refused, or the turn
|
|
19593
19982
|
* ends: the backend's reply claims it, but it has not landed by then.
|
|
19594
19983
|
*/
|
|
19595
|
-
showHeld(turn,
|
|
19596
|
-
const result =
|
|
19984
|
+
showHeld(turn, held2) {
|
|
19985
|
+
const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
|
|
19597
19986
|
if (!result?.ok) {
|
|
19598
19987
|
this.clearHeld(turn);
|
|
19599
19988
|
return;
|
|
@@ -19612,9 +20001,9 @@ var PlannerConversation = class {
|
|
|
19612
20001
|
* a whole plan, which an edit cannot be layered on.
|
|
19613
20002
|
*/
|
|
19614
20003
|
pendingOps() {
|
|
19615
|
-
const
|
|
19616
|
-
if (!
|
|
19617
|
-
return
|
|
20004
|
+
const held2 = this.openTurn?.submitted;
|
|
20005
|
+
if (!held2) return [];
|
|
20006
|
+
return held2.kind === "task_ops" ? held2.ops : null;
|
|
19618
20007
|
}
|
|
19619
20008
|
/**
|
|
19620
20009
|
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
@@ -19654,7 +20043,8 @@ var PlannerConversation = class {
|
|
|
19654
20043
|
history: plan.conversationHistory,
|
|
19655
20044
|
researchLog: plan.researchLog,
|
|
19656
20045
|
persisted: this.persisted,
|
|
19657
|
-
savedInBackground: this.savedInBackground
|
|
20046
|
+
savedInBackground: this.savedInBackground,
|
|
20047
|
+
toldScope: this.toldScope
|
|
19658
20048
|
};
|
|
19659
20049
|
}
|
|
19660
20050
|
/**
|
|
@@ -19668,7 +20058,9 @@ var PlannerConversation = class {
|
|
|
19668
20058
|
* the undo is saved in turn, or a reload would bring the writes back.
|
|
19669
20059
|
*/
|
|
19670
20060
|
restore(snapshot) {
|
|
19671
|
-
if (this.host.plan() !== snapshot.plan
|
|
20061
|
+
if (this.host.plan() !== snapshot.plan) return false;
|
|
20062
|
+
this.toldScope = snapshot.toldScope;
|
|
20063
|
+
if (this.persisted !== snapshot.persisted) return false;
|
|
19672
20064
|
const undo = () => {
|
|
19673
20065
|
const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
|
|
19674
20066
|
snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
|
|
@@ -19703,8 +20095,9 @@ var PlannerConversation = class {
|
|
|
19703
20095
|
* at it.
|
|
19704
20096
|
*/
|
|
19705
20097
|
rewindTargets() {
|
|
20098
|
+
const opener = this.transcript.findIndex((m) => m.kind !== "error");
|
|
19706
20099
|
return this.transcript.flatMap((m, index) => {
|
|
19707
|
-
if (!isUserMessage(m) || index ===
|
|
20100
|
+
if (!isUserMessage(m) || index === opener) return [];
|
|
19708
20101
|
const line = m.content.split("\n")[0];
|
|
19709
20102
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
19710
20103
|
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
@@ -19831,8 +20224,10 @@ var PlannerConversation = class {
|
|
|
19831
20224
|
return this.userTurn("supersede", goal, signal, async (userTurn) => {
|
|
19832
20225
|
const opening = await prepare();
|
|
19833
20226
|
this.assertCurrent(userTurn);
|
|
20227
|
+
userTurn.checkpoint = this.snapshot();
|
|
19834
20228
|
this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
|
|
19835
20229
|
this.toldAutorun = opening.autorun ?? false;
|
|
20230
|
+
this.toldScope = this.host.assignmentScope();
|
|
19836
20231
|
const turn = await this.host.aiService().startConversation({
|
|
19837
20232
|
...opening,
|
|
19838
20233
|
// The system prompt's goal is the text as typed; skill bodies ride the
|
|
@@ -19842,7 +20237,9 @@ var PlannerConversation = class {
|
|
|
19842
20237
|
onProgress: userTurn.stream.sink(),
|
|
19843
20238
|
signal: userTurn.signal
|
|
19844
20239
|
});
|
|
19845
|
-
|
|
20240
|
+
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
20241
|
+
this.assertNotLimited(settleable, userTurn);
|
|
20242
|
+
return this.settle(settleable, userTurn);
|
|
19846
20243
|
});
|
|
19847
20244
|
}
|
|
19848
20245
|
/**
|
|
@@ -19875,13 +20272,16 @@ var PlannerConversation = class {
|
|
|
19875
20272
|
// anything the turn appends as it runs goes after this point.
|
|
19876
20273
|
at: priorHistory.length
|
|
19877
20274
|
};
|
|
19878
|
-
const
|
|
20275
|
+
const toldBefore = this.toldScope;
|
|
20276
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
|
|
19879
20277
|
const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
|
|
19880
20278
|
const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
|
|
19881
20279
|
const ai = this.host.aiService();
|
|
19882
20280
|
try {
|
|
19883
20281
|
const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
|
|
19884
20282
|
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
20283
|
+
if (settleable.kind === "message" && (settleable.failed || userTurn.signal.aborted)) this.toldScope = toldBefore;
|
|
20284
|
+
this.assertNotLimited(settleable, userTurn);
|
|
19885
20285
|
if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
|
|
19886
20286
|
this.assertCurrent(userTurn, settleable);
|
|
19887
20287
|
return await this.settle(settleable, userTurn);
|
|
@@ -19900,6 +20300,34 @@ var PlannerConversation = class {
|
|
|
19900
20300
|
if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
|
|
19901
20301
|
if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
|
|
19902
20302
|
}
|
|
20303
|
+
/**
|
|
20304
|
+
* A turn that ended on the planner's usage limit lands nothing. What it
|
|
20305
|
+
* wrote is taken back, its failure is kept in its place, and the limit's
|
|
20306
|
+
* wait is armed in the same save. Only a turn Ordewell started gets here,
|
|
20307
|
+
* so a limit is never acted on for a planner nobody asked anything of.
|
|
20308
|
+
*/
|
|
20309
|
+
assertNotLimited(turn, userTurn) {
|
|
20310
|
+
if (turn.kind !== "message" || !turn.failed || userTurn.signal.aborted) return;
|
|
20311
|
+
const said2 = { message: turn.text, usageLimit: turn.usageLimit };
|
|
20312
|
+
if (classifyRunnerStop(said2) !== "usage-limit") return;
|
|
20313
|
+
this.assertCurrent(userTurn);
|
|
20314
|
+
if (userTurn.checkpoint) this.restore(userTurn.checkpoint);
|
|
20315
|
+
const limit = this.host.usageLimit(said2);
|
|
20316
|
+
this.host.mutate(() => {
|
|
20317
|
+
this.recordFailure(limit.text);
|
|
20318
|
+
return true;
|
|
20319
|
+
}, () => this.host.broadcastPlan());
|
|
20320
|
+
throw new PlannerUsageLimitError(limit);
|
|
20321
|
+
}
|
|
20322
|
+
/**
|
|
20323
|
+
* A failure kept in the transcript, drawn as one on reload too, and never
|
|
20324
|
+
* replayed to the planner. Stamped after the newest entry: surfaces take
|
|
20325
|
+
* only entries newer than the last they synced.
|
|
20326
|
+
*/
|
|
20327
|
+
recordFailure(text3) {
|
|
20328
|
+
const newest = this.transcript.reduce((latest, e) => Math.max(latest, Date.parse(e.timestamp) || 0), 0);
|
|
20329
|
+
this.append("assistant", text3, { kind: "error", timestamp: new Date(Math.max(Date.now(), newest + 1)).toISOString() });
|
|
20330
|
+
}
|
|
19903
20331
|
assertIdle(operation) {
|
|
19904
20332
|
if (this.isTurnInFlight) throw new ConversationBusyError(operation);
|
|
19905
20333
|
}
|
|
@@ -20041,6 +20469,7 @@ var PlannerConversation = class {
|
|
|
20041
20469
|
const goal = opener || this.host.goal() || message;
|
|
20042
20470
|
this.reset();
|
|
20043
20471
|
this.toldAutorun = opening.autorun ?? false;
|
|
20472
|
+
this.toldScope = this.host.assignmentScope();
|
|
20044
20473
|
return this.host.aiService().startConversation({
|
|
20045
20474
|
...opening,
|
|
20046
20475
|
runners,
|
|
@@ -20279,6 +20708,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
20279
20708
|
this.toldAutorun = now;
|
|
20280
20709
|
return now ? AUTORUN_ON : AUTORUN_OFF;
|
|
20281
20710
|
}
|
|
20711
|
+
/**
|
|
20712
|
+
* The user's runner or allowlist change, when the planner was not told yet —
|
|
20713
|
+
* the same once-per-change rule as autorun, for API and harness planners
|
|
20714
|
+
* alike. Tasks the change leaves outside the scope are named, not touched.
|
|
20715
|
+
*
|
|
20716
|
+
* A reloaded session is not told: {@link resume} takes the scope as it is
|
|
20717
|
+
* then, as it does the autorun rule, since the catalog the first turn carries
|
|
20718
|
+
* and `list_runners` already show the runners as they are now.
|
|
20719
|
+
*/
|
|
20720
|
+
runnersChangedBlock() {
|
|
20721
|
+
const now = this.host.assignmentScope();
|
|
20722
|
+
const told = this.toldScope;
|
|
20723
|
+
this.toldScope = now;
|
|
20724
|
+
const change = told && scopeChange(told, now);
|
|
20725
|
+
if (!change) return null;
|
|
20726
|
+
const flagged = flaggedTasks(flattenTasksWithParents(this.host.tasks()), now);
|
|
20727
|
+
return runnersChangedBlock(change, flagged, this.host.aiService().plannerToolsAttached?.() ?? false);
|
|
20728
|
+
}
|
|
20282
20729
|
runUpdatesBlock() {
|
|
20283
20730
|
const updates = this.host.takeRunUpdates();
|
|
20284
20731
|
return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
|
|
@@ -20359,7 +20806,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
|
|
|
20359
20806
|
var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
|
|
20360
20807
|
var HUNG_TURN_GRACE_MS = 3e4;
|
|
20361
20808
|
var DEFAULT_WAKE_UP_READ_BUDGET = 12;
|
|
20362
|
-
var RESET_MARGIN_MS = 6e4;
|
|
20363
20809
|
var DETAIL_CHARS = 240;
|
|
20364
20810
|
var WakeUpTimeoutError = class extends Error {
|
|
20365
20811
|
constructor(timeoutMs) {
|
|
@@ -20385,8 +20831,6 @@ var Supervisor = class {
|
|
|
20385
20831
|
/** The run's end was reported: the scheduler can announce it more than once. */
|
|
20386
20832
|
finished = false;
|
|
20387
20833
|
runEndPending = false;
|
|
20388
|
-
/** While the planner's usage limit holds wake-ups back: when it lets them go. */
|
|
20389
|
-
limited = null;
|
|
20390
20834
|
/** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
|
|
20391
20835
|
wakeUps = [];
|
|
20392
20836
|
debounceMs;
|
|
@@ -20407,9 +20851,9 @@ var Supervisor = class {
|
|
|
20407
20851
|
get awaitsRunEnd() {
|
|
20408
20852
|
return this.runEndPending;
|
|
20409
20853
|
}
|
|
20410
|
-
/** Whether wake-ups alone may open a turn now: the supervisor watches
|
|
20854
|
+
/** Whether wake-ups alone may open a turn now: the supervisor watches. A usage limit holds every turn, and is the planner turns' to wait out. */
|
|
20411
20855
|
mayWake() {
|
|
20412
|
-
return this.watching
|
|
20856
|
+
return this.watching;
|
|
20413
20857
|
}
|
|
20414
20858
|
/**
|
|
20415
20859
|
* What a wake-up turn is held to under the run's level. The brief is the
|
|
@@ -20476,8 +20920,6 @@ var Supervisor = class {
|
|
|
20476
20920
|
/** The plan these events were about is gone: nothing gathered or waited on carries over. */
|
|
20477
20921
|
reset() {
|
|
20478
20922
|
this.disarm();
|
|
20479
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
20480
|
-
this.limited = null;
|
|
20481
20923
|
this.failures = 0;
|
|
20482
20924
|
this.finished = false;
|
|
20483
20925
|
this.runEndPending = false;
|
|
@@ -20528,9 +20970,13 @@ var Supervisor = class {
|
|
|
20528
20970
|
}
|
|
20529
20971
|
failed(err) {
|
|
20530
20972
|
if (err instanceof PlannerTurnStoppedError || err instanceof PlannerTurnDiscardedError) return;
|
|
20973
|
+
if (err instanceof PlannerUsageLimitError) {
|
|
20974
|
+
if (err.resendAt === null) this.giveUp(`the planner hit its usage limit and ${err.held ?? "did not say when it resets"}`);
|
|
20975
|
+
return;
|
|
20976
|
+
}
|
|
20531
20977
|
const reason = err instanceof Error ? err.message : String(err);
|
|
20532
|
-
if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
20533
|
-
this.
|
|
20978
|
+
if (!(err instanceof WakeUpTimeoutError) && !(err instanceof PlannerTurnFailedError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
20979
|
+
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
20534
20980
|
return;
|
|
20535
20981
|
}
|
|
20536
20982
|
this.failures++;
|
|
@@ -20541,22 +20987,6 @@ var Supervisor = class {
|
|
|
20541
20987
|
}
|
|
20542
20988
|
this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
|
|
20543
20989
|
}
|
|
20544
|
-
waitForReset(said2) {
|
|
20545
|
-
const runner = this.host.plannerRunner();
|
|
20546
|
-
const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
|
|
20547
|
-
if (!reset) {
|
|
20548
|
-
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
20549
|
-
return;
|
|
20550
|
-
}
|
|
20551
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
20552
|
-
const until = reset.getTime() + RESET_MARGIN_MS;
|
|
20553
|
-
const timer = setTimeout(() => {
|
|
20554
|
-
this.limited = null;
|
|
20555
|
-
this.host.pump();
|
|
20556
|
-
}, Math.max(0, until - this.now()));
|
|
20557
|
-
this.limited = { until, timer };
|
|
20558
|
-
this.host.notice("info", `The planner hit its usage limit. The supervisor's wake-ups wait until ${new Date(until).toISOString()}, and are delivered together then.`);
|
|
20559
|
-
}
|
|
20560
20990
|
/** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
|
|
20561
20991
|
giveUp(reason) {
|
|
20562
20992
|
this.host.goUnsupervised(reason);
|
|
@@ -20570,8 +21000,8 @@ var Supervisor = class {
|
|
|
20570
21000
|
standDown() {
|
|
20571
21001
|
this.disarm();
|
|
20572
21002
|
this.runEndPending = false;
|
|
20573
|
-
const
|
|
20574
|
-
if (
|
|
21003
|
+
const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
|
|
21004
|
+
if (held2) this.deliver(held2);
|
|
20575
21005
|
}
|
|
20576
21006
|
arm() {
|
|
20577
21007
|
const due = this.policy.dueAt();
|
|
@@ -20762,78 +21192,224 @@ function runSettingsOf(settings) {
|
|
|
20762
21192
|
}
|
|
20763
21193
|
return out;
|
|
20764
21194
|
}
|
|
21195
|
+
var SAVE_TRIES = 5;
|
|
21196
|
+
var LOCK_STALE_MS = 2e3;
|
|
21197
|
+
var LOCK_TIMEOUT_MS = 1e4;
|
|
21198
|
+
var LOCK_POLL_MS = 5;
|
|
21199
|
+
var LOCK_POLL_MAX_MS = 100;
|
|
21200
|
+
function sleepSync(ms) {
|
|
21201
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
21202
|
+
}
|
|
21203
|
+
function withFileLock(file, swap) {
|
|
21204
|
+
const lock = `${file}.lock`;
|
|
21205
|
+
const token = `${process.pid}:${(0, import_crypto10.randomUUID)()}`;
|
|
21206
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
21207
|
+
let last = null;
|
|
21208
|
+
let poll = LOCK_POLL_MS;
|
|
21209
|
+
for (; ; ) {
|
|
21210
|
+
try {
|
|
21211
|
+
const fd = fs10.openSync(lock, "wx");
|
|
21212
|
+
try {
|
|
21213
|
+
fs10.writeSync(fd, token);
|
|
21214
|
+
} finally {
|
|
21215
|
+
fs10.closeSync(fd);
|
|
21216
|
+
}
|
|
21217
|
+
break;
|
|
21218
|
+
} catch (err) {
|
|
21219
|
+
if (err.code !== "EEXIST") throw err;
|
|
21220
|
+
}
|
|
21221
|
+
if (Date.now() >= deadline) {
|
|
21222
|
+
throw new Error(`Cannot save the settings file: ${lock} is still held after ${Math.round(LOCK_TIMEOUT_MS / 1e3)} seconds by a process that is not finishing, or it cannot be removed. Check for a process stuck saving there, and delete the file if there is none.`);
|
|
21223
|
+
}
|
|
21224
|
+
let holder;
|
|
21225
|
+
let mtimeMs;
|
|
21226
|
+
try {
|
|
21227
|
+
holder = fs10.readFileSync(lock, "utf8");
|
|
21228
|
+
mtimeMs = fs10.statSync(lock).mtimeMs;
|
|
21229
|
+
} catch (err) {
|
|
21230
|
+
if (gone(err)) {
|
|
21231
|
+
last = null;
|
|
21232
|
+
continue;
|
|
21233
|
+
}
|
|
21234
|
+
throw err;
|
|
21235
|
+
}
|
|
21236
|
+
const stale = Date.now() - mtimeMs > LOCK_STALE_MS;
|
|
21237
|
+
if (last && last.token === holder && last.mtimeMs === mtimeMs && stale) {
|
|
21238
|
+
unlinkLock(lock);
|
|
21239
|
+
last = null;
|
|
21240
|
+
continue;
|
|
21241
|
+
}
|
|
21242
|
+
last = stale ? { token: holder, mtimeMs } : null;
|
|
21243
|
+
sleepSync(poll);
|
|
21244
|
+
poll = Math.min(poll * 2, LOCK_POLL_MAX_MS);
|
|
21245
|
+
}
|
|
21246
|
+
try {
|
|
21247
|
+
return swap();
|
|
21248
|
+
} finally {
|
|
21249
|
+
releaseLock(lock, token);
|
|
21250
|
+
}
|
|
21251
|
+
}
|
|
21252
|
+
function gone(err) {
|
|
21253
|
+
return err.code === "ENOENT";
|
|
21254
|
+
}
|
|
21255
|
+
function releaseLock(lock, token) {
|
|
21256
|
+
let holder;
|
|
21257
|
+
try {
|
|
21258
|
+
holder = fs10.readFileSync(lock, "utf8");
|
|
21259
|
+
} catch (err) {
|
|
21260
|
+
if (gone(err)) return;
|
|
21261
|
+
throw err;
|
|
21262
|
+
}
|
|
21263
|
+
if (holder === token) unlinkLock(lock);
|
|
21264
|
+
}
|
|
21265
|
+
function unlinkLock(lock) {
|
|
21266
|
+
try {
|
|
21267
|
+
fs10.unlinkSync(lock);
|
|
21268
|
+
} catch (err) {
|
|
21269
|
+
if (!gone(err)) throw err;
|
|
21270
|
+
}
|
|
21271
|
+
}
|
|
20765
21272
|
var SettingsService = class {
|
|
20766
21273
|
filePath;
|
|
20767
21274
|
cache = null;
|
|
20768
|
-
|
|
21275
|
+
cachedStamp = null;
|
|
20769
21276
|
constructor(filePath = getSettingsPath()) {
|
|
20770
21277
|
this.filePath = filePath;
|
|
20771
21278
|
}
|
|
20772
21279
|
// Several processes share the settings file (VS Code extension host, web
|
|
20773
|
-
// server, CLI), so the cache is keyed on the file's
|
|
21280
|
+
// server, CLI), so the cache is keyed on the file's identity: a write from any
|
|
20774
21281
|
// other process invalidates it. A stale cache here previously meant an
|
|
20775
21282
|
// allowlist set in one surface never reached another's planner.
|
|
20776
21283
|
getAll() {
|
|
20777
|
-
const
|
|
20778
|
-
if (this.cache &&
|
|
21284
|
+
const stamp = this.fileStamp();
|
|
21285
|
+
if (this.cache && stamp === this.cachedStamp) return this.cache;
|
|
20779
21286
|
this.cache = this.load();
|
|
20780
|
-
this.
|
|
21287
|
+
this.cachedStamp = stamp;
|
|
20781
21288
|
return this.cache;
|
|
20782
21289
|
}
|
|
20783
|
-
|
|
21290
|
+
/**
|
|
21291
|
+
* Every write replaces the file, so its inode changes with it; the mtime
|
|
21292
|
+
* alone cannot tell two writes apart on a filesystem whose timestamps are
|
|
21293
|
+
* coarse, and a write that shares the last one's tick stayed unseen.
|
|
21294
|
+
*/
|
|
21295
|
+
fileStamp(file = this.filePath) {
|
|
20784
21296
|
try {
|
|
20785
|
-
|
|
21297
|
+
const stat = fs10.statSync(file);
|
|
21298
|
+
return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
20786
21299
|
} catch {
|
|
20787
21300
|
return null;
|
|
20788
21301
|
}
|
|
20789
21302
|
}
|
|
21303
|
+
/**
|
|
21304
|
+
* What a change is applied to: the file as it is now, never a copy held since
|
|
21305
|
+
* an earlier read. Another process may have saved in between, and writing
|
|
21306
|
+
* the whole file back would silently undo it.
|
|
21307
|
+
*/
|
|
21308
|
+
current() {
|
|
21309
|
+
this.cache = null;
|
|
21310
|
+
return this.getAll();
|
|
21311
|
+
}
|
|
21312
|
+
/**
|
|
21313
|
+
* Calls `listener` when another process (or this one) saves the file. The
|
|
21314
|
+
* directory is watched, not the file: a write that swaps the file in leaves
|
|
21315
|
+
* a watch on the old one pointing at nothing.
|
|
21316
|
+
*/
|
|
21317
|
+
watch(listener) {
|
|
21318
|
+
const dir = path26.dirname(this.filePath);
|
|
21319
|
+
const name = path26.basename(this.filePath);
|
|
21320
|
+
let seen = this.fileStamp();
|
|
21321
|
+
let timer;
|
|
21322
|
+
let watcher;
|
|
21323
|
+
try {
|
|
21324
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
21325
|
+
watcher = fs10.watch(dir, (_event, changed) => {
|
|
21326
|
+
if (changed && changed !== name) return;
|
|
21327
|
+
clearTimeout(timer);
|
|
21328
|
+
timer = setTimeout(() => {
|
|
21329
|
+
const stamp = this.fileStamp();
|
|
21330
|
+
if (stamp === seen) return;
|
|
21331
|
+
seen = stamp;
|
|
21332
|
+
listener();
|
|
21333
|
+
}, 50);
|
|
21334
|
+
});
|
|
21335
|
+
} catch {
|
|
21336
|
+
return { dispose() {
|
|
21337
|
+
} };
|
|
21338
|
+
}
|
|
21339
|
+
return { dispose: () => {
|
|
21340
|
+
clearTimeout(timer);
|
|
21341
|
+
watcher.close();
|
|
21342
|
+
} };
|
|
21343
|
+
}
|
|
20790
21344
|
getModelAllowlist(runner) {
|
|
20791
21345
|
return this.getAll().modelAllowlist?.[runner];
|
|
20792
21346
|
}
|
|
20793
21347
|
setModelAllowlist(runner, ids) {
|
|
20794
|
-
this.
|
|
20795
|
-
|
|
20796
|
-
|
|
20797
|
-
|
|
20798
|
-
|
|
20799
|
-
|
|
21348
|
+
this.save(() => {
|
|
21349
|
+
if (ids === void 0) {
|
|
21350
|
+
if (this.cache.modelAllowlist) {
|
|
21351
|
+
delete this.cache.modelAllowlist[runner];
|
|
21352
|
+
if (Object.keys(this.cache.modelAllowlist).length === 0) {
|
|
21353
|
+
this.cache.modelAllowlist = void 0;
|
|
21354
|
+
}
|
|
20800
21355
|
}
|
|
21356
|
+
} else {
|
|
21357
|
+
if (!this.cache.modelAllowlist) {
|
|
21358
|
+
this.cache.modelAllowlist = {};
|
|
21359
|
+
}
|
|
21360
|
+
this.cache.modelAllowlist[runner] = ids;
|
|
20801
21361
|
}
|
|
20802
|
-
}
|
|
20803
|
-
if (!this.cache.modelAllowlist) {
|
|
20804
|
-
this.cache.modelAllowlist = {};
|
|
20805
|
-
}
|
|
20806
|
-
this.cache.modelAllowlist[runner] = ids;
|
|
20807
|
-
}
|
|
20808
|
-
this.persist();
|
|
21362
|
+
});
|
|
20809
21363
|
}
|
|
20810
21364
|
getPlannerModel(provider) {
|
|
20811
21365
|
return this.getAll().plannerModels?.[provider];
|
|
20812
21366
|
}
|
|
20813
21367
|
setPlannerModel(provider, entry) {
|
|
20814
|
-
this.
|
|
20815
|
-
|
|
20816
|
-
|
|
20817
|
-
|
|
20818
|
-
|
|
20819
|
-
|
|
21368
|
+
this.save(() => {
|
|
21369
|
+
if (entry === void 0) {
|
|
21370
|
+
if (this.cache.plannerModels) {
|
|
21371
|
+
delete this.cache.plannerModels[provider];
|
|
21372
|
+
if (Object.keys(this.cache.plannerModels).length === 0) {
|
|
21373
|
+
this.cache.plannerModels = void 0;
|
|
21374
|
+
}
|
|
20820
21375
|
}
|
|
21376
|
+
} else {
|
|
21377
|
+
if (!this.cache.plannerModels) {
|
|
21378
|
+
this.cache.plannerModels = {};
|
|
21379
|
+
}
|
|
21380
|
+
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
20821
21381
|
}
|
|
20822
|
-
}
|
|
20823
|
-
if (!this.cache.plannerModels) {
|
|
20824
|
-
this.cache.plannerModels = {};
|
|
20825
|
-
}
|
|
20826
|
-
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
20827
|
-
}
|
|
20828
|
-
this.persist();
|
|
21382
|
+
});
|
|
20829
21383
|
}
|
|
20830
21384
|
getEnabledRunners() {
|
|
20831
21385
|
return this.getAll().enabledRunners;
|
|
20832
21386
|
}
|
|
20833
21387
|
setEnabledRunners(ids) {
|
|
20834
|
-
this.
|
|
20835
|
-
|
|
20836
|
-
|
|
21388
|
+
this.save(() => {
|
|
21389
|
+
this.cache.enabledRunners = [...ids];
|
|
21390
|
+
});
|
|
21391
|
+
}
|
|
21392
|
+
/**
|
|
21393
|
+
* Change the list from what the file holds at this moment: the change is
|
|
21394
|
+
* handed the saved list (undefined when the user never chose) and its result
|
|
21395
|
+
* is saved in the same call. A list worked out from an earlier read would
|
|
21396
|
+
* save over what another surface set since. It may be handed the list again,
|
|
21397
|
+
* when another process saved before this one could (see {@link save}).
|
|
21398
|
+
*/
|
|
21399
|
+
updateEnabledRunners(change) {
|
|
21400
|
+
return this.save(() => {
|
|
21401
|
+
const next = [...change(this.cache.enabledRunners)];
|
|
21402
|
+
this.cache.enabledRunners = next;
|
|
21403
|
+
return next;
|
|
21404
|
+
});
|
|
21405
|
+
}
|
|
21406
|
+
/** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
|
|
21407
|
+
setEnabledRunnersIfUnset(ids) {
|
|
21408
|
+
return this.save(() => {
|
|
21409
|
+
if (this.cache.enabledRunners !== void 0) return false;
|
|
21410
|
+
this.cache.enabledRunners = [...ids];
|
|
21411
|
+
return true;
|
|
21412
|
+
});
|
|
20837
21413
|
}
|
|
20838
21414
|
/**
|
|
20839
21415
|
* Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
|
|
@@ -20851,9 +21427,9 @@ var SettingsService = class {
|
|
|
20851
21427
|
return env !== void 0 && env.trim() !== "";
|
|
20852
21428
|
}
|
|
20853
21429
|
setAutonomousMode(enabled) {
|
|
20854
|
-
this.
|
|
20855
|
-
|
|
20856
|
-
|
|
21430
|
+
this.save(() => {
|
|
21431
|
+
this.cache.autonomousMode = enabled;
|
|
21432
|
+
});
|
|
20857
21433
|
}
|
|
20858
21434
|
getRunSettings() {
|
|
20859
21435
|
return runSettingsOf(this.getAll());
|
|
@@ -20865,11 +21441,11 @@ var SettingsService = class {
|
|
|
20865
21441
|
throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
|
|
20866
21442
|
}
|
|
20867
21443
|
}
|
|
20868
|
-
this.
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
|
|
21444
|
+
this.save(() => {
|
|
21445
|
+
for (const key of RUN_SETTING_KEYS) {
|
|
21446
|
+
if (changes[key] !== void 0) this.cache[key] = changes[key];
|
|
21447
|
+
}
|
|
21448
|
+
});
|
|
20873
21449
|
return this.getRunSettings();
|
|
20874
21450
|
}
|
|
20875
21451
|
load() {
|
|
@@ -20898,13 +21474,43 @@ var SettingsService = class {
|
|
|
20898
21474
|
}
|
|
20899
21475
|
return {};
|
|
20900
21476
|
}
|
|
20901
|
-
|
|
21477
|
+
/**
|
|
21478
|
+
* Makes `change` to the file as it is now, and saves it. Another process can
|
|
21479
|
+
* save between that read and this write; the file is looked at again just
|
|
21480
|
+
* before ours replaces it, and when it moved the change is made afresh from
|
|
21481
|
+
* what that process saved, so neither save undoes the other. A file that
|
|
21482
|
+
* keeps moving is written over after {@link SAVE_TRIES} tries rather than
|
|
21483
|
+
* never. The look and the swap are one step under the file's lock
|
|
21484
|
+
* ({@link withFileLock}). A change that changes nothing writes nothing.
|
|
21485
|
+
*/
|
|
21486
|
+
save(change) {
|
|
21487
|
+
for (let tries = 1; ; tries++) {
|
|
21488
|
+
const before = JSON.stringify(this.current());
|
|
21489
|
+
const read2 = this.cachedStamp;
|
|
21490
|
+
const result = change();
|
|
21491
|
+
if (JSON.stringify(this.cache) === before || this.persist(tries < SAVE_TRIES ? read2 : void 0)) return result;
|
|
21492
|
+
}
|
|
21493
|
+
}
|
|
21494
|
+
/** False, saving nothing, when the file is no longer the one stamped `expected`. */
|
|
21495
|
+
persist(expected) {
|
|
20902
21496
|
const dir = path26.dirname(this.filePath);
|
|
20903
21497
|
if (!fs10.existsSync(dir)) {
|
|
20904
21498
|
fs10.mkdirSync(dir, { recursive: true });
|
|
20905
21499
|
}
|
|
20906
|
-
|
|
20907
|
-
this.
|
|
21500
|
+
const staging = `${this.filePath}.${process.pid}.tmp`;
|
|
21501
|
+
fs10.writeFileSync(staging, JSON.stringify(this.cache, null, 2));
|
|
21502
|
+
const stamp = this.fileStamp(staging);
|
|
21503
|
+
const swapped = withFileLock(this.filePath, () => {
|
|
21504
|
+
if (expected !== void 0 && this.fileStamp() !== expected) return false;
|
|
21505
|
+
fs10.renameSync(staging, this.filePath);
|
|
21506
|
+
return true;
|
|
21507
|
+
});
|
|
21508
|
+
if (!swapped) {
|
|
21509
|
+
fs10.rmSync(staging, { force: true });
|
|
21510
|
+
return false;
|
|
21511
|
+
}
|
|
21512
|
+
this.cachedStamp = stamp;
|
|
21513
|
+
return true;
|
|
20908
21514
|
}
|
|
20909
21515
|
};
|
|
20910
21516
|
|
|
@@ -21393,7 +21999,7 @@ function capConflictFiles(files, max = 5) {
|
|
|
21393
21999
|
}
|
|
21394
22000
|
|
|
21395
22001
|
// src/services/mergeResultNotice.ts
|
|
21396
|
-
function blockNotice({ repo, reason, files }) {
|
|
22002
|
+
function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
|
|
21397
22003
|
switch (reason) {
|
|
21398
22004
|
case "merge-in-progress":
|
|
21399
22005
|
return `${repo} has a merge in progress`;
|
|
@@ -21403,10 +22009,22 @@ function blockNotice({ repo, reason, files }) {
|
|
|
21403
22009
|
return `${repo} has uncommitted changes to ${files.join(", ")}`;
|
|
21404
22010
|
case "partial-landing":
|
|
21405
22011
|
return `${repo} holds part of a task whose landing could not be rolled back`;
|
|
22012
|
+
case "wrong-branch":
|
|
22013
|
+
return `${repo === SELF_REPO ? "your checkout" : repo} is ${actual ? `on ${actual}` : rebasing ? "in the middle of a rebase" : "on a detached HEAD"}; this run started on ${expected}`;
|
|
21406
22014
|
case "git-error":
|
|
21407
22015
|
return `git could not check ${repo}`;
|
|
21408
22016
|
}
|
|
21409
22017
|
}
|
|
22018
|
+
function branchFix(blocked) {
|
|
22019
|
+
if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
|
|
22020
|
+
const rebasing = blocked.filter((b) => b.rebasing);
|
|
22021
|
+
if (rebasing.length > 0) {
|
|
22022
|
+
const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
|
|
22023
|
+
return ` Finish or abort the rebase in ${where}${rebasing.length < blocked.length ? " and check out the branch each other repository started on" : ""}, then press Merge all again.`;
|
|
22024
|
+
}
|
|
22025
|
+
const started = [...new Set(blocked.map((b) => b.expected))];
|
|
22026
|
+
return started.length === 1 && blocked.length === 1 ? ` Check out ${started[0]} and press Merge all again.` : " Check out the branch each repository started on and press Merge all again.";
|
|
22027
|
+
}
|
|
21410
22028
|
function landedNotice(landed2) {
|
|
21411
22029
|
if (landed2.length === 0) return "Nothing was merged.";
|
|
21412
22030
|
return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
|
|
@@ -21423,7 +22041,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
|
|
|
21423
22041
|
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
21424
22042
|
}
|
|
21425
22043
|
case "blocked":
|
|
21426
|
-
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}
|
|
22044
|
+
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.${branchFix(result.blocked)}` };
|
|
21427
22045
|
case "conflict":
|
|
21428
22046
|
return {
|
|
21429
22047
|
level: "warn",
|
|
@@ -21533,7 +22151,7 @@ ${await file.patch()}`);
|
|
|
21533
22151
|
}
|
|
21534
22152
|
|
|
21535
22153
|
// src/services/GitWorktreeIsolation.ts
|
|
21536
|
-
var
|
|
22154
|
+
var import_crypto11 = require("crypto");
|
|
21537
22155
|
var fs13 = __toESM(require("fs"));
|
|
21538
22156
|
var path29 = __toESM(require("path"));
|
|
21539
22157
|
|
|
@@ -21661,7 +22279,7 @@ var GitWorktreeIsolation = class {
|
|
|
21661
22279
|
this.resolvePath = deps.resolvePath ?? (() => augmentedPath());
|
|
21662
22280
|
this.gitInvoker = { exec: deps.execFileImpl ?? defaultExecFile2, resolvePath: this.resolvePath };
|
|
21663
22281
|
this.platform = deps.platform ?? process.platform;
|
|
21664
|
-
this.mintRunId = deps.mintRunId ?? (() => (0,
|
|
22282
|
+
this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto11.randomBytes)(4).toString("hex"));
|
|
21665
22283
|
}
|
|
21666
22284
|
deps;
|
|
21667
22285
|
resolvePath;
|
|
@@ -21787,7 +22405,7 @@ var GitWorktreeIsolation = class {
|
|
|
21787
22405
|
path: repoPath,
|
|
21788
22406
|
root,
|
|
21789
22407
|
baseRef,
|
|
21790
|
-
...branch ? { baseBranch: branch } : {},
|
|
22408
|
+
...branch ? { baseBranch: branch } : { startedDetached: true },
|
|
21791
22409
|
integrationBranch: integrationBranchFor(run.id)
|
|
21792
22410
|
};
|
|
21793
22411
|
if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
|
|
@@ -22140,7 +22758,9 @@ var GitWorktreeIsolation = class {
|
|
|
22140
22758
|
async mergeAll(run) {
|
|
22141
22759
|
const partial = await this.settleLanding(run);
|
|
22142
22760
|
if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
|
|
22761
|
+
const misplaced = await this.offBaseBranch(run.repos);
|
|
22143
22762
|
const repos = await this.reposWithWork(run);
|
|
22763
|
+
if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
|
|
22144
22764
|
if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
|
|
22145
22765
|
const blocked = [];
|
|
22146
22766
|
for (const repo of repos) {
|
|
@@ -22158,6 +22778,27 @@ var GitWorktreeIsolation = class {
|
|
|
22158
22778
|
}
|
|
22159
22779
|
return withWork;
|
|
22160
22780
|
}
|
|
22781
|
+
/**
|
|
22782
|
+
* A merge goes into whatever is checked out, which is only what the user
|
|
22783
|
+
* meant if it is the branch the run started on. A run that started on a
|
|
22784
|
+
* detached HEAD named no branch, so anything is as good as it was.
|
|
22785
|
+
*/
|
|
22786
|
+
async offBaseBranch(repos) {
|
|
22787
|
+
const off = [];
|
|
22788
|
+
for (const repo of repos) {
|
|
22789
|
+
if (!repo.baseBranch) continue;
|
|
22790
|
+
const current = (await this.tryGit(repo.root, ["symbolic-ref", "--short", "-q", "HEAD"])).stdout.trim();
|
|
22791
|
+
if (current === repo.baseBranch) continue;
|
|
22792
|
+
off.push({
|
|
22793
|
+
repo: repo.path,
|
|
22794
|
+
reason: "wrong-branch",
|
|
22795
|
+
files: [],
|
|
22796
|
+
expected: repo.baseBranch,
|
|
22797
|
+
...current ? { actual: current } : await this.rebaseInProgress(repo.root) ? { rebasing: true } : {}
|
|
22798
|
+
});
|
|
22799
|
+
}
|
|
22800
|
+
return off;
|
|
22801
|
+
}
|
|
22161
22802
|
/** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
|
|
22162
22803
|
async canPreflight() {
|
|
22163
22804
|
const version = await this.tryGit(void 0, ["--version"]);
|
|
@@ -22645,6 +23286,14 @@ var GitWorktreeIsolation = class {
|
|
|
22645
23286
|
if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
|
|
22646
23287
|
return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
|
|
22647
23288
|
}
|
|
23289
|
+
/** A rebase leaves HEAD detached, so it is told from a checkout the user detached on purpose. */
|
|
23290
|
+
async rebaseInProgress(cwd) {
|
|
23291
|
+
for (const dir of ["rebase-merge", "rebase-apply"]) {
|
|
23292
|
+
const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
|
|
23293
|
+
if (found.ok && fs13.existsSync(path29.resolve(cwd, found.stdout.trim()))) return true;
|
|
23294
|
+
}
|
|
23295
|
+
return false;
|
|
23296
|
+
}
|
|
22648
23297
|
async abortMerge(cwd) {
|
|
22649
23298
|
if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
|
|
22650
23299
|
}
|
|
@@ -22665,6 +23314,29 @@ function createWorktreeIsolation(deps) {
|
|
|
22665
23314
|
return new GitWorktreeIsolation(deps);
|
|
22666
23315
|
}
|
|
22667
23316
|
|
|
23317
|
+
// src/services/mergeWording.ts
|
|
23318
|
+
function isRepoGroup(handoff) {
|
|
23319
|
+
return handoff.repos.some((r) => r.path !== ".");
|
|
23320
|
+
}
|
|
23321
|
+
function reposWithWork(handoff) {
|
|
23322
|
+
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
23323
|
+
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
23324
|
+
}
|
|
23325
|
+
function mergeTarget(handoff) {
|
|
23326
|
+
if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
|
|
23327
|
+
const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
|
|
23328
|
+
const branches = new Set(targets.map((r) => r.baseBranch));
|
|
23329
|
+
const [only] = branches;
|
|
23330
|
+
if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
|
|
23331
|
+
return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
|
|
23332
|
+
}
|
|
23333
|
+
function checkoutRule(handoff) {
|
|
23334
|
+
const named3 = handoff.repos.filter((r) => r.baseBranch);
|
|
23335
|
+
if (named3.length === 0) return "";
|
|
23336
|
+
if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named3[0].baseBranch} is checked out.`;
|
|
23337
|
+
return " Nothing is merged unless each repository is on the branch it started on.";
|
|
23338
|
+
}
|
|
23339
|
+
|
|
22668
23340
|
// src/interfaces/ILogger.ts
|
|
22669
23341
|
var ConsoleLogger = class {
|
|
22670
23342
|
warn(scope, message, err) {
|
|
@@ -22717,7 +23389,10 @@ async function settleReply(opts) {
|
|
|
22717
23389
|
opts.onProgress({ type: "interrupted" });
|
|
22718
23390
|
return { done: turn };
|
|
22719
23391
|
}
|
|
22720
|
-
if (attempt.failure !== void 0)
|
|
23392
|
+
if (attempt.failure !== void 0) {
|
|
23393
|
+
const { usageLimit } = attempt;
|
|
23394
|
+
return { done: { ...message(attempt.failure), failed: true, ...usageLimit !== void 0 && { usageLimit } } };
|
|
23395
|
+
}
|
|
22721
23396
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
22722
23397
|
const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
|
|
22723
23398
|
switch (reply2.kind) {
|
|
@@ -23164,8 +23839,8 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
|
|
|
23164
23839
|
}
|
|
23165
23840
|
const path35 = args.filePath || args.file_path || args.path;
|
|
23166
23841
|
if (path35) {
|
|
23167
|
-
const
|
|
23168
|
-
return `${display} ${
|
|
23842
|
+
const basename5 = String(path35).split("/").pop() || String(path35);
|
|
23843
|
+
return `${display} ${basename5}`;
|
|
23169
23844
|
}
|
|
23170
23845
|
if (args.pattern) {
|
|
23171
23846
|
return `${display} ${args.pattern}`;
|
|
@@ -23715,7 +24390,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
|
|
|
23715
24390
|
function tokenCount(value) {
|
|
23716
24391
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
23717
24392
|
}
|
|
23718
|
-
function usageRecordFrom(usage) {
|
|
24393
|
+
function usageRecordFrom(usage, model) {
|
|
23719
24394
|
if (!usage) return void 0;
|
|
23720
24395
|
const inputTokens = tokenCount(usage.promptTokenCount);
|
|
23721
24396
|
const answerTokens = tokenCount(usage.candidatesTokenCount);
|
|
@@ -23723,6 +24398,7 @@ function usageRecordFrom(usage) {
|
|
|
23723
24398
|
const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
|
|
23724
24399
|
const record = {
|
|
23725
24400
|
source: "google",
|
|
24401
|
+
model,
|
|
23726
24402
|
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
23727
24403
|
...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
|
|
23728
24404
|
...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
|
|
@@ -23731,15 +24407,31 @@ function usageRecordFrom(usage) {
|
|
|
23731
24407
|
return anyReported ? record : void 0;
|
|
23732
24408
|
}
|
|
23733
24409
|
var GeminiResearchChat = class {
|
|
23734
|
-
constructor(
|
|
23735
|
-
this.
|
|
24410
|
+
constructor(getModel, open, hooks = {}) {
|
|
24411
|
+
this.getModel = getModel;
|
|
24412
|
+
this.open = open;
|
|
23736
24413
|
this.hooks = hooks;
|
|
24414
|
+
this.model = getModel();
|
|
24415
|
+
this.chat = open(this.model);
|
|
23737
24416
|
}
|
|
23738
|
-
|
|
24417
|
+
getModel;
|
|
24418
|
+
open;
|
|
23739
24419
|
hooks;
|
|
24420
|
+
chat;
|
|
24421
|
+
/** The model `chat` calls, named on its usage records. */
|
|
24422
|
+
model;
|
|
24423
|
+
/** The SDK binds a chat to one model, so a switch reopens it on the new one over the history so far. */
|
|
24424
|
+
async current() {
|
|
24425
|
+
const model = this.getModel();
|
|
24426
|
+
if (model !== this.model) {
|
|
24427
|
+
this.chat = this.open(model, await this.chat.getHistory());
|
|
24428
|
+
this.model = model;
|
|
24429
|
+
}
|
|
24430
|
+
return this.chat;
|
|
24431
|
+
}
|
|
23740
24432
|
async sendMessage(text3, signal) {
|
|
23741
24433
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
23742
|
-
const result = await this.
|
|
24434
|
+
const result = await (await this.current()).sendMessageStream(text3);
|
|
23743
24435
|
return this.consume(result.stream);
|
|
23744
24436
|
}
|
|
23745
24437
|
async sendToolResults(results, signal) {
|
|
@@ -23750,7 +24442,7 @@ var GeminiResearchChat = class {
|
|
|
23750
24442
|
response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
|
|
23751
24443
|
}
|
|
23752
24444
|
}));
|
|
23753
|
-
const result = await this.
|
|
24445
|
+
const result = await (await this.current()).sendMessageStream(funcResponses);
|
|
23754
24446
|
return this.consume(result.stream);
|
|
23755
24447
|
}
|
|
23756
24448
|
/**
|
|
@@ -23790,7 +24482,7 @@ var GeminiResearchChat = class {
|
|
|
23790
24482
|
}
|
|
23791
24483
|
}
|
|
23792
24484
|
}
|
|
23793
|
-
const usageRecord3 = usageRecordFrom(usage);
|
|
24485
|
+
const usageRecord3 = usageRecordFrom(usage, this.model);
|
|
23794
24486
|
if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
|
|
23795
24487
|
return {
|
|
23796
24488
|
text: text3,
|
|
@@ -23806,6 +24498,8 @@ var GeminiService = class extends BaseAiService {
|
|
|
23806
24498
|
genAI = null;
|
|
23807
24499
|
genAIKey = "";
|
|
23808
24500
|
model = null;
|
|
24501
|
+
/** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
|
|
24502
|
+
modelId = "";
|
|
23809
24503
|
constructor(config) {
|
|
23810
24504
|
super(config);
|
|
23811
24505
|
}
|
|
@@ -23819,13 +24513,18 @@ var GeminiService = class extends BaseAiService {
|
|
|
23819
24513
|
}
|
|
23820
24514
|
return true;
|
|
23821
24515
|
}
|
|
23822
|
-
|
|
24516
|
+
/** The planner model picked for this session, as the API names it. */
|
|
24517
|
+
plannerModel() {
|
|
24518
|
+
return normalizeGeminiModel(this.config.orchestratorModel || this.config.planningModel);
|
|
24519
|
+
}
|
|
24520
|
+
getPlanningModel(model = this.plannerModel()) {
|
|
23823
24521
|
this.init();
|
|
23824
|
-
if (!this.model || this.
|
|
24522
|
+
if (!this.model || this.modelId !== model) {
|
|
23825
24523
|
this.model = this.genAI.getGenerativeModel({
|
|
23826
|
-
model
|
|
24524
|
+
model,
|
|
23827
24525
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
23828
24526
|
});
|
|
24527
|
+
this.modelId = model;
|
|
23829
24528
|
}
|
|
23830
24529
|
return this.model;
|
|
23831
24530
|
}
|
|
@@ -23840,7 +24539,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
23840
24539
|
}
|
|
23841
24540
|
async startConversation(req) {
|
|
23842
24541
|
this.ensureInit();
|
|
23843
|
-
const genModel = this.getPlanningModel();
|
|
23844
24542
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
23845
24543
|
const systemPrompt = buildConversationSystemPrompt(
|
|
23846
24544
|
req.goal,
|
|
@@ -23869,13 +24567,14 @@ ${m.content}` });
|
|
|
23869
24567
|
if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
|
|
23870
24568
|
seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
|
|
23871
24569
|
}
|
|
23872
|
-
const
|
|
23873
|
-
|
|
23874
|
-
|
|
24570
|
+
const tools = [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }];
|
|
24571
|
+
const open = (model, history = seededHistory) => this.getPlanningModel(model).startChat({
|
|
24572
|
+
history,
|
|
24573
|
+
tools,
|
|
23875
24574
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
23876
24575
|
});
|
|
23877
24576
|
let currentProgress = req.onProgress;
|
|
23878
|
-
const researchChat = new GeminiResearchChat(
|
|
24577
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
23879
24578
|
onReasoning: (text3, segmentId) => currentProgress({ type: "thinking", text: text3, segmentId }),
|
|
23880
24579
|
onContent: (text3, segmentId) => currentProgress({ type: "text_delta", text: text3, segmentId }),
|
|
23881
24580
|
onUsage: (record) => currentProgress({ type: "usage", record })
|
|
@@ -23948,18 +24647,17 @@ ${repairHint}` }] : parts;
|
|
|
23948
24647
|
async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
23949
24648
|
this.ensureInit();
|
|
23950
24649
|
const { autonomousDefault } = modes;
|
|
23951
|
-
const genModel = this.getPlanningModel();
|
|
23952
24650
|
const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
|
|
23953
24651
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
23954
24652
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
23955
24653
|
|
|
23956
24654
|
Explore the workspace to understand the codebase, then generate the plan.`;
|
|
23957
|
-
const
|
|
23958
|
-
history
|
|
24655
|
+
const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
|
|
24656
|
+
history,
|
|
23959
24657
|
tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
|
|
23960
24658
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
23961
24659
|
});
|
|
23962
|
-
const researchChat = new GeminiResearchChat(
|
|
24660
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
23963
24661
|
onUsage: (record) => onProgress({ type: "usage", record })
|
|
23964
24662
|
});
|
|
23965
24663
|
const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
@@ -23995,7 +24693,7 @@ var import_openai = __toESM(require("openai"));
|
|
|
23995
24693
|
var import_uuid7 = require("uuid");
|
|
23996
24694
|
var KEYLESS_PLACEHOLDER = "not-needed";
|
|
23997
24695
|
var OpenAiResearchChat = class {
|
|
23998
|
-
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage
|
|
24696
|
+
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
|
|
23999
24697
|
this.messages = messages;
|
|
24000
24698
|
this.getClient = getClient;
|
|
24001
24699
|
this.getModel = getModel;
|
|
@@ -24004,7 +24702,6 @@ var OpenAiResearchChat = class {
|
|
|
24004
24702
|
this.onContent = onContent;
|
|
24005
24703
|
this.getSource = getSource;
|
|
24006
24704
|
this.onUsage = onUsage;
|
|
24007
|
-
this.contextWindow = contextWindow;
|
|
24008
24705
|
}
|
|
24009
24706
|
messages;
|
|
24010
24707
|
getClient;
|
|
@@ -24014,7 +24711,6 @@ var OpenAiResearchChat = class {
|
|
|
24014
24711
|
onContent;
|
|
24015
24712
|
getSource;
|
|
24016
24713
|
onUsage;
|
|
24017
|
-
contextWindow;
|
|
24018
24714
|
async sendMessage(text3, signal) {
|
|
24019
24715
|
this.answerAbandonedCalls();
|
|
24020
24716
|
this.messages.push({ role: "user", content: text3 });
|
|
@@ -24129,7 +24825,6 @@ var OpenAiResearchChat = class {
|
|
|
24129
24825
|
record.reportedCost = { amount: reported.cost, currency: "USD" };
|
|
24130
24826
|
hasMeasure = true;
|
|
24131
24827
|
}
|
|
24132
|
-
if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
|
|
24133
24828
|
return hasMeasure ? record : void 0;
|
|
24134
24829
|
}
|
|
24135
24830
|
};
|
|
@@ -24256,8 +24951,7 @@ ${buildResearchToolsPrompt()}` }
|
|
|
24256
24951
|
(delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
|
|
24257
24952
|
(delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
|
|
24258
24953
|
() => this.config.aiProvider,
|
|
24259
|
-
(record) => currentProgress({ type: "usage", record })
|
|
24260
|
-
req.contextWindow
|
|
24954
|
+
(record) => currentProgress({ type: "usage", record })
|
|
24261
24955
|
);
|
|
24262
24956
|
const ctx = {
|
|
24263
24957
|
chat,
|
|
@@ -24819,6 +25513,10 @@ var VerdictEngine = class {
|
|
|
24819
25513
|
this.generations.clear();
|
|
24820
25514
|
}
|
|
24821
25515
|
};
|
|
25516
|
+
var EXIT_CHECK = "exit_code";
|
|
25517
|
+
function endedByExit(verdict) {
|
|
25518
|
+
return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
|
|
25519
|
+
}
|
|
24822
25520
|
function exitVerdict(exitCode) {
|
|
24823
25521
|
const normalized2 = exitCode == null ? 0 : exitCode;
|
|
24824
25522
|
const exitOk = normalized2 === 0;
|
|
@@ -24827,7 +25525,7 @@ function exitVerdict(exitCode) {
|
|
|
24827
25525
|
reason: exitOk ? "Failed verification: the runner exited cleanly but never called task_complete." : `Failed verification: the runner exited with code ${normalized2} without calling task_complete.`,
|
|
24828
25526
|
checks: [
|
|
24829
25527
|
{ name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
|
|
24830
|
-
{ name:
|
|
25528
|
+
{ name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
|
|
24831
25529
|
],
|
|
24832
25530
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
24833
25531
|
};
|
|
@@ -25026,6 +25724,7 @@ var PlanStore = class {
|
|
|
25026
25724
|
this.rebuild();
|
|
25027
25725
|
for (const task of this._allTasks) {
|
|
25028
25726
|
if (task.status === "failed") task.status = "pending";
|
|
25727
|
+
if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
|
|
25029
25728
|
}
|
|
25030
25729
|
this.validateAssignedRunners();
|
|
25031
25730
|
}
|
|
@@ -25155,9 +25854,17 @@ var PlanStore = class {
|
|
|
25155
25854
|
const task = this._taskMap.get(id);
|
|
25156
25855
|
if (task) task.outputSummary = summary;
|
|
25157
25856
|
}
|
|
25158
|
-
|
|
25857
|
+
/** The owner is saved with the session it belongs to, and dropped with it. */
|
|
25858
|
+
setTaskRunnerSessionId(id, runnerSessionId, owner) {
|
|
25859
|
+
const task = this._taskMap.get(id);
|
|
25860
|
+
if (!task) return;
|
|
25861
|
+
task.runnerSessionId = runnerSessionId;
|
|
25862
|
+
if (runnerSessionId && owner) task.runnerSessionOwner = owner;
|
|
25863
|
+
else delete task.runnerSessionOwner;
|
|
25864
|
+
}
|
|
25865
|
+
setCarriedFrom(id, runner) {
|
|
25159
25866
|
const task = this._taskMap.get(id);
|
|
25160
|
-
if (task) task.
|
|
25867
|
+
if (task) task.carriedFrom = runner;
|
|
25161
25868
|
}
|
|
25162
25869
|
clearCarriedFrom(id) {
|
|
25163
25870
|
const task = this._taskMap.get(id);
|
|
@@ -25171,9 +25878,9 @@ var PlanStore = class {
|
|
|
25171
25878
|
const task = this._taskMap.get(id);
|
|
25172
25879
|
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
|
|
25173
25880
|
}
|
|
25174
|
-
endAttemptUsage(id, attempt, now) {
|
|
25881
|
+
endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
|
|
25175
25882
|
const task = this._taskMap.get(id);
|
|
25176
|
-
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now) : usage);
|
|
25883
|
+
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now, runnerSessionCost, sessionId) : usage);
|
|
25177
25884
|
}
|
|
25178
25885
|
setTaskAttemptSkills(id, skills) {
|
|
25179
25886
|
const task = this._taskMap.get(id);
|
|
@@ -26031,6 +26738,8 @@ function conflictResolverTask(task, run, plan) {
|
|
|
26031
26738
|
}
|
|
26032
26739
|
|
|
26033
26740
|
// src/services/UsageLimitWaits.ts
|
|
26741
|
+
var RESUME_MARGIN_MS = 6e4;
|
|
26742
|
+
var RELAPSE_MS = 5 * 6e4;
|
|
26034
26743
|
var UsageLimitWaits = class {
|
|
26035
26744
|
constructor(onDue) {
|
|
26036
26745
|
this.onDue = onDue;
|
|
@@ -26153,6 +26862,9 @@ function selectReadyTasks(input) {
|
|
|
26153
26862
|
gated
|
|
26154
26863
|
};
|
|
26155
26864
|
}
|
|
26865
|
+
function isBlocked(task, store) {
|
|
26866
|
+
return blockedIn(task, blockingFailures(store.allTasks));
|
|
26867
|
+
}
|
|
26156
26868
|
var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
|
|
26157
26869
|
function dependencyMet(depId, store, runs) {
|
|
26158
26870
|
return waitingBehind([depId], store, runs).length === 0;
|
|
@@ -26282,6 +26994,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26282
26994
|
* task whose spawn always throws.
|
|
26283
26995
|
*/
|
|
26284
26996
|
onHold = /* @__PURE__ */ new Set();
|
|
26997
|
+
/**
|
|
26998
|
+
* Force starts queued behind dependencies the scheduler will not start them
|
|
26999
|
+
* after: outside a run it drives, or past a failed dependency, which it
|
|
27000
|
+
* never forces. Each starts once those still under way have finished.
|
|
27001
|
+
*/
|
|
27002
|
+
queuedStarts = /* @__PURE__ */ new Set();
|
|
26285
27003
|
/**
|
|
26286
27004
|
* Tasks Mark complete is landing after it ended their attempt: in progress
|
|
26287
27005
|
* with no attempt for as long as that takes, and not orphans for it.
|
|
@@ -26303,6 +27021,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26303
27021
|
observers = [];
|
|
26304
27022
|
skillsAt;
|
|
26305
27023
|
previousAttemptFromLog;
|
|
27024
|
+
lastLoggedAttempt;
|
|
26306
27025
|
runSettings;
|
|
26307
27026
|
isApproved;
|
|
26308
27027
|
constructor(deps) {
|
|
@@ -26318,6 +27037,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26318
27037
|
this.workspaceEnv = deps.workspaceEnv;
|
|
26319
27038
|
this.skillsAt = deps.skillsAt;
|
|
26320
27039
|
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
27040
|
+
this.lastLoggedAttempt = deps.lastLoggedAttempt;
|
|
26321
27041
|
this.runSettings = deps.runSettings;
|
|
26322
27042
|
this.isApproved = deps.isApproved;
|
|
26323
27043
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
@@ -26382,6 +27102,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26382
27102
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
26383
27103
|
skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
|
|
26384
27104
|
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
|
|
27105
|
+
lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
|
|
26385
27106
|
runSettings: options.runSettings ?? (() => void 0),
|
|
26386
27107
|
isApproved: options.isApproved ?? (() => true)
|
|
26387
27108
|
});
|
|
@@ -26638,10 +27359,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26638
27359
|
*/
|
|
26639
27360
|
holdStarts() {
|
|
26640
27361
|
this.startHolds++;
|
|
26641
|
-
let
|
|
27362
|
+
let held2 = true;
|
|
26642
27363
|
return () => {
|
|
26643
|
-
if (!
|
|
26644
|
-
|
|
27364
|
+
if (!held2) return;
|
|
27365
|
+
held2 = false;
|
|
26645
27366
|
this.startHolds--;
|
|
26646
27367
|
};
|
|
26647
27368
|
}
|
|
@@ -26652,24 +27373,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26652
27373
|
*/
|
|
26653
27374
|
admitStart() {
|
|
26654
27375
|
const generation2 = this.stopGeneration;
|
|
26655
|
-
let
|
|
27376
|
+
let held2 = true;
|
|
26656
27377
|
this.admissions++;
|
|
26657
27378
|
return {
|
|
26658
27379
|
current: () => generation2 === this.stopGeneration,
|
|
26659
27380
|
release: () => {
|
|
26660
|
-
if (!
|
|
26661
|
-
|
|
27381
|
+
if (!held2) return;
|
|
27382
|
+
held2 = false;
|
|
26662
27383
|
this.admissions--;
|
|
26663
27384
|
}
|
|
26664
27385
|
};
|
|
26665
27386
|
}
|
|
26666
27387
|
/**
|
|
26667
27388
|
* Something can still start that nobody has to ask for again: a start
|
|
26668
|
-
* admitted and not yet claimed,
|
|
26669
|
-
*
|
|
27389
|
+
* admitted and not yet claimed, a force start queued behind its
|
|
27390
|
+
* dependencies, or a task that resumes on its own at its usage limit's
|
|
27391
|
+
* reset. A Stop is owed to it, whether or not anything runs.
|
|
26670
27392
|
*/
|
|
26671
27393
|
get startsPending() {
|
|
26672
|
-
return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
|
|
27394
|
+
return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
|
|
26673
27395
|
}
|
|
26674
27396
|
/** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
|
|
26675
27397
|
async whileAdmitted(work) {
|
|
@@ -26768,6 +27490,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26768
27490
|
this.retryCounts.clear();
|
|
26769
27491
|
this.spawnCounts.clear();
|
|
26770
27492
|
this.onHold.clear();
|
|
27493
|
+
this.queuedStarts.clear();
|
|
26771
27494
|
this.recoverOrphans();
|
|
26772
27495
|
this.rearmLimitWaits();
|
|
26773
27496
|
}
|
|
@@ -26855,6 +27578,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26855
27578
|
}
|
|
26856
27579
|
this.runs.interrupt();
|
|
26857
27580
|
this.onHold.clear();
|
|
27581
|
+
if (reason === "user") this.queuedStarts.clear();
|
|
26858
27582
|
for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
|
|
26859
27583
|
this.sayStopped(stopped);
|
|
26860
27584
|
this.recoverOrphans();
|
|
@@ -26954,6 +27678,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26954
27678
|
const attempt = this.attempts.get(taskId);
|
|
26955
27679
|
if (!task || !attempt) return;
|
|
26956
27680
|
attempt.decided = true;
|
|
27681
|
+
const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
|
|
26957
27682
|
console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
|
|
26958
27683
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
26959
27684
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
@@ -26975,8 +27700,8 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
26975
27700
|
this.tell("warn", `Ops task "${task.title}" finished, but changed tracked files in your checkout: ${capConflictFiles(changedFiles)}. An ops task must not change repository files, so nothing was committed: look at the changes, then mark it complete or retry it.`);
|
|
26976
27701
|
} else if (landing) {
|
|
26977
27702
|
await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
|
|
26978
|
-
} else if (classifyRunnerStop(
|
|
26979
|
-
this.pauseOnLimit(task, attempt,
|
|
27703
|
+
} else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
|
|
27704
|
+
this.pauseOnLimit(task, attempt, stoppedMidTurn);
|
|
26980
27705
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
26981
27706
|
} else {
|
|
26982
27707
|
this.store.markFailed(taskId);
|
|
@@ -26991,16 +27716,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
26991
27716
|
const status = this.store.get(taskId)?.status;
|
|
26992
27717
|
if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
|
|
26993
27718
|
this.emit("onTaskChanged");
|
|
26994
|
-
if (
|
|
26995
|
-
|
|
26996
|
-
if (this.store.isFinished()) this.planStatus = "completed";
|
|
26997
|
-
this.emit("onTick");
|
|
26998
|
-
if (this.settled) await this.closeRun();
|
|
26999
|
-
this.emit("onExecutionComplete");
|
|
27000
|
-
}
|
|
27719
|
+
if (this.running) {
|
|
27720
|
+
await this.tick();
|
|
27001
27721
|
return;
|
|
27002
27722
|
}
|
|
27003
|
-
await this.
|
|
27723
|
+
await this.startQueued();
|
|
27724
|
+
if (this.attempts.size === 0) {
|
|
27725
|
+
if (this.store.isFinished()) this.planStatus = "completed";
|
|
27726
|
+
this.emit("onTick");
|
|
27727
|
+
if (this.settled) await this.closeRun();
|
|
27728
|
+
this.emit("onExecutionComplete");
|
|
27729
|
+
}
|
|
27004
27730
|
}
|
|
27005
27731
|
/**
|
|
27006
27732
|
* A repair's verdict never replaces the verdict the task's own work earned,
|
|
@@ -27268,11 +27994,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27268
27994
|
const resumes = this.awaitsResume;
|
|
27269
27995
|
this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
|
|
27270
27996
|
const ended = this.endAttempt(taskId, "retry");
|
|
27997
|
+
const movedFrom = this.movedFrom(task);
|
|
27271
27998
|
this.store.retry(taskId);
|
|
27999
|
+
if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
|
|
27272
28000
|
this.store.unblockDependents(taskId);
|
|
27273
28001
|
this.onHold.delete(taskId);
|
|
27274
28002
|
this.emit("onTaskChanged");
|
|
27275
|
-
|
|
28003
|
+
if (movedFrom) {
|
|
28004
|
+
await ended?.integration;
|
|
28005
|
+
} else {
|
|
28006
|
+
await this.runs.release(taskId, { keep: false }, ended?.integration);
|
|
28007
|
+
}
|
|
27276
28008
|
if (admittedAt !== this.stopGeneration) return;
|
|
27277
28009
|
if (resumes) await this.start(origin);
|
|
27278
28010
|
else await this.tick();
|
|
@@ -27323,6 +28055,16 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27323
28055
|
this.emit("onTaskChanged");
|
|
27324
28056
|
return true;
|
|
27325
28057
|
}
|
|
28058
|
+
/**
|
|
28059
|
+
* The runner whose attempt left the task's work, when the task has been
|
|
28060
|
+
* moved off it since: a session cannot cross runners, so whatever starts
|
|
28061
|
+
* the task next is a carry-over of that work, the rule a paused task's move
|
|
28062
|
+
* follows (ADR-0018, U1).
|
|
28063
|
+
*/
|
|
28064
|
+
movedFrom(task) {
|
|
28065
|
+
const owner = sessionOwner(task);
|
|
28066
|
+
return owner && owner !== this.store.resolveTaskRunner(task) ? owner : void 0;
|
|
28067
|
+
}
|
|
27326
28068
|
/**
|
|
27327
28069
|
* Run before writing assignments so cancellation sees the old runner's
|
|
27328
28070
|
* shared wait. A session cannot cross runners, so the next attempt is a
|
|
@@ -27380,7 +28122,7 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27380
28122
|
const summary = this.output.finalText(taskId);
|
|
27381
28123
|
this.endAttempt(taskId, "usage-limit");
|
|
27382
28124
|
this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
|
|
27383
|
-
this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError ?? `${attempt.runner} hit its usage limit`, summary));
|
|
28125
|
+
this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError?.message ?? `${attempt.runner} hit its usage limit`, summary));
|
|
27384
28126
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
27385
28127
|
await this.afterVerdict(taskId);
|
|
27386
28128
|
}
|
|
@@ -27479,10 +28221,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27479
28221
|
return false;
|
|
27480
28222
|
}
|
|
27481
28223
|
/**
|
|
27482
|
-
* Manually start a single AI task right now
|
|
27483
|
-
*
|
|
27484
|
-
* it still needs the user's approval,
|
|
27485
|
-
*
|
|
28224
|
+
* Manually start a single AI task right now (the "force start" affordance on
|
|
28225
|
+
* a task card), past a failed dependency (ADR-0034) or a merge gate
|
|
28226
|
+
* (ADR-0020), but never past the claim: it still needs the user's approval,
|
|
28227
|
+
* a free slot, and no Stop since it was asked for, and throws
|
|
28228
|
+
* {@link TaskControlError} saying which it lacks. A dependency still under
|
|
28229
|
+
* way or yet to run is not forced past: the task is released to the
|
|
28230
|
+
* scheduler instead, which starts it once they finish, and the line saying
|
|
28231
|
+
* so is the answer.
|
|
27486
28232
|
* Reuses the scheduler's own claim so a force-started task gets the same
|
|
27487
28233
|
* augmented prompt, session tracking, and exit handling — callers must not
|
|
27488
28234
|
* re-spawn the runner themselves. No-op if the task is unknown, not an AI
|
|
@@ -27493,8 +28239,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27493
28239
|
if (!task || task.type !== "ai") return;
|
|
27494
28240
|
if (this.attempts.has(taskId)) return;
|
|
27495
28241
|
this.refuseOpsDuringMerge(task);
|
|
28242
|
+
const queued2 = this.queuedBehindDependencies(task);
|
|
28243
|
+
if (queued2) return queued2;
|
|
27496
28244
|
await this.whileAdmitted(async (admittedAt) => {
|
|
27497
|
-
if (!await this.openForAdmitted(taskId, origin, () =>
|
|
28245
|
+
if (!await this.openForAdmitted(taskId, origin, async () => {
|
|
28246
|
+
await this.forceStartTask(taskId, origin);
|
|
28247
|
+
}, admittedAt)) return;
|
|
27498
28248
|
if (this.attempts.has(taskId)) return;
|
|
27499
28249
|
this.refuseOpsDuringMerge(task);
|
|
27500
28250
|
this.refuseClaim(task, admittedAt);
|
|
@@ -27503,6 +28253,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27503
28253
|
await this.claimed(task, admittedAt);
|
|
27504
28254
|
});
|
|
27505
28255
|
}
|
|
28256
|
+
/**
|
|
28257
|
+
* A force start of a task whose dependencies are still under way or yet to
|
|
28258
|
+
* run: started now, it would build on work that is not there, so it waits
|
|
28259
|
+
* for them in the run, off any hold, and says so. Dependencies are never
|
|
28260
|
+
* bypassed silently. A blocked task is forced past its failure as asked.
|
|
28261
|
+
*/
|
|
28262
|
+
queuedBehindDependencies(task) {
|
|
28263
|
+
const waiting = this.underWayBefore(task);
|
|
28264
|
+
if (waiting.length === 0) return null;
|
|
28265
|
+
this.onHold.delete(task.id);
|
|
28266
|
+
if (!this.running || isBlocked(task, this.store)) this.queuedStarts.add(task.id);
|
|
28267
|
+
const names = waiting.map((id) => `task "${this.store.get(id)?.title ?? id}"`);
|
|
28268
|
+
const listed3 = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
28269
|
+
const idle = this.running || waiting.some((id) => this.attempts.has(id)) ? "" : " Nothing is running now: run the plan to start them.";
|
|
28270
|
+
const queued2 = `Task "${task.title}" is approved and starts once ${listed3} ${names.length === 1 ? "finishes" : "finish"}.${idle}`;
|
|
28271
|
+
this.tell("info", queued2);
|
|
28272
|
+
this.emit("onTaskChanged");
|
|
28273
|
+
return queued2;
|
|
28274
|
+
}
|
|
28275
|
+
/**
|
|
28276
|
+
* A task's unmet dependencies, less the failed or blocked ones a force start
|
|
28277
|
+
* goes past. A blocked one is gone past for its failure only: the work under
|
|
28278
|
+
* way or yet to run that it waits on is still waited for, through it.
|
|
28279
|
+
*/
|
|
28280
|
+
underWayBefore(task) {
|
|
28281
|
+
const blocked = blockingFailures(this.store.allTasks);
|
|
28282
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
28283
|
+
const seen = /* @__PURE__ */ new Set();
|
|
28284
|
+
const walk = (of) => {
|
|
28285
|
+
for (const id of unmetDependencies(of, this.store, this.runs)) {
|
|
28286
|
+
if (seen.has(id)) continue;
|
|
28287
|
+
seen.add(id);
|
|
28288
|
+
const dep = this.store.get(id);
|
|
28289
|
+
if (dep?.status === "failed") continue;
|
|
28290
|
+
if (dep && (dep.status === "blocked" || blocked.has(id))) walk(dep);
|
|
28291
|
+
else waiting.add(id);
|
|
28292
|
+
}
|
|
28293
|
+
};
|
|
28294
|
+
walk(task);
|
|
28295
|
+
return [...waiting];
|
|
28296
|
+
}
|
|
28297
|
+
/**
|
|
28298
|
+
* Starts the queued force starts whose dependencies have now finished. One
|
|
28299
|
+
* the scheduler now drives the run for, and nothing blocks, is left to it.
|
|
28300
|
+
*/
|
|
28301
|
+
async startQueued() {
|
|
28302
|
+
for (const id of [...this.queuedStarts]) {
|
|
28303
|
+
const task = this.store.get(id);
|
|
28304
|
+
if (!task || this.attempts.has(id) || task.status === "completed" || task.status === "skipped") {
|
|
28305
|
+
this.queuedStarts.delete(id);
|
|
28306
|
+
continue;
|
|
28307
|
+
}
|
|
28308
|
+
if (this.underWayBefore(task).length > 0) continue;
|
|
28309
|
+
this.queuedStarts.delete(id);
|
|
28310
|
+
if (this.running && !isBlocked(task, this.store)) continue;
|
|
28311
|
+
try {
|
|
28312
|
+
await this.forceStartTask(id);
|
|
28313
|
+
} catch (err) {
|
|
28314
|
+
this.tell("warn", `Task "${task.title}" did not start once its dependencies finished: ${err instanceof Error ? err.message : String(err)}`);
|
|
28315
|
+
}
|
|
28316
|
+
}
|
|
28317
|
+
}
|
|
27506
28318
|
refuseOpsDuringMerge(task) {
|
|
27507
28319
|
if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
|
|
27508
28320
|
throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
|
|
@@ -27567,6 +28379,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27567
28379
|
async tick() {
|
|
27568
28380
|
const admittedAt = this.stopGeneration;
|
|
27569
28381
|
this.recoverOrphans();
|
|
28382
|
+
await this.startQueued();
|
|
27570
28383
|
if (!this.running) {
|
|
27571
28384
|
if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
|
|
27572
28385
|
return;
|
|
@@ -27723,7 +28536,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27723
28536
|
if (refused3) return { refused: refused3 };
|
|
27724
28537
|
const attempt = {
|
|
27725
28538
|
taskId: task.id,
|
|
27726
|
-
attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
|
|
28539
|
+
attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, this.lastLoggedAttempt(task.id), ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
|
|
27727
28540
|
phase: "starting",
|
|
27728
28541
|
session: null,
|
|
27729
28542
|
runner: this.store.resolveTaskRunner(task),
|
|
@@ -27737,6 +28550,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27737
28550
|
skills: [],
|
|
27738
28551
|
autoResumed: opts.autoResumed === true,
|
|
27739
28552
|
turnError: null,
|
|
28553
|
+
turnOutputFrom: 0,
|
|
27740
28554
|
reminded: false
|
|
27741
28555
|
};
|
|
27742
28556
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -27810,6 +28624,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27810
28624
|
title: task.title,
|
|
27811
28625
|
env,
|
|
27812
28626
|
resumeSessionId: kind.kind === "continuation" ? kind.resumeSessionId : void 0,
|
|
28627
|
+
resumeCost: kind.kind === "continuation" ? countedSessionCost(task.attemptUsage, kind.resumeSessionId) : void 0,
|
|
28628
|
+
resumeWindow: kind.kind === "continuation" ? resumedSessionWindow(task.attemptUsage, attempt.runner, attempt.attempt, kind.resumeSessionId) : void 0,
|
|
27813
28629
|
attempt: attempt.attempt,
|
|
27814
28630
|
...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
|
|
27815
28631
|
onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
|
|
@@ -27834,8 +28650,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27834
28650
|
this.output.attach(task.id, session);
|
|
27835
28651
|
this.verifier.watch(task, session);
|
|
27836
28652
|
session.onEvent((event) => {
|
|
27837
|
-
if (event.type === "turn_start")
|
|
27838
|
-
|
|
28653
|
+
if (event.type === "turn_start") {
|
|
28654
|
+
attempt.turnError = null;
|
|
28655
|
+
attempt.turnOutputFrom = session.getOutput().length;
|
|
28656
|
+
} else if (event.type === "error") attempt.turnError = event;
|
|
28657
|
+
else if (event.type === "model_switched" || event.type === "model_drifted") {
|
|
28658
|
+
this.emit("onModelSwitched", {
|
|
28659
|
+
taskId: task.id,
|
|
28660
|
+
sessionId: session.id,
|
|
28661
|
+
from: event.from,
|
|
28662
|
+
model: event.to,
|
|
28663
|
+
...event.type === "model_switched" && event.effort ? { effort: event.effort } : {},
|
|
28664
|
+
asked: event.type === "model_switched",
|
|
28665
|
+
attemptEnded: this.attempts.get(task.id) !== attempt
|
|
28666
|
+
});
|
|
28667
|
+
}
|
|
27839
28668
|
});
|
|
27840
28669
|
session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
|
|
27841
28670
|
return true;
|
|
@@ -27845,6 +28674,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27845
28674
|
return false;
|
|
27846
28675
|
}
|
|
27847
28676
|
this.endAttempt(task.id, "spawn-failed");
|
|
28677
|
+
const refusal = err instanceof Error ? err.message : String(err);
|
|
28678
|
+
if (attempt.kind.kind !== "repair" && classifyRunnerStop(refusal) === "usage-limit") {
|
|
28679
|
+
this.pauseOnLimit(task, attempt, refusal);
|
|
28680
|
+
this.store.setTaskOutputSummary(task.id, summarizeOutput(refusal, ""));
|
|
28681
|
+
await this.runs.release(task.id, { keep: true });
|
|
28682
|
+
this.emit("onTaskSettled", { taskId: task.id });
|
|
28683
|
+
this.emit("onTaskChanged");
|
|
28684
|
+
await this.tick();
|
|
28685
|
+
return false;
|
|
28686
|
+
}
|
|
27848
28687
|
if (attempt.kind.kind === "continuation") {
|
|
27849
28688
|
const refused3 = err instanceof RunnerStartupExit;
|
|
27850
28689
|
const reason = refused3 ? unresumedMessage(task, `${attempt.runner} could not find its saved session`) : unresumedMessage(task, `could not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -27964,12 +28803,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27964
28803
|
endAttempt(taskId, reason) {
|
|
27965
28804
|
const attempt = this.attempts.get(taskId);
|
|
27966
28805
|
this.attempts.delete(taskId);
|
|
28806
|
+
const session = attempt?.session ?? null;
|
|
28807
|
+
const nativeSessionId = session?.nativeSessionId() ?? null;
|
|
27967
28808
|
if (attempt) {
|
|
27968
|
-
this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString());
|
|
28809
|
+
this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
|
|
27969
28810
|
this.output.detach(taskId);
|
|
27970
28811
|
}
|
|
27971
|
-
|
|
27972
|
-
if (session) this.saveNativeSession(taskId, session.nativeSessionId());
|
|
28812
|
+
if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
|
|
27973
28813
|
if (stopsRunner(reason)) {
|
|
27974
28814
|
const task = this.store.get(taskId);
|
|
27975
28815
|
if (task) this.verifier.clear(task);
|
|
@@ -27978,8 +28818,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27978
28818
|
return attempt;
|
|
27979
28819
|
}
|
|
27980
28820
|
/** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
|
|
27981
|
-
saveNativeSession(taskId, nativeSessionId) {
|
|
27982
|
-
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
|
|
28821
|
+
saveNativeSession(taskId, nativeSessionId, runner) {
|
|
28822
|
+
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
|
|
27983
28823
|
}
|
|
27984
28824
|
endAllAttempts(reason) {
|
|
27985
28825
|
const ended = [...this.attempts.values()];
|
|
@@ -27989,13 +28829,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27989
28829
|
return ended;
|
|
27990
28830
|
}
|
|
27991
28831
|
};
|
|
27992
|
-
var RESUME_MARGIN_MS = 6e4;
|
|
27993
|
-
var RELAPSE_MS = 5 * 6e4;
|
|
27994
28832
|
var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
|
|
27995
28833
|
var OPS_RETRY_TAIL_LINES = 60;
|
|
27996
28834
|
function unresumedMessage(task, why) {
|
|
27997
28835
|
return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
|
|
27998
28836
|
}
|
|
28837
|
+
function saidInFlight(attempt) {
|
|
28838
|
+
const session = attempt.session;
|
|
28839
|
+
if (!session || session.turnState() !== "working") return null;
|
|
28840
|
+
if (attempt.turnError?.usageLimit !== void 0) return attempt.turnError;
|
|
28841
|
+
return session.getOutput().slice(attempt.turnOutputFrom);
|
|
28842
|
+
}
|
|
27999
28843
|
|
|
28000
28844
|
// src/services/Planner.ts
|
|
28001
28845
|
function plannerModesWith(req) {
|
|
@@ -28098,15 +28942,15 @@ var ModelResolver = class {
|
|
|
28098
28942
|
return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
|
|
28099
28943
|
}
|
|
28100
28944
|
async modelsForRunners(runners) {
|
|
28101
|
-
const
|
|
28102
|
-
|
|
28945
|
+
const unique = [...new Set(runners)];
|
|
28946
|
+
const discovered = await Promise.all(unique.map(async (runner) => {
|
|
28103
28947
|
try {
|
|
28104
|
-
|
|
28948
|
+
return [runner, await this.discovery.discover(runner)];
|
|
28105
28949
|
} catch {
|
|
28106
|
-
|
|
28950
|
+
return [runner, []];
|
|
28107
28951
|
}
|
|
28108
|
-
}
|
|
28109
|
-
return
|
|
28952
|
+
}));
|
|
28953
|
+
return Object.fromEntries(discovered);
|
|
28110
28954
|
}
|
|
28111
28955
|
async pickerOptions() {
|
|
28112
28956
|
if (this.cachedPickerOptions) return this.cachedPickerOptions;
|
|
@@ -28153,21 +28997,18 @@ var ModelResolver = class {
|
|
|
28153
28997
|
return this.cachedPickerOptions ?? [];
|
|
28154
28998
|
}
|
|
28155
28999
|
/**
|
|
28156
|
-
*
|
|
28157
|
-
*
|
|
28158
|
-
*
|
|
28159
|
-
*
|
|
28160
|
-
*
|
|
29000
|
+
* A model's context window from whatever catalog is already cached (#49),
|
|
29001
|
+
* for a usage record whose reporter stated none: a vendor's model from the
|
|
29002
|
+
* picker catalog (its own `/models` endpoint), a runner's model from that
|
|
29003
|
+
* runner's own discovered listing. `source` is the record's reporter, and
|
|
29004
|
+
* `model` the id it named. Reads only — never triggers a fetch or discovery
|
|
29005
|
+
* of its own, so an unknown window stays unknown and context fill is
|
|
29006
|
+
* omitted rather than guessed.
|
|
28161
29007
|
*/
|
|
28162
|
-
contextWindowFor(
|
|
28163
|
-
if (!
|
|
28164
|
-
const
|
|
28165
|
-
|
|
28166
|
-
for (const runner of this.config.enabledRunners ?? []) {
|
|
28167
|
-
const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
|
|
28168
|
-
if (found && found > 0) return found;
|
|
28169
|
-
}
|
|
28170
|
-
return void 0;
|
|
29008
|
+
contextWindowFor(source, model) {
|
|
29009
|
+
if (!model) return void 0;
|
|
29010
|
+
const window = isApiProvider(source) ? this.cachedPickerOptions?.find((o) => o.apiProvider === source && o.id === prefixModelId(source, stripModelPrefix(model, source)))?.contextWindow : this.discovery.getCached(source)?.find((m) => m.modelId === model)?.contextWindow;
|
|
29011
|
+
return window && window > 0 ? window : void 0;
|
|
28171
29012
|
}
|
|
28172
29013
|
invalidate() {
|
|
28173
29014
|
this.discovery.clear();
|
|
@@ -28193,24 +29034,28 @@ var defaultExec3 = async (command, options) => {
|
|
|
28193
29034
|
return { stdout: String(stdout) };
|
|
28194
29035
|
};
|
|
28195
29036
|
var CHECK_TIMEOUT_MS = 8e3;
|
|
29037
|
+
var NEGATIVE_TTL_MS2 = 6e4;
|
|
28196
29038
|
var RunnerInstallation = class {
|
|
28197
29039
|
cache = /* @__PURE__ */ new Map();
|
|
28198
29040
|
inflight = /* @__PURE__ */ new Map();
|
|
28199
29041
|
registry;
|
|
28200
29042
|
execImpl;
|
|
28201
|
-
|
|
29043
|
+
now;
|
|
29044
|
+
constructor(registry, execImpl = defaultExec3, now = Date.now) {
|
|
28202
29045
|
this.registry = registry;
|
|
28203
29046
|
this.execImpl = execImpl;
|
|
29047
|
+
this.now = now;
|
|
28204
29048
|
}
|
|
28205
29049
|
/** True if the runner's CLI responds to `--version`. Cached per process. */
|
|
28206
29050
|
async isInstalled(runner) {
|
|
28207
|
-
|
|
29051
|
+
const cached2 = this.cache.get(runner);
|
|
29052
|
+
if (cached2 && (cached2.installed || this.now() - cached2.at < NEGATIVE_TTL_MS2)) return cached2.installed;
|
|
28208
29053
|
const existing = this.inflight.get(runner);
|
|
28209
29054
|
if (existing) return existing;
|
|
28210
29055
|
const manifest = this.registry.getManifest(runner);
|
|
28211
29056
|
const command = manifest?.runner.command;
|
|
28212
29057
|
if (!command) {
|
|
28213
|
-
this.cache.set(runner, false);
|
|
29058
|
+
this.cache.set(runner, { installed: false, at: this.now() });
|
|
28214
29059
|
return false;
|
|
28215
29060
|
}
|
|
28216
29061
|
const probe = (async () => {
|
|
@@ -28221,13 +29066,22 @@ var RunnerInstallation = class {
|
|
|
28221
29066
|
return false;
|
|
28222
29067
|
}
|
|
28223
29068
|
})().then((installed) => {
|
|
28224
|
-
this.cache.set(runner, installed);
|
|
29069
|
+
this.cache.set(runner, { installed, at: this.now() });
|
|
28225
29070
|
this.inflight.delete(runner);
|
|
28226
29071
|
return installed;
|
|
28227
29072
|
});
|
|
28228
29073
|
this.inflight.set(runner, probe);
|
|
28229
29074
|
return probe;
|
|
28230
29075
|
}
|
|
29076
|
+
/**
|
|
29077
|
+
* Forget cached "not installed" verdicts so the next probe re-runs them. A
|
|
29078
|
+
* runner-list refresh calls this, so a CLI installed while the host runs is
|
|
29079
|
+
* offered without a restart. An installed verdict stays: it cannot have
|
|
29080
|
+
* changed without the binary going away, and re-probing one costs a spawn.
|
|
29081
|
+
*/
|
|
29082
|
+
refresh() {
|
|
29083
|
+
for (const [runner, probe] of this.cache) if (!probe.installed) this.cache.delete(runner);
|
|
29084
|
+
}
|
|
28231
29085
|
/**
|
|
28232
29086
|
* Whether this runner can serve as a harness planner (ADR-0009, T9), and why
|
|
28233
29087
|
* not when it cannot. Surfaces call this to grey out an unusable agent in the
|
|
@@ -28658,6 +29512,8 @@ var PlannerSession = class {
|
|
|
28658
29512
|
* outlives the session — nothing from one goal may reach the next.
|
|
28659
29513
|
*/
|
|
28660
29514
|
nativeSessionId = null;
|
|
29515
|
+
/** What was counted of that session's running cost, so a resume counts on from it rather than dropping its first turn. */
|
|
29516
|
+
sessionCost = null;
|
|
28661
29517
|
/** Set once the first spawn attached. */
|
|
28662
29518
|
start = null;
|
|
28663
29519
|
/**
|
|
@@ -28848,6 +29704,7 @@ var CliAgentAiService = class {
|
|
|
28848
29704
|
// hit its rate limit, or lost its login must say so in the chat rather
|
|
28849
29705
|
// than leave an empty planner bubble.
|
|
28850
29706
|
failure: turn.error,
|
|
29707
|
+
usageLimit: turn.usageLimit,
|
|
28851
29708
|
fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
|
|
28852
29709
|
};
|
|
28853
29710
|
};
|
|
@@ -28914,6 +29771,7 @@ var CliAgentAiService = class {
|
|
|
28914
29771
|
};
|
|
28915
29772
|
const subagentUsage = /* @__PURE__ */ new Map();
|
|
28916
29773
|
let error;
|
|
29774
|
+
let usageLimit;
|
|
28917
29775
|
let stepIndex = 0;
|
|
28918
29776
|
let backgroundAgents = 0;
|
|
28919
29777
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
@@ -29022,6 +29880,7 @@ var CliAgentAiService = class {
|
|
|
29022
29880
|
}
|
|
29023
29881
|
case "error":
|
|
29024
29882
|
error = event.message;
|
|
29883
|
+
usageLimit = event.usageLimit;
|
|
29025
29884
|
return;
|
|
29026
29885
|
case "turn_end":
|
|
29027
29886
|
return;
|
|
@@ -29040,12 +29899,13 @@ var CliAgentAiService = class {
|
|
|
29040
29899
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
29041
29900
|
}
|
|
29042
29901
|
session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
|
|
29902
|
+
session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
|
|
29043
29903
|
if (error || signal?.aborted) session.stop();
|
|
29044
29904
|
if (error) {
|
|
29045
29905
|
const failure = failureAfterReply(text3.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
|
|
29046
29906
|
onProgress({ type: "text_retracted", segmentId: finalSegment });
|
|
29047
29907
|
text3 = text3.slice(0, finalRunStart);
|
|
29048
|
-
return { text: text3, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
|
|
29908
|
+
return { text: text3, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
|
|
29049
29909
|
}
|
|
29050
29910
|
return { text: text3, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
29051
29911
|
}
|
|
@@ -29101,7 +29961,7 @@ var CliAgentAiService = class {
|
|
|
29101
29961
|
session.stop();
|
|
29102
29962
|
const start = session.start;
|
|
29103
29963
|
if (!start) throw new Error("No active planner conversation.");
|
|
29104
|
-
return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0 }, start.tools, session);
|
|
29964
|
+
return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
|
|
29105
29965
|
}
|
|
29106
29966
|
/**
|
|
29107
29967
|
* What the planner gets of the repository's config, its runner being
|
|
@@ -29113,9 +29973,9 @@ var CliAgentAiService = class {
|
|
|
29113
29973
|
const reader = connectorFor(this.runner)?.repoConfig;
|
|
29114
29974
|
if (!reader) return { systemPrompt };
|
|
29115
29975
|
const found = reader.projectServers(cwd);
|
|
29116
|
-
const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
|
|
29976
|
+
const { started, held: held2 } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
|
|
29117
29977
|
return {
|
|
29118
|
-
systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(
|
|
29978
|
+
systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held2.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
|
|
29119
29979
|
...started.length > 0 ? { projectServers: started } : {}
|
|
29120
29980
|
};
|
|
29121
29981
|
}
|
|
@@ -29280,6 +30140,10 @@ var SessionCatalog = class {
|
|
|
29280
30140
|
allowlist() {
|
|
29281
30141
|
return this.deps.settings().modelAllowlist ?? {};
|
|
29282
30142
|
}
|
|
30143
|
+
/** The enabled runners and allowlist together, for whoever compares an assignment with them. */
|
|
30144
|
+
scope() {
|
|
30145
|
+
return { enabledRunners: this.enabledRunners(), allowlist: this.allowlist(), models: this.models() };
|
|
30146
|
+
}
|
|
29283
30147
|
/** The models the user allows on one runner; unset means no restriction. */
|
|
29284
30148
|
allowlistFor(runner) {
|
|
29285
30149
|
return this.allowlist()[runner];
|
|
@@ -29294,7 +30158,7 @@ var SessionCatalog = class {
|
|
|
29294
30158
|
*/
|
|
29295
30159
|
models() {
|
|
29296
30160
|
const out = { ...this.discovered };
|
|
29297
|
-
for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners()])) {
|
|
30161
|
+
for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners(), ...this.enabledRunners()])) {
|
|
29298
30162
|
const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
|
|
29299
30163
|
if (cached2.length > 0) out[runner] = cached2;
|
|
29300
30164
|
}
|
|
@@ -29331,9 +30195,16 @@ var SessionCatalog = class {
|
|
|
29331
30195
|
* asked for.
|
|
29332
30196
|
*/
|
|
29333
30197
|
async live() {
|
|
29334
|
-
|
|
29335
|
-
|
|
29336
|
-
|
|
30198
|
+
await this.discover(this.enabledRunners());
|
|
30199
|
+
return this.current();
|
|
30200
|
+
}
|
|
30201
|
+
/**
|
|
30202
|
+
* What {@link live} answers, from what is cached and without discovering:
|
|
30203
|
+
* the enabled runners, however the plan was started. The per-turn block
|
|
30204
|
+
* reads this, so it can never offer a different set than `list_runners`.
|
|
30205
|
+
*/
|
|
30206
|
+
current() {
|
|
30207
|
+
return this.queryCatalog(this.enabledRunners());
|
|
29337
30208
|
}
|
|
29338
30209
|
queryCatalog(runners) {
|
|
29339
30210
|
const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
|
|
@@ -29359,7 +30230,8 @@ var SessionCatalog = class {
|
|
|
29359
30230
|
modelsByRunner: this.models(),
|
|
29360
30231
|
runnerModes: this.modes(runners),
|
|
29361
30232
|
perRunnerAllowlist: this.allowlist(),
|
|
29362
|
-
autonomousDefault: this.deps.config.autonomousMode
|
|
30233
|
+
autonomousDefault: this.deps.config.autonomousMode,
|
|
30234
|
+
...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
|
|
29363
30235
|
};
|
|
29364
30236
|
}
|
|
29365
30237
|
/** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
|
|
@@ -29424,6 +30296,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
|
|
|
29424
30296
|
};
|
|
29425
30297
|
}
|
|
29426
30298
|
|
|
30299
|
+
// src/services/PlannerResend.ts
|
|
30300
|
+
var HELD = "Send a message once it has reset: what waits goes with it.";
|
|
30301
|
+
var WAIT = "planner";
|
|
30302
|
+
var PlannerResend = class {
|
|
30303
|
+
constructor(host) {
|
|
30304
|
+
this.host = host;
|
|
30305
|
+
}
|
|
30306
|
+
host;
|
|
30307
|
+
waits = new UsageLimitWaits(() => this.due());
|
|
30308
|
+
/** When the last resend at a reset went out, until a turn lands: a limit in that window is a relapse. */
|
|
30309
|
+
resentAt = null;
|
|
30310
|
+
/** Whether turns wait on the limit: until the reset, or for the user's next message. */
|
|
30311
|
+
holds() {
|
|
30312
|
+
return this.host.plan()?.plannerResend !== void 0;
|
|
30313
|
+
}
|
|
30314
|
+
/** A planner turn Ordewell started ended on the planner's usage limit, which it reported as `said`. */
|
|
30315
|
+
limited(said2) {
|
|
30316
|
+
const plan = this.host.plan();
|
|
30317
|
+
const runner = this.host.plannerRunner() ?? void 0;
|
|
30318
|
+
const now = /* @__PURE__ */ new Date();
|
|
30319
|
+
const relapsed = this.resentAt !== null && now.getTime() - this.resentAt < RELAPSE_MS;
|
|
30320
|
+
this.resentAt = null;
|
|
30321
|
+
this.waits.clear();
|
|
30322
|
+
const name = runner ?? "Planner";
|
|
30323
|
+
const reset = runner && !relapsed ? readUsageLimitReset(runner, said2, now) : null;
|
|
30324
|
+
if (!reset) {
|
|
30325
|
+
if (plan) plan.plannerResend = runner ? { runner } : {};
|
|
30326
|
+
const held2 = relapsed ? "hit it again right after the resend" : "did not say when it resets";
|
|
30327
|
+
const text3 = relapsed ? `${name} usage limit again right after the resend, so nothing is resent on its own. ${HELD}` : `${name} usage limit; it did not say when it resets. ${HELD}`;
|
|
30328
|
+
return { text: text3, resendAt: null, held: held2 };
|
|
30329
|
+
}
|
|
30330
|
+
const at = new Date(reset.getTime() + RESUME_MARGIN_MS);
|
|
30331
|
+
if (plan) plan.plannerResend = { runner, at: at.toISOString() };
|
|
30332
|
+
this.waits.arm(WAIT, at);
|
|
30333
|
+
return { text: `${name} usage limit, resets ${resumeTime(reset, now)}. Resending at ${resumeTime(at, now)}; anything you send meanwhile goes with it.`, resendAt: at };
|
|
30334
|
+
}
|
|
30335
|
+
/**
|
|
30336
|
+
* A new input arrived — the user's message, or a wake-up the supervisor
|
|
30337
|
+
* asked for. What was held for one goes with it now; so does what waits on
|
|
30338
|
+
* a runner the planner has left, since nothing it waits for applies to the
|
|
30339
|
+
* planner that will answer. True when it lifted a wait.
|
|
30340
|
+
*/
|
|
30341
|
+
arrived() {
|
|
30342
|
+
const saved = this.host.plan()?.plannerResend;
|
|
30343
|
+
if (!saved || saved.at !== void 0 && !this.switched(saved)) return false;
|
|
30344
|
+
this.lift();
|
|
30345
|
+
return true;
|
|
30346
|
+
}
|
|
30347
|
+
/** Keep what waits from going on its own; it goes with the user's next message. False when nothing waits on a reset. */
|
|
30348
|
+
cancel() {
|
|
30349
|
+
const plan = this.host.plan();
|
|
30350
|
+
const saved = plan?.plannerResend;
|
|
30351
|
+
if (!plan || saved?.at === void 0) return false;
|
|
30352
|
+
this.waits.clear();
|
|
30353
|
+
plan.plannerResend = held(saved);
|
|
30354
|
+
return true;
|
|
30355
|
+
}
|
|
30356
|
+
/** A turn landed: the resend before it got past the limit. */
|
|
30357
|
+
landed() {
|
|
30358
|
+
this.resentAt = null;
|
|
30359
|
+
}
|
|
30360
|
+
/** A plan adopted off disk: re-arm the wait it saved. A time already past resends at once. */
|
|
30361
|
+
restore() {
|
|
30362
|
+
this.reset();
|
|
30363
|
+
const saved = this.host.plan()?.plannerResend;
|
|
30364
|
+
if (saved?.at === void 0) return;
|
|
30365
|
+
const at = Date.parse(saved.at);
|
|
30366
|
+
if (Number.isNaN(at)) delete saved.at;
|
|
30367
|
+
else this.waits.arm(WAIT, new Date(at));
|
|
30368
|
+
}
|
|
30369
|
+
/** The plan is gone: nothing it waited on carries over. */
|
|
30370
|
+
reset() {
|
|
30371
|
+
this.waits.clear();
|
|
30372
|
+
this.resentAt = null;
|
|
30373
|
+
}
|
|
30374
|
+
due() {
|
|
30375
|
+
const plan = this.host.plan();
|
|
30376
|
+
const saved = plan?.plannerResend;
|
|
30377
|
+
if (!plan || saved?.at === void 0) return;
|
|
30378
|
+
if (this.switched(saved)) {
|
|
30379
|
+
plan.plannerResend = held(saved);
|
|
30380
|
+
this.host.changed();
|
|
30381
|
+
this.host.notice(`The planner no longer runs on ${saved.runner}, whose usage limit it waited on, so nothing was resent. ${HELD}`);
|
|
30382
|
+
return;
|
|
30383
|
+
}
|
|
30384
|
+
delete plan.plannerResend;
|
|
30385
|
+
this.resentAt = Date.now();
|
|
30386
|
+
this.host.due();
|
|
30387
|
+
}
|
|
30388
|
+
lift() {
|
|
30389
|
+
this.waits.clear();
|
|
30390
|
+
const plan = this.host.plan();
|
|
30391
|
+
if (plan) delete plan.plannerResend;
|
|
30392
|
+
}
|
|
30393
|
+
switched(saved) {
|
|
30394
|
+
return saved.runner !== void 0 && saved.runner !== (this.host.plannerRunner() ?? void 0);
|
|
30395
|
+
}
|
|
30396
|
+
};
|
|
30397
|
+
function held({ runner }) {
|
|
30398
|
+
return runner ? { runner } : {};
|
|
30399
|
+
}
|
|
30400
|
+
|
|
29427
30401
|
// src/services/PlanTurns.ts
|
|
29428
30402
|
var START_HOLD_TIMEOUT_MS = 5 * 6e4;
|
|
29429
30403
|
function turnPrompt(batch) {
|
|
@@ -29440,6 +30414,16 @@ var PlanTurns = class {
|
|
|
29440
30414
|
constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
|
|
29441
30415
|
this.host = host;
|
|
29442
30416
|
this.holdTimeoutMs = holdTimeoutMs;
|
|
30417
|
+
this.resend = new PlannerResend({
|
|
30418
|
+
plan: () => host.plan(),
|
|
30419
|
+
plannerRunner: () => host.plannerRunner(),
|
|
30420
|
+
due: () => {
|
|
30421
|
+
host.announce();
|
|
30422
|
+
this.pump();
|
|
30423
|
+
},
|
|
30424
|
+
changed: () => host.announce(),
|
|
30425
|
+
notice: (message) => host.notice(message)
|
|
30426
|
+
});
|
|
29443
30427
|
}
|
|
29444
30428
|
host;
|
|
29445
30429
|
holdTimeoutMs;
|
|
@@ -29452,6 +30436,33 @@ var PlanTurns = class {
|
|
|
29452
30436
|
/** The user stopped the turn wake-ups waited behind: they wait for the next input to open one, not for the stop's release. */
|
|
29453
30437
|
quiet = false;
|
|
29454
30438
|
seq = 0;
|
|
30439
|
+
resend;
|
|
30440
|
+
/** A turn ended on the planner's usage limit: arm the wait for its inputs. Saved with the failure the turn records. */
|
|
30441
|
+
usageLimit(said2) {
|
|
30442
|
+
return this.resend.limited(said2);
|
|
30443
|
+
}
|
|
30444
|
+
/** Keep what waits on the planner's usage limit from being sent at the reset; see {@link PlannerResend.cancel}. */
|
|
30445
|
+
cancelResend() {
|
|
30446
|
+
if (!this.resend.cancel()) return false;
|
|
30447
|
+
this.host.announce();
|
|
30448
|
+
return true;
|
|
30449
|
+
}
|
|
30450
|
+
/**
|
|
30451
|
+
* The conversation's opening ended on the planner's usage limit: its goal
|
|
30452
|
+
* waits with the limit, ahead of what was sent behind it, whose senders are
|
|
30453
|
+
* answered now as any sent during the wait are.
|
|
30454
|
+
*/
|
|
30455
|
+
waitOnLimit(input) {
|
|
30456
|
+
const plan = this.host.plan();
|
|
30457
|
+
if (!plan) return;
|
|
30458
|
+
plan.pendingTurns = [this.pending(input, false), ...plan.pendingTurns ?? []];
|
|
30459
|
+
this.host.persist();
|
|
30460
|
+
for (const t of plan.pendingTurns) if (t.source === "user") this.settle(t.id, (w) => w.resolve(plan));
|
|
30461
|
+
}
|
|
30462
|
+
/** A plan adopted off disk: the wait it saved is armed again. */
|
|
30463
|
+
restoreResend() {
|
|
30464
|
+
this.resend.restore();
|
|
30465
|
+
}
|
|
29455
30466
|
/** Whether a turn this module opened is in flight. */
|
|
29456
30467
|
get busy() {
|
|
29457
30468
|
return this.running !== null;
|
|
@@ -29493,25 +30504,33 @@ var PlanTurns = class {
|
|
|
29493
30504
|
admit(input, forced) {
|
|
29494
30505
|
const plan = this.host.plan();
|
|
29495
30506
|
if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
|
|
29496
|
-
const entry =
|
|
29497
|
-
id: `turn-${Date.now()}-${++this.seq}`,
|
|
29498
|
-
source: input.source,
|
|
29499
|
-
text: input.text,
|
|
29500
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29501
|
-
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
29502
|
-
...forced ? { forced: true } : {}
|
|
29503
|
-
};
|
|
30507
|
+
const entry = this.pending(input, forced);
|
|
29504
30508
|
this.quiet = false;
|
|
30509
|
+
if (this.resend.arrived()) this.host.announce();
|
|
29505
30510
|
const pending = plan.pendingTurns ?? [];
|
|
29506
30511
|
const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
|
|
29507
30512
|
plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
|
|
29508
30513
|
this.host.persist();
|
|
30514
|
+
if (input.source === "user" && this.resend.holds()) {
|
|
30515
|
+
this.pump();
|
|
30516
|
+
return Promise.resolve(plan);
|
|
30517
|
+
}
|
|
29509
30518
|
const settled = new Promise((resolve9, reject) => {
|
|
29510
30519
|
this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
|
|
29511
30520
|
});
|
|
29512
30521
|
this.pump();
|
|
29513
30522
|
return settled;
|
|
29514
30523
|
}
|
|
30524
|
+
pending(input, forced) {
|
|
30525
|
+
return {
|
|
30526
|
+
id: `turn-${Date.now()}-${++this.seq}`,
|
|
30527
|
+
source: input.source,
|
|
30528
|
+
text: input.text,
|
|
30529
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
30530
|
+
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
30531
|
+
...forced ? { forced: true } : {}
|
|
30532
|
+
};
|
|
30533
|
+
}
|
|
29515
30534
|
/**
|
|
29516
30535
|
* Start the next turn if one is due: nothing of ours is in flight, the
|
|
29517
30536
|
* conversation is free, and inputs wait. The host calls it when any planner
|
|
@@ -29522,7 +30541,7 @@ var PlanTurns = class {
|
|
|
29522
30541
|
* the plan, what still waits, and the planner in use at that moment.
|
|
29523
30542
|
*/
|
|
29524
30543
|
pump() {
|
|
29525
|
-
if (this.running || this.detecting) return;
|
|
30544
|
+
if (this.running || this.detecting || this.resend.holds()) return;
|
|
29526
30545
|
const plan = this.host.plan();
|
|
29527
30546
|
if (!plan || this.host.conversationBusy()) return;
|
|
29528
30547
|
const batch = this.liveBatch(plan);
|
|
@@ -29602,6 +30621,7 @@ var PlanTurns = class {
|
|
|
29602
30621
|
* conversation's to abandon; whatever it settles lands nothing here.
|
|
29603
30622
|
*/
|
|
29604
30623
|
abandon() {
|
|
30624
|
+
this.resend.reset();
|
|
29605
30625
|
this.running?.release();
|
|
29606
30626
|
this.running = null;
|
|
29607
30627
|
this.quiet = false;
|
|
@@ -29645,8 +30665,10 @@ var PlanTurns = class {
|
|
|
29645
30665
|
clearTimeout(timeout);
|
|
29646
30666
|
if (this.running !== running2) return;
|
|
29647
30667
|
const forceStopped = !outcome.ok && running2.forceStopped === true;
|
|
30668
|
+
const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
|
|
30669
|
+
if (outcome.ok) this.resend.landed();
|
|
29648
30670
|
try {
|
|
29649
|
-
const dropped = outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
|
|
30671
|
+
const dropped = limited ? /* @__PURE__ */ new Set() : outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
|
|
29650
30672
|
const took = this.take(plan, dropped);
|
|
29651
30673
|
if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
|
|
29652
30674
|
if (took || forceStopped) this.host.persist();
|
|
@@ -29660,7 +30682,11 @@ var PlanTurns = class {
|
|
|
29660
30682
|
const ended = outcome;
|
|
29661
30683
|
const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
|
|
29662
30684
|
for (const t of batch) {
|
|
29663
|
-
if (
|
|
30685
|
+
if (limited && t.source === "user") {
|
|
30686
|
+
this.settle(t.id, (w) => w.resolve(plan));
|
|
30687
|
+
continue;
|
|
30688
|
+
}
|
|
30689
|
+
if ((forceStopped || limited) && waiting.has(t.id)) continue;
|
|
29664
30690
|
const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
|
|
29665
30691
|
if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
|
|
29666
30692
|
}
|
|
@@ -29737,7 +30763,6 @@ var RunSupervision = class {
|
|
|
29737
30763
|
blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
|
|
29738
30764
|
readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
|
|
29739
30765
|
retryLimit: () => this.retryLimit(),
|
|
29740
|
-
plannerRunner: () => host.plannerRunner(),
|
|
29741
30766
|
restartPlanner: () => host.restartPlanner(),
|
|
29742
30767
|
abandonTurn: () => host.abandonTurn(),
|
|
29743
30768
|
goUnsupervised: (reason) => this.goUnsupervised(reason),
|
|
@@ -29946,14 +30971,14 @@ function split({ verdict, idleSince, isolation, checkpoint, queued: queued2, con
|
|
|
29946
30971
|
return { plan, status: { verdict, idleSince, isolation, checkpoint, queued: queued2, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2 } };
|
|
29947
30972
|
}
|
|
29948
30973
|
function refresh(previous, tasks) {
|
|
29949
|
-
const
|
|
30974
|
+
const held2 = /* @__PURE__ */ new Map();
|
|
29950
30975
|
const index = (list) => list.forEach((t) => {
|
|
29951
|
-
|
|
30976
|
+
held2.set(t.id, t);
|
|
29952
30977
|
index(t.subtasks);
|
|
29953
30978
|
});
|
|
29954
30979
|
index(previous);
|
|
29955
30980
|
const fold = ({ subtasks, ...task }) => {
|
|
29956
|
-
const was =
|
|
30981
|
+
const was = held2.get(task.id);
|
|
29957
30982
|
const { checkpoint, ...status } = was ? split(was).status : {};
|
|
29958
30983
|
const waits = task.status === "awaiting_user" && task.awaitingReason === "checkpoint";
|
|
29959
30984
|
return { ...task, ...status, ...waits && checkpoint ? { checkpoint } : {}, subtasks: subtasks.map(fold) };
|
|
@@ -30327,9 +31352,9 @@ var fs14 = __toESM(require("fs"));
|
|
|
30327
31352
|
var path31 = __toESM(require("path"));
|
|
30328
31353
|
|
|
30329
31354
|
// src/utils/sessionId.ts
|
|
30330
|
-
var
|
|
31355
|
+
var import_crypto12 = require("crypto");
|
|
30331
31356
|
function mintSessionId() {
|
|
30332
|
-
return `session-${(0,
|
|
31357
|
+
return `session-${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
|
|
30333
31358
|
}
|
|
30334
31359
|
|
|
30335
31360
|
// src/utils/sessionStore.ts
|
|
@@ -30366,8 +31391,8 @@ function dropLegacyFile(sessionsDir, meta) {
|
|
|
30366
31391
|
const legacy = path31.join(sessionsDir, legacySessionFilename(meta));
|
|
30367
31392
|
if (legacy === path31.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
|
|
30368
31393
|
try {
|
|
30369
|
-
const
|
|
30370
|
-
if (
|
|
31394
|
+
const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
|
|
31395
|
+
if (held2 === meta.id) fs14.unlinkSync(legacy);
|
|
30371
31396
|
} catch {
|
|
30372
31397
|
}
|
|
30373
31398
|
}
|
|
@@ -30500,24 +31525,25 @@ function listTaskLogAttempts(location, taskId) {
|
|
|
30500
31525
|
}
|
|
30501
31526
|
return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
|
|
30502
31527
|
}
|
|
30503
|
-
function openTaskLog(location, taskId) {
|
|
31528
|
+
function openTaskLog(location, taskId, attempt) {
|
|
30504
31529
|
ensureStateDirIgnored(location.baseDir);
|
|
30505
31530
|
const dir = taskDir(location, taskId);
|
|
30506
31531
|
fs15.mkdirSync(dir, { recursive: true });
|
|
30507
|
-
|
|
31532
|
+
const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
|
|
31533
|
+
let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
|
|
30508
31534
|
for (; ; ) {
|
|
30509
|
-
const file = path32.join(dir, `${
|
|
31535
|
+
const file = path32.join(dir, `${number}${EXTENSION}`);
|
|
30510
31536
|
try {
|
|
30511
31537
|
fs15.writeFileSync(file, "", { flag: "wx" });
|
|
30512
31538
|
} catch (err) {
|
|
30513
31539
|
if (err.code === "EEXIST") {
|
|
30514
|
-
|
|
31540
|
+
number = Math.max(number + 1, next);
|
|
30515
31541
|
continue;
|
|
30516
31542
|
}
|
|
30517
31543
|
throw err;
|
|
30518
31544
|
}
|
|
30519
31545
|
return {
|
|
30520
|
-
attempt,
|
|
31546
|
+
attempt: number,
|
|
30521
31547
|
append(events) {
|
|
30522
31548
|
if (events.length === 0) return;
|
|
30523
31549
|
fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
|
|
@@ -30563,11 +31589,13 @@ var TaskLogRecorder = class {
|
|
|
30563
31589
|
open;
|
|
30564
31590
|
flushMs;
|
|
30565
31591
|
logger;
|
|
31592
|
+
/** Each task's latest attempt, by the session it records: its log takes a {@link note} until the next attempt starts. */
|
|
31593
|
+
latest = /* @__PURE__ */ new Map();
|
|
30566
31594
|
wrap(runner) {
|
|
30567
31595
|
return {
|
|
30568
31596
|
spawn: async (opts) => {
|
|
30569
31597
|
const session = await runner.spawn(opts);
|
|
30570
|
-
this.record(opts.taskId, session, opts.skills);
|
|
31598
|
+
this.record(opts.taskId, session, opts.skills, opts.attempt);
|
|
30571
31599
|
return session;
|
|
30572
31600
|
},
|
|
30573
31601
|
stop: (sessionId) => runner.stop(sessionId),
|
|
@@ -30577,11 +31605,20 @@ var TaskLogRecorder = class {
|
|
|
30577
31605
|
}
|
|
30578
31606
|
};
|
|
30579
31607
|
}
|
|
31608
|
+
/**
|
|
31609
|
+
* An entry the session did not emit, written into the log of the attempt
|
|
31610
|
+
* that runs `sessionId` and sent at once: what Ordewell made of one of its
|
|
31611
|
+
* events once that settled. Dropped once the task's next attempt started.
|
|
31612
|
+
*/
|
|
31613
|
+
note(sessionId, taskId, entry) {
|
|
31614
|
+
const attempt = this.latest.get(taskId);
|
|
31615
|
+
if (attempt?.sessionId === sessionId) attempt.note(entry);
|
|
31616
|
+
}
|
|
30580
31617
|
/** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
|
|
30581
|
-
record(taskId, session, skills = []) {
|
|
31618
|
+
record(taskId, session, skills = [], attemptNumber) {
|
|
30582
31619
|
let file;
|
|
30583
31620
|
try {
|
|
30584
|
-
file = this.open(this.deps.location(), taskId);
|
|
31621
|
+
file = this.open(this.deps.location(), taskId, attemptNumber);
|
|
30585
31622
|
} catch (err) {
|
|
30586
31623
|
this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
|
|
30587
31624
|
file = null;
|
|
@@ -30608,6 +31645,10 @@ var TaskLogRecorder = class {
|
|
|
30608
31645
|
}
|
|
30609
31646
|
this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
|
|
30610
31647
|
};
|
|
31648
|
+
this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
|
|
31649
|
+
pending.push(entry);
|
|
31650
|
+
flush();
|
|
31651
|
+
} });
|
|
30611
31652
|
const openCalls = /* @__PURE__ */ new Set();
|
|
30612
31653
|
session.onEvent((event) => {
|
|
30613
31654
|
const entry = toTaskLogEvent(event);
|
|
@@ -30628,21 +31669,82 @@ var TaskLogRecorder = class {
|
|
|
30628
31669
|
}
|
|
30629
31670
|
};
|
|
30630
31671
|
|
|
31672
|
+
// src/services/ContextWindowFill.ts
|
|
31673
|
+
var ContextWindowFill = class {
|
|
31674
|
+
constructor(catalog, last = {}) {
|
|
31675
|
+
this.catalog = catalog;
|
|
31676
|
+
if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
|
|
31677
|
+
}
|
|
31678
|
+
catalog;
|
|
31679
|
+
/** The window known for each model of the stream, by reporter and model. */
|
|
31680
|
+
known = /* @__PURE__ */ new Map();
|
|
31681
|
+
fill(record) {
|
|
31682
|
+
if (record.subagentId || record.aggregate) return record;
|
|
31683
|
+
const key = modelKey(record.source, record.model);
|
|
31684
|
+
const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
|
|
31685
|
+
const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
|
|
31686
|
+
if (window !== void 0) this.known.set(key, window);
|
|
31687
|
+
return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
|
|
31688
|
+
}
|
|
31689
|
+
};
|
|
31690
|
+
function modelKey(source, model) {
|
|
31691
|
+
return JSON.stringify([source ?? null, model ?? null]);
|
|
31692
|
+
}
|
|
31693
|
+
function withContextWindows(runner, catalog) {
|
|
31694
|
+
return {
|
|
31695
|
+
spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
|
|
31696
|
+
stop: (sessionId) => runner.stop(sessionId),
|
|
31697
|
+
stopAll: () => runner.stopAll(),
|
|
31698
|
+
get activeCount() {
|
|
31699
|
+
return runner.activeCount;
|
|
31700
|
+
}
|
|
31701
|
+
};
|
|
31702
|
+
}
|
|
31703
|
+
function windowedSession(session, windows) {
|
|
31704
|
+
const listeners = [];
|
|
31705
|
+
session.onEvent((event) => {
|
|
31706
|
+
const filled = event.type === "usage" ? { ...event, record: windows.fill(event.record) } : event;
|
|
31707
|
+
for (const listener of listeners) listener(filled);
|
|
31708
|
+
});
|
|
31709
|
+
const onEvent = (listener) => {
|
|
31710
|
+
listeners.push(listener);
|
|
31711
|
+
};
|
|
31712
|
+
return new Proxy(session, {
|
|
31713
|
+
get: (target, prop) => {
|
|
31714
|
+
if (prop === "onEvent") return onEvent;
|
|
31715
|
+
const value = Reflect.get(target, prop, target);
|
|
31716
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
31717
|
+
}
|
|
31718
|
+
});
|
|
31719
|
+
}
|
|
31720
|
+
function positive(n) {
|
|
31721
|
+
return n !== void 0 && n > 0 ? n : void 0;
|
|
31722
|
+
}
|
|
31723
|
+
|
|
30631
31724
|
// src/services/PlannerUsage.ts
|
|
30632
31725
|
var PlannerUsageLedger = class {
|
|
31726
|
+
/** `catalogWindow` serves the window of a planner model whose backend reports none. */
|
|
31727
|
+
constructor(catalogWindow = () => void 0) {
|
|
31728
|
+
this.catalogWindow = catalogWindow;
|
|
31729
|
+
this.windows = new ContextWindowFill(catalogWindow);
|
|
31730
|
+
}
|
|
31731
|
+
catalogWindow;
|
|
30633
31732
|
usage = { totals: {} };
|
|
31733
|
+
windows;
|
|
30634
31734
|
/** Fold one model call's record into the totals; returns the running ledger. */
|
|
30635
31735
|
record(record) {
|
|
30636
|
-
this.usage = addPlannerUsage(this.usage, record);
|
|
31736
|
+
this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
|
|
30637
31737
|
return this.usage;
|
|
30638
31738
|
}
|
|
30639
31739
|
/** Adopt a persisted ledger (a reopened session) or start from zero. */
|
|
30640
31740
|
restore(usage) {
|
|
30641
31741
|
this.usage = usage ?? { totals: {} };
|
|
31742
|
+
this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
|
|
30642
31743
|
}
|
|
30643
31744
|
/** Start a fresh session's ledger from zero. */
|
|
30644
31745
|
clear() {
|
|
30645
31746
|
this.usage = { totals: {} };
|
|
31747
|
+
this.windows = new ContextWindowFill(this.catalogWindow);
|
|
30646
31748
|
}
|
|
30647
31749
|
/** Whether anything has been recorded — a plan with no usage says nothing. */
|
|
30648
31750
|
get hasUsage() {
|
|
@@ -30737,19 +31839,54 @@ var PlanEditor = class {
|
|
|
30737
31839
|
* between AI and MAN, are the patches that can leave a task incoherent
|
|
30738
31840
|
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
30739
31841
|
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
30740
|
-
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
30741
|
-
*
|
|
30742
|
-
* say why. Gated on the task existing so an edit to an unknown id still
|
|
31842
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
31843
|
+
* but not a live runner's hold on its model) rather than a second copy of
|
|
31844
|
+
* the rules — and throws, so the surface can say why. Gated on the task existing so an edit to an unknown id still
|
|
30743
31845
|
* falls through to the no-op `store.update` below instead of throwing.
|
|
30744
31846
|
* Attached skills meet the rule a planner's do: a planner skill is refused,
|
|
30745
|
-
* a name not found yet only warns.
|
|
31847
|
+
* a name not found yet only warns. A runner change is made by
|
|
31848
|
+
* {@link setTaskRunner}, which re-derives what the runner scopes, ahead of
|
|
31849
|
+
* the rest of the patch.
|
|
30746
31850
|
*
|
|
30747
31851
|
* Edits to one task commit in call order: the skill check waits on git, so
|
|
30748
31852
|
* two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
|
|
30749
31853
|
* one last.
|
|
30750
31854
|
*/
|
|
30751
31855
|
updateTask(taskId, changes) {
|
|
30752
|
-
|
|
31856
|
+
return this.inTaskOrder(taskId, () => this.applyTaskUpdate(taskId, changes));
|
|
31857
|
+
}
|
|
31858
|
+
/**
|
|
31859
|
+
* The plan follows a model switch the user made with the runner's own
|
|
31860
|
+
* command (#106), as a direct edit of theirs: saved and announced as
|
|
31861
|
+
* `task_updated`, so every surface shows it, and what a later retry or
|
|
31862
|
+
* continue spawns on. It is the one model edit a live runner allows, since
|
|
31863
|
+
* it says what that runner already runs, and the one that skips the
|
|
31864
|
+
* allowlist: the user picked it in the runner. The runner's own label is
|
|
31865
|
+
* used only for a model discovery does not list. Effort is the runner's
|
|
31866
|
+
* when it reported one, else the plan's, either clamped to what the new
|
|
31867
|
+
* model offers; none for a model discovery does not list. `live` is asked
|
|
31868
|
+
* as the edit lands, after the edits queued ahead of it: by then the plan
|
|
31869
|
+
* may be another one, whose task of the same id the switch says nothing of.
|
|
31870
|
+
*/
|
|
31871
|
+
followRunnerModel(taskId, model, effort, live) {
|
|
31872
|
+
return this.inTaskOrder(taskId, async () => {
|
|
31873
|
+
const task = this.store.get(taskId);
|
|
31874
|
+
if (!task || task.type === "user" || !live()) return null;
|
|
31875
|
+
const listed3 = this.catalog.models()[task.assignedRunner]?.find((m) => m.modelId === model.model);
|
|
31876
|
+
const thinkingEffort = clampThinkingEffort(effort ?? task.assignedModel?.thinkingEffort, listed3?.variants ?? []);
|
|
31877
|
+
if (task.assignedModel?.modelId === model.model && task.assignedModel.thinkingEffort === thinkingEffort) return this.plan();
|
|
31878
|
+
const assignedModel = {
|
|
31879
|
+
modelId: model.model,
|
|
31880
|
+
modelLabel: listed3?.modelLabel ?? model.label ?? model.model,
|
|
31881
|
+
...thinkingEffort ? { thinkingEffort } : {},
|
|
31882
|
+
...listed3 ? { availableVariants: listed3.variants.map((v) => v.id) } : {}
|
|
31883
|
+
};
|
|
31884
|
+
return this.commitTaskUpdate(taskId, { assignedModel });
|
|
31885
|
+
});
|
|
31886
|
+
}
|
|
31887
|
+
/** Edits to one task commit in call order; see {@link updateTask}. */
|
|
31888
|
+
inTaskOrder(taskId, apply) {
|
|
31889
|
+
const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(apply);
|
|
30753
31890
|
const settled = edit.catch(() => {
|
|
30754
31891
|
});
|
|
30755
31892
|
this.taskEdits.set(taskId, settled);
|
|
@@ -30765,22 +31902,31 @@ var PlanEditor = class {
|
|
|
30765
31902
|
const target = this.store.get(taskId);
|
|
30766
31903
|
const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
|
|
30767
31904
|
if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
|
|
30768
|
-
if ((changes.dependencies || changes.type ||
|
|
31905
|
+
if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
|
|
30769
31906
|
const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
|
|
30770
31907
|
if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
|
|
30771
31908
|
if (check2.clear?.length || check2.set) {
|
|
30772
31909
|
changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
|
|
30773
31910
|
}
|
|
30774
31911
|
}
|
|
30775
|
-
const
|
|
31912
|
+
const { assignedRunner, ...rest } = changes;
|
|
31913
|
+
if (target && assignedRunner && assignedRunner !== target.assignedRunner) {
|
|
31914
|
+
const moved = await this.setTaskRunner(taskId, assignedRunner);
|
|
31915
|
+
if (!moved || Object.keys(rest).length === 0) return moved;
|
|
31916
|
+
changes = rest;
|
|
31917
|
+
}
|
|
31918
|
+
const plan = await this.commitTaskUpdate(taskId, changes);
|
|
31919
|
+
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
31920
|
+
return plan;
|
|
31921
|
+
}
|
|
31922
|
+
commitTaskUpdate(taskId, changes) {
|
|
31923
|
+
return this.commit(
|
|
30776
31924
|
() => {
|
|
30777
31925
|
changes = this.scheduler.releaseMovedResume(taskId, changes);
|
|
30778
31926
|
return Boolean(this.store.update(taskId, changes));
|
|
30779
31927
|
},
|
|
30780
31928
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
30781
31929
|
);
|
|
30782
|
-
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
30783
|
-
return plan;
|
|
30784
31930
|
}
|
|
30785
31931
|
/**
|
|
30786
31932
|
* Replace one task's dependency list — the named entry point the surfaces'
|
|
@@ -30797,7 +31943,8 @@ var PlanEditor = class {
|
|
|
30797
31943
|
* because a runner change is never a single-field edit: the task's model,
|
|
30798
31944
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
30799
31945
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
30800
|
-
* That needs discovery, which is why
|
|
31946
|
+
* That needs discovery, which is why `updateTask` hands a runner change here
|
|
31947
|
+
* rather than writing it as a field.
|
|
30801
31948
|
*
|
|
30802
31949
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
30803
31950
|
*/
|
|
@@ -30807,11 +31954,13 @@ var PlanEditor = class {
|
|
|
30807
31954
|
const task = this.store.get(taskId);
|
|
30808
31955
|
if (!task) return null;
|
|
30809
31956
|
if (task.assignedRunner === runner || task.type === "user") return plan;
|
|
31957
|
+
this.refuseLiveRetarget(task);
|
|
30810
31958
|
const catalog = await this.catalog.runner(runner);
|
|
30811
31959
|
const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
|
|
30812
31960
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
|
|
30813
31961
|
if (Object.keys(changes).length === 0) return this.plan();
|
|
30814
31962
|
this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
|
|
31963
|
+
this.refuseLiveRetarget(task);
|
|
30815
31964
|
return this.commit(() => {
|
|
30816
31965
|
if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
|
|
30817
31966
|
this.admitRunner(runner, catalog.models);
|
|
@@ -30845,6 +31994,9 @@ var PlanEditor = class {
|
|
|
30845
31994
|
return true;
|
|
30846
31995
|
});
|
|
30847
31996
|
}
|
|
31997
|
+
refuseLiveRetarget(task) {
|
|
31998
|
+
if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
|
|
31999
|
+
}
|
|
30848
32000
|
/**
|
|
30849
32001
|
* A mode the host cannot run is refused, with its reason, not swapped for
|
|
30850
32002
|
* another: the plan is the source of truth for what runs (ADR-0001), and a
|
|
@@ -31174,6 +32326,10 @@ var ExecutionCommands = class {
|
|
|
31174
32326
|
const all = this.all(tasks);
|
|
31175
32327
|
const live = all.filter(({ task }) => this.host.running(task.id));
|
|
31176
32328
|
if (this.host.hasLiveWork()) {
|
|
32329
|
+
if (actor.kind === "user") {
|
|
32330
|
+
const waiting = all.filter(({ task }) => this.host.waitsForApproval(task.id)).length;
|
|
32331
|
+
return { act: async () => ({ message: await this.host.joinRun(waiting) }) };
|
|
32332
|
+
}
|
|
31177
32333
|
return { refusal: `The plan is already running (${named2(live)} ${live.length === 1 ? "is" : "are"} running). Start one more ready task with run_task, or wait.` };
|
|
31178
32334
|
}
|
|
31179
32335
|
const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
|
|
@@ -31599,8 +32755,11 @@ function mergeStopped(result) {
|
|
|
31599
32755
|
switch (result.outcome) {
|
|
31600
32756
|
case "merged":
|
|
31601
32757
|
return "it merged";
|
|
31602
|
-
case "blocked":
|
|
31603
|
-
|
|
32758
|
+
case "blocked": {
|
|
32759
|
+
const off = result.blocked.filter((b) => b.reason === "wrong-branch");
|
|
32760
|
+
if (off.length === 0) return "the user's checkout has changes that overlap what it would merge";
|
|
32761
|
+
return off.map((b) => `${b.repo === "." ? "the checkout" : b.repo} is ${b.actual ? `on ${b.actual}` : b.rebasing ? "in the middle of a rebase" : "on a detached HEAD"}, not ${b.expected}, which the run started on`).join("; ");
|
|
32762
|
+
}
|
|
31604
32763
|
case "conflict":
|
|
31605
32764
|
return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
|
|
31606
32765
|
case "failed":
|
|
@@ -31725,11 +32884,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
|
|
|
31725
32884
|
function isPlannerApproval(request) {
|
|
31726
32885
|
return !isRunnerApproval(request);
|
|
31727
32886
|
}
|
|
32887
|
+
function writtenTasks(before, after) {
|
|
32888
|
+
const said2 = ({ subtasks: _subtasks, status: _status, ...task }) => JSON.stringify(task);
|
|
32889
|
+
const prior = new Map(flattenTasks(before).map((t) => [t.id, said2(t)]));
|
|
32890
|
+
return flattenTasks(after).filter((t) => prior.get(t.id) !== said2(t)).map((t) => t.id);
|
|
32891
|
+
}
|
|
31728
32892
|
function createSession(deps) {
|
|
31729
32893
|
const pinnedAiService = deps.aiService;
|
|
31730
32894
|
const mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
31731
32895
|
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
|
|
31732
32896
|
const store = new PlanStore();
|
|
32897
|
+
const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
|
|
31733
32898
|
const taskLogs = new TaskLogRecorder({
|
|
31734
32899
|
broadcast: deps.broadcast,
|
|
31735
32900
|
// Read through the Session, whose id and workspace are the ones it saves
|
|
@@ -31763,7 +32928,7 @@ function createSession(deps) {
|
|
|
31763
32928
|
const orchestrator = TaskOrchestrator.compose({
|
|
31764
32929
|
config: deps.config,
|
|
31765
32930
|
notifications: deps.notifications,
|
|
31766
|
-
runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
|
|
32931
|
+
runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
|
|
31767
32932
|
store,
|
|
31768
32933
|
output: deps.taskOutput,
|
|
31769
32934
|
isolation: deps.isolation,
|
|
@@ -31771,10 +32936,11 @@ function createSession(deps) {
|
|
|
31771
32936
|
workspaceRoot: deps.workspaceRoot,
|
|
31772
32937
|
skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
|
|
31773
32938
|
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
|
|
32939
|
+
lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
|
|
31774
32940
|
runSettings: () => session.planState?.runSettings,
|
|
31775
32941
|
isApproved: (taskId) => session.isApproved(taskId)
|
|
31776
32942
|
});
|
|
31777
|
-
const usage = new PlannerUsageLedger();
|
|
32943
|
+
const usage = new PlannerUsageLedger(catalogWindow);
|
|
31778
32944
|
const events = new SessionEventRelay({
|
|
31779
32945
|
broadcast: deps.broadcast,
|
|
31780
32946
|
onNotice: deps.onNotice,
|
|
@@ -31823,7 +32989,8 @@ function createSession(deps) {
|
|
|
31823
32989
|
modelResolver: deps.modelResolver,
|
|
31824
32990
|
settings: deps.settings,
|
|
31825
32991
|
// Read through the Session, which holds the plan; the catalog is built first.
|
|
31826
|
-
planRunners: () => session.planState?.runners ?? []
|
|
32992
|
+
planRunners: () => session.planState?.runners ?? [],
|
|
32993
|
+
liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
|
|
31827
32994
|
});
|
|
31828
32995
|
const session = new Session({
|
|
31829
32996
|
config: deps.config,
|
|
@@ -31845,6 +33012,7 @@ function createSession(deps) {
|
|
|
31845
33012
|
usage,
|
|
31846
33013
|
approvals,
|
|
31847
33014
|
approvalPolicy,
|
|
33015
|
+
taskLogs,
|
|
31848
33016
|
// Every planner's web reads, carried here or natively, are decided by the
|
|
31849
33017
|
// one policy, which asks through the same channel as paths and commands.
|
|
31850
33018
|
// The response cache and search provider are process-wide, so parallel
|
|
@@ -31895,6 +33063,7 @@ var Session = class {
|
|
|
31895
33063
|
fsAdapter;
|
|
31896
33064
|
approvals;
|
|
31897
33065
|
approvalPolicy;
|
|
33066
|
+
taskLogs;
|
|
31898
33067
|
fetcher;
|
|
31899
33068
|
settingsFn;
|
|
31900
33069
|
save;
|
|
@@ -31930,7 +33099,7 @@ var Session = class {
|
|
|
31930
33099
|
turnId: () => this.conversation.currentTurnId,
|
|
31931
33100
|
recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
|
|
31932
33101
|
liveCatalog: () => this.liveCatalog(),
|
|
31933
|
-
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
|
|
33102
|
+
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
|
|
31934
33103
|
submitPlan: (tasks) => this.submitPlanFromTool(tasks),
|
|
31935
33104
|
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
31936
33105
|
tasks: () => this.store.planTasks,
|
|
@@ -31975,6 +33144,7 @@ var Session = class {
|
|
|
31975
33144
|
this.usage = parts.usage;
|
|
31976
33145
|
this.approvals = parts.approvals;
|
|
31977
33146
|
this.approvalPolicy = parts.approvalPolicy;
|
|
33147
|
+
this.taskLogs = parts.taskLogs;
|
|
31978
33148
|
this.fetcher = parts.fetcher;
|
|
31979
33149
|
this.skillsService = parts.skillsService;
|
|
31980
33150
|
this.save = parts.saveSession;
|
|
@@ -31984,7 +33154,8 @@ var Session = class {
|
|
|
31984
33154
|
aiService: () => this.aiService(),
|
|
31985
33155
|
onProgress: (p) => this.events.progress(p),
|
|
31986
33156
|
opening: (runners) => this.conversationOpening(runners),
|
|
31987
|
-
catalog: () => this.catalog.
|
|
33157
|
+
catalog: () => this.catalog.current(),
|
|
33158
|
+
assignmentScope: () => this.catalog.scope(),
|
|
31988
33159
|
autorun: () => this.userRunSettings().autorun,
|
|
31989
33160
|
takeRunUpdates: () => this.untoldRunUpdates.splice(0),
|
|
31990
33161
|
tasks: () => this.store.planTasks,
|
|
@@ -32004,13 +33175,18 @@ var Session = class {
|
|
|
32004
33175
|
turnAborted: () => this.approvals.clear(isPlannerApproval),
|
|
32005
33176
|
// After the turn has announced its end, so the next one never starts
|
|
32006
33177
|
// ahead of it on a surface.
|
|
32007
|
-
turnReleased: () => queueMicrotask(() => this.turns.pump())
|
|
33178
|
+
turnReleased: () => queueMicrotask(() => this.turns.pump()),
|
|
33179
|
+
usageLimit: (said2) => this.turns.usageLimit(said2)
|
|
32008
33180
|
});
|
|
32009
33181
|
this.turns = new PlanTurns({
|
|
32010
33182
|
plan: () => this.closed ? null : this.plan,
|
|
32011
33183
|
conversationBusy: () => this.conversation.isTurnInFlight,
|
|
32012
33184
|
mayWake: () => this.supervision.mayWake(),
|
|
32013
33185
|
persist: () => this.persist({ background: true }),
|
|
33186
|
+
announce: () => {
|
|
33187
|
+
this.persist({ background: true });
|
|
33188
|
+
this.events.planGenerated(this.plan, this.goal);
|
|
33189
|
+
},
|
|
32014
33190
|
interrupt: () => {
|
|
32015
33191
|
this.conversation.stopTurn("force_send");
|
|
32016
33192
|
},
|
|
@@ -32027,7 +33203,8 @@ var Session = class {
|
|
|
32027
33203
|
},
|
|
32028
33204
|
notice: (message) => this.notice("error", message),
|
|
32029
33205
|
plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
|
|
32030
|
-
assertPlannerReady: () => this.assertPlannerReady()
|
|
33206
|
+
assertPlannerReady: () => this.assertPlannerReady(),
|
|
33207
|
+
plannerRunner: () => runnerForProvider(this.config.aiProvider)
|
|
32031
33208
|
});
|
|
32032
33209
|
this.supervision = new RunSupervision({
|
|
32033
33210
|
plan: () => this.plan,
|
|
@@ -32045,7 +33222,6 @@ var Session = class {
|
|
|
32045
33222
|
},
|
|
32046
33223
|
deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
|
|
32047
33224
|
pump: () => this.turns.pump(),
|
|
32048
|
-
plannerRunner: () => runnerForProvider(this.config.aiProvider),
|
|
32049
33225
|
restartPlanner: () => this.aiService().restart?.(),
|
|
32050
33226
|
abandonTurn: () => this.conversation.abandonHungTurn(),
|
|
32051
33227
|
notice: (level, message) => this.notice(level, message)
|
|
@@ -32127,6 +33303,7 @@ var Session = class {
|
|
|
32127
33303
|
tasks: () => this.plan ? this.store.planTasks : null,
|
|
32128
33304
|
planApproved: () => this.plan?.approved === true,
|
|
32129
33305
|
taskApproved: (taskId) => this.isApproved(taskId),
|
|
33306
|
+
waitsForApproval: (taskId) => this.waitsForApproval(taskId),
|
|
32130
33307
|
approve: (taskIds) => this.approve(taskIds),
|
|
32131
33308
|
autonomous: () => this.config.autonomousMode,
|
|
32132
33309
|
autorun: () => this.userRunSettings().autorun,
|
|
@@ -32148,6 +33325,7 @@ var Session = class {
|
|
|
32148
33325
|
blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
|
|
32149
33326
|
waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
|
|
32150
33327
|
runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
|
|
33328
|
+
joinRun: (approved) => this.joinRun(approved),
|
|
32151
33329
|
startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
|
|
32152
33330
|
stopTask: (taskId) => this.cancelTask(taskId),
|
|
32153
33331
|
stopRun: () => this.stopExecution(),
|
|
@@ -32176,8 +33354,8 @@ var Session = class {
|
|
|
32176
33354
|
* one: nothing it would start is approved until the user executes it.
|
|
32177
33355
|
*/
|
|
32178
33356
|
heldEdit(taskId) {
|
|
32179
|
-
const
|
|
32180
|
-
if (!
|
|
33357
|
+
const held2 = this.conversation.pendingOps();
|
|
33358
|
+
if (!held2 || held2.length === 0 || !this.plan) return false;
|
|
32181
33359
|
if (taskId === void 0) return true;
|
|
32182
33360
|
const before = this.store.get(taskId);
|
|
32183
33361
|
const after = this.asEdited(taskId);
|
|
@@ -32190,10 +33368,10 @@ var Session = class {
|
|
|
32190
33368
|
/** {@link editedTasks}, with the id each of the held batch's handles names in them. */
|
|
32191
33369
|
editedPlan() {
|
|
32192
33370
|
const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
|
|
32193
|
-
const
|
|
32194
|
-
if (!
|
|
33371
|
+
const held2 = this.conversation.pendingOps();
|
|
33372
|
+
if (!held2 || held2.length === 0 || !this.plan) return unedited;
|
|
32195
33373
|
const runners = this.editRunners();
|
|
32196
|
-
const result = applyTaskOps(this.store.planTasks,
|
|
33374
|
+
const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
|
|
32197
33375
|
return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
|
|
32198
33376
|
}
|
|
32199
33377
|
/** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
|
|
@@ -32217,9 +33395,9 @@ var Session = class {
|
|
|
32217
33395
|
* rules, then, for the supervisor, the oscillation guard. `held` are the
|
|
32218
33396
|
* ops the turn already holds, which `ops` join; errors are numbered by `ops`.
|
|
32219
33397
|
*/
|
|
32220
|
-
checkPlannerOps(ops,
|
|
33398
|
+
checkPlannerOps(ops, held2 = []) {
|
|
32221
33399
|
const runners = this.editRunners();
|
|
32222
|
-
const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners),
|
|
33400
|
+
const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held2);
|
|
32223
33401
|
if (!result.ok || !this.supervisorEditing()) return result;
|
|
32224
33402
|
const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
|
|
32225
33403
|
if (!trip) return result;
|
|
@@ -32389,6 +33567,9 @@ var Session = class {
|
|
|
32389
33567
|
onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
|
|
32390
33568
|
if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
|
|
32391
33569
|
},
|
|
33570
|
+
onModelSwitched: (switched) => {
|
|
33571
|
+
void this.followRunnerModel(switched);
|
|
33572
|
+
},
|
|
32392
33573
|
onTaskIdle: ({ taskId, idleSince }) => {
|
|
32393
33574
|
this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
|
|
32394
33575
|
},
|
|
@@ -32509,10 +33690,12 @@ var Session = class {
|
|
|
32509
33690
|
* boundary rewrites `in_progress` to `pending` — nothing is running when a
|
|
32510
33691
|
* session comes off a file — so a surface that re-reads the plan mid-run must
|
|
32511
33692
|
* come here instead, or every task it is watching reads as never started.
|
|
33693
|
+
* An archived snapshot carries the task as it is now: a retry or continue
|
|
33694
|
+
* keeps the archive while the live task runs on and spends more.
|
|
32512
33695
|
*/
|
|
32513
33696
|
get currentPlanState() {
|
|
32514
33697
|
if (!this.plan) return null;
|
|
32515
|
-
const executionLog = this.store.getExecutionLog();
|
|
33698
|
+
const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
|
|
32516
33699
|
const logged = new Set(executionLog.map((s) => s.id));
|
|
32517
33700
|
const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
|
|
32518
33701
|
const record = approvalRecordOf(this.plan);
|
|
@@ -32523,7 +33706,7 @@ var Session = class {
|
|
|
32523
33706
|
phase: "executing",
|
|
32524
33707
|
history: [],
|
|
32525
33708
|
message: "",
|
|
32526
|
-
executionLog
|
|
33709
|
+
executionLog,
|
|
32527
33710
|
pendingTasks,
|
|
32528
33711
|
goal: this.goal,
|
|
32529
33712
|
runners: this.plan.runners,
|
|
@@ -32693,16 +33876,20 @@ var Session = class {
|
|
|
32693
33876
|
opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
|
|
32694
33877
|
} catch (err) {
|
|
32695
33878
|
if (this.opening === pending) this.opening = null;
|
|
32696
|
-
for (const
|
|
33879
|
+
for (const held3 of pending.held ?? []) held3.reject(err);
|
|
32697
33880
|
throw err;
|
|
32698
33881
|
}
|
|
32699
|
-
const
|
|
33882
|
+
const held2 = pending.held ?? [];
|
|
32700
33883
|
pending.held = null;
|
|
32701
|
-
for (const h of
|
|
33884
|
+
for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
|
|
32702
33885
|
try {
|
|
32703
33886
|
const { landed: landed2, effects } = await opened;
|
|
32704
33887
|
this.autorunSubmitted(effects);
|
|
32705
33888
|
return landed2;
|
|
33889
|
+
} catch (err) {
|
|
33890
|
+
if (!(err instanceof PlannerUsageLimitError) || !this.plan) throw err;
|
|
33891
|
+
this.turns.waitOnLimit({ source: "user", ...this.resolveSkillInvocation(goal) });
|
|
33892
|
+
return this.plan;
|
|
32706
33893
|
} finally {
|
|
32707
33894
|
if (this.opening === pending) this.opening = null;
|
|
32708
33895
|
}
|
|
@@ -32717,10 +33904,10 @@ var Session = class {
|
|
|
32717
33904
|
behindOpening(input, forced = false) {
|
|
32718
33905
|
const pending = this.opening;
|
|
32719
33906
|
if (!pending) return null;
|
|
32720
|
-
const
|
|
32721
|
-
if (!
|
|
33907
|
+
const held2 = pending.held;
|
|
33908
|
+
if (!held2) return this.turns.follow(input, forced);
|
|
32722
33909
|
return new Promise((resolve9, reject) => {
|
|
32723
|
-
|
|
33910
|
+
held2.push({ input, forced, resolve: resolve9, reject });
|
|
32724
33911
|
});
|
|
32725
33912
|
}
|
|
32726
33913
|
/**
|
|
@@ -32819,12 +34006,12 @@ var Session = class {
|
|
|
32819
34006
|
await this.liveCatalog();
|
|
32820
34007
|
if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
|
|
32821
34008
|
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
32822
|
-
const
|
|
32823
|
-
if (!
|
|
32824
|
-
const result = this.checkPlannerOps(ops,
|
|
34009
|
+
const held2 = this.conversation.pendingOps();
|
|
34010
|
+
if (!held2) return refuse("A whole plan was submitted in this reply and replaces the plan when it ends: make the edit in your next reply.");
|
|
34011
|
+
const result = this.checkPlannerOps(ops, held2);
|
|
32825
34012
|
if (!result.ok) return { ok: false, errors: result.errors };
|
|
32826
|
-
if (!this.conversation.submit({ kind: "task_ops", ops: [...
|
|
32827
|
-
const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models());
|
|
34013
|
+
if (!this.conversation.submit({ kind: "task_ops", ops: [...held2, ...ops] })) return refuse("No planning turn is open to take the edit.");
|
|
34014
|
+
const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks);
|
|
32828
34015
|
return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
|
|
32829
34016
|
}
|
|
32830
34017
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
@@ -32861,9 +34048,6 @@ var Session = class {
|
|
|
32861
34048
|
autonomousDefault: this.config.autonomousMode,
|
|
32862
34049
|
autorun: this.userRunSettings().autorun,
|
|
32863
34050
|
isolatedExecution: await this.runs.plannerLayout(),
|
|
32864
|
-
// The planner's own model window, when a cached catalog knows it, so the
|
|
32865
|
-
// usage line can show context fill (#49). Unknown stays absent.
|
|
32866
|
-
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
32867
34051
|
fs: this.fsAdapter,
|
|
32868
34052
|
fetcher: this.fetcher,
|
|
32869
34053
|
approval: this.approvalPolicy,
|
|
@@ -32890,7 +34074,7 @@ var Session = class {
|
|
|
32890
34074
|
for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
|
|
32891
34075
|
const runners = this.plan.runners;
|
|
32892
34076
|
const before = this.store.planTasks;
|
|
32893
|
-
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
|
|
34077
|
+
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
|
|
32894
34078
|
if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
|
|
32895
34079
|
const bySupervisor = this.supervisorEditing();
|
|
32896
34080
|
const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
|
|
@@ -32903,6 +34087,7 @@ var Session = class {
|
|
|
32903
34087
|
this.store.resetForRun({ keepScheduledResumes: true });
|
|
32904
34088
|
}
|
|
32905
34089
|
if (bySupervisor) this.approve(added);
|
|
34090
|
+
else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
|
|
32906
34091
|
return coerced.length;
|
|
32907
34092
|
}
|
|
32908
34093
|
preparePlannerTaskUpdates(tasks) {
|
|
@@ -32937,6 +34122,11 @@ var Session = class {
|
|
|
32937
34122
|
if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
|
|
32938
34123
|
}
|
|
32939
34124
|
async executePlan() {
|
|
34125
|
+
if (this.orchestrator.hasLiveWork) {
|
|
34126
|
+
const outcome = await this.execute({ command: "run_plan" }, { kind: "user" });
|
|
34127
|
+
if (!outcome.ok) throw new TaskControlError(outcome.message);
|
|
34128
|
+
return;
|
|
34129
|
+
}
|
|
32940
34130
|
this.idlePlan();
|
|
32941
34131
|
this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
|
|
32942
34132
|
await this.runWholePlan("user");
|
|
@@ -32958,6 +34148,25 @@ var Session = class {
|
|
|
32958
34148
|
await this.orchestrator.approveReview(origin);
|
|
32959
34149
|
});
|
|
32960
34150
|
}
|
|
34151
|
+
/**
|
|
34152
|
+
* Execute while a run is under way (#95): the seam has approved every task,
|
|
34153
|
+
* as executing does, and the run takes them in. A run the scheduler was not
|
|
34154
|
+
* driving, one task started on its own, becomes the plan's run, as Execute
|
|
34155
|
+
* would make it.
|
|
34156
|
+
*/
|
|
34157
|
+
async joinRun(approved) {
|
|
34158
|
+
const plan = this.plan;
|
|
34159
|
+
const scheduling = this.orchestrator.isScheduling;
|
|
34160
|
+
await this.withSave(async () => {
|
|
34161
|
+
plan.approved = true;
|
|
34162
|
+
if (scheduling) await this.orchestrator.tick();
|
|
34163
|
+
else if (this.orchestrator.isReviewApproved) await this.orchestrator.start("user");
|
|
34164
|
+
else await this.orchestrator.approveReview("user");
|
|
34165
|
+
});
|
|
34166
|
+
const line = approved === 0 ? scheduling ? "The plan is already running, and no task waits for your approval." : "The plan is running: each task starts once its dependencies finish." : approved === 1 ? "Approved 1 task; it starts once its dependencies finish." : `Approved ${approved} tasks; each starts once its dependencies finish.`;
|
|
34167
|
+
this.notice("info", line);
|
|
34168
|
+
return line;
|
|
34169
|
+
}
|
|
32961
34170
|
async approveReview() {
|
|
32962
34171
|
if (!this.plan) throw new NoPlanError("review");
|
|
32963
34172
|
const plan = this.plan;
|
|
@@ -32966,9 +34175,12 @@ var Session = class {
|
|
|
32966
34175
|
await this.withSave(() => this.orchestrator.approveReview("user"));
|
|
32967
34176
|
return plan;
|
|
32968
34177
|
}
|
|
34178
|
+
/** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
|
|
32969
34179
|
async forceStartTask(taskId) {
|
|
32970
34180
|
this.approveTask(taskId);
|
|
32971
|
-
|
|
34181
|
+
if (!this.plan) return;
|
|
34182
|
+
this.requireTask(taskId);
|
|
34183
|
+
return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
|
|
32972
34184
|
}
|
|
32973
34185
|
async startOneTask(taskId, origin) {
|
|
32974
34186
|
if (!this.plan) return;
|
|
@@ -33025,10 +34237,51 @@ var Session = class {
|
|
|
33025
34237
|
async resumeNow(taskId) {
|
|
33026
34238
|
await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
|
|
33027
34239
|
}
|
|
34240
|
+
/**
|
|
34241
|
+
* Keep what waits on the planner's usage limit from being sent at the reset:
|
|
34242
|
+
* it goes with the user's next message instead. False when nothing waits on
|
|
34243
|
+
* a reset.
|
|
34244
|
+
*/
|
|
34245
|
+
cancelPlannerResend() {
|
|
34246
|
+
return this.turns.cancelResend();
|
|
34247
|
+
}
|
|
33028
34248
|
/** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
|
|
33029
34249
|
cancelScheduledResume(taskId) {
|
|
33030
34250
|
return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
|
|
33031
34251
|
}
|
|
34252
|
+
/**
|
|
34253
|
+
* The plan follows a model switch the user made in the runner (#106), and
|
|
34254
|
+
* the task's log says what the plan holds once that settled: a write-back
|
|
34255
|
+
* not made — no model command asked for the change, or the edit failed — is
|
|
34256
|
+
* stated, never shown as made. Once the switch's attempt has ended, the plan
|
|
34257
|
+
* may be another one, so nothing of it is said.
|
|
34258
|
+
*/
|
|
34259
|
+
async followRunnerModel({ taskId, sessionId, from, model, effort, asked, attemptEnded }) {
|
|
34260
|
+
let kept = asked ? void 0 : "no model command in the chat asked for it";
|
|
34261
|
+
const live = () => this.orchestrator.activeSessionMap.get(taskId) === sessionId;
|
|
34262
|
+
const ended = () => this.taskLogs.note(sessionId, taskId, { type: "model_switched", from, to: model, kept: kept ?? "its attempt had already ended" });
|
|
34263
|
+
if (attemptEnded) return ended();
|
|
34264
|
+
if (asked) {
|
|
34265
|
+
try {
|
|
34266
|
+
if (!await this.editor.followRunnerModel(taskId, model, effort, live) && !live()) return ended();
|
|
34267
|
+
} catch (err) {
|
|
34268
|
+
kept = `the plan could not follow it: ${err instanceof Error ? err.message : String(err)}`;
|
|
34269
|
+
this.notice("warn", `The plan could not follow the model switch of task ${this.store.get(taskId)?.order ?? taskId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
34270
|
+
}
|
|
34271
|
+
}
|
|
34272
|
+
const assigned = this.store.get(taskId)?.assignedModel;
|
|
34273
|
+
if (!kept && assigned?.modelId !== model.model) kept = "the plan did not take the edit";
|
|
34274
|
+
this.taskLogs.note(sessionId, taskId, {
|
|
34275
|
+
type: "model_switched",
|
|
34276
|
+
from,
|
|
34277
|
+
to: model,
|
|
34278
|
+
plan: {
|
|
34279
|
+
...assigned ? { model: { model: assigned.modelId, label: assigned.modelLabel } } : {},
|
|
34280
|
+
...assigned?.thinkingEffort ? { effort: assigned.thinkingEffort } : {}
|
|
34281
|
+
},
|
|
34282
|
+
...kept ? { kept } : {}
|
|
34283
|
+
});
|
|
34284
|
+
}
|
|
33032
34285
|
/** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
|
|
33033
34286
|
notice(level, message) {
|
|
33034
34287
|
this.notifications[level](message);
|
|
@@ -33100,8 +34353,8 @@ var Session = class {
|
|
|
33100
34353
|
if (!this.plan) return EMPTY_RUN;
|
|
33101
34354
|
const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
|
|
33102
34355
|
const view = reduceRun(planned, this.events.statusMessage());
|
|
33103
|
-
const
|
|
33104
|
-
return this.sentHandoff ? reduceRun(
|
|
34356
|
+
const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
|
|
34357
|
+
return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
|
|
33105
34358
|
}
|
|
33106
34359
|
/** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
|
|
33107
34360
|
mergeGate(taskId) {
|
|
@@ -33260,6 +34513,7 @@ var Session = class {
|
|
|
33260
34513
|
if (adopting) this.dropPlanScope();
|
|
33261
34514
|
this.plan = plan;
|
|
33262
34515
|
if (adopting) this.supervision.restore(plan.supervision);
|
|
34516
|
+
if (adopting) this.turns.restoreResend();
|
|
33263
34517
|
this.goal = goal;
|
|
33264
34518
|
this.workspace = workspace;
|
|
33265
34519
|
plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
|
|
@@ -33502,6 +34756,7 @@ function syncTranscript(view, { plan, turnId }) {
|
|
|
33502
34756
|
if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
|
|
33503
34757
|
if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
|
|
33504
34758
|
if (isNew && entry.kind === "system") next = append(next, (id) => settledMessage(id, "system", entry.content));
|
|
34759
|
+
if (isNew && entry.kind === "error") next = append(next, (id) => settledMessage(id, "error", entry.content));
|
|
33505
34760
|
if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
|
|
33506
34761
|
if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
|
|
33507
34762
|
}
|
|
@@ -33582,6 +34837,8 @@ function roleOf(entry) {
|
|
|
33582
34837
|
return "wake_up";
|
|
33583
34838
|
case "supervisor":
|
|
33584
34839
|
return "supervisor";
|
|
34840
|
+
case "error":
|
|
34841
|
+
return "error";
|
|
33585
34842
|
default:
|
|
33586
34843
|
return entry.role === "user" ? "user" : "planner";
|
|
33587
34844
|
}
|
|
@@ -33846,13 +35103,18 @@ function blockedBy(task, tasks) {
|
|
|
33846
35103
|
if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
|
|
33847
35104
|
return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
|
|
33848
35105
|
}
|
|
35106
|
+
function flagOf(task, scope) {
|
|
35107
|
+
const issue = scope && task.assignedRunner !== void 0 ? assignmentIssue({ ...task, assignedRunner: task.assignedRunner }, scope) : null;
|
|
35108
|
+
return issue ? { assignmentFlag: assignmentIssueText(issue), assignmentConsequence: assignmentConsequence(task) } : { assignmentFlag: null, assignmentConsequence: null };
|
|
35109
|
+
}
|
|
33849
35110
|
function taskRowView(task, context = {}) {
|
|
33850
35111
|
const parent = context.parent ?? void 0;
|
|
33851
35112
|
const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
|
|
33852
|
-
const
|
|
35113
|
+
const spend = taskUsageSummary(task);
|
|
35114
|
+
const latestAttempt = task.attemptUsage?.at(-1);
|
|
33853
35115
|
const blocked = blockedBy(task, context.tasks);
|
|
33854
35116
|
return {
|
|
33855
|
-
usage:
|
|
35117
|
+
usage: latestAttempt ? { totals: spend.totals, ...spend.contextFill ? { contextFill: spend.contextFill } : {}, attempt: latestAttempt.attempt } : task.type === "ai" && ["completed", "failed", "in_progress", "awaiting_user"].includes(task.status) ? { totals: {}, attempt: 0 } : null,
|
|
33856
35118
|
orderLabel: taskOrderLabel(task, parent),
|
|
33857
35119
|
kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
|
|
33858
35120
|
status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
|
|
@@ -33861,6 +35123,7 @@ function taskRowView(task, context = {}) {
|
|
|
33861
35123
|
resumes: resumeLabel(task, context.now),
|
|
33862
35124
|
approvals: approvalLabel(task.awaitingApproval),
|
|
33863
35125
|
waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
|
|
35126
|
+
...flagOf(task, context.runnerScope),
|
|
33864
35127
|
autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
|
|
33865
35128
|
repos: namedRepos(isolation?.repos),
|
|
33866
35129
|
conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
|
|
@@ -34066,6 +35329,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34066
35329
|
messageCount = 0;
|
|
34067
35330
|
interrupting = null;
|
|
34068
35331
|
lastSessionId = null;
|
|
35332
|
+
lastSessionCost = null;
|
|
34069
35333
|
structuredEmitter = new import_events2.EventEmitter();
|
|
34070
35334
|
permissionCount = 0;
|
|
34071
35335
|
/**
|
|
@@ -34270,6 +35534,9 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34270
35534
|
nativeSessionId() {
|
|
34271
35535
|
return this.adapter?.nativeSessionId() ?? this.lastSessionId;
|
|
34272
35536
|
}
|
|
35537
|
+
sessionCost() {
|
|
35538
|
+
return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
|
|
35539
|
+
}
|
|
34273
35540
|
answerPermission(id, decision) {
|
|
34274
35541
|
const open = this.permissions.get(id);
|
|
34275
35542
|
if (!open) return false;
|
|
@@ -34317,6 +35584,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34317
35584
|
*/
|
|
34318
35585
|
async restartInterrupted(turn) {
|
|
34319
35586
|
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
35587
|
+
const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
|
|
34320
35588
|
this.generation += 1;
|
|
34321
35589
|
turn.abort.abort();
|
|
34322
35590
|
if (this.adapter) {
|
|
@@ -34326,7 +35594,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34326
35594
|
for (const message of this.queue) this.requeue(message);
|
|
34327
35595
|
try {
|
|
34328
35596
|
this.revokeTools();
|
|
34329
|
-
await this.startAttached({ ...this.startOptions, resumeSessionId });
|
|
35597
|
+
await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
|
|
34330
35598
|
} catch (err) {
|
|
34331
35599
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
34332
35600
|
this.withdrawPermissions();
|
|
@@ -34353,6 +35621,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34353
35621
|
return;
|
|
34354
35622
|
}
|
|
34355
35623
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
35624
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
34356
35625
|
this.withdrawPermissions();
|
|
34357
35626
|
this.baseHandleExit(code);
|
|
34358
35627
|
});
|
|
@@ -34390,6 +35659,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34390
35659
|
this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
34391
35660
|
}).then(() => {
|
|
34392
35661
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
35662
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
34393
35663
|
if (generation2 === this.generation) this.endTurn(turn, turn.reason);
|
|
34394
35664
|
});
|
|
34395
35665
|
this.offerNext();
|
|
@@ -34516,6 +35786,11 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34516
35786
|
case "error":
|
|
34517
35787
|
this.text.failure(event.message);
|
|
34518
35788
|
break;
|
|
35789
|
+
// The plan follows the switch (#106), so a restart after an interrupt
|
|
35790
|
+
// runs what the plan now says rather than what this attempt started on.
|
|
35791
|
+
case "model_switched":
|
|
35792
|
+
this.startOptions = { ...this.startOptions, model: event.to.model };
|
|
35793
|
+
break;
|
|
34519
35794
|
default:
|
|
34520
35795
|
break;
|
|
34521
35796
|
}
|
|
@@ -34596,7 +35871,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
34596
35871
|
model: opts.modelId,
|
|
34597
35872
|
mode: opts.mode ?? "default",
|
|
34598
35873
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
34599
|
-
resumeSessionId: opts.resumeSessionId
|
|
35874
|
+
resumeSessionId: opts.resumeSessionId,
|
|
35875
|
+
resumeCost: opts.resumeCost
|
|
34600
35876
|
},
|
|
34601
35877
|
interruptGraceMs: this.interruptGraceMs,
|
|
34602
35878
|
tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
|
|
@@ -34899,6 +36175,7 @@ var PlannerModelMemory = class {
|
|
|
34899
36175
|
PlannerNotReadyError,
|
|
34900
36176
|
PlannerTurnDiscardedError,
|
|
34901
36177
|
PlannerTurnStoppedError,
|
|
36178
|
+
PlannerUsageLimitError,
|
|
34902
36179
|
REFUSED_COMMANDS,
|
|
34903
36180
|
RUN_SETTINGS_DEFAULTS,
|
|
34904
36181
|
RunSettingsError,
|
|
@@ -34955,6 +36232,8 @@ var PlannerModelMemory = class {
|
|
|
34955
36232
|
approvalScopes,
|
|
34956
36233
|
assertWorkspaceExists,
|
|
34957
36234
|
assertWorkspaceIsProject,
|
|
36235
|
+
assignmentIssue,
|
|
36236
|
+
assignmentIssueText,
|
|
34958
36237
|
augmentedPath,
|
|
34959
36238
|
automergeSummary,
|
|
34960
36239
|
autonomyLevelLabel,
|
|
@@ -34971,6 +36250,7 @@ var PlannerModelMemory = class {
|
|
|
34971
36250
|
canSetDependencies,
|
|
34972
36251
|
canSplitTask,
|
|
34973
36252
|
capConflictFiles,
|
|
36253
|
+
checkoutRule,
|
|
34974
36254
|
classifyCommand,
|
|
34975
36255
|
classifyOutcome,
|
|
34976
36256
|
clearAugmentedPathCache,
|
|
@@ -34984,6 +36264,7 @@ var PlannerModelMemory = class {
|
|
|
34984
36264
|
commandAllowedBy,
|
|
34985
36265
|
configuredProviders,
|
|
34986
36266
|
continuability,
|
|
36267
|
+
countedSessionCost,
|
|
34987
36268
|
createAiService,
|
|
34988
36269
|
createEmptyPlan,
|
|
34989
36270
|
createSession,
|
|
@@ -35047,6 +36328,7 @@ var PlannerModelMemory = class {
|
|
|
35047
36328
|
isMeasured,
|
|
35048
36329
|
isOpenAiProvider,
|
|
35049
36330
|
isPlannerBackend,
|
|
36331
|
+
isRepoGroup,
|
|
35050
36332
|
isRunnerApproval,
|
|
35051
36333
|
isSupervisorLevel,
|
|
35052
36334
|
isTaskRunning,
|
|
@@ -35069,6 +36351,7 @@ var PlannerModelMemory = class {
|
|
|
35069
36351
|
markdownHtml,
|
|
35070
36352
|
markdownTokens,
|
|
35071
36353
|
mcpClientConfig,
|
|
36354
|
+
mergeTarget,
|
|
35072
36355
|
migrateOldConfigDir,
|
|
35073
36356
|
migratePlanIsolation,
|
|
35074
36357
|
migratePlanState,
|
|
@@ -35118,6 +36401,7 @@ var PlannerModelMemory = class {
|
|
|
35118
36401
|
removedPluginNotice,
|
|
35119
36402
|
renderTaskQueryAnswer,
|
|
35120
36403
|
replayTaskLog,
|
|
36404
|
+
reposWithWork,
|
|
35121
36405
|
researchShellWarning,
|
|
35122
36406
|
researchToolsPath,
|
|
35123
36407
|
resetInstalledPlanner,
|
|
@@ -35131,6 +36415,7 @@ var PlannerModelMemory = class {
|
|
|
35131
36415
|
resolveWithin,
|
|
35132
36416
|
resumeLabel,
|
|
35133
36417
|
resumeTime,
|
|
36418
|
+
resumedSessionWindow,
|
|
35134
36419
|
runSettingsOf,
|
|
35135
36420
|
runnerForProvider,
|
|
35136
36421
|
runnerModesFrom,
|
|
@@ -35171,6 +36456,7 @@ var PlannerModelMemory = class {
|
|
|
35171
36456
|
taskRowView,
|
|
35172
36457
|
taskStartedNotice,
|
|
35173
36458
|
taskStatusKind,
|
|
36459
|
+
taskUsageSummary,
|
|
35174
36460
|
textHasTaskOps,
|
|
35175
36461
|
textHasTaskQuery,
|
|
35176
36462
|
titledRefs,
|