@ordewell/core 0.10.4 → 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-_effm8-b.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
- package/dist/{IFileSystem-vyqMdL-x.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
- package/dist/{IRunner-D_UkkMbJ.d.ts → IRunner-Crkt3tHD.d.ts} +64 -3
- package/dist/{IRunner-NtTHQgLR.d.mts → IRunner-lMQIa5Wc.d.mts} +64 -3
- package/dist/{ModeResolver-wnTFsDnF.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
- package/dist/{ModeResolver-Dy3Ef6p-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
- package/dist/{Task-DZs9eKgg.d.mts → Task-D4QHjdGv.d.mts} +92 -10
- package/dist/{Task-DZs9eKgg.d.ts → Task-D4QHjdGv.d.ts} +92 -10
- 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 +296 -64
- package/dist/index.d.ts +296 -64
- package/dist/index.js +1576 -376
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1455 -362
- 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-POIz5d8z.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
- package/dist/{plan-utils-CtKceSsQ.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",
|
|
@@ -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,7 +7931,8 @@ 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
7938
|
return preferredFirst(models, manifest);
|
|
@@ -7920,7 +7973,8 @@ function parseOpenCodeModelsApi(stdout, manifest) {
|
|
|
7920
7973
|
modelId: fullId,
|
|
7921
7974
|
modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
|
|
7922
7975
|
runnerProvider: entry.providerID,
|
|
7923
|
-
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
|
|
7976
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
7977
|
+
...contextLimit(entry.limit)
|
|
7924
7978
|
});
|
|
7925
7979
|
}
|
|
7926
7980
|
return preferredFirst(models, manifest);
|
|
@@ -9804,6 +9858,7 @@ var LOG_KINDS = {
|
|
|
9804
9858
|
turn_start: true,
|
|
9805
9859
|
command: true,
|
|
9806
9860
|
command_output: true,
|
|
9861
|
+
model_switched: true,
|
|
9807
9862
|
compaction: true,
|
|
9808
9863
|
runner_loaded: true,
|
|
9809
9864
|
turn_end: true,
|
|
@@ -9839,6 +9894,11 @@ ${s.content}` : ""}`).join("\n");
|
|
|
9839
9894
|
return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
|
|
9840
9895
|
case "command_output":
|
|
9841
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
|
+
}
|
|
9842
9902
|
case "compaction":
|
|
9843
9903
|
return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
|
|
9844
9904
|
case "runner_loaded":
|
|
@@ -11398,6 +11458,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
|
|
|
11398
11458
|
...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
|
|
11399
11459
|
};
|
|
11400
11460
|
}
|
|
11461
|
+
var MODEL_COMMAND = "model";
|
|
11401
11462
|
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
11402
11463
|
function usageRecord(usage, model, subagentId) {
|
|
11403
11464
|
const record = {
|
|
@@ -11459,9 +11520,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11459
11520
|
/** The model answering the planner's current message, as its `message_start` named it. */
|
|
11460
11521
|
plannerModel;
|
|
11461
11522
|
/**
|
|
11462
|
-
* The session's `total_cost_usd` as last
|
|
11463
|
-
*
|
|
11464
|
-
*
|
|
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.
|
|
11465
11526
|
*/
|
|
11466
11527
|
reportedCostUsd = 0;
|
|
11467
11528
|
reportedModelCostUsd = /* @__PURE__ */ new Map();
|
|
@@ -11502,6 +11563,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11502
11563
|
terminalOnly = /* @__PURE__ */ new Set();
|
|
11503
11564
|
/** Whether this process's `runner_loaded` went out: its first `init` sends it. */
|
|
11504
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 = [];
|
|
11505
11579
|
spawnSpec(opts, workspace) {
|
|
11506
11580
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
11507
11581
|
const args = [
|
|
@@ -11527,12 +11601,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11527
11601
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
11528
11602
|
if (opts.resumeSessionId) {
|
|
11529
11603
|
args.push("--resume", opts.resumeSessionId);
|
|
11530
|
-
this.
|
|
11604
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
11531
11605
|
this.pendingResume = opts.resumeSessionId;
|
|
11532
11606
|
}
|
|
11533
11607
|
this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
|
|
11534
11608
|
return { command: "claude", args, env: NO_AUTO_MEMORY };
|
|
11535
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
|
+
}
|
|
11536
11621
|
savedOutput(abs) {
|
|
11537
11622
|
return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
|
|
11538
11623
|
}
|
|
@@ -11547,6 +11632,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11547
11632
|
*/
|
|
11548
11633
|
taskSpawnSpec(opts) {
|
|
11549
11634
|
this.requestedMode = opts.flags.permissionMode;
|
|
11635
|
+
this.followedModel = opts.model;
|
|
11550
11636
|
const args = [
|
|
11551
11637
|
...TASK_PROTOCOL_ARGS,
|
|
11552
11638
|
"--permission-prompt-tool",
|
|
@@ -11560,7 +11646,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11560
11646
|
if (opts.model) args.push("--model", opts.model);
|
|
11561
11647
|
if (opts.resumeSessionId) {
|
|
11562
11648
|
args.push("--resume", opts.resumeSessionId);
|
|
11563
|
-
this.
|
|
11649
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
11564
11650
|
this.pendingResume = opts.resumeSessionId;
|
|
11565
11651
|
}
|
|
11566
11652
|
if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
|
|
@@ -11602,6 +11688,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11602
11688
|
if (opts.kind !== "task") return;
|
|
11603
11689
|
const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
|
|
11604
11690
|
this.listedCommands = initialized?.commands ?? [];
|
|
11691
|
+
this.listedModels = initialized?.models ?? [];
|
|
11605
11692
|
if (opts.flags.permissionMode !== "plan" || !opts.model) return;
|
|
11606
11693
|
const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
|
|
11607
11694
|
if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
|
|
@@ -11655,6 +11742,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11655
11742
|
if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
|
|
11656
11743
|
const uuid = (0, import_crypto4.randomUUID)();
|
|
11657
11744
|
this.steers.set(uuid, id);
|
|
11745
|
+
this.noteModelCommand(text3, 2);
|
|
11658
11746
|
this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text: text3 }] } });
|
|
11659
11747
|
return true;
|
|
11660
11748
|
}
|
|
@@ -11706,12 +11794,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
11706
11794
|
}
|
|
11707
11795
|
turnPayload(message) {
|
|
11708
11796
|
this.openTurnState();
|
|
11797
|
+
this.noteModelCommand(message, 1);
|
|
11709
11798
|
return `${JSON.stringify({
|
|
11710
11799
|
type: "user",
|
|
11711
11800
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
11712
11801
|
})}
|
|
11713
11802
|
`;
|
|
11714
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
|
+
}
|
|
11715
11814
|
openTurnState() {
|
|
11716
11815
|
this.turnHasText = false;
|
|
11717
11816
|
this.interruptRequested = false;
|
|
@@ -11823,6 +11922,7 @@ ${block.text}` : block.text });
|
|
|
11823
11922
|
this.clearFollowOnTimer();
|
|
11824
11923
|
if (this.refuseOtherMode(msg, emit)) return;
|
|
11825
11924
|
this.readLoaded(msg, emit);
|
|
11925
|
+
this.followModel(msg.model, emit);
|
|
11826
11926
|
}
|
|
11827
11927
|
if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
|
|
11828
11928
|
if (msg.subtype === "compact_boundary") {
|
|
@@ -11845,6 +11945,7 @@ ${block.text}` : block.text });
|
|
|
11845
11945
|
return;
|
|
11846
11946
|
case "result":
|
|
11847
11947
|
this.reportSessionCost(msg, emit);
|
|
11948
|
+
for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
|
|
11848
11949
|
if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
|
|
11849
11950
|
this.interruptRequested = false;
|
|
11850
11951
|
this.closeTurn({ type: "turn_end", interrupted: true }, emit);
|
|
@@ -11864,6 +11965,55 @@ ${block.text}` : block.text });
|
|
|
11864
11965
|
return;
|
|
11865
11966
|
}
|
|
11866
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
|
+
}
|
|
11867
12017
|
/**
|
|
11868
12018
|
* `--permission-mode auto` on a model or account without auto mode is not
|
|
11869
12019
|
* refused: the CLI says nothing and starts in `default`, then asks about every
|
|
@@ -11947,8 +12097,10 @@ ${block.text}` : block.text });
|
|
|
11947
12097
|
* {@link AgentEvent}), so nothing here has to reconcile with it.
|
|
11948
12098
|
*/
|
|
11949
12099
|
handleStreamEvent(event, emit) {
|
|
11950
|
-
if (event.type === "message_start")
|
|
11951
|
-
|
|
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) });
|
|
11952
12104
|
else if (event.type === "content_block_start" && event.content_block?.type === "text") {
|
|
11953
12105
|
this.pendingBreak = this.turnHasText;
|
|
11954
12106
|
} else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
|
@@ -11963,7 +12115,8 @@ ${block.text}` : block.text });
|
|
|
11963
12115
|
* What only the result line knows: the cost, which covers every call the
|
|
11964
12116
|
* session made — subagents included, since none of their lines carries one —
|
|
11965
12117
|
* and the planner model's window. `total_cost_usd` is a running total, so a
|
|
11966
|
-
* 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
|
|
11967
12120
|
* baseline: its total includes turns counted before, and a turn's own share
|
|
11968
12121
|
* cannot be told apart from them.
|
|
11969
12122
|
*/
|
|
@@ -12543,6 +12696,97 @@ var CodexRpc = class {
|
|
|
12543
12696
|
}
|
|
12544
12697
|
};
|
|
12545
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
|
+
|
|
12546
12790
|
// src/services/harness/CodexAdapter.ts
|
|
12547
12791
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
12548
12792
|
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
@@ -12686,6 +12930,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
12686
12930
|
turn = null;
|
|
12687
12931
|
/** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
|
|
12688
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();
|
|
12689
12938
|
/** An interrupt was asked for during this turn, so however it ends, it was cut short. */
|
|
12690
12939
|
interruptRequested = false;
|
|
12691
12940
|
/**
|
|
@@ -13081,6 +13330,9 @@ ${this.exitMessage()}`);
|
|
|
13081
13330
|
case "thread/tokenUsage/updated":
|
|
13082
13331
|
this.emitUsage(msg.params, emit);
|
|
13083
13332
|
return;
|
|
13333
|
+
case "account/rateLimits/updated":
|
|
13334
|
+
this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
|
|
13335
|
+
return;
|
|
13084
13336
|
case "turn/started": {
|
|
13085
13337
|
if (this.subagentOf(msg.params?.threadId)) return;
|
|
13086
13338
|
this.turnHasText = false;
|
|
@@ -13121,7 +13373,7 @@ ${this.exitMessage()}`);
|
|
|
13121
13373
|
const failure = msg.params;
|
|
13122
13374
|
if (failure?.willRetry) return;
|
|
13123
13375
|
this.closeTurn(emit);
|
|
13124
|
-
emit(
|
|
13376
|
+
emit(this.turnFailed(failure?.error));
|
|
13125
13377
|
return;
|
|
13126
13378
|
}
|
|
13127
13379
|
case "turn/completed": {
|
|
@@ -13132,7 +13384,7 @@ ${this.exitMessage()}`);
|
|
|
13132
13384
|
if (this.interruptRequested || turn?.status === "interrupted") {
|
|
13133
13385
|
emit({ type: "turn_end", interrupted: true });
|
|
13134
13386
|
} else if (turn?.status === "failed") {
|
|
13135
|
-
emit(
|
|
13387
|
+
emit(this.turnFailed(turn.error));
|
|
13136
13388
|
} else {
|
|
13137
13389
|
emit({ type: "turn_end" });
|
|
13138
13390
|
}
|
|
@@ -13144,6 +13396,11 @@ ${this.exitMessage()}`);
|
|
|
13144
13396
|
return;
|
|
13145
13397
|
}
|
|
13146
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
|
+
}
|
|
13147
13404
|
turnStarted(id) {
|
|
13148
13405
|
const turn = this.turn;
|
|
13149
13406
|
if (!turn || turn.id || typeof id !== "string") return;
|
|
@@ -13601,9 +13858,9 @@ function recordUnusable(servers, approval) {
|
|
|
13601
13858
|
for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
|
|
13602
13859
|
}
|
|
13603
13860
|
var NOT_APPROVED = "not approved for planning";
|
|
13604
|
-
function heldServersNote(
|
|
13861
|
+
function heldServersNote(held2, unreadable = []) {
|
|
13605
13862
|
const lines2 = [
|
|
13606
|
-
...
|
|
13863
|
+
...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
|
|
13607
13864
|
...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
|
|
13608
13865
|
];
|
|
13609
13866
|
if (lines2.length === 0) return "";
|
|
@@ -13740,8 +13997,8 @@ var PendingSteers = class {
|
|
|
13740
13997
|
* False when it was already delivered, so a late failure does not recall it.
|
|
13741
13998
|
*/
|
|
13742
13999
|
forget(id) {
|
|
13743
|
-
for (const [messageId,
|
|
13744
|
-
if (
|
|
14000
|
+
for (const [messageId, held2] of this.pending) {
|
|
14001
|
+
if (held2 !== id) continue;
|
|
13745
14002
|
this.pending.delete(messageId);
|
|
13746
14003
|
return true;
|
|
13747
14004
|
}
|
|
@@ -13819,9 +14076,9 @@ var ChildSessions = class {
|
|
|
13819
14076
|
adopt(child, callId) {
|
|
13820
14077
|
if (this.owners.get(child)) return null;
|
|
13821
14078
|
this.owners.set(child, callId);
|
|
13822
|
-
const
|
|
14079
|
+
const held2 = this.held.get(child) ?? [];
|
|
13823
14080
|
this.held.delete(child);
|
|
13824
|
-
return
|
|
14081
|
+
return held2;
|
|
13825
14082
|
}
|
|
13826
14083
|
owns(callId) {
|
|
13827
14084
|
return [...this.owners.values()].includes(callId);
|
|
@@ -14345,11 +14602,11 @@ var OpenCodeV2 = class {
|
|
|
14345
14602
|
adoptChild(data, state, onEvent, turn) {
|
|
14346
14603
|
const child = data.metadata?.sessionID;
|
|
14347
14604
|
const callId = data.id;
|
|
14348
|
-
const
|
|
14349
|
-
if (!
|
|
14605
|
+
const held2 = child && callId ? state.children.adopt(child, callId) : null;
|
|
14606
|
+
if (!held2 || !callId) return;
|
|
14350
14607
|
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
14351
14608
|
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
14352
|
-
for (const frame of
|
|
14609
|
+
for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
|
|
14353
14610
|
}
|
|
14354
14611
|
finishChild(callId, success, output, state, onEvent) {
|
|
14355
14612
|
if (!state.children.owns(callId)) return;
|
|
@@ -15061,13 +15318,13 @@ var OpenCodeAdapter = class {
|
|
|
15061
15318
|
trackSubagent(part, callId, turn, onEvent) {
|
|
15062
15319
|
const state = part.state;
|
|
15063
15320
|
const child = state?.metadata?.sessionId;
|
|
15064
|
-
const
|
|
15065
|
-
if (
|
|
15321
|
+
const held2 = child ? turn.children.adopt(child, callId) : null;
|
|
15322
|
+
if (held2) {
|
|
15066
15323
|
const input = state?.input ?? {};
|
|
15067
15324
|
const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
15068
15325
|
const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
|
|
15069
15326
|
onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
|
|
15070
|
-
for (const frame of
|
|
15327
|
+
for (const frame of held2) this.onFrame(frame, turn, onEvent);
|
|
15071
15328
|
}
|
|
15072
15329
|
const status = state?.status;
|
|
15073
15330
|
if (status === "completed" || status === "error") {
|
|
@@ -15297,61 +15554,6 @@ var OPENCODE_REPO_CONFIG = {
|
|
|
15297
15554
|
}
|
|
15298
15555
|
};
|
|
15299
15556
|
|
|
15300
|
-
// src/utils/wallTime.ts
|
|
15301
|
-
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
15302
|
-
function lastMatch(re, text3) {
|
|
15303
|
-
let last = null;
|
|
15304
|
-
for (const m of text3.matchAll(re)) last = m;
|
|
15305
|
-
return last;
|
|
15306
|
-
}
|
|
15307
|
-
function hour24(hour, meridiem) {
|
|
15308
|
-
const h = Number(hour) % 12;
|
|
15309
|
-
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
15310
|
-
}
|
|
15311
|
-
function month(name) {
|
|
15312
|
-
if (!name) return void 0;
|
|
15313
|
-
const at = MONTHS.indexOf(name.toLowerCase());
|
|
15314
|
-
return at < 0 ? void 0 : at;
|
|
15315
|
-
}
|
|
15316
|
-
function placeWallTime(wall, zone, now) {
|
|
15317
|
-
const today = wallFields(now, zone);
|
|
15318
|
-
const year = wall.year ?? today.year;
|
|
15319
|
-
const monthIndex = wall.month ?? today.month;
|
|
15320
|
-
const day = wall.day ?? today.day;
|
|
15321
|
-
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
15322
|
-
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
15323
|
-
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
15324
|
-
}
|
|
15325
|
-
var HOUR_MS = 60 * 6e4;
|
|
15326
|
-
function wallFields(instant, zone) {
|
|
15327
|
-
if (!zone) {
|
|
15328
|
-
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
15329
|
-
}
|
|
15330
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
15331
|
-
timeZone: zone,
|
|
15332
|
-
hourCycle: "h23",
|
|
15333
|
-
year: "numeric",
|
|
15334
|
-
month: "numeric",
|
|
15335
|
-
day: "numeric",
|
|
15336
|
-
hour: "numeric",
|
|
15337
|
-
minute: "numeric",
|
|
15338
|
-
second: "numeric"
|
|
15339
|
-
}).formatToParts(instant);
|
|
15340
|
-
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
15341
|
-
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
15342
|
-
}
|
|
15343
|
-
function zoneOffsetMs(instant, zone) {
|
|
15344
|
-
const f = wallFields(new Date(instant), zone);
|
|
15345
|
-
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
15346
|
-
}
|
|
15347
|
-
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
15348
|
-
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
15349
|
-
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
15350
|
-
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
15351
|
-
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
15352
|
-
return new Date(instant);
|
|
15353
|
-
}
|
|
15354
|
-
|
|
15355
15557
|
// src/services/harness/claudeUsageLimit.ts
|
|
15356
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;
|
|
15357
15559
|
function claudeUsageLimitReset(text3, now) {
|
|
@@ -15367,21 +15569,6 @@ function claudeUsageLimitReset(text3, now) {
|
|
|
15367
15569
|
return placeWallTime(wall, zone, now);
|
|
15368
15570
|
}
|
|
15369
15571
|
|
|
15370
|
-
// src/services/harness/codexUsageLimit.ts
|
|
15371
|
-
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;
|
|
15372
|
-
function codexUsageLimitReset(text3, now) {
|
|
15373
|
-
const m = lastMatch(CODEX_RESET_RE, text3);
|
|
15374
|
-
if (!m) return null;
|
|
15375
|
-
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
15376
|
-
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
15377
|
-
if (mon) {
|
|
15378
|
-
const at = month(mon);
|
|
15379
|
-
if (at === void 0) return null;
|
|
15380
|
-
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
15381
|
-
}
|
|
15382
|
-
return placeWallTime(wall, void 0, now);
|
|
15383
|
-
}
|
|
15384
|
-
|
|
15385
15572
|
// src/services/harness/connectors.ts
|
|
15386
15573
|
var CONNECTORS = {
|
|
15387
15574
|
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
|
|
@@ -15780,7 +15967,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
|
|
|
15780
15967
|
modelId,
|
|
15781
15968
|
modelLabel: m.displayName || modelId,
|
|
15782
15969
|
runnerProvider: "gemini",
|
|
15783
|
-
variants: []
|
|
15970
|
+
variants: [],
|
|
15971
|
+
...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
|
|
15784
15972
|
});
|
|
15785
15973
|
}
|
|
15786
15974
|
}
|
|
@@ -16188,6 +16376,7 @@ function parseMaxParallel(value) {
|
|
|
16188
16376
|
// src/services/SettingsService.ts
|
|
16189
16377
|
var fs10 = __toESM(require("fs"));
|
|
16190
16378
|
var path26 = __toESM(require("path"));
|
|
16379
|
+
var import_crypto10 = require("crypto");
|
|
16191
16380
|
|
|
16192
16381
|
// src/services/TaskOps.ts
|
|
16193
16382
|
var import_uuid3 = require("uuid");
|
|
@@ -16241,7 +16430,14 @@ function clampThinkingEffort(effort, variants) {
|
|
|
16241
16430
|
}
|
|
16242
16431
|
return void 0;
|
|
16243
16432
|
}
|
|
16244
|
-
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
|
+
};
|
|
16245
16441
|
const clampVariant = (task) => {
|
|
16246
16442
|
if (!task.assignedModel || !modelsByRunner) return task;
|
|
16247
16443
|
const model = modelsByRunner[task.assignedRunner]?.find(
|
|
@@ -16270,6 +16466,11 @@ function coerceAssignments(tasks, perRunnerAllowlist, modelsByRunner) {
|
|
|
16270
16466
|
};
|
|
16271
16467
|
const coerceOne = (task) => {
|
|
16272
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
|
+
}
|
|
16273
16474
|
const allowlist = allowedFor(task.assignedRunner);
|
|
16274
16475
|
if (!task.assignedModel) {
|
|
16275
16476
|
if (allowlist && allowlist.length > 0) {
|
|
@@ -16332,7 +16533,7 @@ function blockedByLabel(refs) {
|
|
|
16332
16533
|
function skippedLabel(reason) {
|
|
16333
16534
|
return reason ? `Skipped: ${reason}` : "Skipped";
|
|
16334
16535
|
}
|
|
16335
|
-
var WAITS_FOR_APPROVAL_LABEL = "
|
|
16536
|
+
var WAITS_FOR_APPROVAL_LABEL = "Waits for your approval";
|
|
16336
16537
|
function approvalLabel(count) {
|
|
16337
16538
|
if (!count) return null;
|
|
16338
16539
|
return count > 1 ? `Waiting for approval (${count})` : "Waiting for approval";
|
|
@@ -16517,6 +16718,15 @@ function lockedTaskRefusal(task, action = "changed") {
|
|
|
16517
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`;
|
|
16518
16719
|
return null;
|
|
16519
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
|
+
}
|
|
16520
16730
|
var AI_ONLY_FIELDS = ["assignedModel", "taskMode", "autonomy", "ops"];
|
|
16521
16731
|
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
16522
16732
|
if (!catalog) return { ok: true };
|
|
@@ -16576,11 +16786,18 @@ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
|
16576
16786
|
const { task: target, parent } = found;
|
|
16577
16787
|
const locked = actor === "planner" ? lockedTaskRefusal(target, "modified") : null;
|
|
16578
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
|
+
}
|
|
16579
16797
|
if (changes.dependencies) {
|
|
16580
16798
|
const depCheck = canSetDependencies(parent?.subtasks ?? tasks, taskId, changes.dependencies);
|
|
16581
16799
|
if (!depCheck.ok) return depCheck;
|
|
16582
16800
|
}
|
|
16583
|
-
const coherence = typeCoherenceCheck(target, changes);
|
|
16584
16801
|
if (coherence && !coherence.ok) return coherence;
|
|
16585
16802
|
const ops = opsCheck(target, parent !== null, changes);
|
|
16586
16803
|
if (ops) return ops;
|
|
@@ -16818,9 +17035,9 @@ function idsOf(op) {
|
|
|
16818
17035
|
return ids[next++];
|
|
16819
17036
|
};
|
|
16820
17037
|
}
|
|
16821
|
-
function applyTaskOps(currentTasks, newOps, runners, catalog,
|
|
16822
|
-
const ops = [...
|
|
16823
|
-
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})`;
|
|
16824
17041
|
let tasks = JSON.parse(JSON.stringify(currentTasks));
|
|
16825
17042
|
const errors = [];
|
|
16826
17043
|
const summary = [];
|
|
@@ -17512,23 +17729,34 @@ function createWakePolicy(options) {
|
|
|
17512
17729
|
// src/services/runnerExit.ts
|
|
17513
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;
|
|
17514
17731
|
var USAGE_LIMIT_TAIL = 4096;
|
|
17515
|
-
function classifyRunnerStop(
|
|
17516
|
-
|
|
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;
|
|
17517
17738
|
}
|
|
17518
17739
|
function stopsRunner(reason) {
|
|
17519
17740
|
return reason !== "stop" && reason !== "load";
|
|
17520
17741
|
}
|
|
17521
|
-
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) {
|
|
17522
17753
|
const read2 = connectorFor(runner)?.usageLimitReset;
|
|
17523
17754
|
if (!read2) return null;
|
|
17524
|
-
let reset;
|
|
17525
17755
|
try {
|
|
17526
|
-
|
|
17756
|
+
return read2(output.slice(-USAGE_LIMIT_TAIL), now);
|
|
17527
17757
|
} catch {
|
|
17528
17758
|
return null;
|
|
17529
17759
|
}
|
|
17530
|
-
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
17531
|
-
return reset;
|
|
17532
17760
|
}
|
|
17533
17761
|
var DAY_MS = 24 * 60 * 6e4;
|
|
17534
17762
|
|
|
@@ -17709,15 +17937,15 @@ var ReplySplitter = class {
|
|
|
17709
17937
|
push(segmentId, text3) {
|
|
17710
17938
|
const state = this.segments.get(segmentId);
|
|
17711
17939
|
if (state === "text" || state === "plan") return { route: state, text: text3 };
|
|
17712
|
-
const
|
|
17713
|
-
const opensWithObject = opensWithJsonObject(
|
|
17940
|
+
const held2 = (state?.held ?? "") + text3;
|
|
17941
|
+
const opensWithObject = opensWithJsonObject(held2);
|
|
17714
17942
|
if (opensWithObject === void 0) {
|
|
17715
|
-
this.segments.set(segmentId, { held });
|
|
17943
|
+
this.segments.set(segmentId, { held: held2 });
|
|
17716
17944
|
return null;
|
|
17717
17945
|
}
|
|
17718
17946
|
const route = opensWithObject ? "plan" : "text";
|
|
17719
17947
|
this.segments.set(segmentId, route);
|
|
17720
|
-
return { route, text:
|
|
17948
|
+
return { route, text: held2 };
|
|
17721
17949
|
}
|
|
17722
17950
|
};
|
|
17723
17951
|
var TurnStream = class {
|
|
@@ -17872,6 +18100,7 @@ function plannerTranscript(history) {
|
|
|
17872
18100
|
const loadsOf = /* @__PURE__ */ new Map();
|
|
17873
18101
|
let owner = -1;
|
|
17874
18102
|
for (const entry of history) {
|
|
18103
|
+
if (entry.kind === "error") continue;
|
|
17875
18104
|
if (entry.kind === "skill_load" && entry.skill) {
|
|
17876
18105
|
const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
|
|
17877
18106
|
if (loads) loads.push(entry.skill);
|
|
@@ -17960,6 +18189,8 @@ function handoffOf(run) {
|
|
|
17960
18189
|
path: repo.path,
|
|
17961
18190
|
integrationBranch: repo.integrationBranch,
|
|
17962
18191
|
baseRef: repo.baseRef,
|
|
18192
|
+
...repo.baseBranch ? { baseBranch: repo.baseBranch } : {},
|
|
18193
|
+
...repo.startedDetached ? { startedDetached: true } : {},
|
|
17963
18194
|
landed: merged.filter((r) => r.repos[repo.path]?.changed).map(entry)
|
|
17964
18195
|
})),
|
|
17965
18196
|
landed: merged.map(entry)
|
|
@@ -19289,6 +19520,9 @@ ${task.prompt ?? task.description}`;
|
|
|
19289
19520
|
}
|
|
19290
19521
|
|
|
19291
19522
|
// src/services/continuation.ts
|
|
19523
|
+
function sessionOwner(task) {
|
|
19524
|
+
return task.runnerSessionOwner ?? task.attemptUsage?.at(-1)?.runner;
|
|
19525
|
+
}
|
|
19292
19526
|
function continuability(task) {
|
|
19293
19527
|
const refuse = (reason) => ({ ok: false, reason: `Task "${task.title}" cannot be continued: ${reason}` });
|
|
19294
19528
|
if (task.type !== "ai") return refuse("it is not run by an agent.");
|
|
@@ -19298,6 +19532,10 @@ function continuability(task) {
|
|
|
19298
19532
|
const finished = task.status === "completed" || task.status === "failed" || task.status === "skipped";
|
|
19299
19533
|
if (!finished && !pausedOnLimit(task)) return refuse("only a completed, failed or skipped task, or one paused on a usage limit, can be.");
|
|
19300
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
|
+
}
|
|
19301
19539
|
return { ok: true, sessionId: task.runnerSessionId };
|
|
19302
19540
|
}
|
|
19303
19541
|
function canContinue(task) {
|
|
@@ -19421,6 +19659,7 @@ function serializePlan(plan) {
|
|
|
19421
19659
|
runners: plan.runners,
|
|
19422
19660
|
generatedAt: plan.generatedAt,
|
|
19423
19661
|
runSettings: plan.runSettings,
|
|
19662
|
+
...plan.plannerResend ? { plannerResend: plan.plannerResend } : {},
|
|
19424
19663
|
conversationHistory: surfaceConversation(plan.conversationHistory),
|
|
19425
19664
|
prdMarkdown: plan.prdMarkdown
|
|
19426
19665
|
};
|
|
@@ -19461,6 +19700,70 @@ function markOf(was, now) {
|
|
|
19461
19700
|
return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
|
|
19462
19701
|
}
|
|
19463
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
|
+
|
|
19464
19767
|
// src/services/PlannerConversation.ts
|
|
19465
19768
|
var CATALOG_MODEL_CAP = 100;
|
|
19466
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.";
|
|
@@ -19525,6 +19828,17 @@ var PlannerTurnFailedError = class extends Error {
|
|
|
19525
19828
|
this.name = "PlannerTurnFailedError";
|
|
19526
19829
|
}
|
|
19527
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
|
+
};
|
|
19528
19842
|
var PlannerTurnStoppedError = class extends Error {
|
|
19529
19843
|
constructor(options) {
|
|
19530
19844
|
super("The planner turn was stopped.", options);
|
|
@@ -19550,6 +19864,8 @@ var PlannerConversation = class {
|
|
|
19550
19864
|
savedInBackground = 0;
|
|
19551
19865
|
/** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
|
|
19552
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;
|
|
19553
19869
|
live = null;
|
|
19554
19870
|
openTurn = null;
|
|
19555
19871
|
get transcript() {
|
|
@@ -19665,8 +19981,8 @@ var PlannerConversation = class {
|
|
|
19665
19981
|
* from the moment it is handed in until it lands, is refused, or the turn
|
|
19666
19982
|
* ends: the backend's reply claims it, but it has not landed by then.
|
|
19667
19983
|
*/
|
|
19668
|
-
showHeld(turn,
|
|
19669
|
-
const result =
|
|
19984
|
+
showHeld(turn, held2) {
|
|
19985
|
+
const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
|
|
19670
19986
|
if (!result?.ok) {
|
|
19671
19987
|
this.clearHeld(turn);
|
|
19672
19988
|
return;
|
|
@@ -19685,9 +20001,9 @@ var PlannerConversation = class {
|
|
|
19685
20001
|
* a whole plan, which an edit cannot be layered on.
|
|
19686
20002
|
*/
|
|
19687
20003
|
pendingOps() {
|
|
19688
|
-
const
|
|
19689
|
-
if (!
|
|
19690
|
-
return
|
|
20004
|
+
const held2 = this.openTurn?.submitted;
|
|
20005
|
+
if (!held2) return [];
|
|
20006
|
+
return held2.kind === "task_ops" ? held2.ops : null;
|
|
19691
20007
|
}
|
|
19692
20008
|
/**
|
|
19693
20009
|
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
@@ -19727,7 +20043,8 @@ var PlannerConversation = class {
|
|
|
19727
20043
|
history: plan.conversationHistory,
|
|
19728
20044
|
researchLog: plan.researchLog,
|
|
19729
20045
|
persisted: this.persisted,
|
|
19730
|
-
savedInBackground: this.savedInBackground
|
|
20046
|
+
savedInBackground: this.savedInBackground,
|
|
20047
|
+
toldScope: this.toldScope
|
|
19731
20048
|
};
|
|
19732
20049
|
}
|
|
19733
20050
|
/**
|
|
@@ -19741,7 +20058,9 @@ var PlannerConversation = class {
|
|
|
19741
20058
|
* the undo is saved in turn, or a reload would bring the writes back.
|
|
19742
20059
|
*/
|
|
19743
20060
|
restore(snapshot) {
|
|
19744
|
-
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;
|
|
19745
20064
|
const undo = () => {
|
|
19746
20065
|
const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
|
|
19747
20066
|
snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
|
|
@@ -19776,8 +20095,9 @@ var PlannerConversation = class {
|
|
|
19776
20095
|
* at it.
|
|
19777
20096
|
*/
|
|
19778
20097
|
rewindTargets() {
|
|
20098
|
+
const opener = this.transcript.findIndex((m) => m.kind !== "error");
|
|
19779
20099
|
return this.transcript.flatMap((m, index) => {
|
|
19780
|
-
if (!isUserMessage(m) || index ===
|
|
20100
|
+
if (!isUserMessage(m) || index === opener) return [];
|
|
19781
20101
|
const line = m.content.split("\n")[0];
|
|
19782
20102
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
19783
20103
|
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
@@ -19904,8 +20224,10 @@ var PlannerConversation = class {
|
|
|
19904
20224
|
return this.userTurn("supersede", goal, signal, async (userTurn) => {
|
|
19905
20225
|
const opening = await prepare();
|
|
19906
20226
|
this.assertCurrent(userTurn);
|
|
20227
|
+
userTurn.checkpoint = this.snapshot();
|
|
19907
20228
|
this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
|
|
19908
20229
|
this.toldAutorun = opening.autorun ?? false;
|
|
20230
|
+
this.toldScope = this.host.assignmentScope();
|
|
19909
20231
|
const turn = await this.host.aiService().startConversation({
|
|
19910
20232
|
...opening,
|
|
19911
20233
|
// The system prompt's goal is the text as typed; skill bodies ride the
|
|
@@ -19915,7 +20237,9 @@ var PlannerConversation = class {
|
|
|
19915
20237
|
onProgress: userTurn.stream.sink(),
|
|
19916
20238
|
signal: userTurn.signal
|
|
19917
20239
|
});
|
|
19918
|
-
|
|
20240
|
+
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
20241
|
+
this.assertNotLimited(settleable, userTurn);
|
|
20242
|
+
return this.settle(settleable, userTurn);
|
|
19919
20243
|
});
|
|
19920
20244
|
}
|
|
19921
20245
|
/**
|
|
@@ -19948,13 +20272,16 @@ var PlannerConversation = class {
|
|
|
19948
20272
|
// anything the turn appends as it runs goes after this point.
|
|
19949
20273
|
at: priorHistory.length
|
|
19950
20274
|
};
|
|
19951
|
-
const
|
|
20275
|
+
const toldBefore = this.toldScope;
|
|
20276
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
|
|
19952
20277
|
const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
|
|
19953
20278
|
const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
|
|
19954
20279
|
const ai = this.host.aiService();
|
|
19955
20280
|
try {
|
|
19956
20281
|
const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
|
|
19957
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);
|
|
19958
20285
|
if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
|
|
19959
20286
|
this.assertCurrent(userTurn, settleable);
|
|
19960
20287
|
return await this.settle(settleable, userTurn);
|
|
@@ -19973,6 +20300,34 @@ var PlannerConversation = class {
|
|
|
19973
20300
|
if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
|
|
19974
20301
|
if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
|
|
19975
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
|
+
}
|
|
19976
20331
|
assertIdle(operation) {
|
|
19977
20332
|
if (this.isTurnInFlight) throw new ConversationBusyError(operation);
|
|
19978
20333
|
}
|
|
@@ -20114,6 +20469,7 @@ var PlannerConversation = class {
|
|
|
20114
20469
|
const goal = opener || this.host.goal() || message;
|
|
20115
20470
|
this.reset();
|
|
20116
20471
|
this.toldAutorun = opening.autorun ?? false;
|
|
20472
|
+
this.toldScope = this.host.assignmentScope();
|
|
20117
20473
|
return this.host.aiService().startConversation({
|
|
20118
20474
|
...opening,
|
|
20119
20475
|
runners,
|
|
@@ -20352,6 +20708,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
20352
20708
|
this.toldAutorun = now;
|
|
20353
20709
|
return now ? AUTORUN_ON : AUTORUN_OFF;
|
|
20354
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
|
+
}
|
|
20355
20729
|
runUpdatesBlock() {
|
|
20356
20730
|
const updates = this.host.takeRunUpdates();
|
|
20357
20731
|
return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
|
|
@@ -20432,7 +20806,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
|
|
|
20432
20806
|
var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
|
|
20433
20807
|
var HUNG_TURN_GRACE_MS = 3e4;
|
|
20434
20808
|
var DEFAULT_WAKE_UP_READ_BUDGET = 12;
|
|
20435
|
-
var RESET_MARGIN_MS = 6e4;
|
|
20436
20809
|
var DETAIL_CHARS = 240;
|
|
20437
20810
|
var WakeUpTimeoutError = class extends Error {
|
|
20438
20811
|
constructor(timeoutMs) {
|
|
@@ -20458,8 +20831,6 @@ var Supervisor = class {
|
|
|
20458
20831
|
/** The run's end was reported: the scheduler can announce it more than once. */
|
|
20459
20832
|
finished = false;
|
|
20460
20833
|
runEndPending = false;
|
|
20461
|
-
/** While the planner's usage limit holds wake-ups back: when it lets them go. */
|
|
20462
|
-
limited = null;
|
|
20463
20834
|
/** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
|
|
20464
20835
|
wakeUps = [];
|
|
20465
20836
|
debounceMs;
|
|
@@ -20480,9 +20851,9 @@ var Supervisor = class {
|
|
|
20480
20851
|
get awaitsRunEnd() {
|
|
20481
20852
|
return this.runEndPending;
|
|
20482
20853
|
}
|
|
20483
|
-
/** 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. */
|
|
20484
20855
|
mayWake() {
|
|
20485
|
-
return this.watching
|
|
20856
|
+
return this.watching;
|
|
20486
20857
|
}
|
|
20487
20858
|
/**
|
|
20488
20859
|
* What a wake-up turn is held to under the run's level. The brief is the
|
|
@@ -20549,8 +20920,6 @@ var Supervisor = class {
|
|
|
20549
20920
|
/** The plan these events were about is gone: nothing gathered or waited on carries over. */
|
|
20550
20921
|
reset() {
|
|
20551
20922
|
this.disarm();
|
|
20552
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
20553
|
-
this.limited = null;
|
|
20554
20923
|
this.failures = 0;
|
|
20555
20924
|
this.finished = false;
|
|
20556
20925
|
this.runEndPending = false;
|
|
@@ -20601,9 +20970,13 @@ var Supervisor = class {
|
|
|
20601
20970
|
}
|
|
20602
20971
|
failed(err) {
|
|
20603
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
|
+
}
|
|
20604
20977
|
const reason = err instanceof Error ? err.message : String(err);
|
|
20605
|
-
if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
20606
|
-
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");
|
|
20607
20980
|
return;
|
|
20608
20981
|
}
|
|
20609
20982
|
this.failures++;
|
|
@@ -20614,22 +20987,6 @@ var Supervisor = class {
|
|
|
20614
20987
|
}
|
|
20615
20988
|
this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
|
|
20616
20989
|
}
|
|
20617
|
-
waitForReset(said2) {
|
|
20618
|
-
const runner = this.host.plannerRunner();
|
|
20619
|
-
const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
|
|
20620
|
-
if (!reset) {
|
|
20621
|
-
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
20622
|
-
return;
|
|
20623
|
-
}
|
|
20624
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
20625
|
-
const until = reset.getTime() + RESET_MARGIN_MS;
|
|
20626
|
-
const timer = setTimeout(() => {
|
|
20627
|
-
this.limited = null;
|
|
20628
|
-
this.host.pump();
|
|
20629
|
-
}, Math.max(0, until - this.now()));
|
|
20630
|
-
this.limited = { until, timer };
|
|
20631
|
-
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.`);
|
|
20632
|
-
}
|
|
20633
20990
|
/** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
|
|
20634
20991
|
giveUp(reason) {
|
|
20635
20992
|
this.host.goUnsupervised(reason);
|
|
@@ -20643,8 +21000,8 @@ var Supervisor = class {
|
|
|
20643
21000
|
standDown() {
|
|
20644
21001
|
this.disarm();
|
|
20645
21002
|
this.runEndPending = false;
|
|
20646
|
-
const
|
|
20647
|
-
if (
|
|
21003
|
+
const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
|
|
21004
|
+
if (held2) this.deliver(held2);
|
|
20648
21005
|
}
|
|
20649
21006
|
arm() {
|
|
20650
21007
|
const due = this.policy.dueAt();
|
|
@@ -20835,78 +21192,224 @@ function runSettingsOf(settings) {
|
|
|
20835
21192
|
}
|
|
20836
21193
|
return out;
|
|
20837
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
|
+
}
|
|
20838
21272
|
var SettingsService = class {
|
|
20839
21273
|
filePath;
|
|
20840
21274
|
cache = null;
|
|
20841
|
-
|
|
21275
|
+
cachedStamp = null;
|
|
20842
21276
|
constructor(filePath = getSettingsPath()) {
|
|
20843
21277
|
this.filePath = filePath;
|
|
20844
21278
|
}
|
|
20845
21279
|
// Several processes share the settings file (VS Code extension host, web
|
|
20846
|
-
// 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
|
|
20847
21281
|
// other process invalidates it. A stale cache here previously meant an
|
|
20848
21282
|
// allowlist set in one surface never reached another's planner.
|
|
20849
21283
|
getAll() {
|
|
20850
|
-
const
|
|
20851
|
-
if (this.cache &&
|
|
21284
|
+
const stamp = this.fileStamp();
|
|
21285
|
+
if (this.cache && stamp === this.cachedStamp) return this.cache;
|
|
20852
21286
|
this.cache = this.load();
|
|
20853
|
-
this.
|
|
21287
|
+
this.cachedStamp = stamp;
|
|
20854
21288
|
return this.cache;
|
|
20855
21289
|
}
|
|
20856
|
-
|
|
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) {
|
|
20857
21296
|
try {
|
|
20858
|
-
|
|
21297
|
+
const stat = fs10.statSync(file);
|
|
21298
|
+
return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
20859
21299
|
} catch {
|
|
20860
21300
|
return null;
|
|
20861
21301
|
}
|
|
20862
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
|
+
}
|
|
20863
21344
|
getModelAllowlist(runner) {
|
|
20864
21345
|
return this.getAll().modelAllowlist?.[runner];
|
|
20865
21346
|
}
|
|
20866
21347
|
setModelAllowlist(runner, ids) {
|
|
20867
|
-
this.
|
|
20868
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
20872
|
-
|
|
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
|
+
}
|
|
20873
21355
|
}
|
|
21356
|
+
} else {
|
|
21357
|
+
if (!this.cache.modelAllowlist) {
|
|
21358
|
+
this.cache.modelAllowlist = {};
|
|
21359
|
+
}
|
|
21360
|
+
this.cache.modelAllowlist[runner] = ids;
|
|
20874
21361
|
}
|
|
20875
|
-
}
|
|
20876
|
-
if (!this.cache.modelAllowlist) {
|
|
20877
|
-
this.cache.modelAllowlist = {};
|
|
20878
|
-
}
|
|
20879
|
-
this.cache.modelAllowlist[runner] = ids;
|
|
20880
|
-
}
|
|
20881
|
-
this.persist();
|
|
21362
|
+
});
|
|
20882
21363
|
}
|
|
20883
21364
|
getPlannerModel(provider) {
|
|
20884
21365
|
return this.getAll().plannerModels?.[provider];
|
|
20885
21366
|
}
|
|
20886
21367
|
setPlannerModel(provider, entry) {
|
|
20887
|
-
this.
|
|
20888
|
-
|
|
20889
|
-
|
|
20890
|
-
|
|
20891
|
-
|
|
20892
|
-
|
|
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
|
+
}
|
|
20893
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 };
|
|
20894
21381
|
}
|
|
20895
|
-
}
|
|
20896
|
-
if (!this.cache.plannerModels) {
|
|
20897
|
-
this.cache.plannerModels = {};
|
|
20898
|
-
}
|
|
20899
|
-
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
20900
|
-
}
|
|
20901
|
-
this.persist();
|
|
21382
|
+
});
|
|
20902
21383
|
}
|
|
20903
21384
|
getEnabledRunners() {
|
|
20904
21385
|
return this.getAll().enabledRunners;
|
|
20905
21386
|
}
|
|
20906
21387
|
setEnabledRunners(ids) {
|
|
20907
|
-
this.
|
|
20908
|
-
|
|
20909
|
-
|
|
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
|
+
});
|
|
20910
21413
|
}
|
|
20911
21414
|
/**
|
|
20912
21415
|
* Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
|
|
@@ -20924,9 +21427,9 @@ var SettingsService = class {
|
|
|
20924
21427
|
return env !== void 0 && env.trim() !== "";
|
|
20925
21428
|
}
|
|
20926
21429
|
setAutonomousMode(enabled) {
|
|
20927
|
-
this.
|
|
20928
|
-
|
|
20929
|
-
|
|
21430
|
+
this.save(() => {
|
|
21431
|
+
this.cache.autonomousMode = enabled;
|
|
21432
|
+
});
|
|
20930
21433
|
}
|
|
20931
21434
|
getRunSettings() {
|
|
20932
21435
|
return runSettingsOf(this.getAll());
|
|
@@ -20938,11 +21441,11 @@ var SettingsService = class {
|
|
|
20938
21441
|
throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
|
|
20939
21442
|
}
|
|
20940
21443
|
}
|
|
20941
|
-
this.
|
|
20942
|
-
|
|
20943
|
-
|
|
20944
|
-
|
|
20945
|
-
|
|
21444
|
+
this.save(() => {
|
|
21445
|
+
for (const key of RUN_SETTING_KEYS) {
|
|
21446
|
+
if (changes[key] !== void 0) this.cache[key] = changes[key];
|
|
21447
|
+
}
|
|
21448
|
+
});
|
|
20946
21449
|
return this.getRunSettings();
|
|
20947
21450
|
}
|
|
20948
21451
|
load() {
|
|
@@ -20971,13 +21474,43 @@ var SettingsService = class {
|
|
|
20971
21474
|
}
|
|
20972
21475
|
return {};
|
|
20973
21476
|
}
|
|
20974
|
-
|
|
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) {
|
|
20975
21496
|
const dir = path26.dirname(this.filePath);
|
|
20976
21497
|
if (!fs10.existsSync(dir)) {
|
|
20977
21498
|
fs10.mkdirSync(dir, { recursive: true });
|
|
20978
21499
|
}
|
|
20979
|
-
|
|
20980
|
-
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;
|
|
20981
21514
|
}
|
|
20982
21515
|
};
|
|
20983
21516
|
|
|
@@ -21466,7 +21999,7 @@ function capConflictFiles(files, max = 5) {
|
|
|
21466
21999
|
}
|
|
21467
22000
|
|
|
21468
22001
|
// src/services/mergeResultNotice.ts
|
|
21469
|
-
function blockNotice({ repo, reason, files }) {
|
|
22002
|
+
function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
|
|
21470
22003
|
switch (reason) {
|
|
21471
22004
|
case "merge-in-progress":
|
|
21472
22005
|
return `${repo} has a merge in progress`;
|
|
@@ -21476,10 +22009,22 @@ function blockNotice({ repo, reason, files }) {
|
|
|
21476
22009
|
return `${repo} has uncommitted changes to ${files.join(", ")}`;
|
|
21477
22010
|
case "partial-landing":
|
|
21478
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}`;
|
|
21479
22014
|
case "git-error":
|
|
21480
22015
|
return `git could not check ${repo}`;
|
|
21481
22016
|
}
|
|
21482
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
|
+
}
|
|
21483
22028
|
function landedNotice(landed2) {
|
|
21484
22029
|
if (landed2.length === 0) return "Nothing was merged.";
|
|
21485
22030
|
return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
|
|
@@ -21496,7 +22041,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
|
|
|
21496
22041
|
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
21497
22042
|
}
|
|
21498
22043
|
case "blocked":
|
|
21499
|
-
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)}` };
|
|
21500
22045
|
case "conflict":
|
|
21501
22046
|
return {
|
|
21502
22047
|
level: "warn",
|
|
@@ -21606,7 +22151,7 @@ ${await file.patch()}`);
|
|
|
21606
22151
|
}
|
|
21607
22152
|
|
|
21608
22153
|
// src/services/GitWorktreeIsolation.ts
|
|
21609
|
-
var
|
|
22154
|
+
var import_crypto11 = require("crypto");
|
|
21610
22155
|
var fs13 = __toESM(require("fs"));
|
|
21611
22156
|
var path29 = __toESM(require("path"));
|
|
21612
22157
|
|
|
@@ -21734,7 +22279,7 @@ var GitWorktreeIsolation = class {
|
|
|
21734
22279
|
this.resolvePath = deps.resolvePath ?? (() => augmentedPath());
|
|
21735
22280
|
this.gitInvoker = { exec: deps.execFileImpl ?? defaultExecFile2, resolvePath: this.resolvePath };
|
|
21736
22281
|
this.platform = deps.platform ?? process.platform;
|
|
21737
|
-
this.mintRunId = deps.mintRunId ?? (() => (0,
|
|
22282
|
+
this.mintRunId = deps.mintRunId ?? (() => (0, import_crypto11.randomBytes)(4).toString("hex"));
|
|
21738
22283
|
}
|
|
21739
22284
|
deps;
|
|
21740
22285
|
resolvePath;
|
|
@@ -21860,7 +22405,7 @@ var GitWorktreeIsolation = class {
|
|
|
21860
22405
|
path: repoPath,
|
|
21861
22406
|
root,
|
|
21862
22407
|
baseRef,
|
|
21863
|
-
...branch ? { baseBranch: branch } : {},
|
|
22408
|
+
...branch ? { baseBranch: branch } : { startedDetached: true },
|
|
21864
22409
|
integrationBranch: integrationBranchFor(run.id)
|
|
21865
22410
|
};
|
|
21866
22411
|
if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
|
|
@@ -22213,7 +22758,9 @@ var GitWorktreeIsolation = class {
|
|
|
22213
22758
|
async mergeAll(run) {
|
|
22214
22759
|
const partial = await this.settleLanding(run);
|
|
22215
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);
|
|
22216
22762
|
const repos = await this.reposWithWork(run);
|
|
22763
|
+
if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
|
|
22217
22764
|
if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
|
|
22218
22765
|
const blocked = [];
|
|
22219
22766
|
for (const repo of repos) {
|
|
@@ -22231,6 +22778,27 @@ var GitWorktreeIsolation = class {
|
|
|
22231
22778
|
}
|
|
22232
22779
|
return withWork;
|
|
22233
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
|
+
}
|
|
22234
22802
|
/** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
|
|
22235
22803
|
async canPreflight() {
|
|
22236
22804
|
const version = await this.tryGit(void 0, ["--version"]);
|
|
@@ -22718,6 +23286,14 @@ var GitWorktreeIsolation = class {
|
|
|
22718
23286
|
if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
|
|
22719
23287
|
return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
|
|
22720
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
|
+
}
|
|
22721
23297
|
async abortMerge(cwd) {
|
|
22722
23298
|
if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
|
|
22723
23299
|
}
|
|
@@ -22738,6 +23314,29 @@ function createWorktreeIsolation(deps) {
|
|
|
22738
23314
|
return new GitWorktreeIsolation(deps);
|
|
22739
23315
|
}
|
|
22740
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
|
+
|
|
22741
23340
|
// src/interfaces/ILogger.ts
|
|
22742
23341
|
var ConsoleLogger = class {
|
|
22743
23342
|
warn(scope, message, err) {
|
|
@@ -22790,7 +23389,10 @@ async function settleReply(opts) {
|
|
|
22790
23389
|
opts.onProgress({ type: "interrupted" });
|
|
22791
23390
|
return { done: turn };
|
|
22792
23391
|
}
|
|
22793
|
-
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
|
+
}
|
|
22794
23396
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
22795
23397
|
const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
|
|
22796
23398
|
switch (reply2.kind) {
|
|
@@ -23237,8 +23839,8 @@ function summarizeToolCall(tool2, argsJson, toolLabel) {
|
|
|
23237
23839
|
}
|
|
23238
23840
|
const path35 = args.filePath || args.file_path || args.path;
|
|
23239
23841
|
if (path35) {
|
|
23240
|
-
const
|
|
23241
|
-
return `${display} ${
|
|
23842
|
+
const basename5 = String(path35).split("/").pop() || String(path35);
|
|
23843
|
+
return `${display} ${basename5}`;
|
|
23242
23844
|
}
|
|
23243
23845
|
if (args.pattern) {
|
|
23244
23846
|
return `${display} ${args.pattern}`;
|
|
@@ -23788,7 +24390,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
|
|
|
23788
24390
|
function tokenCount(value) {
|
|
23789
24391
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
23790
24392
|
}
|
|
23791
|
-
function usageRecordFrom(usage) {
|
|
24393
|
+
function usageRecordFrom(usage, model) {
|
|
23792
24394
|
if (!usage) return void 0;
|
|
23793
24395
|
const inputTokens = tokenCount(usage.promptTokenCount);
|
|
23794
24396
|
const answerTokens = tokenCount(usage.candidatesTokenCount);
|
|
@@ -23796,6 +24398,7 @@ function usageRecordFrom(usage) {
|
|
|
23796
24398
|
const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
|
|
23797
24399
|
const record = {
|
|
23798
24400
|
source: "google",
|
|
24401
|
+
model,
|
|
23799
24402
|
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
23800
24403
|
...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
|
|
23801
24404
|
...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
|
|
@@ -23804,15 +24407,31 @@ function usageRecordFrom(usage) {
|
|
|
23804
24407
|
return anyReported ? record : void 0;
|
|
23805
24408
|
}
|
|
23806
24409
|
var GeminiResearchChat = class {
|
|
23807
|
-
constructor(
|
|
23808
|
-
this.
|
|
24410
|
+
constructor(getModel, open, hooks = {}) {
|
|
24411
|
+
this.getModel = getModel;
|
|
24412
|
+
this.open = open;
|
|
23809
24413
|
this.hooks = hooks;
|
|
24414
|
+
this.model = getModel();
|
|
24415
|
+
this.chat = open(this.model);
|
|
23810
24416
|
}
|
|
23811
|
-
|
|
24417
|
+
getModel;
|
|
24418
|
+
open;
|
|
23812
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
|
+
}
|
|
23813
24432
|
async sendMessage(text3, signal) {
|
|
23814
24433
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
23815
|
-
const result = await this.
|
|
24434
|
+
const result = await (await this.current()).sendMessageStream(text3);
|
|
23816
24435
|
return this.consume(result.stream);
|
|
23817
24436
|
}
|
|
23818
24437
|
async sendToolResults(results, signal) {
|
|
@@ -23823,7 +24442,7 @@ var GeminiResearchChat = class {
|
|
|
23823
24442
|
response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
|
|
23824
24443
|
}
|
|
23825
24444
|
}));
|
|
23826
|
-
const result = await this.
|
|
24445
|
+
const result = await (await this.current()).sendMessageStream(funcResponses);
|
|
23827
24446
|
return this.consume(result.stream);
|
|
23828
24447
|
}
|
|
23829
24448
|
/**
|
|
@@ -23863,7 +24482,7 @@ var GeminiResearchChat = class {
|
|
|
23863
24482
|
}
|
|
23864
24483
|
}
|
|
23865
24484
|
}
|
|
23866
|
-
const usageRecord3 = usageRecordFrom(usage);
|
|
24485
|
+
const usageRecord3 = usageRecordFrom(usage, this.model);
|
|
23867
24486
|
if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
|
|
23868
24487
|
return {
|
|
23869
24488
|
text: text3,
|
|
@@ -23879,6 +24498,8 @@ var GeminiService = class extends BaseAiService {
|
|
|
23879
24498
|
genAI = null;
|
|
23880
24499
|
genAIKey = "";
|
|
23881
24500
|
model = null;
|
|
24501
|
+
/** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
|
|
24502
|
+
modelId = "";
|
|
23882
24503
|
constructor(config) {
|
|
23883
24504
|
super(config);
|
|
23884
24505
|
}
|
|
@@ -23892,13 +24513,18 @@ var GeminiService = class extends BaseAiService {
|
|
|
23892
24513
|
}
|
|
23893
24514
|
return true;
|
|
23894
24515
|
}
|
|
23895
|
-
|
|
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()) {
|
|
23896
24521
|
this.init();
|
|
23897
|
-
if (!this.model || this.
|
|
24522
|
+
if (!this.model || this.modelId !== model) {
|
|
23898
24523
|
this.model = this.genAI.getGenerativeModel({
|
|
23899
|
-
model
|
|
24524
|
+
model,
|
|
23900
24525
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
23901
24526
|
});
|
|
24527
|
+
this.modelId = model;
|
|
23902
24528
|
}
|
|
23903
24529
|
return this.model;
|
|
23904
24530
|
}
|
|
@@ -23913,7 +24539,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
23913
24539
|
}
|
|
23914
24540
|
async startConversation(req) {
|
|
23915
24541
|
this.ensureInit();
|
|
23916
|
-
const genModel = this.getPlanningModel();
|
|
23917
24542
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
23918
24543
|
const systemPrompt = buildConversationSystemPrompt(
|
|
23919
24544
|
req.goal,
|
|
@@ -23942,13 +24567,14 @@ ${m.content}` });
|
|
|
23942
24567
|
if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
|
|
23943
24568
|
seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
|
|
23944
24569
|
}
|
|
23945
|
-
const
|
|
23946
|
-
|
|
23947
|
-
|
|
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,
|
|
23948
24574
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
23949
24575
|
});
|
|
23950
24576
|
let currentProgress = req.onProgress;
|
|
23951
|
-
const researchChat = new GeminiResearchChat(
|
|
24577
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
23952
24578
|
onReasoning: (text3, segmentId) => currentProgress({ type: "thinking", text: text3, segmentId }),
|
|
23953
24579
|
onContent: (text3, segmentId) => currentProgress({ type: "text_delta", text: text3, segmentId }),
|
|
23954
24580
|
onUsage: (record) => currentProgress({ type: "usage", record })
|
|
@@ -24021,18 +24647,17 @@ ${repairHint}` }] : parts;
|
|
|
24021
24647
|
async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
24022
24648
|
this.ensureInit();
|
|
24023
24649
|
const { autonomousDefault } = modes;
|
|
24024
|
-
const genModel = this.getPlanningModel();
|
|
24025
24650
|
const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
|
|
24026
24651
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
24027
24652
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
24028
24653
|
|
|
24029
24654
|
Explore the workspace to understand the codebase, then generate the plan.`;
|
|
24030
|
-
const
|
|
24031
|
-
history
|
|
24655
|
+
const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
|
|
24656
|
+
history,
|
|
24032
24657
|
tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
|
|
24033
24658
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
24034
24659
|
});
|
|
24035
|
-
const researchChat = new GeminiResearchChat(
|
|
24660
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
24036
24661
|
onUsage: (record) => onProgress({ type: "usage", record })
|
|
24037
24662
|
});
|
|
24038
24663
|
const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
@@ -24068,7 +24693,7 @@ var import_openai = __toESM(require("openai"));
|
|
|
24068
24693
|
var import_uuid7 = require("uuid");
|
|
24069
24694
|
var KEYLESS_PLACEHOLDER = "not-needed";
|
|
24070
24695
|
var OpenAiResearchChat = class {
|
|
24071
|
-
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage
|
|
24696
|
+
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
|
|
24072
24697
|
this.messages = messages;
|
|
24073
24698
|
this.getClient = getClient;
|
|
24074
24699
|
this.getModel = getModel;
|
|
@@ -24077,7 +24702,6 @@ var OpenAiResearchChat = class {
|
|
|
24077
24702
|
this.onContent = onContent;
|
|
24078
24703
|
this.getSource = getSource;
|
|
24079
24704
|
this.onUsage = onUsage;
|
|
24080
|
-
this.contextWindow = contextWindow;
|
|
24081
24705
|
}
|
|
24082
24706
|
messages;
|
|
24083
24707
|
getClient;
|
|
@@ -24087,7 +24711,6 @@ var OpenAiResearchChat = class {
|
|
|
24087
24711
|
onContent;
|
|
24088
24712
|
getSource;
|
|
24089
24713
|
onUsage;
|
|
24090
|
-
contextWindow;
|
|
24091
24714
|
async sendMessage(text3, signal) {
|
|
24092
24715
|
this.answerAbandonedCalls();
|
|
24093
24716
|
this.messages.push({ role: "user", content: text3 });
|
|
@@ -24202,7 +24825,6 @@ var OpenAiResearchChat = class {
|
|
|
24202
24825
|
record.reportedCost = { amount: reported.cost, currency: "USD" };
|
|
24203
24826
|
hasMeasure = true;
|
|
24204
24827
|
}
|
|
24205
|
-
if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
|
|
24206
24828
|
return hasMeasure ? record : void 0;
|
|
24207
24829
|
}
|
|
24208
24830
|
};
|
|
@@ -24329,8 +24951,7 @@ ${buildResearchToolsPrompt()}` }
|
|
|
24329
24951
|
(delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
|
|
24330
24952
|
(delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
|
|
24331
24953
|
() => this.config.aiProvider,
|
|
24332
|
-
(record) => currentProgress({ type: "usage", record })
|
|
24333
|
-
req.contextWindow
|
|
24954
|
+
(record) => currentProgress({ type: "usage", record })
|
|
24334
24955
|
);
|
|
24335
24956
|
const ctx = {
|
|
24336
24957
|
chat,
|
|
@@ -24892,6 +25513,10 @@ var VerdictEngine = class {
|
|
|
24892
25513
|
this.generations.clear();
|
|
24893
25514
|
}
|
|
24894
25515
|
};
|
|
25516
|
+
var EXIT_CHECK = "exit_code";
|
|
25517
|
+
function endedByExit(verdict) {
|
|
25518
|
+
return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
|
|
25519
|
+
}
|
|
24895
25520
|
function exitVerdict(exitCode) {
|
|
24896
25521
|
const normalized2 = exitCode == null ? 0 : exitCode;
|
|
24897
25522
|
const exitOk = normalized2 === 0;
|
|
@@ -24900,7 +25525,7 @@ function exitVerdict(exitCode) {
|
|
|
24900
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.`,
|
|
24901
25526
|
checks: [
|
|
24902
25527
|
{ name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
|
|
24903
|
-
{ name:
|
|
25528
|
+
{ name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
|
|
24904
25529
|
],
|
|
24905
25530
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
24906
25531
|
};
|
|
@@ -25099,6 +25724,7 @@ var PlanStore = class {
|
|
|
25099
25724
|
this.rebuild();
|
|
25100
25725
|
for (const task of this._allTasks) {
|
|
25101
25726
|
if (task.status === "failed") task.status = "pending";
|
|
25727
|
+
if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
|
|
25102
25728
|
}
|
|
25103
25729
|
this.validateAssignedRunners();
|
|
25104
25730
|
}
|
|
@@ -25228,9 +25854,17 @@ var PlanStore = class {
|
|
|
25228
25854
|
const task = this._taskMap.get(id);
|
|
25229
25855
|
if (task) task.outputSummary = summary;
|
|
25230
25856
|
}
|
|
25231
|
-
|
|
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) {
|
|
25232
25866
|
const task = this._taskMap.get(id);
|
|
25233
|
-
if (task) task.
|
|
25867
|
+
if (task) task.carriedFrom = runner;
|
|
25234
25868
|
}
|
|
25235
25869
|
clearCarriedFrom(id) {
|
|
25236
25870
|
const task = this._taskMap.get(id);
|
|
@@ -25244,9 +25878,9 @@ var PlanStore = class {
|
|
|
25244
25878
|
const task = this._taskMap.get(id);
|
|
25245
25879
|
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
|
|
25246
25880
|
}
|
|
25247
|
-
endAttemptUsage(id, attempt, now) {
|
|
25881
|
+
endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
|
|
25248
25882
|
const task = this._taskMap.get(id);
|
|
25249
|
-
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);
|
|
25250
25884
|
}
|
|
25251
25885
|
setTaskAttemptSkills(id, skills) {
|
|
25252
25886
|
const task = this._taskMap.get(id);
|
|
@@ -26104,6 +26738,8 @@ function conflictResolverTask(task, run, plan) {
|
|
|
26104
26738
|
}
|
|
26105
26739
|
|
|
26106
26740
|
// src/services/UsageLimitWaits.ts
|
|
26741
|
+
var RESUME_MARGIN_MS = 6e4;
|
|
26742
|
+
var RELAPSE_MS = 5 * 6e4;
|
|
26107
26743
|
var UsageLimitWaits = class {
|
|
26108
26744
|
constructor(onDue) {
|
|
26109
26745
|
this.onDue = onDue;
|
|
@@ -26226,6 +26862,9 @@ function selectReadyTasks(input) {
|
|
|
26226
26862
|
gated
|
|
26227
26863
|
};
|
|
26228
26864
|
}
|
|
26865
|
+
function isBlocked(task, store) {
|
|
26866
|
+
return blockedIn(task, blockingFailures(store.allTasks));
|
|
26867
|
+
}
|
|
26229
26868
|
var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
|
|
26230
26869
|
function dependencyMet(depId, store, runs) {
|
|
26231
26870
|
return waitingBehind([depId], store, runs).length === 0;
|
|
@@ -26355,6 +26994,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26355
26994
|
* task whose spawn always throws.
|
|
26356
26995
|
*/
|
|
26357
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();
|
|
26358
27003
|
/**
|
|
26359
27004
|
* Tasks Mark complete is landing after it ended their attempt: in progress
|
|
26360
27005
|
* with no attempt for as long as that takes, and not orphans for it.
|
|
@@ -26376,6 +27021,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26376
27021
|
observers = [];
|
|
26377
27022
|
skillsAt;
|
|
26378
27023
|
previousAttemptFromLog;
|
|
27024
|
+
lastLoggedAttempt;
|
|
26379
27025
|
runSettings;
|
|
26380
27026
|
isApproved;
|
|
26381
27027
|
constructor(deps) {
|
|
@@ -26391,6 +27037,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26391
27037
|
this.workspaceEnv = deps.workspaceEnv;
|
|
26392
27038
|
this.skillsAt = deps.skillsAt;
|
|
26393
27039
|
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
27040
|
+
this.lastLoggedAttempt = deps.lastLoggedAttempt;
|
|
26394
27041
|
this.runSettings = deps.runSettings;
|
|
26395
27042
|
this.isApproved = deps.isApproved;
|
|
26396
27043
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
@@ -26455,6 +27102,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26455
27102
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
26456
27103
|
skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
|
|
26457
27104
|
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
|
|
27105
|
+
lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
|
|
26458
27106
|
runSettings: options.runSettings ?? (() => void 0),
|
|
26459
27107
|
isApproved: options.isApproved ?? (() => true)
|
|
26460
27108
|
});
|
|
@@ -26711,10 +27359,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26711
27359
|
*/
|
|
26712
27360
|
holdStarts() {
|
|
26713
27361
|
this.startHolds++;
|
|
26714
|
-
let
|
|
27362
|
+
let held2 = true;
|
|
26715
27363
|
return () => {
|
|
26716
|
-
if (!
|
|
26717
|
-
|
|
27364
|
+
if (!held2) return;
|
|
27365
|
+
held2 = false;
|
|
26718
27366
|
this.startHolds--;
|
|
26719
27367
|
};
|
|
26720
27368
|
}
|
|
@@ -26725,24 +27373,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26725
27373
|
*/
|
|
26726
27374
|
admitStart() {
|
|
26727
27375
|
const generation2 = this.stopGeneration;
|
|
26728
|
-
let
|
|
27376
|
+
let held2 = true;
|
|
26729
27377
|
this.admissions++;
|
|
26730
27378
|
return {
|
|
26731
27379
|
current: () => generation2 === this.stopGeneration,
|
|
26732
27380
|
release: () => {
|
|
26733
|
-
if (!
|
|
26734
|
-
|
|
27381
|
+
if (!held2) return;
|
|
27382
|
+
held2 = false;
|
|
26735
27383
|
this.admissions--;
|
|
26736
27384
|
}
|
|
26737
27385
|
};
|
|
26738
27386
|
}
|
|
26739
27387
|
/**
|
|
26740
27388
|
* Something can still start that nobody has to ask for again: a start
|
|
26741
|
-
* admitted and not yet claimed,
|
|
26742
|
-
*
|
|
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.
|
|
26743
27392
|
*/
|
|
26744
27393
|
get startsPending() {
|
|
26745
|
-
return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
|
|
27394
|
+
return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
|
|
26746
27395
|
}
|
|
26747
27396
|
/** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
|
|
26748
27397
|
async whileAdmitted(work) {
|
|
@@ -26841,6 +27490,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26841
27490
|
this.retryCounts.clear();
|
|
26842
27491
|
this.spawnCounts.clear();
|
|
26843
27492
|
this.onHold.clear();
|
|
27493
|
+
this.queuedStarts.clear();
|
|
26844
27494
|
this.recoverOrphans();
|
|
26845
27495
|
this.rearmLimitWaits();
|
|
26846
27496
|
}
|
|
@@ -26928,6 +27578,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
26928
27578
|
}
|
|
26929
27579
|
this.runs.interrupt();
|
|
26930
27580
|
this.onHold.clear();
|
|
27581
|
+
if (reason === "user") this.queuedStarts.clear();
|
|
26931
27582
|
for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
|
|
26932
27583
|
this.sayStopped(stopped);
|
|
26933
27584
|
this.recoverOrphans();
|
|
@@ -27027,6 +27678,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
27027
27678
|
const attempt = this.attempts.get(taskId);
|
|
27028
27679
|
if (!task || !attempt) return;
|
|
27029
27680
|
attempt.decided = true;
|
|
27681
|
+
const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
|
|
27030
27682
|
console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
|
|
27031
27683
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
27032
27684
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
@@ -27048,8 +27700,8 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27048
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.`);
|
|
27049
27701
|
} else if (landing) {
|
|
27050
27702
|
await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
|
|
27051
|
-
} else if (classifyRunnerStop(
|
|
27052
|
-
this.pauseOnLimit(task, attempt,
|
|
27703
|
+
} else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
|
|
27704
|
+
this.pauseOnLimit(task, attempt, stoppedMidTurn);
|
|
27053
27705
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
27054
27706
|
} else {
|
|
27055
27707
|
this.store.markFailed(taskId);
|
|
@@ -27064,16 +27716,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27064
27716
|
const status = this.store.get(taskId)?.status;
|
|
27065
27717
|
if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
|
|
27066
27718
|
this.emit("onTaskChanged");
|
|
27067
|
-
if (
|
|
27068
|
-
|
|
27069
|
-
if (this.store.isFinished()) this.planStatus = "completed";
|
|
27070
|
-
this.emit("onTick");
|
|
27071
|
-
if (this.settled) await this.closeRun();
|
|
27072
|
-
this.emit("onExecutionComplete");
|
|
27073
|
-
}
|
|
27719
|
+
if (this.running) {
|
|
27720
|
+
await this.tick();
|
|
27074
27721
|
return;
|
|
27075
27722
|
}
|
|
27076
|
-
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
|
+
}
|
|
27077
27730
|
}
|
|
27078
27731
|
/**
|
|
27079
27732
|
* A repair's verdict never replaces the verdict the task's own work earned,
|
|
@@ -27341,11 +27994,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27341
27994
|
const resumes = this.awaitsResume;
|
|
27342
27995
|
this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
|
|
27343
27996
|
const ended = this.endAttempt(taskId, "retry");
|
|
27997
|
+
const movedFrom = this.movedFrom(task);
|
|
27344
27998
|
this.store.retry(taskId);
|
|
27999
|
+
if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
|
|
27345
28000
|
this.store.unblockDependents(taskId);
|
|
27346
28001
|
this.onHold.delete(taskId);
|
|
27347
28002
|
this.emit("onTaskChanged");
|
|
27348
|
-
|
|
28003
|
+
if (movedFrom) {
|
|
28004
|
+
await ended?.integration;
|
|
28005
|
+
} else {
|
|
28006
|
+
await this.runs.release(taskId, { keep: false }, ended?.integration);
|
|
28007
|
+
}
|
|
27349
28008
|
if (admittedAt !== this.stopGeneration) return;
|
|
27350
28009
|
if (resumes) await this.start(origin);
|
|
27351
28010
|
else await this.tick();
|
|
@@ -27396,6 +28055,16 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27396
28055
|
this.emit("onTaskChanged");
|
|
27397
28056
|
return true;
|
|
27398
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
|
+
}
|
|
27399
28068
|
/**
|
|
27400
28069
|
* Run before writing assignments so cancellation sees the old runner's
|
|
27401
28070
|
* shared wait. A session cannot cross runners, so the next attempt is a
|
|
@@ -27453,7 +28122,7 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
27453
28122
|
const summary = this.output.finalText(taskId);
|
|
27454
28123
|
this.endAttempt(taskId, "usage-limit");
|
|
27455
28124
|
this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
|
|
27456
|
-
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));
|
|
27457
28126
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
27458
28127
|
await this.afterVerdict(taskId);
|
|
27459
28128
|
}
|
|
@@ -27552,10 +28221,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27552
28221
|
return false;
|
|
27553
28222
|
}
|
|
27554
28223
|
/**
|
|
27555
|
-
* Manually start a single AI task right now
|
|
27556
|
-
*
|
|
27557
|
-
* it still needs the user's approval,
|
|
27558
|
-
*
|
|
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.
|
|
27559
28232
|
* Reuses the scheduler's own claim so a force-started task gets the same
|
|
27560
28233
|
* augmented prompt, session tracking, and exit handling — callers must not
|
|
27561
28234
|
* re-spawn the runner themselves. No-op if the task is unknown, not an AI
|
|
@@ -27566,8 +28239,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27566
28239
|
if (!task || task.type !== "ai") return;
|
|
27567
28240
|
if (this.attempts.has(taskId)) return;
|
|
27568
28241
|
this.refuseOpsDuringMerge(task);
|
|
28242
|
+
const queued2 = this.queuedBehindDependencies(task);
|
|
28243
|
+
if (queued2) return queued2;
|
|
27569
28244
|
await this.whileAdmitted(async (admittedAt) => {
|
|
27570
|
-
if (!await this.openForAdmitted(taskId, origin, () =>
|
|
28245
|
+
if (!await this.openForAdmitted(taskId, origin, async () => {
|
|
28246
|
+
await this.forceStartTask(taskId, origin);
|
|
28247
|
+
}, admittedAt)) return;
|
|
27571
28248
|
if (this.attempts.has(taskId)) return;
|
|
27572
28249
|
this.refuseOpsDuringMerge(task);
|
|
27573
28250
|
this.refuseClaim(task, admittedAt);
|
|
@@ -27576,6 +28253,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27576
28253
|
await this.claimed(task, admittedAt);
|
|
27577
28254
|
});
|
|
27578
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
|
+
}
|
|
27579
28318
|
refuseOpsDuringMerge(task) {
|
|
27580
28319
|
if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
|
|
27581
28320
|
throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
|
|
@@ -27640,6 +28379,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27640
28379
|
async tick() {
|
|
27641
28380
|
const admittedAt = this.stopGeneration;
|
|
27642
28381
|
this.recoverOrphans();
|
|
28382
|
+
await this.startQueued();
|
|
27643
28383
|
if (!this.running) {
|
|
27644
28384
|
if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
|
|
27645
28385
|
return;
|
|
@@ -27796,7 +28536,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27796
28536
|
if (refused3) return { refused: refused3 };
|
|
27797
28537
|
const attempt = {
|
|
27798
28538
|
taskId: task.id,
|
|
27799
|
-
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,
|
|
27800
28540
|
phase: "starting",
|
|
27801
28541
|
session: null,
|
|
27802
28542
|
runner: this.store.resolveTaskRunner(task),
|
|
@@ -27810,6 +28550,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27810
28550
|
skills: [],
|
|
27811
28551
|
autoResumed: opts.autoResumed === true,
|
|
27812
28552
|
turnError: null,
|
|
28553
|
+
turnOutputFrom: 0,
|
|
27813
28554
|
reminded: false
|
|
27814
28555
|
};
|
|
27815
28556
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -27883,6 +28624,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27883
28624
|
title: task.title,
|
|
27884
28625
|
env,
|
|
27885
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,
|
|
27886
28629
|
attempt: attempt.attempt,
|
|
27887
28630
|
...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
|
|
27888
28631
|
onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
|
|
@@ -27907,8 +28650,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27907
28650
|
this.output.attach(task.id, session);
|
|
27908
28651
|
this.verifier.watch(task, session);
|
|
27909
28652
|
session.onEvent((event) => {
|
|
27910
|
-
if (event.type === "turn_start")
|
|
27911
|
-
|
|
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
|
+
}
|
|
27912
28668
|
});
|
|
27913
28669
|
session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
|
|
27914
28670
|
return true;
|
|
@@ -27918,6 +28674,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
27918
28674
|
return false;
|
|
27919
28675
|
}
|
|
27920
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
|
+
}
|
|
27921
28687
|
if (attempt.kind.kind === "continuation") {
|
|
27922
28688
|
const refused3 = err instanceof RunnerStartupExit;
|
|
27923
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)}`);
|
|
@@ -28037,12 +28803,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
28037
28803
|
endAttempt(taskId, reason) {
|
|
28038
28804
|
const attempt = this.attempts.get(taskId);
|
|
28039
28805
|
this.attempts.delete(taskId);
|
|
28806
|
+
const session = attempt?.session ?? null;
|
|
28807
|
+
const nativeSessionId = session?.nativeSessionId() ?? null;
|
|
28040
28808
|
if (attempt) {
|
|
28041
|
-
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);
|
|
28042
28810
|
this.output.detach(taskId);
|
|
28043
28811
|
}
|
|
28044
|
-
|
|
28045
|
-
if (session) this.saveNativeSession(taskId, session.nativeSessionId());
|
|
28812
|
+
if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
|
|
28046
28813
|
if (stopsRunner(reason)) {
|
|
28047
28814
|
const task = this.store.get(taskId);
|
|
28048
28815
|
if (task) this.verifier.clear(task);
|
|
@@ -28051,8 +28818,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
28051
28818
|
return attempt;
|
|
28052
28819
|
}
|
|
28053
28820
|
/** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
|
|
28054
|
-
saveNativeSession(taskId, nativeSessionId) {
|
|
28055
|
-
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
|
|
28821
|
+
saveNativeSession(taskId, nativeSessionId, runner) {
|
|
28822
|
+
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
|
|
28056
28823
|
}
|
|
28057
28824
|
endAllAttempts(reason) {
|
|
28058
28825
|
const ended = [...this.attempts.values()];
|
|
@@ -28062,13 +28829,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
28062
28829
|
return ended;
|
|
28063
28830
|
}
|
|
28064
28831
|
};
|
|
28065
|
-
var RESUME_MARGIN_MS = 6e4;
|
|
28066
|
-
var RELAPSE_MS = 5 * 6e4;
|
|
28067
28832
|
var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
|
|
28068
28833
|
var OPS_RETRY_TAIL_LINES = 60;
|
|
28069
28834
|
function unresumedMessage(task, why) {
|
|
28070
28835
|
return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
|
|
28071
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
|
+
}
|
|
28072
28843
|
|
|
28073
28844
|
// src/services/Planner.ts
|
|
28074
28845
|
function plannerModesWith(req) {
|
|
@@ -28226,21 +28997,18 @@ var ModelResolver = class {
|
|
|
28226
28997
|
return this.cachedPickerOptions ?? [];
|
|
28227
28998
|
}
|
|
28228
28999
|
/**
|
|
28229
|
-
*
|
|
28230
|
-
*
|
|
28231
|
-
*
|
|
28232
|
-
*
|
|
28233
|
-
*
|
|
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.
|
|
28234
29007
|
*/
|
|
28235
|
-
contextWindowFor(
|
|
28236
|
-
if (!
|
|
28237
|
-
const
|
|
28238
|
-
|
|
28239
|
-
for (const runner of this.config.enabledRunners ?? []) {
|
|
28240
|
-
const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
|
|
28241
|
-
if (found && found > 0) return found;
|
|
28242
|
-
}
|
|
28243
|
-
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;
|
|
28244
29012
|
}
|
|
28245
29013
|
invalidate() {
|
|
28246
29014
|
this.discovery.clear();
|
|
@@ -28744,6 +29512,8 @@ var PlannerSession = class {
|
|
|
28744
29512
|
* outlives the session — nothing from one goal may reach the next.
|
|
28745
29513
|
*/
|
|
28746
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;
|
|
28747
29517
|
/** Set once the first spawn attached. */
|
|
28748
29518
|
start = null;
|
|
28749
29519
|
/**
|
|
@@ -28934,6 +29704,7 @@ var CliAgentAiService = class {
|
|
|
28934
29704
|
// hit its rate limit, or lost its login must say so in the chat rather
|
|
28935
29705
|
// than leave an empty planner bubble.
|
|
28936
29706
|
failure: turn.error,
|
|
29707
|
+
usageLimit: turn.usageLimit,
|
|
28937
29708
|
fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
|
|
28938
29709
|
};
|
|
28939
29710
|
};
|
|
@@ -29000,6 +29771,7 @@ var CliAgentAiService = class {
|
|
|
29000
29771
|
};
|
|
29001
29772
|
const subagentUsage = /* @__PURE__ */ new Map();
|
|
29002
29773
|
let error;
|
|
29774
|
+
let usageLimit;
|
|
29003
29775
|
let stepIndex = 0;
|
|
29004
29776
|
let backgroundAgents = 0;
|
|
29005
29777
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
@@ -29108,6 +29880,7 @@ var CliAgentAiService = class {
|
|
|
29108
29880
|
}
|
|
29109
29881
|
case "error":
|
|
29110
29882
|
error = event.message;
|
|
29883
|
+
usageLimit = event.usageLimit;
|
|
29111
29884
|
return;
|
|
29112
29885
|
case "turn_end":
|
|
29113
29886
|
return;
|
|
@@ -29126,12 +29899,13 @@ var CliAgentAiService = class {
|
|
|
29126
29899
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
29127
29900
|
}
|
|
29128
29901
|
session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
|
|
29902
|
+
session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
|
|
29129
29903
|
if (error || signal?.aborted) session.stop();
|
|
29130
29904
|
if (error) {
|
|
29131
29905
|
const failure = failureAfterReply(text3.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
|
|
29132
29906
|
onProgress({ type: "text_retracted", segmentId: finalSegment });
|
|
29133
29907
|
text3 = text3.slice(0, finalRunStart);
|
|
29134
|
-
return { text: text3, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
|
|
29908
|
+
return { text: text3, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
|
|
29135
29909
|
}
|
|
29136
29910
|
return { text: text3, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
29137
29911
|
}
|
|
@@ -29187,7 +29961,7 @@ var CliAgentAiService = class {
|
|
|
29187
29961
|
session.stop();
|
|
29188
29962
|
const start = session.start;
|
|
29189
29963
|
if (!start) throw new Error("No active planner conversation.");
|
|
29190
|
-
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);
|
|
29191
29965
|
}
|
|
29192
29966
|
/**
|
|
29193
29967
|
* What the planner gets of the repository's config, its runner being
|
|
@@ -29199,9 +29973,9 @@ var CliAgentAiService = class {
|
|
|
29199
29973
|
const reader = connectorFor(this.runner)?.repoConfig;
|
|
29200
29974
|
if (!reader) return { systemPrompt };
|
|
29201
29975
|
const found = reader.projectServers(cwd);
|
|
29202
|
-
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 });
|
|
29203
29977
|
return {
|
|
29204
|
-
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"),
|
|
29205
29979
|
...started.length > 0 ? { projectServers: started } : {}
|
|
29206
29980
|
};
|
|
29207
29981
|
}
|
|
@@ -29366,6 +30140,10 @@ var SessionCatalog = class {
|
|
|
29366
30140
|
allowlist() {
|
|
29367
30141
|
return this.deps.settings().modelAllowlist ?? {};
|
|
29368
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
|
+
}
|
|
29369
30147
|
/** The models the user allows on one runner; unset means no restriction. */
|
|
29370
30148
|
allowlistFor(runner) {
|
|
29371
30149
|
return this.allowlist()[runner];
|
|
@@ -29380,7 +30158,7 @@ var SessionCatalog = class {
|
|
|
29380
30158
|
*/
|
|
29381
30159
|
models() {
|
|
29382
30160
|
const out = { ...this.discovered };
|
|
29383
|
-
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()])) {
|
|
29384
30162
|
const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
|
|
29385
30163
|
if (cached2.length > 0) out[runner] = cached2;
|
|
29386
30164
|
}
|
|
@@ -29417,9 +30195,16 @@ var SessionCatalog = class {
|
|
|
29417
30195
|
* asked for.
|
|
29418
30196
|
*/
|
|
29419
30197
|
async live() {
|
|
29420
|
-
|
|
29421
|
-
|
|
29422
|
-
|
|
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());
|
|
29423
30208
|
}
|
|
29424
30209
|
queryCatalog(runners) {
|
|
29425
30210
|
const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
|
|
@@ -29445,7 +30230,8 @@ var SessionCatalog = class {
|
|
|
29445
30230
|
modelsByRunner: this.models(),
|
|
29446
30231
|
runnerModes: this.modes(runners),
|
|
29447
30232
|
perRunnerAllowlist: this.allowlist(),
|
|
29448
|
-
autonomousDefault: this.deps.config.autonomousMode
|
|
30233
|
+
autonomousDefault: this.deps.config.autonomousMode,
|
|
30234
|
+
...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
|
|
29449
30235
|
};
|
|
29450
30236
|
}
|
|
29451
30237
|
/** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
|
|
@@ -29510,6 +30296,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
|
|
|
29510
30296
|
};
|
|
29511
30297
|
}
|
|
29512
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
|
+
|
|
29513
30401
|
// src/services/PlanTurns.ts
|
|
29514
30402
|
var START_HOLD_TIMEOUT_MS = 5 * 6e4;
|
|
29515
30403
|
function turnPrompt(batch) {
|
|
@@ -29526,6 +30414,16 @@ var PlanTurns = class {
|
|
|
29526
30414
|
constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
|
|
29527
30415
|
this.host = host;
|
|
29528
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
|
+
});
|
|
29529
30427
|
}
|
|
29530
30428
|
host;
|
|
29531
30429
|
holdTimeoutMs;
|
|
@@ -29538,6 +30436,33 @@ var PlanTurns = class {
|
|
|
29538
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. */
|
|
29539
30437
|
quiet = false;
|
|
29540
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
|
+
}
|
|
29541
30466
|
/** Whether a turn this module opened is in flight. */
|
|
29542
30467
|
get busy() {
|
|
29543
30468
|
return this.running !== null;
|
|
@@ -29579,25 +30504,33 @@ var PlanTurns = class {
|
|
|
29579
30504
|
admit(input, forced) {
|
|
29580
30505
|
const plan = this.host.plan();
|
|
29581
30506
|
if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
|
|
29582
|
-
const entry =
|
|
29583
|
-
id: `turn-${Date.now()}-${++this.seq}`,
|
|
29584
|
-
source: input.source,
|
|
29585
|
-
text: input.text,
|
|
29586
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29587
|
-
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
29588
|
-
...forced ? { forced: true } : {}
|
|
29589
|
-
};
|
|
30507
|
+
const entry = this.pending(input, forced);
|
|
29590
30508
|
this.quiet = false;
|
|
30509
|
+
if (this.resend.arrived()) this.host.announce();
|
|
29591
30510
|
const pending = plan.pendingTurns ?? [];
|
|
29592
30511
|
const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
|
|
29593
30512
|
plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
|
|
29594
30513
|
this.host.persist();
|
|
30514
|
+
if (input.source === "user" && this.resend.holds()) {
|
|
30515
|
+
this.pump();
|
|
30516
|
+
return Promise.resolve(plan);
|
|
30517
|
+
}
|
|
29595
30518
|
const settled = new Promise((resolve9, reject) => {
|
|
29596
30519
|
this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
|
|
29597
30520
|
});
|
|
29598
30521
|
this.pump();
|
|
29599
30522
|
return settled;
|
|
29600
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
|
+
}
|
|
29601
30534
|
/**
|
|
29602
30535
|
* Start the next turn if one is due: nothing of ours is in flight, the
|
|
29603
30536
|
* conversation is free, and inputs wait. The host calls it when any planner
|
|
@@ -29608,7 +30541,7 @@ var PlanTurns = class {
|
|
|
29608
30541
|
* the plan, what still waits, and the planner in use at that moment.
|
|
29609
30542
|
*/
|
|
29610
30543
|
pump() {
|
|
29611
|
-
if (this.running || this.detecting) return;
|
|
30544
|
+
if (this.running || this.detecting || this.resend.holds()) return;
|
|
29612
30545
|
const plan = this.host.plan();
|
|
29613
30546
|
if (!plan || this.host.conversationBusy()) return;
|
|
29614
30547
|
const batch = this.liveBatch(plan);
|
|
@@ -29688,6 +30621,7 @@ var PlanTurns = class {
|
|
|
29688
30621
|
* conversation's to abandon; whatever it settles lands nothing here.
|
|
29689
30622
|
*/
|
|
29690
30623
|
abandon() {
|
|
30624
|
+
this.resend.reset();
|
|
29691
30625
|
this.running?.release();
|
|
29692
30626
|
this.running = null;
|
|
29693
30627
|
this.quiet = false;
|
|
@@ -29731,8 +30665,10 @@ var PlanTurns = class {
|
|
|
29731
30665
|
clearTimeout(timeout);
|
|
29732
30666
|
if (this.running !== running2) return;
|
|
29733
30667
|
const forceStopped = !outcome.ok && running2.forceStopped === true;
|
|
30668
|
+
const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
|
|
30669
|
+
if (outcome.ok) this.resend.landed();
|
|
29734
30670
|
try {
|
|
29735
|
-
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));
|
|
29736
30672
|
const took = this.take(plan, dropped);
|
|
29737
30673
|
if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
|
|
29738
30674
|
if (took || forceStopped) this.host.persist();
|
|
@@ -29746,7 +30682,11 @@ var PlanTurns = class {
|
|
|
29746
30682
|
const ended = outcome;
|
|
29747
30683
|
const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
|
|
29748
30684
|
for (const t of batch) {
|
|
29749
|
-
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;
|
|
29750
30690
|
const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
|
|
29751
30691
|
if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
|
|
29752
30692
|
}
|
|
@@ -29823,7 +30763,6 @@ var RunSupervision = class {
|
|
|
29823
30763
|
blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
|
|
29824
30764
|
readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
|
|
29825
30765
|
retryLimit: () => this.retryLimit(),
|
|
29826
|
-
plannerRunner: () => host.plannerRunner(),
|
|
29827
30766
|
restartPlanner: () => host.restartPlanner(),
|
|
29828
30767
|
abandonTurn: () => host.abandonTurn(),
|
|
29829
30768
|
goUnsupervised: (reason) => this.goUnsupervised(reason),
|
|
@@ -30032,14 +30971,14 @@ function split({ verdict, idleSince, isolation, checkpoint, queued: queued2, con
|
|
|
30032
30971
|
return { plan, status: { verdict, idleSince, isolation, checkpoint, queued: queued2, continuable, awaitingApproval, mergeGate: mergeGate2, forcedPastGate, waitsForApproval: waitsForApproval2 } };
|
|
30033
30972
|
}
|
|
30034
30973
|
function refresh(previous, tasks) {
|
|
30035
|
-
const
|
|
30974
|
+
const held2 = /* @__PURE__ */ new Map();
|
|
30036
30975
|
const index = (list) => list.forEach((t) => {
|
|
30037
|
-
|
|
30976
|
+
held2.set(t.id, t);
|
|
30038
30977
|
index(t.subtasks);
|
|
30039
30978
|
});
|
|
30040
30979
|
index(previous);
|
|
30041
30980
|
const fold = ({ subtasks, ...task }) => {
|
|
30042
|
-
const was =
|
|
30981
|
+
const was = held2.get(task.id);
|
|
30043
30982
|
const { checkpoint, ...status } = was ? split(was).status : {};
|
|
30044
30983
|
const waits = task.status === "awaiting_user" && task.awaitingReason === "checkpoint";
|
|
30045
30984
|
return { ...task, ...status, ...waits && checkpoint ? { checkpoint } : {}, subtasks: subtasks.map(fold) };
|
|
@@ -30413,9 +31352,9 @@ var fs14 = __toESM(require("fs"));
|
|
|
30413
31352
|
var path31 = __toESM(require("path"));
|
|
30414
31353
|
|
|
30415
31354
|
// src/utils/sessionId.ts
|
|
30416
|
-
var
|
|
31355
|
+
var import_crypto12 = require("crypto");
|
|
30417
31356
|
function mintSessionId() {
|
|
30418
|
-
return `session-${(0,
|
|
31357
|
+
return `session-${(0, import_crypto12.randomBytes)(16).toString("hex")}`;
|
|
30419
31358
|
}
|
|
30420
31359
|
|
|
30421
31360
|
// src/utils/sessionStore.ts
|
|
@@ -30452,8 +31391,8 @@ function dropLegacyFile(sessionsDir, meta) {
|
|
|
30452
31391
|
const legacy = path31.join(sessionsDir, legacySessionFilename(meta));
|
|
30453
31392
|
if (legacy === path31.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
|
|
30454
31393
|
try {
|
|
30455
|
-
const
|
|
30456
|
-
if (
|
|
31394
|
+
const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
|
|
31395
|
+
if (held2 === meta.id) fs14.unlinkSync(legacy);
|
|
30457
31396
|
} catch {
|
|
30458
31397
|
}
|
|
30459
31398
|
}
|
|
@@ -30586,24 +31525,25 @@ function listTaskLogAttempts(location, taskId) {
|
|
|
30586
31525
|
}
|
|
30587
31526
|
return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
|
|
30588
31527
|
}
|
|
30589
|
-
function openTaskLog(location, taskId) {
|
|
31528
|
+
function openTaskLog(location, taskId, attempt) {
|
|
30590
31529
|
ensureStateDirIgnored(location.baseDir);
|
|
30591
31530
|
const dir = taskDir(location, taskId);
|
|
30592
31531
|
fs15.mkdirSync(dir, { recursive: true });
|
|
30593
|
-
|
|
31532
|
+
const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
|
|
31533
|
+
let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
|
|
30594
31534
|
for (; ; ) {
|
|
30595
|
-
const file = path32.join(dir, `${
|
|
31535
|
+
const file = path32.join(dir, `${number}${EXTENSION}`);
|
|
30596
31536
|
try {
|
|
30597
31537
|
fs15.writeFileSync(file, "", { flag: "wx" });
|
|
30598
31538
|
} catch (err) {
|
|
30599
31539
|
if (err.code === "EEXIST") {
|
|
30600
|
-
|
|
31540
|
+
number = Math.max(number + 1, next);
|
|
30601
31541
|
continue;
|
|
30602
31542
|
}
|
|
30603
31543
|
throw err;
|
|
30604
31544
|
}
|
|
30605
31545
|
return {
|
|
30606
|
-
attempt,
|
|
31546
|
+
attempt: number,
|
|
30607
31547
|
append(events) {
|
|
30608
31548
|
if (events.length === 0) return;
|
|
30609
31549
|
fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
|
|
@@ -30649,11 +31589,13 @@ var TaskLogRecorder = class {
|
|
|
30649
31589
|
open;
|
|
30650
31590
|
flushMs;
|
|
30651
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();
|
|
30652
31594
|
wrap(runner) {
|
|
30653
31595
|
return {
|
|
30654
31596
|
spawn: async (opts) => {
|
|
30655
31597
|
const session = await runner.spawn(opts);
|
|
30656
|
-
this.record(opts.taskId, session, opts.skills);
|
|
31598
|
+
this.record(opts.taskId, session, opts.skills, opts.attempt);
|
|
30657
31599
|
return session;
|
|
30658
31600
|
},
|
|
30659
31601
|
stop: (sessionId) => runner.stop(sessionId),
|
|
@@ -30663,11 +31605,20 @@ var TaskLogRecorder = class {
|
|
|
30663
31605
|
}
|
|
30664
31606
|
};
|
|
30665
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
|
+
}
|
|
30666
31617
|
/** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
|
|
30667
|
-
record(taskId, session, skills = []) {
|
|
31618
|
+
record(taskId, session, skills = [], attemptNumber) {
|
|
30668
31619
|
let file;
|
|
30669
31620
|
try {
|
|
30670
|
-
file = this.open(this.deps.location(), taskId);
|
|
31621
|
+
file = this.open(this.deps.location(), taskId, attemptNumber);
|
|
30671
31622
|
} catch (err) {
|
|
30672
31623
|
this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
|
|
30673
31624
|
file = null;
|
|
@@ -30694,6 +31645,10 @@ var TaskLogRecorder = class {
|
|
|
30694
31645
|
}
|
|
30695
31646
|
this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
|
|
30696
31647
|
};
|
|
31648
|
+
this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
|
|
31649
|
+
pending.push(entry);
|
|
31650
|
+
flush();
|
|
31651
|
+
} });
|
|
30697
31652
|
const openCalls = /* @__PURE__ */ new Set();
|
|
30698
31653
|
session.onEvent((event) => {
|
|
30699
31654
|
const entry = toTaskLogEvent(event);
|
|
@@ -30714,21 +31669,82 @@ var TaskLogRecorder = class {
|
|
|
30714
31669
|
}
|
|
30715
31670
|
};
|
|
30716
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
|
+
|
|
30717
31724
|
// src/services/PlannerUsage.ts
|
|
30718
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;
|
|
30719
31732
|
usage = { totals: {} };
|
|
31733
|
+
windows;
|
|
30720
31734
|
/** Fold one model call's record into the totals; returns the running ledger. */
|
|
30721
31735
|
record(record) {
|
|
30722
|
-
this.usage = addPlannerUsage(this.usage, record);
|
|
31736
|
+
this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
|
|
30723
31737
|
return this.usage;
|
|
30724
31738
|
}
|
|
30725
31739
|
/** Adopt a persisted ledger (a reopened session) or start from zero. */
|
|
30726
31740
|
restore(usage) {
|
|
30727
31741
|
this.usage = usage ?? { totals: {} };
|
|
31742
|
+
this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
|
|
30728
31743
|
}
|
|
30729
31744
|
/** Start a fresh session's ledger from zero. */
|
|
30730
31745
|
clear() {
|
|
30731
31746
|
this.usage = { totals: {} };
|
|
31747
|
+
this.windows = new ContextWindowFill(this.catalogWindow);
|
|
30732
31748
|
}
|
|
30733
31749
|
/** Whether anything has been recorded — a plan with no usage says nothing. */
|
|
30734
31750
|
get hasUsage() {
|
|
@@ -30823,19 +31839,54 @@ var PlanEditor = class {
|
|
|
30823
31839
|
* between AI and MAN, are the patches that can leave a task incoherent
|
|
30824
31840
|
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
30825
31841
|
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
30826
|
-
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
30827
|
-
*
|
|
30828
|
-
* 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
|
|
30829
31845
|
* falls through to the no-op `store.update` below instead of throwing.
|
|
30830
31846
|
* Attached skills meet the rule a planner's do: a planner skill is refused,
|
|
30831
|
-
* 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.
|
|
30832
31850
|
*
|
|
30833
31851
|
* Edits to one task commit in call order: the skill check waits on git, so
|
|
30834
31852
|
* two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
|
|
30835
31853
|
* one last.
|
|
30836
31854
|
*/
|
|
30837
31855
|
updateTask(taskId, changes) {
|
|
30838
|
-
|
|
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);
|
|
30839
31890
|
const settled = edit.catch(() => {
|
|
30840
31891
|
});
|
|
30841
31892
|
this.taskEdits.set(taskId, settled);
|
|
@@ -30851,22 +31902,31 @@ var PlanEditor = class {
|
|
|
30851
31902
|
const target = this.store.get(taskId);
|
|
30852
31903
|
const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
|
|
30853
31904
|
if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
|
|
30854
|
-
if ((changes.dependencies || changes.type ||
|
|
31905
|
+
if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
|
|
30855
31906
|
const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
|
|
30856
31907
|
if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
|
|
30857
31908
|
if (check2.clear?.length || check2.set) {
|
|
30858
31909
|
changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
|
|
30859
31910
|
}
|
|
30860
31911
|
}
|
|
30861
|
-
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(
|
|
30862
31924
|
() => {
|
|
30863
31925
|
changes = this.scheduler.releaseMovedResume(taskId, changes);
|
|
30864
31926
|
return Boolean(this.store.update(taskId, changes));
|
|
30865
31927
|
},
|
|
30866
31928
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
30867
31929
|
);
|
|
30868
|
-
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
30869
|
-
return plan;
|
|
30870
31930
|
}
|
|
30871
31931
|
/**
|
|
30872
31932
|
* Replace one task's dependency list — the named entry point the surfaces'
|
|
@@ -30883,7 +31943,8 @@ var PlanEditor = class {
|
|
|
30883
31943
|
* because a runner change is never a single-field edit: the task's model,
|
|
30884
31944
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
30885
31945
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
30886
|
-
* 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.
|
|
30887
31948
|
*
|
|
30888
31949
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
30889
31950
|
*/
|
|
@@ -30893,11 +31954,13 @@ var PlanEditor = class {
|
|
|
30893
31954
|
const task = this.store.get(taskId);
|
|
30894
31955
|
if (!task) return null;
|
|
30895
31956
|
if (task.assignedRunner === runner || task.type === "user") return plan;
|
|
31957
|
+
this.refuseLiveRetarget(task);
|
|
30896
31958
|
const catalog = await this.catalog.runner(runner);
|
|
30897
31959
|
const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
|
|
30898
31960
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
|
|
30899
31961
|
if (Object.keys(changes).length === 0) return this.plan();
|
|
30900
31962
|
this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
|
|
31963
|
+
this.refuseLiveRetarget(task);
|
|
30901
31964
|
return this.commit(() => {
|
|
30902
31965
|
if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
|
|
30903
31966
|
this.admitRunner(runner, catalog.models);
|
|
@@ -30931,6 +31994,9 @@ var PlanEditor = class {
|
|
|
30931
31994
|
return true;
|
|
30932
31995
|
});
|
|
30933
31996
|
}
|
|
31997
|
+
refuseLiveRetarget(task) {
|
|
31998
|
+
if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
|
|
31999
|
+
}
|
|
30934
32000
|
/**
|
|
30935
32001
|
* A mode the host cannot run is refused, with its reason, not swapped for
|
|
30936
32002
|
* another: the plan is the source of truth for what runs (ADR-0001), and a
|
|
@@ -31260,6 +32326,10 @@ var ExecutionCommands = class {
|
|
|
31260
32326
|
const all = this.all(tasks);
|
|
31261
32327
|
const live = all.filter(({ task }) => this.host.running(task.id));
|
|
31262
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
|
+
}
|
|
31263
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.` };
|
|
31264
32334
|
}
|
|
31265
32335
|
const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
|
|
@@ -31685,8 +32755,11 @@ function mergeStopped(result) {
|
|
|
31685
32755
|
switch (result.outcome) {
|
|
31686
32756
|
case "merged":
|
|
31687
32757
|
return "it merged";
|
|
31688
|
-
case "blocked":
|
|
31689
|
-
|
|
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
|
+
}
|
|
31690
32763
|
case "conflict":
|
|
31691
32764
|
return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
|
|
31692
32765
|
case "failed":
|
|
@@ -31811,11 +32884,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
|
|
|
31811
32884
|
function isPlannerApproval(request) {
|
|
31812
32885
|
return !isRunnerApproval(request);
|
|
31813
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
|
+
}
|
|
31814
32892
|
function createSession(deps) {
|
|
31815
32893
|
const pinnedAiService = deps.aiService;
|
|
31816
32894
|
const mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
31817
32895
|
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
|
|
31818
32896
|
const store = new PlanStore();
|
|
32897
|
+
const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
|
|
31819
32898
|
const taskLogs = new TaskLogRecorder({
|
|
31820
32899
|
broadcast: deps.broadcast,
|
|
31821
32900
|
// Read through the Session, whose id and workspace are the ones it saves
|
|
@@ -31849,7 +32928,7 @@ function createSession(deps) {
|
|
|
31849
32928
|
const orchestrator = TaskOrchestrator.compose({
|
|
31850
32929
|
config: deps.config,
|
|
31851
32930
|
notifications: deps.notifications,
|
|
31852
|
-
runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
|
|
32931
|
+
runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
|
|
31853
32932
|
store,
|
|
31854
32933
|
output: deps.taskOutput,
|
|
31855
32934
|
isolation: deps.isolation,
|
|
@@ -31857,10 +32936,11 @@ function createSession(deps) {
|
|
|
31857
32936
|
workspaceRoot: deps.workspaceRoot,
|
|
31858
32937
|
skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
|
|
31859
32938
|
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
|
|
32939
|
+
lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
|
|
31860
32940
|
runSettings: () => session.planState?.runSettings,
|
|
31861
32941
|
isApproved: (taskId) => session.isApproved(taskId)
|
|
31862
32942
|
});
|
|
31863
|
-
const usage = new PlannerUsageLedger();
|
|
32943
|
+
const usage = new PlannerUsageLedger(catalogWindow);
|
|
31864
32944
|
const events = new SessionEventRelay({
|
|
31865
32945
|
broadcast: deps.broadcast,
|
|
31866
32946
|
onNotice: deps.onNotice,
|
|
@@ -31909,7 +32989,8 @@ function createSession(deps) {
|
|
|
31909
32989
|
modelResolver: deps.modelResolver,
|
|
31910
32990
|
settings: deps.settings,
|
|
31911
32991
|
// Read through the Session, which holds the plan; the catalog is built first.
|
|
31912
|
-
planRunners: () => session.planState?.runners ?? []
|
|
32992
|
+
planRunners: () => session.planState?.runners ?? [],
|
|
32993
|
+
liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
|
|
31913
32994
|
});
|
|
31914
32995
|
const session = new Session({
|
|
31915
32996
|
config: deps.config,
|
|
@@ -31931,6 +33012,7 @@ function createSession(deps) {
|
|
|
31931
33012
|
usage,
|
|
31932
33013
|
approvals,
|
|
31933
33014
|
approvalPolicy,
|
|
33015
|
+
taskLogs,
|
|
31934
33016
|
// Every planner's web reads, carried here or natively, are decided by the
|
|
31935
33017
|
// one policy, which asks through the same channel as paths and commands.
|
|
31936
33018
|
// The response cache and search provider are process-wide, so parallel
|
|
@@ -31981,6 +33063,7 @@ var Session = class {
|
|
|
31981
33063
|
fsAdapter;
|
|
31982
33064
|
approvals;
|
|
31983
33065
|
approvalPolicy;
|
|
33066
|
+
taskLogs;
|
|
31984
33067
|
fetcher;
|
|
31985
33068
|
settingsFn;
|
|
31986
33069
|
save;
|
|
@@ -32016,7 +33099,7 @@ var Session = class {
|
|
|
32016
33099
|
turnId: () => this.conversation.currentTurnId,
|
|
32017
33100
|
recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
|
|
32018
33101
|
liveCatalog: () => this.liveCatalog(),
|
|
32019
|
-
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
|
|
33102
|
+
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
|
|
32020
33103
|
submitPlan: (tasks) => this.submitPlanFromTool(tasks),
|
|
32021
33104
|
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
32022
33105
|
tasks: () => this.store.planTasks,
|
|
@@ -32061,6 +33144,7 @@ var Session = class {
|
|
|
32061
33144
|
this.usage = parts.usage;
|
|
32062
33145
|
this.approvals = parts.approvals;
|
|
32063
33146
|
this.approvalPolicy = parts.approvalPolicy;
|
|
33147
|
+
this.taskLogs = parts.taskLogs;
|
|
32064
33148
|
this.fetcher = parts.fetcher;
|
|
32065
33149
|
this.skillsService = parts.skillsService;
|
|
32066
33150
|
this.save = parts.saveSession;
|
|
@@ -32070,7 +33154,8 @@ var Session = class {
|
|
|
32070
33154
|
aiService: () => this.aiService(),
|
|
32071
33155
|
onProgress: (p) => this.events.progress(p),
|
|
32072
33156
|
opening: (runners) => this.conversationOpening(runners),
|
|
32073
|
-
catalog: () => this.catalog.
|
|
33157
|
+
catalog: () => this.catalog.current(),
|
|
33158
|
+
assignmentScope: () => this.catalog.scope(),
|
|
32074
33159
|
autorun: () => this.userRunSettings().autorun,
|
|
32075
33160
|
takeRunUpdates: () => this.untoldRunUpdates.splice(0),
|
|
32076
33161
|
tasks: () => this.store.planTasks,
|
|
@@ -32090,13 +33175,18 @@ var Session = class {
|
|
|
32090
33175
|
turnAborted: () => this.approvals.clear(isPlannerApproval),
|
|
32091
33176
|
// After the turn has announced its end, so the next one never starts
|
|
32092
33177
|
// ahead of it on a surface.
|
|
32093
|
-
turnReleased: () => queueMicrotask(() => this.turns.pump())
|
|
33178
|
+
turnReleased: () => queueMicrotask(() => this.turns.pump()),
|
|
33179
|
+
usageLimit: (said2) => this.turns.usageLimit(said2)
|
|
32094
33180
|
});
|
|
32095
33181
|
this.turns = new PlanTurns({
|
|
32096
33182
|
plan: () => this.closed ? null : this.plan,
|
|
32097
33183
|
conversationBusy: () => this.conversation.isTurnInFlight,
|
|
32098
33184
|
mayWake: () => this.supervision.mayWake(),
|
|
32099
33185
|
persist: () => this.persist({ background: true }),
|
|
33186
|
+
announce: () => {
|
|
33187
|
+
this.persist({ background: true });
|
|
33188
|
+
this.events.planGenerated(this.plan, this.goal);
|
|
33189
|
+
},
|
|
32100
33190
|
interrupt: () => {
|
|
32101
33191
|
this.conversation.stopTurn("force_send");
|
|
32102
33192
|
},
|
|
@@ -32113,7 +33203,8 @@ var Session = class {
|
|
|
32113
33203
|
},
|
|
32114
33204
|
notice: (message) => this.notice("error", message),
|
|
32115
33205
|
plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
|
|
32116
|
-
assertPlannerReady: () => this.assertPlannerReady()
|
|
33206
|
+
assertPlannerReady: () => this.assertPlannerReady(),
|
|
33207
|
+
plannerRunner: () => runnerForProvider(this.config.aiProvider)
|
|
32117
33208
|
});
|
|
32118
33209
|
this.supervision = new RunSupervision({
|
|
32119
33210
|
plan: () => this.plan,
|
|
@@ -32131,7 +33222,6 @@ var Session = class {
|
|
|
32131
33222
|
},
|
|
32132
33223
|
deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
|
|
32133
33224
|
pump: () => this.turns.pump(),
|
|
32134
|
-
plannerRunner: () => runnerForProvider(this.config.aiProvider),
|
|
32135
33225
|
restartPlanner: () => this.aiService().restart?.(),
|
|
32136
33226
|
abandonTurn: () => this.conversation.abandonHungTurn(),
|
|
32137
33227
|
notice: (level, message) => this.notice(level, message)
|
|
@@ -32213,6 +33303,7 @@ var Session = class {
|
|
|
32213
33303
|
tasks: () => this.plan ? this.store.planTasks : null,
|
|
32214
33304
|
planApproved: () => this.plan?.approved === true,
|
|
32215
33305
|
taskApproved: (taskId) => this.isApproved(taskId),
|
|
33306
|
+
waitsForApproval: (taskId) => this.waitsForApproval(taskId),
|
|
32216
33307
|
approve: (taskIds) => this.approve(taskIds),
|
|
32217
33308
|
autonomous: () => this.config.autonomousMode,
|
|
32218
33309
|
autorun: () => this.userRunSettings().autorun,
|
|
@@ -32234,6 +33325,7 @@ var Session = class {
|
|
|
32234
33325
|
blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
|
|
32235
33326
|
waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
|
|
32236
33327
|
runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
|
|
33328
|
+
joinRun: (approved) => this.joinRun(approved),
|
|
32237
33329
|
startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
|
|
32238
33330
|
stopTask: (taskId) => this.cancelTask(taskId),
|
|
32239
33331
|
stopRun: () => this.stopExecution(),
|
|
@@ -32262,8 +33354,8 @@ var Session = class {
|
|
|
32262
33354
|
* one: nothing it would start is approved until the user executes it.
|
|
32263
33355
|
*/
|
|
32264
33356
|
heldEdit(taskId) {
|
|
32265
|
-
const
|
|
32266
|
-
if (!
|
|
33357
|
+
const held2 = this.conversation.pendingOps();
|
|
33358
|
+
if (!held2 || held2.length === 0 || !this.plan) return false;
|
|
32267
33359
|
if (taskId === void 0) return true;
|
|
32268
33360
|
const before = this.store.get(taskId);
|
|
32269
33361
|
const after = this.asEdited(taskId);
|
|
@@ -32276,10 +33368,10 @@ var Session = class {
|
|
|
32276
33368
|
/** {@link editedTasks}, with the id each of the held batch's handles names in them. */
|
|
32277
33369
|
editedPlan() {
|
|
32278
33370
|
const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
|
|
32279
|
-
const
|
|
32280
|
-
if (!
|
|
33371
|
+
const held2 = this.conversation.pendingOps();
|
|
33372
|
+
if (!held2 || held2.length === 0 || !this.plan) return unedited;
|
|
32281
33373
|
const runners = this.editRunners();
|
|
32282
|
-
const result = applyTaskOps(this.store.planTasks,
|
|
33374
|
+
const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
|
|
32283
33375
|
return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
|
|
32284
33376
|
}
|
|
32285
33377
|
/** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
|
|
@@ -32303,9 +33395,9 @@ var Session = class {
|
|
|
32303
33395
|
* rules, then, for the supervisor, the oscillation guard. `held` are the
|
|
32304
33396
|
* ops the turn already holds, which `ops` join; errors are numbered by `ops`.
|
|
32305
33397
|
*/
|
|
32306
|
-
checkPlannerOps(ops,
|
|
33398
|
+
checkPlannerOps(ops, held2 = []) {
|
|
32307
33399
|
const runners = this.editRunners();
|
|
32308
|
-
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);
|
|
32309
33401
|
if (!result.ok || !this.supervisorEditing()) return result;
|
|
32310
33402
|
const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
|
|
32311
33403
|
if (!trip) return result;
|
|
@@ -32475,6 +33567,9 @@ var Session = class {
|
|
|
32475
33567
|
onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
|
|
32476
33568
|
if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
|
|
32477
33569
|
},
|
|
33570
|
+
onModelSwitched: (switched) => {
|
|
33571
|
+
void this.followRunnerModel(switched);
|
|
33572
|
+
},
|
|
32478
33573
|
onTaskIdle: ({ taskId, idleSince }) => {
|
|
32479
33574
|
this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
|
|
32480
33575
|
},
|
|
@@ -32595,10 +33690,12 @@ var Session = class {
|
|
|
32595
33690
|
* boundary rewrites `in_progress` to `pending` — nothing is running when a
|
|
32596
33691
|
* session comes off a file — so a surface that re-reads the plan mid-run must
|
|
32597
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.
|
|
32598
33695
|
*/
|
|
32599
33696
|
get currentPlanState() {
|
|
32600
33697
|
if (!this.plan) return null;
|
|
32601
|
-
const executionLog = this.store.getExecutionLog();
|
|
33698
|
+
const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
|
|
32602
33699
|
const logged = new Set(executionLog.map((s) => s.id));
|
|
32603
33700
|
const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
|
|
32604
33701
|
const record = approvalRecordOf(this.plan);
|
|
@@ -32609,7 +33706,7 @@ var Session = class {
|
|
|
32609
33706
|
phase: "executing",
|
|
32610
33707
|
history: [],
|
|
32611
33708
|
message: "",
|
|
32612
|
-
executionLog
|
|
33709
|
+
executionLog,
|
|
32613
33710
|
pendingTasks,
|
|
32614
33711
|
goal: this.goal,
|
|
32615
33712
|
runners: this.plan.runners,
|
|
@@ -32779,16 +33876,20 @@ var Session = class {
|
|
|
32779
33876
|
opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
|
|
32780
33877
|
} catch (err) {
|
|
32781
33878
|
if (this.opening === pending) this.opening = null;
|
|
32782
|
-
for (const
|
|
33879
|
+
for (const held3 of pending.held ?? []) held3.reject(err);
|
|
32783
33880
|
throw err;
|
|
32784
33881
|
}
|
|
32785
|
-
const
|
|
33882
|
+
const held2 = pending.held ?? [];
|
|
32786
33883
|
pending.held = null;
|
|
32787
|
-
for (const h of
|
|
33884
|
+
for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
|
|
32788
33885
|
try {
|
|
32789
33886
|
const { landed: landed2, effects } = await opened;
|
|
32790
33887
|
this.autorunSubmitted(effects);
|
|
32791
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;
|
|
32792
33893
|
} finally {
|
|
32793
33894
|
if (this.opening === pending) this.opening = null;
|
|
32794
33895
|
}
|
|
@@ -32803,10 +33904,10 @@ var Session = class {
|
|
|
32803
33904
|
behindOpening(input, forced = false) {
|
|
32804
33905
|
const pending = this.opening;
|
|
32805
33906
|
if (!pending) return null;
|
|
32806
|
-
const
|
|
32807
|
-
if (!
|
|
33907
|
+
const held2 = pending.held;
|
|
33908
|
+
if (!held2) return this.turns.follow(input, forced);
|
|
32808
33909
|
return new Promise((resolve9, reject) => {
|
|
32809
|
-
|
|
33910
|
+
held2.push({ input, forced, resolve: resolve9, reject });
|
|
32810
33911
|
});
|
|
32811
33912
|
}
|
|
32812
33913
|
/**
|
|
@@ -32905,12 +34006,12 @@ var Session = class {
|
|
|
32905
34006
|
await this.liveCatalog();
|
|
32906
34007
|
if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
|
|
32907
34008
|
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
32908
|
-
const
|
|
32909
|
-
if (!
|
|
32910
|
-
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);
|
|
32911
34012
|
if (!result.ok) return { ok: false, errors: result.errors };
|
|
32912
|
-
if (!this.conversation.submit({ kind: "task_ops", ops: [...
|
|
32913
|
-
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);
|
|
32914
34015
|
return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
|
|
32915
34016
|
}
|
|
32916
34017
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
@@ -32947,9 +34048,6 @@ var Session = class {
|
|
|
32947
34048
|
autonomousDefault: this.config.autonomousMode,
|
|
32948
34049
|
autorun: this.userRunSettings().autorun,
|
|
32949
34050
|
isolatedExecution: await this.runs.plannerLayout(),
|
|
32950
|
-
// The planner's own model window, when a cached catalog knows it, so the
|
|
32951
|
-
// usage line can show context fill (#49). Unknown stays absent.
|
|
32952
|
-
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
32953
34051
|
fs: this.fsAdapter,
|
|
32954
34052
|
fetcher: this.fetcher,
|
|
32955
34053
|
approval: this.approvalPolicy,
|
|
@@ -32976,7 +34074,7 @@ var Session = class {
|
|
|
32976
34074
|
for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
|
|
32977
34075
|
const runners = this.plan.runners;
|
|
32978
34076
|
const before = this.store.planTasks;
|
|
32979
|
-
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
|
|
34077
|
+
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
|
|
32980
34078
|
if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
|
|
32981
34079
|
const bySupervisor = this.supervisorEditing();
|
|
32982
34080
|
const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
|
|
@@ -32989,6 +34087,7 @@ var Session = class {
|
|
|
32989
34087
|
this.store.resetForRun({ keepScheduledResumes: true });
|
|
32990
34088
|
}
|
|
32991
34089
|
if (bySupervisor) this.approve(added);
|
|
34090
|
+
else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
|
|
32992
34091
|
return coerced.length;
|
|
32993
34092
|
}
|
|
32994
34093
|
preparePlannerTaskUpdates(tasks) {
|
|
@@ -33023,6 +34122,11 @@ var Session = class {
|
|
|
33023
34122
|
if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
|
|
33024
34123
|
}
|
|
33025
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
|
+
}
|
|
33026
34130
|
this.idlePlan();
|
|
33027
34131
|
this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
|
|
33028
34132
|
await this.runWholePlan("user");
|
|
@@ -33044,6 +34148,25 @@ var Session = class {
|
|
|
33044
34148
|
await this.orchestrator.approveReview(origin);
|
|
33045
34149
|
});
|
|
33046
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
|
+
}
|
|
33047
34170
|
async approveReview() {
|
|
33048
34171
|
if (!this.plan) throw new NoPlanError("review");
|
|
33049
34172
|
const plan = this.plan;
|
|
@@ -33052,9 +34175,12 @@ var Session = class {
|
|
|
33052
34175
|
await this.withSave(() => this.orchestrator.approveReview("user"));
|
|
33053
34176
|
return plan;
|
|
33054
34177
|
}
|
|
34178
|
+
/** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
|
|
33055
34179
|
async forceStartTask(taskId) {
|
|
33056
34180
|
this.approveTask(taskId);
|
|
33057
|
-
|
|
34181
|
+
if (!this.plan) return;
|
|
34182
|
+
this.requireTask(taskId);
|
|
34183
|
+
return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
|
|
33058
34184
|
}
|
|
33059
34185
|
async startOneTask(taskId, origin) {
|
|
33060
34186
|
if (!this.plan) return;
|
|
@@ -33111,10 +34237,51 @@ var Session = class {
|
|
|
33111
34237
|
async resumeNow(taskId) {
|
|
33112
34238
|
await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
|
|
33113
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
|
+
}
|
|
33114
34248
|
/** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
|
|
33115
34249
|
cancelScheduledResume(taskId) {
|
|
33116
34250
|
return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
|
|
33117
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
|
+
}
|
|
33118
34285
|
/** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
|
|
33119
34286
|
notice(level, message) {
|
|
33120
34287
|
this.notifications[level](message);
|
|
@@ -33186,8 +34353,8 @@ var Session = class {
|
|
|
33186
34353
|
if (!this.plan) return EMPTY_RUN;
|
|
33187
34354
|
const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
|
|
33188
34355
|
const view = reduceRun(planned, this.events.statusMessage());
|
|
33189
|
-
const
|
|
33190
|
-
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;
|
|
33191
34358
|
}
|
|
33192
34359
|
/** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
|
|
33193
34360
|
mergeGate(taskId) {
|
|
@@ -33346,6 +34513,7 @@ var Session = class {
|
|
|
33346
34513
|
if (adopting) this.dropPlanScope();
|
|
33347
34514
|
this.plan = plan;
|
|
33348
34515
|
if (adopting) this.supervision.restore(plan.supervision);
|
|
34516
|
+
if (adopting) this.turns.restoreResend();
|
|
33349
34517
|
this.goal = goal;
|
|
33350
34518
|
this.workspace = workspace;
|
|
33351
34519
|
plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
|
|
@@ -33588,6 +34756,7 @@ function syncTranscript(view, { plan, turnId }) {
|
|
|
33588
34756
|
if (isNew && entry.kind === "wake_up") next = takePendingInputs(next, void 0, [entry.content]).view;
|
|
33589
34757
|
if (isNew && entry.kind === "plan_generated") next = markPlan(next, entry.content, i === committed ? turnId : void 0);
|
|
33590
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));
|
|
33591
34760
|
if (isNew && entry.kind === "compaction") next = markCompaction(next, entry.content);
|
|
33592
34761
|
if (latest === void 0 || entry.timestamp > latest) latest = entry.timestamp;
|
|
33593
34762
|
}
|
|
@@ -33668,6 +34837,8 @@ function roleOf(entry) {
|
|
|
33668
34837
|
return "wake_up";
|
|
33669
34838
|
case "supervisor":
|
|
33670
34839
|
return "supervisor";
|
|
34840
|
+
case "error":
|
|
34841
|
+
return "error";
|
|
33671
34842
|
default:
|
|
33672
34843
|
return entry.role === "user" ? "user" : "planner";
|
|
33673
34844
|
}
|
|
@@ -33932,13 +35103,18 @@ function blockedBy(task, tasks) {
|
|
|
33932
35103
|
if (!tasks?.length || !NOT_STARTED.has(task.status)) return [];
|
|
33933
35104
|
return blockingFailures(asBlocking(tasks)).get(task.id) ?? [];
|
|
33934
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
|
+
}
|
|
33935
35110
|
function taskRowView(task, context = {}) {
|
|
33936
35111
|
const parent = context.parent ?? void 0;
|
|
33937
35112
|
const isolation = task.isolation && task.isolation.state !== "none" ? task.isolation : null;
|
|
33938
|
-
const
|
|
35113
|
+
const spend = taskUsageSummary(task);
|
|
35114
|
+
const latestAttempt = task.attemptUsage?.at(-1);
|
|
33939
35115
|
const blocked = blockedBy(task, context.tasks);
|
|
33940
35116
|
return {
|
|
33941
|
-
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,
|
|
33942
35118
|
orderLabel: taskOrderLabel(task, parent),
|
|
33943
35119
|
kind: task.type === "user" ? "user" : task.ops ? "ops" : "ai",
|
|
33944
35120
|
status: blocked.length > 0 ? "blocked" : taskStatusKind(task),
|
|
@@ -33947,6 +35123,7 @@ function taskRowView(task, context = {}) {
|
|
|
33947
35123
|
resumes: resumeLabel(task, context.now),
|
|
33948
35124
|
approvals: approvalLabel(task.awaitingApproval),
|
|
33949
35125
|
waitsForApproval: task.waitsForApproval && NOT_STARTED.has(task.status) ? WAITS_FOR_APPROVAL_LABEL : null,
|
|
35126
|
+
...flagOf(task, context.runnerScope),
|
|
33950
35127
|
autonomous: task.type === "ai" && context.modes?.find((m) => m.id === task.taskMode)?.autonomous === true,
|
|
33951
35128
|
repos: namedRepos(isolation?.repos),
|
|
33952
35129
|
conflict: isolation?.state === "conflict" ? { repo: namedRepo(isolation.conflictRepo), files: filesOf(isolation.conflictFiles) } : null,
|
|
@@ -34152,6 +35329,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34152
35329
|
messageCount = 0;
|
|
34153
35330
|
interrupting = null;
|
|
34154
35331
|
lastSessionId = null;
|
|
35332
|
+
lastSessionCost = null;
|
|
34155
35333
|
structuredEmitter = new import_events2.EventEmitter();
|
|
34156
35334
|
permissionCount = 0;
|
|
34157
35335
|
/**
|
|
@@ -34356,6 +35534,9 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34356
35534
|
nativeSessionId() {
|
|
34357
35535
|
return this.adapter?.nativeSessionId() ?? this.lastSessionId;
|
|
34358
35536
|
}
|
|
35537
|
+
sessionCost() {
|
|
35538
|
+
return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
|
|
35539
|
+
}
|
|
34359
35540
|
answerPermission(id, decision) {
|
|
34360
35541
|
const open = this.permissions.get(id);
|
|
34361
35542
|
if (!open) return false;
|
|
@@ -34403,6 +35584,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34403
35584
|
*/
|
|
34404
35585
|
async restartInterrupted(turn) {
|
|
34405
35586
|
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
35587
|
+
const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
|
|
34406
35588
|
this.generation += 1;
|
|
34407
35589
|
turn.abort.abort();
|
|
34408
35590
|
if (this.adapter) {
|
|
@@ -34412,7 +35594,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34412
35594
|
for (const message of this.queue) this.requeue(message);
|
|
34413
35595
|
try {
|
|
34414
35596
|
this.revokeTools();
|
|
34415
|
-
await this.startAttached({ ...this.startOptions, resumeSessionId });
|
|
35597
|
+
await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
|
|
34416
35598
|
} catch (err) {
|
|
34417
35599
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
34418
35600
|
this.withdrawPermissions();
|
|
@@ -34439,6 +35621,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34439
35621
|
return;
|
|
34440
35622
|
}
|
|
34441
35623
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
35624
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
34442
35625
|
this.withdrawPermissions();
|
|
34443
35626
|
this.baseHandleExit(code);
|
|
34444
35627
|
});
|
|
@@ -34476,6 +35659,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34476
35659
|
this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
34477
35660
|
}).then(() => {
|
|
34478
35661
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
35662
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
34479
35663
|
if (generation2 === this.generation) this.endTurn(turn, turn.reason);
|
|
34480
35664
|
});
|
|
34481
35665
|
this.offerNext();
|
|
@@ -34602,6 +35786,11 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
34602
35786
|
case "error":
|
|
34603
35787
|
this.text.failure(event.message);
|
|
34604
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;
|
|
34605
35794
|
default:
|
|
34606
35795
|
break;
|
|
34607
35796
|
}
|
|
@@ -34682,7 +35871,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
34682
35871
|
model: opts.modelId,
|
|
34683
35872
|
mode: opts.mode ?? "default",
|
|
34684
35873
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
34685
|
-
resumeSessionId: opts.resumeSessionId
|
|
35874
|
+
resumeSessionId: opts.resumeSessionId,
|
|
35875
|
+
resumeCost: opts.resumeCost
|
|
34686
35876
|
},
|
|
34687
35877
|
interruptGraceMs: this.interruptGraceMs,
|
|
34688
35878
|
tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
|
|
@@ -34985,6 +36175,7 @@ var PlannerModelMemory = class {
|
|
|
34985
36175
|
PlannerNotReadyError,
|
|
34986
36176
|
PlannerTurnDiscardedError,
|
|
34987
36177
|
PlannerTurnStoppedError,
|
|
36178
|
+
PlannerUsageLimitError,
|
|
34988
36179
|
REFUSED_COMMANDS,
|
|
34989
36180
|
RUN_SETTINGS_DEFAULTS,
|
|
34990
36181
|
RunSettingsError,
|
|
@@ -35041,6 +36232,8 @@ var PlannerModelMemory = class {
|
|
|
35041
36232
|
approvalScopes,
|
|
35042
36233
|
assertWorkspaceExists,
|
|
35043
36234
|
assertWorkspaceIsProject,
|
|
36235
|
+
assignmentIssue,
|
|
36236
|
+
assignmentIssueText,
|
|
35044
36237
|
augmentedPath,
|
|
35045
36238
|
automergeSummary,
|
|
35046
36239
|
autonomyLevelLabel,
|
|
@@ -35057,6 +36250,7 @@ var PlannerModelMemory = class {
|
|
|
35057
36250
|
canSetDependencies,
|
|
35058
36251
|
canSplitTask,
|
|
35059
36252
|
capConflictFiles,
|
|
36253
|
+
checkoutRule,
|
|
35060
36254
|
classifyCommand,
|
|
35061
36255
|
classifyOutcome,
|
|
35062
36256
|
clearAugmentedPathCache,
|
|
@@ -35070,6 +36264,7 @@ var PlannerModelMemory = class {
|
|
|
35070
36264
|
commandAllowedBy,
|
|
35071
36265
|
configuredProviders,
|
|
35072
36266
|
continuability,
|
|
36267
|
+
countedSessionCost,
|
|
35073
36268
|
createAiService,
|
|
35074
36269
|
createEmptyPlan,
|
|
35075
36270
|
createSession,
|
|
@@ -35133,6 +36328,7 @@ var PlannerModelMemory = class {
|
|
|
35133
36328
|
isMeasured,
|
|
35134
36329
|
isOpenAiProvider,
|
|
35135
36330
|
isPlannerBackend,
|
|
36331
|
+
isRepoGroup,
|
|
35136
36332
|
isRunnerApproval,
|
|
35137
36333
|
isSupervisorLevel,
|
|
35138
36334
|
isTaskRunning,
|
|
@@ -35155,6 +36351,7 @@ var PlannerModelMemory = class {
|
|
|
35155
36351
|
markdownHtml,
|
|
35156
36352
|
markdownTokens,
|
|
35157
36353
|
mcpClientConfig,
|
|
36354
|
+
mergeTarget,
|
|
35158
36355
|
migrateOldConfigDir,
|
|
35159
36356
|
migratePlanIsolation,
|
|
35160
36357
|
migratePlanState,
|
|
@@ -35204,6 +36401,7 @@ var PlannerModelMemory = class {
|
|
|
35204
36401
|
removedPluginNotice,
|
|
35205
36402
|
renderTaskQueryAnswer,
|
|
35206
36403
|
replayTaskLog,
|
|
36404
|
+
reposWithWork,
|
|
35207
36405
|
researchShellWarning,
|
|
35208
36406
|
researchToolsPath,
|
|
35209
36407
|
resetInstalledPlanner,
|
|
@@ -35217,6 +36415,7 @@ var PlannerModelMemory = class {
|
|
|
35217
36415
|
resolveWithin,
|
|
35218
36416
|
resumeLabel,
|
|
35219
36417
|
resumeTime,
|
|
36418
|
+
resumedSessionWindow,
|
|
35220
36419
|
runSettingsOf,
|
|
35221
36420
|
runnerForProvider,
|
|
35222
36421
|
runnerModesFrom,
|
|
@@ -35257,6 +36456,7 @@ var PlannerModelMemory = class {
|
|
|
35257
36456
|
taskRowView,
|
|
35258
36457
|
taskStartedNotice,
|
|
35259
36458
|
taskStatusKind,
|
|
36459
|
+
taskUsageSummary,
|
|
35260
36460
|
textHasTaskOps,
|
|
35261
36461
|
textHasTaskQuery,
|
|
35262
36462
|
titledRefs,
|