@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.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
looksLikePlanAttempt,
|
|
3
3
|
parsePlanJson,
|
|
4
4
|
validatePlanTasks
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-F3KIX7WR.mjs";
|
|
6
6
|
import {
|
|
7
7
|
ACT_RISK,
|
|
8
8
|
AUTORUN_RISK,
|
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
addUsage,
|
|
20
20
|
addUsageTotals,
|
|
21
21
|
aheadOfDraft,
|
|
22
|
+
assignmentConsequence,
|
|
23
|
+
assignmentIssue,
|
|
24
|
+
assignmentIssueText,
|
|
22
25
|
automergeSummary,
|
|
23
26
|
autorunSummary,
|
|
24
27
|
blockingFailures,
|
|
@@ -26,6 +29,7 @@ import {
|
|
|
26
29
|
capConflictFiles,
|
|
27
30
|
classifyOutcome,
|
|
28
31
|
continuability,
|
|
32
|
+
countedSessionCost,
|
|
29
33
|
dependentsNotice,
|
|
30
34
|
diffRows,
|
|
31
35
|
diffStat,
|
|
@@ -60,11 +64,13 @@ import {
|
|
|
60
64
|
reduceRun,
|
|
61
65
|
reduceTaskLog,
|
|
62
66
|
replayTaskLog,
|
|
67
|
+
resumedSessionWindow,
|
|
63
68
|
runnerToolSubject,
|
|
64
69
|
sameMessage,
|
|
65
70
|
serializePlan,
|
|
66
71
|
serializeTask,
|
|
67
72
|
serializeTaskStatus,
|
|
73
|
+
sessionOwner,
|
|
68
74
|
skillLoadNotice,
|
|
69
75
|
startAttemptUsage,
|
|
70
76
|
stopTurn,
|
|
@@ -78,13 +84,14 @@ import {
|
|
|
78
84
|
taskRowActions,
|
|
79
85
|
taskRowView,
|
|
80
86
|
taskStartedNotice,
|
|
87
|
+
taskUsageSummary,
|
|
81
88
|
titledRefs,
|
|
82
89
|
toolHeadline,
|
|
83
90
|
truncateCheckpointSummary,
|
|
84
91
|
unsendAll,
|
|
85
92
|
unsendLatest,
|
|
86
93
|
usageLine
|
|
87
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-6LEA6WAM.mjs";
|
|
88
95
|
import {
|
|
89
96
|
NOT_STARTED,
|
|
90
97
|
PLAN_ENVELOPE_KEY,
|
|
@@ -111,6 +118,7 @@ import {
|
|
|
111
118
|
extractObjectsWithKey,
|
|
112
119
|
filterModelsForPrompt,
|
|
113
120
|
isTaskRunning,
|
|
121
|
+
liveRunnerRefusal,
|
|
114
122
|
opensWithJsonObject,
|
|
115
123
|
parseAutonomyLevel,
|
|
116
124
|
parseTaskOpsJson,
|
|
@@ -130,7 +138,7 @@ import {
|
|
|
130
138
|
unavailableModeReason,
|
|
131
139
|
unavailableTaskModeMessage,
|
|
132
140
|
validateTaskEdit
|
|
133
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-4QLFM73A.mjs";
|
|
134
142
|
import {
|
|
135
143
|
SELF_REPO,
|
|
136
144
|
absorbRemoval,
|
|
@@ -144,7 +152,7 @@ import {
|
|
|
144
152
|
noRemoval,
|
|
145
153
|
repoRootOf,
|
|
146
154
|
taskIsolationOf
|
|
147
|
-
} from "./chunk-
|
|
155
|
+
} from "./chunk-NJGID6BG.mjs";
|
|
148
156
|
import {
|
|
149
157
|
DEFAULT_RUNNERS,
|
|
150
158
|
addTaskToPlan,
|
|
@@ -176,7 +184,7 @@ import {
|
|
|
176
184
|
updateTaskInPlan,
|
|
177
185
|
waitsForApproval,
|
|
178
186
|
waitsForReset
|
|
179
|
-
} from "./chunk-
|
|
187
|
+
} from "./chunk-RT4NQ2BZ.mjs";
|
|
180
188
|
|
|
181
189
|
// src/models/TaskLog.ts
|
|
182
190
|
var HEAD_LINES = 60;
|
|
@@ -278,6 +286,9 @@ function toTaskLogEvent(event) {
|
|
|
278
286
|
};
|
|
279
287
|
case "command_output":
|
|
280
288
|
return { type: "command_output", text: event.text };
|
|
289
|
+
// Logged once the plan's write-back settles: see `TaskLogRecorder.note`.
|
|
290
|
+
case "model_switched":
|
|
291
|
+
case "model_drifted":
|
|
281
292
|
case "background_agent":
|
|
282
293
|
return null;
|
|
283
294
|
}
|
|
@@ -4729,8 +4740,8 @@ function forms(text) {
|
|
|
4729
4740
|
function sensitiveEnvValues(env) {
|
|
4730
4741
|
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());
|
|
4731
4742
|
}
|
|
4732
|
-
function carriesSecret(texts,
|
|
4733
|
-
const secrets = [...
|
|
4743
|
+
function carriesSecret(texts, held2) {
|
|
4744
|
+
const secrets = [...held2].map((s) => s.trim()).filter((s) => s.length >= MIN_SECRET_LENGTH);
|
|
4734
4745
|
return texts.flatMap(forms).some((text) => secrets.some((secret) => text.includes(secret)) || TOKEN_SHAPES.some((shape) => shape.test(text)) || [...text.matchAll(SK_KEY)].some((m) => looksRandom(m[0].slice(3))));
|
|
4735
4746
|
}
|
|
4736
4747
|
|
|
@@ -6477,6 +6488,10 @@ function parseOpenCodeModels(stdout, manifest) {
|
|
|
6477
6488
|
}
|
|
6478
6489
|
return models;
|
|
6479
6490
|
}
|
|
6491
|
+
function contextLimit(limit) {
|
|
6492
|
+
const context = limit?.context;
|
|
6493
|
+
return typeof context === "number" && Number.isFinite(context) && context > 0 ? { contextWindow: context } : {};
|
|
6494
|
+
}
|
|
6480
6495
|
function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
6481
6496
|
const normalized2 = stdout.endsWith("\n") ? stdout : stdout + "\n";
|
|
6482
6497
|
const parts = normalized2.split(/^([A-Za-z0-9._-]+\/[^\s"{}]+)\n/m);
|
|
@@ -6492,12 +6507,14 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
6492
6507
|
const jsonText = parts[i + 1].trim();
|
|
6493
6508
|
if (seen.has(fullId)) continue;
|
|
6494
6509
|
let variantIds = [];
|
|
6510
|
+
let window = {};
|
|
6495
6511
|
let modelLabel = prefLabels.get(fullId) || fullId.split("/").pop() || fullId;
|
|
6496
6512
|
const runnerProvider = fullId.split("/")[0];
|
|
6497
6513
|
try {
|
|
6498
6514
|
const data = JSON.parse(jsonText);
|
|
6499
6515
|
if (data.name) modelLabel = prefLabels.get(fullId) || data.name;
|
|
6500
6516
|
variantIds = Object.keys(data.variants ?? {});
|
|
6517
|
+
window = contextLimit(data.limit);
|
|
6501
6518
|
} catch {
|
|
6502
6519
|
}
|
|
6503
6520
|
seen.add(fullId);
|
|
@@ -6505,7 +6522,8 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
6505
6522
|
modelId: fullId,
|
|
6506
6523
|
modelLabel,
|
|
6507
6524
|
runnerProvider,
|
|
6508
|
-
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
|
|
6525
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
6526
|
+
...window
|
|
6509
6527
|
});
|
|
6510
6528
|
}
|
|
6511
6529
|
return preferredFirst(models, manifest);
|
|
@@ -6546,7 +6564,8 @@ function parseOpenCodeModelsApi(stdout, manifest) {
|
|
|
6546
6564
|
modelId: fullId,
|
|
6547
6565
|
modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
|
|
6548
6566
|
runnerProvider: entry.providerID,
|
|
6549
|
-
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) }))
|
|
6567
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
6568
|
+
...contextLimit(entry.limit)
|
|
6550
6569
|
});
|
|
6551
6570
|
}
|
|
6552
6571
|
return preferredFirst(models, manifest);
|
|
@@ -8186,6 +8205,7 @@ var LOG_KINDS = {
|
|
|
8186
8205
|
turn_start: true,
|
|
8187
8206
|
command: true,
|
|
8188
8207
|
command_output: true,
|
|
8208
|
+
model_switched: true,
|
|
8189
8209
|
compaction: true,
|
|
8190
8210
|
runner_loaded: true,
|
|
8191
8211
|
turn_end: true,
|
|
@@ -8221,6 +8241,11 @@ ${s.content}` : ""}`).join("\n");
|
|
|
8221
8241
|
return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
|
|
8222
8242
|
case "command_output":
|
|
8223
8243
|
return event.text;
|
|
8244
|
+
case "model_switched": {
|
|
8245
|
+
const { plan } = event;
|
|
8246
|
+
const planned = plan ? `; plan: ${plan.model?.model ?? "no model"}${plan.effort ? ` (${plan.effort})` : ""}` : "";
|
|
8247
|
+
return `${event.from.model} \u2192 ${event.to.model}${planned}${event.kept ? `, kept: ${event.kept}` : ""}`;
|
|
8248
|
+
}
|
|
8224
8249
|
case "compaction":
|
|
8225
8250
|
return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
|
|
8226
8251
|
case "runner_loaded":
|
|
@@ -9780,6 +9805,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
|
|
|
9780
9805
|
...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
|
|
9781
9806
|
};
|
|
9782
9807
|
}
|
|
9808
|
+
var MODEL_COMMAND = "model";
|
|
9783
9809
|
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
9784
9810
|
function usageRecord(usage, model, subagentId) {
|
|
9785
9811
|
const record = {
|
|
@@ -9841,9 +9867,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9841
9867
|
/** The model answering the planner's current message, as its `message_start` named it. */
|
|
9842
9868
|
plannerModel;
|
|
9843
9869
|
/**
|
|
9844
|
-
* The session's `total_cost_usd` as last
|
|
9845
|
-
*
|
|
9846
|
-
*
|
|
9870
|
+
* The session's `total_cost_usd` as last counted. A resume starts from what
|
|
9871
|
+
* was counted of that session before, and is undefined without it: the CLI
|
|
9872
|
+
* restores the session's running total, which holds turns counted earlier.
|
|
9847
9873
|
*/
|
|
9848
9874
|
reportedCostUsd = 0;
|
|
9849
9875
|
reportedModelCostUsd = /* @__PURE__ */ new Map();
|
|
@@ -9884,6 +9910,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9884
9910
|
terminalOnly = /* @__PURE__ */ new Set();
|
|
9885
9911
|
/** Whether this process's `runner_loaded` went out: its first `init` sends it. */
|
|
9886
9912
|
loadedReported = false;
|
|
9913
|
+
/** The models `initialize` listed, which turn the ids the CLI reports back into the ones a plan holds. */
|
|
9914
|
+
listedModels = [];
|
|
9915
|
+
/** The id the task's main thread last reported answering on. */
|
|
9916
|
+
mainModel;
|
|
9917
|
+
/** What the plan should say the task runs: its spawn's model, then each switch followed. */
|
|
9918
|
+
followedModel;
|
|
9919
|
+
/**
|
|
9920
|
+
* Model commands sent through the chat whose switch has not been seen yet,
|
|
9921
|
+
* oldest first, each with the turns still to end before its own has. Several
|
|
9922
|
+
* can be pending: a command's switch is first reported by the next turn's
|
|
9923
|
+
* `init`, which can be another `/model`'s.
|
|
9924
|
+
*/
|
|
9925
|
+
modelCommands = [];
|
|
9887
9926
|
spawnSpec(opts, workspace) {
|
|
9888
9927
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
9889
9928
|
const args = [
|
|
@@ -9909,12 +9948,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9909
9948
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
9910
9949
|
if (opts.resumeSessionId) {
|
|
9911
9950
|
args.push("--resume", opts.resumeSessionId);
|
|
9912
|
-
this.
|
|
9951
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
9913
9952
|
this.pendingResume = opts.resumeSessionId;
|
|
9914
9953
|
}
|
|
9915
9954
|
this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
|
|
9916
9955
|
return { command: "claude", args, env: NO_AUTO_MEMORY };
|
|
9917
9956
|
}
|
|
9957
|
+
sessionCost() {
|
|
9958
|
+
const sessionId = this.sessionId ?? this.pendingResume;
|
|
9959
|
+
if (!sessionId || this.reportedCostUsd === void 0) return null;
|
|
9960
|
+
return { sessionId, usd: this.reportedCostUsd, modelUsd: Object.fromEntries(this.reportedModelCostUsd) };
|
|
9961
|
+
}
|
|
9962
|
+
/** A count kept for another session says nothing of this one's. */
|
|
9963
|
+
countFrom(sessionId, counted) {
|
|
9964
|
+
const known = counted?.sessionId === sessionId ? counted : void 0;
|
|
9965
|
+
this.reportedCostUsd = known?.usd;
|
|
9966
|
+
this.reportedModelCostUsd = new Map(Object.entries(known?.modelUsd ?? {}));
|
|
9967
|
+
}
|
|
9918
9968
|
savedOutput(abs) {
|
|
9919
9969
|
return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
|
|
9920
9970
|
}
|
|
@@ -9929,6 +9979,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9929
9979
|
*/
|
|
9930
9980
|
taskSpawnSpec(opts) {
|
|
9931
9981
|
this.requestedMode = opts.flags.permissionMode;
|
|
9982
|
+
this.followedModel = opts.model;
|
|
9932
9983
|
const args = [
|
|
9933
9984
|
...TASK_PROTOCOL_ARGS,
|
|
9934
9985
|
"--permission-prompt-tool",
|
|
@@ -9942,7 +9993,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9942
9993
|
if (opts.model) args.push("--model", opts.model);
|
|
9943
9994
|
if (opts.resumeSessionId) {
|
|
9944
9995
|
args.push("--resume", opts.resumeSessionId);
|
|
9945
|
-
this.
|
|
9996
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
9946
9997
|
this.pendingResume = opts.resumeSessionId;
|
|
9947
9998
|
}
|
|
9948
9999
|
if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
|
|
@@ -9984,6 +10035,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9984
10035
|
if (opts.kind !== "task") return;
|
|
9985
10036
|
const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
|
|
9986
10037
|
this.listedCommands = initialized?.commands ?? [];
|
|
10038
|
+
this.listedModels = initialized?.models ?? [];
|
|
9987
10039
|
if (opts.flags.permissionMode !== "plan" || !opts.model) return;
|
|
9988
10040
|
const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
|
|
9989
10041
|
if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
|
|
@@ -10037,6 +10089,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10037
10089
|
if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
|
|
10038
10090
|
const uuid = randomUUID();
|
|
10039
10091
|
this.steers.set(uuid, id);
|
|
10092
|
+
this.noteModelCommand(text, 2);
|
|
10040
10093
|
this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text }] } });
|
|
10041
10094
|
return true;
|
|
10042
10095
|
}
|
|
@@ -10088,12 +10141,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10088
10141
|
}
|
|
10089
10142
|
turnPayload(message) {
|
|
10090
10143
|
this.openTurnState();
|
|
10144
|
+
this.noteModelCommand(message, 1);
|
|
10091
10145
|
return `${JSON.stringify({
|
|
10092
10146
|
type: "user",
|
|
10093
10147
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
10094
10148
|
})}
|
|
10095
10149
|
`;
|
|
10096
10150
|
}
|
|
10151
|
+
/**
|
|
10152
|
+
* The CLI runs a message that starts with a slash command as that command,
|
|
10153
|
+
* whether the surface sent it as one or as plain text, so the message
|
|
10154
|
+
* itself says a model switch was asked for: every way into the chat counts.
|
|
10155
|
+
*/
|
|
10156
|
+
noteModelCommand(text, turnsLeft) {
|
|
10157
|
+
const [, name, args = ""] = /^\/(\S+)\s*([\s\S]*)$/.exec(text) ?? [];
|
|
10158
|
+
if (this.role !== "task" || !name || (findRunnerCommand(this.commands(), name)?.name ?? name) !== MODEL_COMMAND) return;
|
|
10159
|
+
this.modelCommands.push({ args: args.trim(), turnsLeft });
|
|
10160
|
+
}
|
|
10097
10161
|
openTurnState() {
|
|
10098
10162
|
this.turnHasText = false;
|
|
10099
10163
|
this.interruptRequested = false;
|
|
@@ -10205,6 +10269,7 @@ ${block.text}` : block.text });
|
|
|
10205
10269
|
this.clearFollowOnTimer();
|
|
10206
10270
|
if (this.refuseOtherMode(msg, emit)) return;
|
|
10207
10271
|
this.readLoaded(msg, emit);
|
|
10272
|
+
this.followModel(msg.model, emit);
|
|
10208
10273
|
}
|
|
10209
10274
|
if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
|
|
10210
10275
|
if (msg.subtype === "compact_boundary") {
|
|
@@ -10227,6 +10292,7 @@ ${block.text}` : block.text });
|
|
|
10227
10292
|
return;
|
|
10228
10293
|
case "result":
|
|
10229
10294
|
this.reportSessionCost(msg, emit);
|
|
10295
|
+
for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
|
|
10230
10296
|
if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
|
|
10231
10297
|
this.interruptRequested = false;
|
|
10232
10298
|
this.closeTurn({ type: "turn_end", interrupted: true }, emit);
|
|
@@ -10246,6 +10312,55 @@ ${block.text}` : block.text });
|
|
|
10246
10312
|
return;
|
|
10247
10313
|
}
|
|
10248
10314
|
}
|
|
10315
|
+
/**
|
|
10316
|
+
* A task's main thread reported the model it answers on. A change is the
|
|
10317
|
+
* user's own switch when a model command went out before it: Ordewell never
|
|
10318
|
+
* passes `--fallback-model`, so nothing else should move it. A change with
|
|
10319
|
+
* no command before it is reported as drift, and the plan does not follow
|
|
10320
|
+
* it. A command whose turn ended with the model unchanged switched nothing,
|
|
10321
|
+
* and is let go.
|
|
10322
|
+
*
|
|
10323
|
+
* Effort is never reported (recorded on 2.1.302, `task-effort-command`):
|
|
10324
|
+
* `init` names the model alone, its `per_turn_effort_active` says effort is
|
|
10325
|
+
* on, not which; `/effort low` only prints that it set it; and `/model`
|
|
10326
|
+
* takes no effort (`/model haiku high` is "not found"). So a switch carries
|
|
10327
|
+
* no effort and the plan's stays as planned.
|
|
10328
|
+
*/
|
|
10329
|
+
followModel(reported, emit) {
|
|
10330
|
+
if (this.role !== "task" || !reported || reported === "<synthetic>") return;
|
|
10331
|
+
const previous = this.mainModel;
|
|
10332
|
+
this.mainModel = reported;
|
|
10333
|
+
if (previous === void 0 || previous === reported) {
|
|
10334
|
+
this.modelCommands = this.modelCommands.filter((c) => c.turnsLeft > 0);
|
|
10335
|
+
return;
|
|
10336
|
+
}
|
|
10337
|
+
const matched = this.modelCommands.findIndex((c) => this.runsAs(c.args, reported));
|
|
10338
|
+
const command = this.modelCommands[Math.max(matched, 0)];
|
|
10339
|
+
this.modelCommands = this.modelCommands.slice(Math.max(matched, 0) + 1);
|
|
10340
|
+
const from = this.listedModel(previous, this.followedModel);
|
|
10341
|
+
if (!command) {
|
|
10342
|
+
emit({ type: "model_drifted", from, to: this.listedModel(reported, void 0) });
|
|
10343
|
+
return;
|
|
10344
|
+
}
|
|
10345
|
+
const to = this.listedModel(reported, command.args);
|
|
10346
|
+
this.followedModel = to.model;
|
|
10347
|
+
emit({ type: "model_switched", from, to });
|
|
10348
|
+
}
|
|
10349
|
+
/** Whether the model value `asked` runs as the id `resolved`, by what `initialize` lists. */
|
|
10350
|
+
runsAs(asked, resolved) {
|
|
10351
|
+
return asked === resolved || this.listedModels.some((m) => m.value === asked && m.resolvedModel === resolved);
|
|
10352
|
+
}
|
|
10353
|
+
/**
|
|
10354
|
+
* The value `initialize` lists for the id a model runs as. Several can share
|
|
10355
|
+
* one (`sonnet`, `default`); the one asked for wins, and `default` only as a
|
|
10356
|
+
* last resort, since it names whatever the CLI's default becomes.
|
|
10357
|
+
*/
|
|
10358
|
+
listedModel(resolved, asked) {
|
|
10359
|
+
const listed3 = this.listedModels.filter((m) => m.value && (m.resolvedModel === resolved || m.value === resolved));
|
|
10360
|
+
const pick = listed3.find((m) => m.value === asked && asked !== "default") ?? listed3.find((m) => m.value !== "default") ?? listed3[0];
|
|
10361
|
+
if (!pick?.value) return { model: resolved };
|
|
10362
|
+
return { model: pick.value, ...pick.displayName ? { label: pick.displayName } : {} };
|
|
10363
|
+
}
|
|
10249
10364
|
/**
|
|
10250
10365
|
* `--permission-mode auto` on a model or account without auto mode is not
|
|
10251
10366
|
* refused: the CLI says nothing and starts in `default`, then asks about every
|
|
@@ -10329,8 +10444,10 @@ ${block.text}` : block.text });
|
|
|
10329
10444
|
* {@link AgentEvent}), so nothing here has to reconcile with it.
|
|
10330
10445
|
*/
|
|
10331
10446
|
handleStreamEvent(event, emit) {
|
|
10332
|
-
if (event.type === "message_start")
|
|
10333
|
-
|
|
10447
|
+
if (event.type === "message_start") {
|
|
10448
|
+
this.plannerModel = event.message?.model;
|
|
10449
|
+
this.followModel(this.plannerModel, emit);
|
|
10450
|
+
} else if (event.type === "message_delta" && event.usage) emit({ type: "usage", record: usageRecord(event.usage, this.plannerModel) });
|
|
10334
10451
|
else if (event.type === "content_block_start" && event.content_block?.type === "text") {
|
|
10335
10452
|
this.pendingBreak = this.turnHasText;
|
|
10336
10453
|
} else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
|
@@ -10345,7 +10462,8 @@ ${block.text}` : block.text });
|
|
|
10345
10462
|
* What only the result line knows: the cost, which covers every call the
|
|
10346
10463
|
* session made — subagents included, since none of their lines carries one —
|
|
10347
10464
|
* and the planner model's window. `total_cost_usd` is a running total, so a
|
|
10348
|
-
* turn reports its growth.
|
|
10465
|
+
* turn reports its growth. After a resume it grows from what was counted of
|
|
10466
|
+
* the session before; with no count kept, the first turn only sets the
|
|
10349
10467
|
* baseline: its total includes turns counted before, and a turn's own share
|
|
10350
10468
|
* cannot be told apart from them.
|
|
10351
10469
|
*/
|
|
@@ -10925,6 +11043,97 @@ var CodexRpc = class {
|
|
|
10925
11043
|
}
|
|
10926
11044
|
};
|
|
10927
11045
|
|
|
11046
|
+
// src/utils/wallTime.ts
|
|
11047
|
+
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
11048
|
+
function lastMatch(re, text) {
|
|
11049
|
+
let last = null;
|
|
11050
|
+
for (const m of text.matchAll(re)) last = m;
|
|
11051
|
+
return last;
|
|
11052
|
+
}
|
|
11053
|
+
function hour24(hour, meridiem) {
|
|
11054
|
+
const h = Number(hour) % 12;
|
|
11055
|
+
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
11056
|
+
}
|
|
11057
|
+
function month(name) {
|
|
11058
|
+
if (!name) return void 0;
|
|
11059
|
+
const at = MONTHS.indexOf(name.toLowerCase());
|
|
11060
|
+
return at < 0 ? void 0 : at;
|
|
11061
|
+
}
|
|
11062
|
+
function placeWallTime(wall, zone, now) {
|
|
11063
|
+
const today = wallFields(now, zone);
|
|
11064
|
+
const year = wall.year ?? today.year;
|
|
11065
|
+
const monthIndex = wall.month ?? today.month;
|
|
11066
|
+
const day = wall.day ?? today.day;
|
|
11067
|
+
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
11068
|
+
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
11069
|
+
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
11070
|
+
}
|
|
11071
|
+
var HOUR_MS = 60 * 6e4;
|
|
11072
|
+
function wallFields(instant, zone) {
|
|
11073
|
+
if (!zone) {
|
|
11074
|
+
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
11075
|
+
}
|
|
11076
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
11077
|
+
timeZone: zone,
|
|
11078
|
+
hourCycle: "h23",
|
|
11079
|
+
year: "numeric",
|
|
11080
|
+
month: "numeric",
|
|
11081
|
+
day: "numeric",
|
|
11082
|
+
hour: "numeric",
|
|
11083
|
+
minute: "numeric",
|
|
11084
|
+
second: "numeric"
|
|
11085
|
+
}).formatToParts(instant);
|
|
11086
|
+
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
11087
|
+
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
11088
|
+
}
|
|
11089
|
+
function zoneOffsetMs(instant, zone) {
|
|
11090
|
+
const f = wallFields(new Date(instant), zone);
|
|
11091
|
+
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
11092
|
+
}
|
|
11093
|
+
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
11094
|
+
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
11095
|
+
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
11096
|
+
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
11097
|
+
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
11098
|
+
return new Date(instant);
|
|
11099
|
+
}
|
|
11100
|
+
|
|
11101
|
+
// src/services/harness/codexUsageLimit.ts
|
|
11102
|
+
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;
|
|
11103
|
+
function codexUsageLimitReset(text, now) {
|
|
11104
|
+
const m = lastMatch(CODEX_RESET_RE, text);
|
|
11105
|
+
if (!m) return null;
|
|
11106
|
+
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
11107
|
+
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
11108
|
+
if (mon) {
|
|
11109
|
+
const at = month(mon);
|
|
11110
|
+
if (at === void 0) return null;
|
|
11111
|
+
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
11112
|
+
}
|
|
11113
|
+
return placeWallTime(wall, void 0, now);
|
|
11114
|
+
}
|
|
11115
|
+
function mergeCodexRateLimits(known, params) {
|
|
11116
|
+
const snapshot = params?.rateLimits;
|
|
11117
|
+
if (!snapshot || typeof snapshot !== "object") return known;
|
|
11118
|
+
const id = snapshot.limitId ?? "";
|
|
11119
|
+
const before = known.get(id);
|
|
11120
|
+
const next = new Map(known);
|
|
11121
|
+
next.set(id, { limitId: id, primary: snapshot.primary ?? before?.primary, secondary: snapshot.secondary ?? before?.secondary });
|
|
11122
|
+
return next;
|
|
11123
|
+
}
|
|
11124
|
+
function isCodexUsageLimit(error) {
|
|
11125
|
+
const info = error?.codexErrorInfo;
|
|
11126
|
+
if (info === void 0 || info === null || info === "other") return void 0;
|
|
11127
|
+
if (info === "usageLimitExceeded") return true;
|
|
11128
|
+
return typeof info === "object" && "usageLimitExceeded" in info;
|
|
11129
|
+
}
|
|
11130
|
+
function codexUsageLimitSignal(error, limits, now) {
|
|
11131
|
+
const limited = isCodexUsageLimit(error);
|
|
11132
|
+
if (limited !== true) return limited;
|
|
11133
|
+
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());
|
|
11134
|
+
return resets.length > 0 ? { resetsAt: new Date(Math.max(...resets)).toISOString() } : {};
|
|
11135
|
+
}
|
|
11136
|
+
|
|
10928
11137
|
// src/services/harness/CodexAdapter.ts
|
|
10929
11138
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
10930
11139
|
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
@@ -11068,6 +11277,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11068
11277
|
turn = null;
|
|
11069
11278
|
/** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
|
|
11070
11279
|
endedTurnId = null;
|
|
11280
|
+
/**
|
|
11281
|
+
* The account's limit windows as Codex last reported them. Only read when a
|
|
11282
|
+
* turn fails on the usage limit; an update on its own acts on nothing.
|
|
11283
|
+
*/
|
|
11284
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
11071
11285
|
/** An interrupt was asked for during this turn, so however it ends, it was cut short. */
|
|
11072
11286
|
interruptRequested = false;
|
|
11073
11287
|
/**
|
|
@@ -11463,6 +11677,9 @@ ${this.exitMessage()}`);
|
|
|
11463
11677
|
case "thread/tokenUsage/updated":
|
|
11464
11678
|
this.emitUsage(msg.params, emit);
|
|
11465
11679
|
return;
|
|
11680
|
+
case "account/rateLimits/updated":
|
|
11681
|
+
this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
|
|
11682
|
+
return;
|
|
11466
11683
|
case "turn/started": {
|
|
11467
11684
|
if (this.subagentOf(msg.params?.threadId)) return;
|
|
11468
11685
|
this.turnHasText = false;
|
|
@@ -11503,7 +11720,7 @@ ${this.exitMessage()}`);
|
|
|
11503
11720
|
const failure = msg.params;
|
|
11504
11721
|
if (failure?.willRetry) return;
|
|
11505
11722
|
this.closeTurn(emit);
|
|
11506
|
-
emit(
|
|
11723
|
+
emit(this.turnFailed(failure?.error));
|
|
11507
11724
|
return;
|
|
11508
11725
|
}
|
|
11509
11726
|
case "turn/completed": {
|
|
@@ -11514,7 +11731,7 @@ ${this.exitMessage()}`);
|
|
|
11514
11731
|
if (this.interruptRequested || turn?.status === "interrupted") {
|
|
11515
11732
|
emit({ type: "turn_end", interrupted: true });
|
|
11516
11733
|
} else if (turn?.status === "failed") {
|
|
11517
|
-
emit(
|
|
11734
|
+
emit(this.turnFailed(turn.error));
|
|
11518
11735
|
} else {
|
|
11519
11736
|
emit({ type: "turn_end" });
|
|
11520
11737
|
}
|
|
@@ -11526,6 +11743,11 @@ ${this.exitMessage()}`);
|
|
|
11526
11743
|
return;
|
|
11527
11744
|
}
|
|
11528
11745
|
}
|
|
11746
|
+
turnFailed(error) {
|
|
11747
|
+
const message = error?.message || "Codex ended the turn with an error.";
|
|
11748
|
+
const usageLimit = codexUsageLimitSignal(error, this.rateLimits, /* @__PURE__ */ new Date());
|
|
11749
|
+
return usageLimit === void 0 ? { type: "error", message } : { type: "error", message, usageLimit };
|
|
11750
|
+
}
|
|
11529
11751
|
turnStarted(id) {
|
|
11530
11752
|
const turn = this.turn;
|
|
11531
11753
|
if (!turn || turn.id || typeof id !== "string") return;
|
|
@@ -11983,9 +12205,9 @@ function recordUnusable(servers, approval) {
|
|
|
11983
12205
|
for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
|
|
11984
12206
|
}
|
|
11985
12207
|
var NOT_APPROVED = "not approved for planning";
|
|
11986
|
-
function heldServersNote(
|
|
12208
|
+
function heldServersNote(held2, unreadable = []) {
|
|
11987
12209
|
const lines = [
|
|
11988
|
-
...
|
|
12210
|
+
...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
|
|
11989
12211
|
...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
|
|
11990
12212
|
];
|
|
11991
12213
|
if (lines.length === 0) return "";
|
|
@@ -12122,8 +12344,8 @@ var PendingSteers = class {
|
|
|
12122
12344
|
* False when it was already delivered, so a late failure does not recall it.
|
|
12123
12345
|
*/
|
|
12124
12346
|
forget(id) {
|
|
12125
|
-
for (const [messageId,
|
|
12126
|
-
if (
|
|
12347
|
+
for (const [messageId, held2] of this.pending) {
|
|
12348
|
+
if (held2 !== id) continue;
|
|
12127
12349
|
this.pending.delete(messageId);
|
|
12128
12350
|
return true;
|
|
12129
12351
|
}
|
|
@@ -12201,9 +12423,9 @@ var ChildSessions = class {
|
|
|
12201
12423
|
adopt(child, callId) {
|
|
12202
12424
|
if (this.owners.get(child)) return null;
|
|
12203
12425
|
this.owners.set(child, callId);
|
|
12204
|
-
const
|
|
12426
|
+
const held2 = this.held.get(child) ?? [];
|
|
12205
12427
|
this.held.delete(child);
|
|
12206
|
-
return
|
|
12428
|
+
return held2;
|
|
12207
12429
|
}
|
|
12208
12430
|
owns(callId) {
|
|
12209
12431
|
return [...this.owners.values()].includes(callId);
|
|
@@ -12727,11 +12949,11 @@ var OpenCodeV2 = class {
|
|
|
12727
12949
|
adoptChild(data, state, onEvent, turn) {
|
|
12728
12950
|
const child = data.metadata?.sessionID;
|
|
12729
12951
|
const callId = data.id;
|
|
12730
|
-
const
|
|
12731
|
-
if (!
|
|
12952
|
+
const held2 = child && callId ? state.children.adopt(child, callId) : null;
|
|
12953
|
+
if (!held2 || !callId) return;
|
|
12732
12954
|
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
12733
12955
|
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
12734
|
-
for (const frame of
|
|
12956
|
+
for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
|
|
12735
12957
|
}
|
|
12736
12958
|
finishChild(callId, success, output, state, onEvent) {
|
|
12737
12959
|
if (!state.children.owns(callId)) return;
|
|
@@ -13443,13 +13665,13 @@ var OpenCodeAdapter = class {
|
|
|
13443
13665
|
trackSubagent(part, callId, turn, onEvent) {
|
|
13444
13666
|
const state = part.state;
|
|
13445
13667
|
const child = state?.metadata?.sessionId;
|
|
13446
|
-
const
|
|
13447
|
-
if (
|
|
13668
|
+
const held2 = child ? turn.children.adopt(child, callId) : null;
|
|
13669
|
+
if (held2) {
|
|
13448
13670
|
const input = state?.input ?? {};
|
|
13449
13671
|
const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
13450
13672
|
const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
|
|
13451
13673
|
onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
|
|
13452
|
-
for (const frame of
|
|
13674
|
+
for (const frame of held2) this.onFrame(frame, turn, onEvent);
|
|
13453
13675
|
}
|
|
13454
13676
|
const status = state?.status;
|
|
13455
13677
|
if (status === "completed" || status === "error") {
|
|
@@ -13679,61 +13901,6 @@ var OPENCODE_REPO_CONFIG = {
|
|
|
13679
13901
|
}
|
|
13680
13902
|
};
|
|
13681
13903
|
|
|
13682
|
-
// src/utils/wallTime.ts
|
|
13683
|
-
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
13684
|
-
function lastMatch(re, text) {
|
|
13685
|
-
let last = null;
|
|
13686
|
-
for (const m of text.matchAll(re)) last = m;
|
|
13687
|
-
return last;
|
|
13688
|
-
}
|
|
13689
|
-
function hour24(hour, meridiem) {
|
|
13690
|
-
const h = Number(hour) % 12;
|
|
13691
|
-
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
13692
|
-
}
|
|
13693
|
-
function month(name) {
|
|
13694
|
-
if (!name) return void 0;
|
|
13695
|
-
const at = MONTHS.indexOf(name.toLowerCase());
|
|
13696
|
-
return at < 0 ? void 0 : at;
|
|
13697
|
-
}
|
|
13698
|
-
function placeWallTime(wall, zone, now) {
|
|
13699
|
-
const today = wallFields(now, zone);
|
|
13700
|
-
const year = wall.year ?? today.year;
|
|
13701
|
-
const monthIndex = wall.month ?? today.month;
|
|
13702
|
-
const day = wall.day ?? today.day;
|
|
13703
|
-
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
13704
|
-
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
13705
|
-
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
13706
|
-
}
|
|
13707
|
-
var HOUR_MS = 60 * 6e4;
|
|
13708
|
-
function wallFields(instant, zone) {
|
|
13709
|
-
if (!zone) {
|
|
13710
|
-
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
13711
|
-
}
|
|
13712
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
13713
|
-
timeZone: zone,
|
|
13714
|
-
hourCycle: "h23",
|
|
13715
|
-
year: "numeric",
|
|
13716
|
-
month: "numeric",
|
|
13717
|
-
day: "numeric",
|
|
13718
|
-
hour: "numeric",
|
|
13719
|
-
minute: "numeric",
|
|
13720
|
-
second: "numeric"
|
|
13721
|
-
}).formatToParts(instant);
|
|
13722
|
-
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
13723
|
-
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
13724
|
-
}
|
|
13725
|
-
function zoneOffsetMs(instant, zone) {
|
|
13726
|
-
const f = wallFields(new Date(instant), zone);
|
|
13727
|
-
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
13728
|
-
}
|
|
13729
|
-
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
13730
|
-
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
13731
|
-
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
13732
|
-
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
13733
|
-
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
13734
|
-
return new Date(instant);
|
|
13735
|
-
}
|
|
13736
|
-
|
|
13737
13904
|
// src/services/harness/claudeUsageLimit.ts
|
|
13738
13905
|
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;
|
|
13739
13906
|
function claudeUsageLimitReset(text, now) {
|
|
@@ -13749,21 +13916,6 @@ function claudeUsageLimitReset(text, now) {
|
|
|
13749
13916
|
return placeWallTime(wall, zone, now);
|
|
13750
13917
|
}
|
|
13751
13918
|
|
|
13752
|
-
// src/services/harness/codexUsageLimit.ts
|
|
13753
|
-
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;
|
|
13754
|
-
function codexUsageLimitReset(text, now) {
|
|
13755
|
-
const m = lastMatch(CODEX_RESET_RE, text);
|
|
13756
|
-
if (!m) return null;
|
|
13757
|
-
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
13758
|
-
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
13759
|
-
if (mon) {
|
|
13760
|
-
const at = month(mon);
|
|
13761
|
-
if (at === void 0) return null;
|
|
13762
|
-
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
13763
|
-
}
|
|
13764
|
-
return placeWallTime(wall, void 0, now);
|
|
13765
|
-
}
|
|
13766
|
-
|
|
13767
13919
|
// src/services/harness/connectors.ts
|
|
13768
13920
|
var CONNECTORS = {
|
|
13769
13921
|
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
|
|
@@ -14162,7 +14314,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
|
|
|
14162
14314
|
modelId,
|
|
14163
14315
|
modelLabel: m.displayName || modelId,
|
|
14164
14316
|
runnerProvider: "gemini",
|
|
14165
|
-
variants: []
|
|
14317
|
+
variants: [],
|
|
14318
|
+
...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
|
|
14166
14319
|
});
|
|
14167
14320
|
}
|
|
14168
14321
|
}
|
|
@@ -14570,6 +14723,7 @@ function parseMaxParallel(value) {
|
|
|
14570
14723
|
// src/services/SettingsService.ts
|
|
14571
14724
|
import * as fs10 from "fs";
|
|
14572
14725
|
import * as path25 from "path";
|
|
14726
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
14573
14727
|
|
|
14574
14728
|
// src/services/LoopGuards.ts
|
|
14575
14729
|
var DEFAULT_SUPERVISOR_RETRY_LIMIT = 2;
|
|
@@ -14836,23 +14990,34 @@ function createWakePolicy(options) {
|
|
|
14836
14990
|
// src/services/runnerExit.ts
|
|
14837
14991
|
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;
|
|
14838
14992
|
var USAGE_LIMIT_TAIL = 4096;
|
|
14839
|
-
function classifyRunnerStop(
|
|
14840
|
-
|
|
14993
|
+
function classifyRunnerStop(said2) {
|
|
14994
|
+
if (typeof said2 !== "string" && said2.usageLimit !== void 0) return said2.usageLimit ? "usage-limit" : "stopped";
|
|
14995
|
+
return USAGE_LIMIT_RE.test(wordsOf(said2).slice(-USAGE_LIMIT_TAIL)) ? "usage-limit" : "stopped";
|
|
14996
|
+
}
|
|
14997
|
+
function wordsOf(said2) {
|
|
14998
|
+
return typeof said2 === "string" ? said2 : said2.message;
|
|
14841
14999
|
}
|
|
14842
15000
|
function stopsRunner(reason) {
|
|
14843
15001
|
return reason !== "stop" && reason !== "load";
|
|
14844
15002
|
}
|
|
14845
|
-
function readUsageLimitReset(runner,
|
|
15003
|
+
function readUsageLimitReset(runner, said2, now = /* @__PURE__ */ new Date()) {
|
|
15004
|
+
const reset = (typeof said2 === "string" ? null : reportedReset(said2)) ?? readReset(runner, wordsOf(said2), now);
|
|
15005
|
+
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
15006
|
+
return reset;
|
|
15007
|
+
}
|
|
15008
|
+
function reportedReset(failure) {
|
|
15009
|
+
const at = failure.usageLimit ? failure.usageLimit.resetsAt : void 0;
|
|
15010
|
+
const reset = at === void 0 ? null : new Date(at);
|
|
15011
|
+
return reset && !Number.isNaN(reset.getTime()) ? reset : null;
|
|
15012
|
+
}
|
|
15013
|
+
function readReset(runner, output, now) {
|
|
14846
15014
|
const read2 = connectorFor(runner)?.usageLimitReset;
|
|
14847
15015
|
if (!read2) return null;
|
|
14848
|
-
let reset;
|
|
14849
15016
|
try {
|
|
14850
|
-
|
|
15017
|
+
return read2(output.slice(-USAGE_LIMIT_TAIL), now);
|
|
14851
15018
|
} catch {
|
|
14852
15019
|
return null;
|
|
14853
15020
|
}
|
|
14854
|
-
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
14855
|
-
return reset;
|
|
14856
15021
|
}
|
|
14857
15022
|
var DAY_MS = 24 * 60 * 6e4;
|
|
14858
15023
|
|
|
@@ -14956,15 +15121,15 @@ var ReplySplitter = class {
|
|
|
14956
15121
|
push(segmentId, text) {
|
|
14957
15122
|
const state = this.segments.get(segmentId);
|
|
14958
15123
|
if (state === "text" || state === "plan") return { route: state, text };
|
|
14959
|
-
const
|
|
14960
|
-
const opensWithObject = opensWithJsonObject(
|
|
15124
|
+
const held2 = (state?.held ?? "") + text;
|
|
15125
|
+
const opensWithObject = opensWithJsonObject(held2);
|
|
14961
15126
|
if (opensWithObject === void 0) {
|
|
14962
|
-
this.segments.set(segmentId, { held });
|
|
15127
|
+
this.segments.set(segmentId, { held: held2 });
|
|
14963
15128
|
return null;
|
|
14964
15129
|
}
|
|
14965
15130
|
const route = opensWithObject ? "plan" : "text";
|
|
14966
15131
|
this.segments.set(segmentId, route);
|
|
14967
|
-
return { route, text:
|
|
15132
|
+
return { route, text: held2 };
|
|
14968
15133
|
}
|
|
14969
15134
|
};
|
|
14970
15135
|
var TurnStream = class {
|
|
@@ -15119,6 +15284,7 @@ function plannerTranscript(history) {
|
|
|
15119
15284
|
const loadsOf = /* @__PURE__ */ new Map();
|
|
15120
15285
|
let owner = -1;
|
|
15121
15286
|
for (const entry of history) {
|
|
15287
|
+
if (entry.kind === "error") continue;
|
|
15122
15288
|
if (entry.kind === "skill_load" && entry.skill) {
|
|
15123
15289
|
const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
|
|
15124
15290
|
if (loads) loads.push(entry.skill);
|
|
@@ -16456,6 +16622,45 @@ function markOf(was, now) {
|
|
|
16456
16622
|
return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
|
|
16457
16623
|
}
|
|
16458
16624
|
|
|
16625
|
+
// src/services/RunnerChange.ts
|
|
16626
|
+
var sameSet = (a, b) => a === void 0 || b === void 0 ? a === b : a.length === b.length && a.every((id) => b.includes(id));
|
|
16627
|
+
function scopeChange(told, now) {
|
|
16628
|
+
const switchedOn = now.enabledRunners.filter((r) => !told.enabledRunners.includes(r));
|
|
16629
|
+
const switchedOff = told.enabledRunners.filter((r) => !now.enabledRunners.includes(r));
|
|
16630
|
+
const models = now.models;
|
|
16631
|
+
const allowlists = [.../* @__PURE__ */ new Set([...Object.keys(told.allowlist), ...Object.keys(now.allowlist)])].map((runner) => ({
|
|
16632
|
+
runner,
|
|
16633
|
+
before: effectiveAllowlist(told.allowlist[runner], runner, models),
|
|
16634
|
+
after: effectiveAllowlist(now.allowlist[runner], runner, models)
|
|
16635
|
+
})).filter((c) => !sameSet(c.before, c.after));
|
|
16636
|
+
return switchedOn.length || switchedOff.length || allowlists.length ? { switchedOn, switchedOff, allowlists } : null;
|
|
16637
|
+
}
|
|
16638
|
+
function flaggedTasks(rows, scope) {
|
|
16639
|
+
return rows.flatMap(({ ref, task }) => {
|
|
16640
|
+
const issue = assignmentIssue(task, scope);
|
|
16641
|
+
return issue ? [{ ref, title: task.title, task, text: assignmentIssueText(issue), status: task.status, awaitingReason: task.awaitingReason, resumeAt: task.resumeAt }] : [];
|
|
16642
|
+
});
|
|
16643
|
+
}
|
|
16644
|
+
function allowlistLine({ runner, before, after }) {
|
|
16645
|
+
if (!after) return `${runner} is no longer limited: any of its models may be assigned.`;
|
|
16646
|
+
if (!before) return `${runner} is now limited to: ${after.join(", ")}.`;
|
|
16647
|
+
const added = after.filter((id) => !before.includes(id));
|
|
16648
|
+
const removed = before.filter((id) => !after.includes(id));
|
|
16649
|
+
return `${runner} models: ${[added.length ? `now allowed ${added.join(", ")}` : "", removed.length ? `no longer allowed ${removed.join(", ")}` : ""].filter(Boolean).join("; ")}.`;
|
|
16650
|
+
}
|
|
16651
|
+
function runnersChangedBlock(change, flagged, tools) {
|
|
16652
|
+
return [
|
|
16653
|
+
"<runners_changed>",
|
|
16654
|
+
"The user changed which runners and models you may assign since your last turn.",
|
|
16655
|
+
change.switchedOn.length ? `Runners switched on: ${change.switchedOn.join(", ")}.` : "",
|
|
16656
|
+
change.switchedOff.length ? `Runners switched off: ${change.switchedOff.join(", ")}.` : "",
|
|
16657
|
+
...change.allowlists.map(allowlistLine),
|
|
16658
|
+
flagged.length ? ["These tasks are now outside that:", ...flagged.map((f) => `- ${f.ref} "${f.title}" (${f.text}) \u2014 ${assignmentConsequence(f)}`)].join("\n") : "",
|
|
16659
|
+
`${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." : ""}`,
|
|
16660
|
+
"</runners_changed>"
|
|
16661
|
+
].filter(Boolean).join("\n");
|
|
16662
|
+
}
|
|
16663
|
+
|
|
16459
16664
|
// src/services/PlannerConversation.ts
|
|
16460
16665
|
var CATALOG_MODEL_CAP = 100;
|
|
16461
16666
|
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.";
|
|
@@ -16520,6 +16725,17 @@ var PlannerTurnFailedError = class extends Error {
|
|
|
16520
16725
|
this.name = "PlannerTurnFailedError";
|
|
16521
16726
|
}
|
|
16522
16727
|
};
|
|
16728
|
+
var PlannerUsageLimitError = class extends PlannerTurnFailedError {
|
|
16729
|
+
resendAt;
|
|
16730
|
+
/** Why nothing is sent again on its own, when nothing is. */
|
|
16731
|
+
held;
|
|
16732
|
+
constructor(limit) {
|
|
16733
|
+
super(limit.text);
|
|
16734
|
+
this.name = "PlannerUsageLimitError";
|
|
16735
|
+
this.resendAt = limit.resendAt;
|
|
16736
|
+
if (limit.held) this.held = limit.held;
|
|
16737
|
+
}
|
|
16738
|
+
};
|
|
16523
16739
|
var PlannerTurnStoppedError = class extends Error {
|
|
16524
16740
|
constructor(options) {
|
|
16525
16741
|
super("The planner turn was stopped.", options);
|
|
@@ -16545,6 +16761,8 @@ var PlannerConversation = class {
|
|
|
16545
16761
|
savedInBackground = 0;
|
|
16546
16762
|
/** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
|
|
16547
16763
|
toldAutorun = false;
|
|
16764
|
+
/** The runners and allowlist the planner was last told of: its opening prompt, or a turn's block since. Null until a conversation opens. */
|
|
16765
|
+
toldScope = null;
|
|
16548
16766
|
live = null;
|
|
16549
16767
|
openTurn = null;
|
|
16550
16768
|
get transcript() {
|
|
@@ -16660,8 +16878,8 @@ var PlannerConversation = class {
|
|
|
16660
16878
|
* from the moment it is handed in until it lands, is refused, or the turn
|
|
16661
16879
|
* ends: the backend's reply claims it, but it has not landed by then.
|
|
16662
16880
|
*/
|
|
16663
|
-
showHeld(turn,
|
|
16664
|
-
const result =
|
|
16881
|
+
showHeld(turn, held2) {
|
|
16882
|
+
const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
|
|
16665
16883
|
if (!result?.ok) {
|
|
16666
16884
|
this.clearHeld(turn);
|
|
16667
16885
|
return;
|
|
@@ -16680,9 +16898,9 @@ var PlannerConversation = class {
|
|
|
16680
16898
|
* a whole plan, which an edit cannot be layered on.
|
|
16681
16899
|
*/
|
|
16682
16900
|
pendingOps() {
|
|
16683
|
-
const
|
|
16684
|
-
if (!
|
|
16685
|
-
return
|
|
16901
|
+
const held2 = this.openTurn?.submitted;
|
|
16902
|
+
if (!held2) return [];
|
|
16903
|
+
return held2.kind === "task_ops" ? held2.ops : null;
|
|
16686
16904
|
}
|
|
16687
16905
|
/**
|
|
16688
16906
|
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
@@ -16722,7 +16940,8 @@ var PlannerConversation = class {
|
|
|
16722
16940
|
history: plan.conversationHistory,
|
|
16723
16941
|
researchLog: plan.researchLog,
|
|
16724
16942
|
persisted: this.persisted,
|
|
16725
|
-
savedInBackground: this.savedInBackground
|
|
16943
|
+
savedInBackground: this.savedInBackground,
|
|
16944
|
+
toldScope: this.toldScope
|
|
16726
16945
|
};
|
|
16727
16946
|
}
|
|
16728
16947
|
/**
|
|
@@ -16736,7 +16955,9 @@ var PlannerConversation = class {
|
|
|
16736
16955
|
* the undo is saved in turn, or a reload would bring the writes back.
|
|
16737
16956
|
*/
|
|
16738
16957
|
restore(snapshot) {
|
|
16739
|
-
if (this.host.plan() !== snapshot.plan
|
|
16958
|
+
if (this.host.plan() !== snapshot.plan) return false;
|
|
16959
|
+
this.toldScope = snapshot.toldScope;
|
|
16960
|
+
if (this.persisted !== snapshot.persisted) return false;
|
|
16740
16961
|
const undo = () => {
|
|
16741
16962
|
const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
|
|
16742
16963
|
snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
|
|
@@ -16771,8 +16992,9 @@ var PlannerConversation = class {
|
|
|
16771
16992
|
* at it.
|
|
16772
16993
|
*/
|
|
16773
16994
|
rewindTargets() {
|
|
16995
|
+
const opener = this.transcript.findIndex((m) => m.kind !== "error");
|
|
16774
16996
|
return this.transcript.flatMap((m, index) => {
|
|
16775
|
-
if (!isUserMessage(m) || index ===
|
|
16997
|
+
if (!isUserMessage(m) || index === opener) return [];
|
|
16776
16998
|
const line = m.content.split("\n")[0];
|
|
16777
16999
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
16778
17000
|
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
@@ -16899,8 +17121,10 @@ var PlannerConversation = class {
|
|
|
16899
17121
|
return this.userTurn("supersede", goal, signal, async (userTurn) => {
|
|
16900
17122
|
const opening = await prepare();
|
|
16901
17123
|
this.assertCurrent(userTurn);
|
|
17124
|
+
userTurn.checkpoint = this.snapshot();
|
|
16902
17125
|
this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
|
|
16903
17126
|
this.toldAutorun = opening.autorun ?? false;
|
|
17127
|
+
this.toldScope = this.host.assignmentScope();
|
|
16904
17128
|
const turn = await this.host.aiService().startConversation({
|
|
16905
17129
|
...opening,
|
|
16906
17130
|
// The system prompt's goal is the text as typed; skill bodies ride the
|
|
@@ -16910,7 +17134,9 @@ var PlannerConversation = class {
|
|
|
16910
17134
|
onProgress: userTurn.stream.sink(),
|
|
16911
17135
|
signal: userTurn.signal
|
|
16912
17136
|
});
|
|
16913
|
-
|
|
17137
|
+
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
17138
|
+
this.assertNotLimited(settleable, userTurn);
|
|
17139
|
+
return this.settle(settleable, userTurn);
|
|
16914
17140
|
});
|
|
16915
17141
|
}
|
|
16916
17142
|
/**
|
|
@@ -16943,13 +17169,16 @@ var PlannerConversation = class {
|
|
|
16943
17169
|
// anything the turn appends as it runs goes after this point.
|
|
16944
17170
|
at: priorHistory.length
|
|
16945
17171
|
};
|
|
16946
|
-
const
|
|
17172
|
+
const toldBefore = this.toldScope;
|
|
17173
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
|
|
16947
17174
|
const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
|
|
16948
17175
|
const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
|
|
16949
17176
|
const ai = this.host.aiService();
|
|
16950
17177
|
try {
|
|
16951
17178
|
const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
|
|
16952
17179
|
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
17180
|
+
if (settleable.kind === "message" && (settleable.failed || userTurn.signal.aborted)) this.toldScope = toldBefore;
|
|
17181
|
+
this.assertNotLimited(settleable, userTurn);
|
|
16953
17182
|
if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
|
|
16954
17183
|
this.assertCurrent(userTurn, settleable);
|
|
16955
17184
|
return await this.settle(settleable, userTurn);
|
|
@@ -16968,6 +17197,34 @@ var PlannerConversation = class {
|
|
|
16968
17197
|
if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
|
|
16969
17198
|
if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
|
|
16970
17199
|
}
|
|
17200
|
+
/**
|
|
17201
|
+
* A turn that ended on the planner's usage limit lands nothing. What it
|
|
17202
|
+
* wrote is taken back, its failure is kept in its place, and the limit's
|
|
17203
|
+
* wait is armed in the same save. Only a turn Ordewell started gets here,
|
|
17204
|
+
* so a limit is never acted on for a planner nobody asked anything of.
|
|
17205
|
+
*/
|
|
17206
|
+
assertNotLimited(turn, userTurn) {
|
|
17207
|
+
if (turn.kind !== "message" || !turn.failed || userTurn.signal.aborted) return;
|
|
17208
|
+
const said2 = { message: turn.text, usageLimit: turn.usageLimit };
|
|
17209
|
+
if (classifyRunnerStop(said2) !== "usage-limit") return;
|
|
17210
|
+
this.assertCurrent(userTurn);
|
|
17211
|
+
if (userTurn.checkpoint) this.restore(userTurn.checkpoint);
|
|
17212
|
+
const limit = this.host.usageLimit(said2);
|
|
17213
|
+
this.host.mutate(() => {
|
|
17214
|
+
this.recordFailure(limit.text);
|
|
17215
|
+
return true;
|
|
17216
|
+
}, () => this.host.broadcastPlan());
|
|
17217
|
+
throw new PlannerUsageLimitError(limit);
|
|
17218
|
+
}
|
|
17219
|
+
/**
|
|
17220
|
+
* A failure kept in the transcript, drawn as one on reload too, and never
|
|
17221
|
+
* replayed to the planner. Stamped after the newest entry: surfaces take
|
|
17222
|
+
* only entries newer than the last they synced.
|
|
17223
|
+
*/
|
|
17224
|
+
recordFailure(text) {
|
|
17225
|
+
const newest = this.transcript.reduce((latest, e) => Math.max(latest, Date.parse(e.timestamp) || 0), 0);
|
|
17226
|
+
this.append("assistant", text, { kind: "error", timestamp: new Date(Math.max(Date.now(), newest + 1)).toISOString() });
|
|
17227
|
+
}
|
|
16971
17228
|
assertIdle(operation) {
|
|
16972
17229
|
if (this.isTurnInFlight) throw new ConversationBusyError(operation);
|
|
16973
17230
|
}
|
|
@@ -17109,6 +17366,7 @@ var PlannerConversation = class {
|
|
|
17109
17366
|
const goal = opener || this.host.goal() || message;
|
|
17110
17367
|
this.reset();
|
|
17111
17368
|
this.toldAutorun = opening.autorun ?? false;
|
|
17369
|
+
this.toldScope = this.host.assignmentScope();
|
|
17112
17370
|
return this.host.aiService().startConversation({
|
|
17113
17371
|
...opening,
|
|
17114
17372
|
runners,
|
|
@@ -17347,6 +17605,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
17347
17605
|
this.toldAutorun = now;
|
|
17348
17606
|
return now ? AUTORUN_ON : AUTORUN_OFF;
|
|
17349
17607
|
}
|
|
17608
|
+
/**
|
|
17609
|
+
* The user's runner or allowlist change, when the planner was not told yet —
|
|
17610
|
+
* the same once-per-change rule as autorun, for API and harness planners
|
|
17611
|
+
* alike. Tasks the change leaves outside the scope are named, not touched.
|
|
17612
|
+
*
|
|
17613
|
+
* A reloaded session is not told: {@link resume} takes the scope as it is
|
|
17614
|
+
* then, as it does the autorun rule, since the catalog the first turn carries
|
|
17615
|
+
* and `list_runners` already show the runners as they are now.
|
|
17616
|
+
*/
|
|
17617
|
+
runnersChangedBlock() {
|
|
17618
|
+
const now = this.host.assignmentScope();
|
|
17619
|
+
const told = this.toldScope;
|
|
17620
|
+
this.toldScope = now;
|
|
17621
|
+
const change = told && scopeChange(told, now);
|
|
17622
|
+
if (!change) return null;
|
|
17623
|
+
const flagged = flaggedTasks(flattenTasksWithParents(this.host.tasks()), now);
|
|
17624
|
+
return runnersChangedBlock(change, flagged, this.host.aiService().plannerToolsAttached?.() ?? false);
|
|
17625
|
+
}
|
|
17350
17626
|
runUpdatesBlock() {
|
|
17351
17627
|
const updates = this.host.takeRunUpdates();
|
|
17352
17628
|
return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
|
|
@@ -17427,7 +17703,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
|
|
|
17427
17703
|
var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
|
|
17428
17704
|
var HUNG_TURN_GRACE_MS = 3e4;
|
|
17429
17705
|
var DEFAULT_WAKE_UP_READ_BUDGET = 12;
|
|
17430
|
-
var RESET_MARGIN_MS = 6e4;
|
|
17431
17706
|
var DETAIL_CHARS = 240;
|
|
17432
17707
|
var WakeUpTimeoutError = class extends Error {
|
|
17433
17708
|
constructor(timeoutMs) {
|
|
@@ -17453,8 +17728,6 @@ var Supervisor = class {
|
|
|
17453
17728
|
/** The run's end was reported: the scheduler can announce it more than once. */
|
|
17454
17729
|
finished = false;
|
|
17455
17730
|
runEndPending = false;
|
|
17456
|
-
/** While the planner's usage limit holds wake-ups back: when it lets them go. */
|
|
17457
|
-
limited = null;
|
|
17458
17731
|
/** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
|
|
17459
17732
|
wakeUps = [];
|
|
17460
17733
|
debounceMs;
|
|
@@ -17475,9 +17748,9 @@ var Supervisor = class {
|
|
|
17475
17748
|
get awaitsRunEnd() {
|
|
17476
17749
|
return this.runEndPending;
|
|
17477
17750
|
}
|
|
17478
|
-
/** Whether wake-ups alone may open a turn now: the supervisor watches
|
|
17751
|
+
/** 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. */
|
|
17479
17752
|
mayWake() {
|
|
17480
|
-
return this.watching
|
|
17753
|
+
return this.watching;
|
|
17481
17754
|
}
|
|
17482
17755
|
/**
|
|
17483
17756
|
* What a wake-up turn is held to under the run's level. The brief is the
|
|
@@ -17544,8 +17817,6 @@ var Supervisor = class {
|
|
|
17544
17817
|
/** The plan these events were about is gone: nothing gathered or waited on carries over. */
|
|
17545
17818
|
reset() {
|
|
17546
17819
|
this.disarm();
|
|
17547
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
17548
|
-
this.limited = null;
|
|
17549
17820
|
this.failures = 0;
|
|
17550
17821
|
this.finished = false;
|
|
17551
17822
|
this.runEndPending = false;
|
|
@@ -17596,9 +17867,13 @@ var Supervisor = class {
|
|
|
17596
17867
|
}
|
|
17597
17868
|
failed(err) {
|
|
17598
17869
|
if (err instanceof PlannerTurnStoppedError || err instanceof PlannerTurnDiscardedError) return;
|
|
17870
|
+
if (err instanceof PlannerUsageLimitError) {
|
|
17871
|
+
if (err.resendAt === null) this.giveUp(`the planner hit its usage limit and ${err.held ?? "did not say when it resets"}`);
|
|
17872
|
+
return;
|
|
17873
|
+
}
|
|
17599
17874
|
const reason = err instanceof Error ? err.message : String(err);
|
|
17600
|
-
if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
17601
|
-
this.
|
|
17875
|
+
if (!(err instanceof WakeUpTimeoutError) && !(err instanceof PlannerTurnFailedError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
17876
|
+
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
17602
17877
|
return;
|
|
17603
17878
|
}
|
|
17604
17879
|
this.failures++;
|
|
@@ -17609,22 +17884,6 @@ var Supervisor = class {
|
|
|
17609
17884
|
}
|
|
17610
17885
|
this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
|
|
17611
17886
|
}
|
|
17612
|
-
waitForReset(said2) {
|
|
17613
|
-
const runner = this.host.plannerRunner();
|
|
17614
|
-
const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
|
|
17615
|
-
if (!reset) {
|
|
17616
|
-
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
17617
|
-
return;
|
|
17618
|
-
}
|
|
17619
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
17620
|
-
const until = reset.getTime() + RESET_MARGIN_MS;
|
|
17621
|
-
const timer = setTimeout(() => {
|
|
17622
|
-
this.limited = null;
|
|
17623
|
-
this.host.pump();
|
|
17624
|
-
}, Math.max(0, until - this.now()));
|
|
17625
|
-
this.limited = { until, timer };
|
|
17626
|
-
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.`);
|
|
17627
|
-
}
|
|
17628
17887
|
/** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
|
|
17629
17888
|
giveUp(reason) {
|
|
17630
17889
|
this.host.goUnsupervised(reason);
|
|
@@ -17638,8 +17897,8 @@ var Supervisor = class {
|
|
|
17638
17897
|
standDown() {
|
|
17639
17898
|
this.disarm();
|
|
17640
17899
|
this.runEndPending = false;
|
|
17641
|
-
const
|
|
17642
|
-
if (
|
|
17900
|
+
const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
|
|
17901
|
+
if (held2) this.deliver(held2);
|
|
17643
17902
|
}
|
|
17644
17903
|
arm() {
|
|
17645
17904
|
const due = this.policy.dueAt();
|
|
@@ -17830,78 +18089,224 @@ function runSettingsOf(settings) {
|
|
|
17830
18089
|
}
|
|
17831
18090
|
return out;
|
|
17832
18091
|
}
|
|
18092
|
+
var SAVE_TRIES = 5;
|
|
18093
|
+
var LOCK_STALE_MS = 2e3;
|
|
18094
|
+
var LOCK_TIMEOUT_MS = 1e4;
|
|
18095
|
+
var LOCK_POLL_MS = 5;
|
|
18096
|
+
var LOCK_POLL_MAX_MS = 100;
|
|
18097
|
+
function sleepSync(ms) {
|
|
18098
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
18099
|
+
}
|
|
18100
|
+
function withFileLock(file, swap) {
|
|
18101
|
+
const lock = `${file}.lock`;
|
|
18102
|
+
const token = `${process.pid}:${randomUUID3()}`;
|
|
18103
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
18104
|
+
let last = null;
|
|
18105
|
+
let poll = LOCK_POLL_MS;
|
|
18106
|
+
for (; ; ) {
|
|
18107
|
+
try {
|
|
18108
|
+
const fd = fs10.openSync(lock, "wx");
|
|
18109
|
+
try {
|
|
18110
|
+
fs10.writeSync(fd, token);
|
|
18111
|
+
} finally {
|
|
18112
|
+
fs10.closeSync(fd);
|
|
18113
|
+
}
|
|
18114
|
+
break;
|
|
18115
|
+
} catch (err) {
|
|
18116
|
+
if (err.code !== "EEXIST") throw err;
|
|
18117
|
+
}
|
|
18118
|
+
if (Date.now() >= deadline) {
|
|
18119
|
+
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.`);
|
|
18120
|
+
}
|
|
18121
|
+
let holder;
|
|
18122
|
+
let mtimeMs;
|
|
18123
|
+
try {
|
|
18124
|
+
holder = fs10.readFileSync(lock, "utf8");
|
|
18125
|
+
mtimeMs = fs10.statSync(lock).mtimeMs;
|
|
18126
|
+
} catch (err) {
|
|
18127
|
+
if (gone(err)) {
|
|
18128
|
+
last = null;
|
|
18129
|
+
continue;
|
|
18130
|
+
}
|
|
18131
|
+
throw err;
|
|
18132
|
+
}
|
|
18133
|
+
const stale = Date.now() - mtimeMs > LOCK_STALE_MS;
|
|
18134
|
+
if (last && last.token === holder && last.mtimeMs === mtimeMs && stale) {
|
|
18135
|
+
unlinkLock(lock);
|
|
18136
|
+
last = null;
|
|
18137
|
+
continue;
|
|
18138
|
+
}
|
|
18139
|
+
last = stale ? { token: holder, mtimeMs } : null;
|
|
18140
|
+
sleepSync(poll);
|
|
18141
|
+
poll = Math.min(poll * 2, LOCK_POLL_MAX_MS);
|
|
18142
|
+
}
|
|
18143
|
+
try {
|
|
18144
|
+
return swap();
|
|
18145
|
+
} finally {
|
|
18146
|
+
releaseLock(lock, token);
|
|
18147
|
+
}
|
|
18148
|
+
}
|
|
18149
|
+
function gone(err) {
|
|
18150
|
+
return err.code === "ENOENT";
|
|
18151
|
+
}
|
|
18152
|
+
function releaseLock(lock, token) {
|
|
18153
|
+
let holder;
|
|
18154
|
+
try {
|
|
18155
|
+
holder = fs10.readFileSync(lock, "utf8");
|
|
18156
|
+
} catch (err) {
|
|
18157
|
+
if (gone(err)) return;
|
|
18158
|
+
throw err;
|
|
18159
|
+
}
|
|
18160
|
+
if (holder === token) unlinkLock(lock);
|
|
18161
|
+
}
|
|
18162
|
+
function unlinkLock(lock) {
|
|
18163
|
+
try {
|
|
18164
|
+
fs10.unlinkSync(lock);
|
|
18165
|
+
} catch (err) {
|
|
18166
|
+
if (!gone(err)) throw err;
|
|
18167
|
+
}
|
|
18168
|
+
}
|
|
17833
18169
|
var SettingsService = class {
|
|
17834
18170
|
filePath;
|
|
17835
18171
|
cache = null;
|
|
17836
|
-
|
|
18172
|
+
cachedStamp = null;
|
|
17837
18173
|
constructor(filePath = getSettingsPath()) {
|
|
17838
18174
|
this.filePath = filePath;
|
|
17839
18175
|
}
|
|
17840
18176
|
// Several processes share the settings file (VS Code extension host, web
|
|
17841
|
-
// server, CLI), so the cache is keyed on the file's
|
|
18177
|
+
// server, CLI), so the cache is keyed on the file's identity: a write from any
|
|
17842
18178
|
// other process invalidates it. A stale cache here previously meant an
|
|
17843
18179
|
// allowlist set in one surface never reached another's planner.
|
|
17844
18180
|
getAll() {
|
|
17845
|
-
const
|
|
17846
|
-
if (this.cache &&
|
|
18181
|
+
const stamp = this.fileStamp();
|
|
18182
|
+
if (this.cache && stamp === this.cachedStamp) return this.cache;
|
|
17847
18183
|
this.cache = this.load();
|
|
17848
|
-
this.
|
|
18184
|
+
this.cachedStamp = stamp;
|
|
17849
18185
|
return this.cache;
|
|
17850
18186
|
}
|
|
17851
|
-
|
|
18187
|
+
/**
|
|
18188
|
+
* Every write replaces the file, so its inode changes with it; the mtime
|
|
18189
|
+
* alone cannot tell two writes apart on a filesystem whose timestamps are
|
|
18190
|
+
* coarse, and a write that shares the last one's tick stayed unseen.
|
|
18191
|
+
*/
|
|
18192
|
+
fileStamp(file = this.filePath) {
|
|
17852
18193
|
try {
|
|
17853
|
-
|
|
18194
|
+
const stat = fs10.statSync(file);
|
|
18195
|
+
return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
17854
18196
|
} catch {
|
|
17855
18197
|
return null;
|
|
17856
18198
|
}
|
|
17857
18199
|
}
|
|
18200
|
+
/**
|
|
18201
|
+
* What a change is applied to: the file as it is now, never a copy held since
|
|
18202
|
+
* an earlier read. Another process may have saved in between, and writing
|
|
18203
|
+
* the whole file back would silently undo it.
|
|
18204
|
+
*/
|
|
18205
|
+
current() {
|
|
18206
|
+
this.cache = null;
|
|
18207
|
+
return this.getAll();
|
|
18208
|
+
}
|
|
18209
|
+
/**
|
|
18210
|
+
* Calls `listener` when another process (or this one) saves the file. The
|
|
18211
|
+
* directory is watched, not the file: a write that swaps the file in leaves
|
|
18212
|
+
* a watch on the old one pointing at nothing.
|
|
18213
|
+
*/
|
|
18214
|
+
watch(listener) {
|
|
18215
|
+
const dir = path25.dirname(this.filePath);
|
|
18216
|
+
const name = path25.basename(this.filePath);
|
|
18217
|
+
let seen = this.fileStamp();
|
|
18218
|
+
let timer;
|
|
18219
|
+
let watcher;
|
|
18220
|
+
try {
|
|
18221
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
18222
|
+
watcher = fs10.watch(dir, (_event, changed) => {
|
|
18223
|
+
if (changed && changed !== name) return;
|
|
18224
|
+
clearTimeout(timer);
|
|
18225
|
+
timer = setTimeout(() => {
|
|
18226
|
+
const stamp = this.fileStamp();
|
|
18227
|
+
if (stamp === seen) return;
|
|
18228
|
+
seen = stamp;
|
|
18229
|
+
listener();
|
|
18230
|
+
}, 50);
|
|
18231
|
+
});
|
|
18232
|
+
} catch {
|
|
18233
|
+
return { dispose() {
|
|
18234
|
+
} };
|
|
18235
|
+
}
|
|
18236
|
+
return { dispose: () => {
|
|
18237
|
+
clearTimeout(timer);
|
|
18238
|
+
watcher.close();
|
|
18239
|
+
} };
|
|
18240
|
+
}
|
|
17858
18241
|
getModelAllowlist(runner) {
|
|
17859
18242
|
return this.getAll().modelAllowlist?.[runner];
|
|
17860
18243
|
}
|
|
17861
18244
|
setModelAllowlist(runner, ids) {
|
|
17862
|
-
this.
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
18245
|
+
this.save(() => {
|
|
18246
|
+
if (ids === void 0) {
|
|
18247
|
+
if (this.cache.modelAllowlist) {
|
|
18248
|
+
delete this.cache.modelAllowlist[runner];
|
|
18249
|
+
if (Object.keys(this.cache.modelAllowlist).length === 0) {
|
|
18250
|
+
this.cache.modelAllowlist = void 0;
|
|
18251
|
+
}
|
|
17868
18252
|
}
|
|
18253
|
+
} else {
|
|
18254
|
+
if (!this.cache.modelAllowlist) {
|
|
18255
|
+
this.cache.modelAllowlist = {};
|
|
18256
|
+
}
|
|
18257
|
+
this.cache.modelAllowlist[runner] = ids;
|
|
17869
18258
|
}
|
|
17870
|
-
}
|
|
17871
|
-
if (!this.cache.modelAllowlist) {
|
|
17872
|
-
this.cache.modelAllowlist = {};
|
|
17873
|
-
}
|
|
17874
|
-
this.cache.modelAllowlist[runner] = ids;
|
|
17875
|
-
}
|
|
17876
|
-
this.persist();
|
|
18259
|
+
});
|
|
17877
18260
|
}
|
|
17878
18261
|
getPlannerModel(provider) {
|
|
17879
18262
|
return this.getAll().plannerModels?.[provider];
|
|
17880
18263
|
}
|
|
17881
18264
|
setPlannerModel(provider, entry) {
|
|
17882
|
-
this.
|
|
17883
|
-
|
|
17884
|
-
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
18265
|
+
this.save(() => {
|
|
18266
|
+
if (entry === void 0) {
|
|
18267
|
+
if (this.cache.plannerModels) {
|
|
18268
|
+
delete this.cache.plannerModels[provider];
|
|
18269
|
+
if (Object.keys(this.cache.plannerModels).length === 0) {
|
|
18270
|
+
this.cache.plannerModels = void 0;
|
|
18271
|
+
}
|
|
17888
18272
|
}
|
|
18273
|
+
} else {
|
|
18274
|
+
if (!this.cache.plannerModels) {
|
|
18275
|
+
this.cache.plannerModels = {};
|
|
18276
|
+
}
|
|
18277
|
+
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
17889
18278
|
}
|
|
17890
|
-
}
|
|
17891
|
-
if (!this.cache.plannerModels) {
|
|
17892
|
-
this.cache.plannerModels = {};
|
|
17893
|
-
}
|
|
17894
|
-
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
17895
|
-
}
|
|
17896
|
-
this.persist();
|
|
18279
|
+
});
|
|
17897
18280
|
}
|
|
17898
18281
|
getEnabledRunners() {
|
|
17899
18282
|
return this.getAll().enabledRunners;
|
|
17900
18283
|
}
|
|
17901
18284
|
setEnabledRunners(ids) {
|
|
17902
|
-
this.
|
|
17903
|
-
|
|
17904
|
-
|
|
18285
|
+
this.save(() => {
|
|
18286
|
+
this.cache.enabledRunners = [...ids];
|
|
18287
|
+
});
|
|
18288
|
+
}
|
|
18289
|
+
/**
|
|
18290
|
+
* Change the list from what the file holds at this moment: the change is
|
|
18291
|
+
* handed the saved list (undefined when the user never chose) and its result
|
|
18292
|
+
* is saved in the same call. A list worked out from an earlier read would
|
|
18293
|
+
* save over what another surface set since. It may be handed the list again,
|
|
18294
|
+
* when another process saved before this one could (see {@link save}).
|
|
18295
|
+
*/
|
|
18296
|
+
updateEnabledRunners(change) {
|
|
18297
|
+
return this.save(() => {
|
|
18298
|
+
const next = [...change(this.cache.enabledRunners)];
|
|
18299
|
+
this.cache.enabledRunners = next;
|
|
18300
|
+
return next;
|
|
18301
|
+
});
|
|
18302
|
+
}
|
|
18303
|
+
/** Save `ids` as the user's choice unless one is saved already, judged on the file as it is now. */
|
|
18304
|
+
setEnabledRunnersIfUnset(ids) {
|
|
18305
|
+
return this.save(() => {
|
|
18306
|
+
if (this.cache.enabledRunners !== void 0) return false;
|
|
18307
|
+
this.cache.enabledRunners = [...ids];
|
|
18308
|
+
return true;
|
|
18309
|
+
});
|
|
17905
18310
|
}
|
|
17906
18311
|
/**
|
|
17907
18312
|
* Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
|
|
@@ -17919,9 +18324,9 @@ var SettingsService = class {
|
|
|
17919
18324
|
return env !== void 0 && env.trim() !== "";
|
|
17920
18325
|
}
|
|
17921
18326
|
setAutonomousMode(enabled) {
|
|
17922
|
-
this.
|
|
17923
|
-
|
|
17924
|
-
|
|
18327
|
+
this.save(() => {
|
|
18328
|
+
this.cache.autonomousMode = enabled;
|
|
18329
|
+
});
|
|
17925
18330
|
}
|
|
17926
18331
|
getRunSettings() {
|
|
17927
18332
|
return runSettingsOf(this.getAll());
|
|
@@ -17933,11 +18338,11 @@ var SettingsService = class {
|
|
|
17933
18338
|
throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
|
|
17934
18339
|
}
|
|
17935
18340
|
}
|
|
17936
|
-
this.
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
18341
|
+
this.save(() => {
|
|
18342
|
+
for (const key of RUN_SETTING_KEYS) {
|
|
18343
|
+
if (changes[key] !== void 0) this.cache[key] = changes[key];
|
|
18344
|
+
}
|
|
18345
|
+
});
|
|
17941
18346
|
return this.getRunSettings();
|
|
17942
18347
|
}
|
|
17943
18348
|
load() {
|
|
@@ -17966,13 +18371,43 @@ var SettingsService = class {
|
|
|
17966
18371
|
}
|
|
17967
18372
|
return {};
|
|
17968
18373
|
}
|
|
17969
|
-
|
|
18374
|
+
/**
|
|
18375
|
+
* Makes `change` to the file as it is now, and saves it. Another process can
|
|
18376
|
+
* save between that read and this write; the file is looked at again just
|
|
18377
|
+
* before ours replaces it, and when it moved the change is made afresh from
|
|
18378
|
+
* what that process saved, so neither save undoes the other. A file that
|
|
18379
|
+
* keeps moving is written over after {@link SAVE_TRIES} tries rather than
|
|
18380
|
+
* never. The look and the swap are one step under the file's lock
|
|
18381
|
+
* ({@link withFileLock}). A change that changes nothing writes nothing.
|
|
18382
|
+
*/
|
|
18383
|
+
save(change) {
|
|
18384
|
+
for (let tries = 1; ; tries++) {
|
|
18385
|
+
const before = JSON.stringify(this.current());
|
|
18386
|
+
const read2 = this.cachedStamp;
|
|
18387
|
+
const result = change();
|
|
18388
|
+
if (JSON.stringify(this.cache) === before || this.persist(tries < SAVE_TRIES ? read2 : void 0)) return result;
|
|
18389
|
+
}
|
|
18390
|
+
}
|
|
18391
|
+
/** False, saving nothing, when the file is no longer the one stamped `expected`. */
|
|
18392
|
+
persist(expected) {
|
|
17970
18393
|
const dir = path25.dirname(this.filePath);
|
|
17971
18394
|
if (!fs10.existsSync(dir)) {
|
|
17972
18395
|
fs10.mkdirSync(dir, { recursive: true });
|
|
17973
18396
|
}
|
|
17974
|
-
|
|
17975
|
-
this.
|
|
18397
|
+
const staging = `${this.filePath}.${process.pid}.tmp`;
|
|
18398
|
+
fs10.writeFileSync(staging, JSON.stringify(this.cache, null, 2));
|
|
18399
|
+
const stamp = this.fileStamp(staging);
|
|
18400
|
+
const swapped = withFileLock(this.filePath, () => {
|
|
18401
|
+
if (expected !== void 0 && this.fileStamp() !== expected) return false;
|
|
18402
|
+
fs10.renameSync(staging, this.filePath);
|
|
18403
|
+
return true;
|
|
18404
|
+
});
|
|
18405
|
+
if (!swapped) {
|
|
18406
|
+
fs10.rmSync(staging, { force: true });
|
|
18407
|
+
return false;
|
|
18408
|
+
}
|
|
18409
|
+
this.cachedStamp = stamp;
|
|
18410
|
+
return true;
|
|
17976
18411
|
}
|
|
17977
18412
|
};
|
|
17978
18413
|
|
|
@@ -18456,7 +18891,7 @@ var EnvConfig = class extends BaseConfig {
|
|
|
18456
18891
|
};
|
|
18457
18892
|
|
|
18458
18893
|
// src/services/mergeResultNotice.ts
|
|
18459
|
-
function blockNotice({ repo, reason, files }) {
|
|
18894
|
+
function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
|
|
18460
18895
|
switch (reason) {
|
|
18461
18896
|
case "merge-in-progress":
|
|
18462
18897
|
return `${repo} has a merge in progress`;
|
|
@@ -18466,10 +18901,22 @@ function blockNotice({ repo, reason, files }) {
|
|
|
18466
18901
|
return `${repo} has uncommitted changes to ${files.join(", ")}`;
|
|
18467
18902
|
case "partial-landing":
|
|
18468
18903
|
return `${repo} holds part of a task whose landing could not be rolled back`;
|
|
18904
|
+
case "wrong-branch":
|
|
18905
|
+
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}`;
|
|
18469
18906
|
case "git-error":
|
|
18470
18907
|
return `git could not check ${repo}`;
|
|
18471
18908
|
}
|
|
18472
18909
|
}
|
|
18910
|
+
function branchFix(blocked) {
|
|
18911
|
+
if (blocked.length === 0 || blocked.some((b) => b.reason !== "wrong-branch")) return "";
|
|
18912
|
+
const rebasing = blocked.filter((b) => b.rebasing);
|
|
18913
|
+
if (rebasing.length > 0) {
|
|
18914
|
+
const where = rebasing.map((b) => b.repo === SELF_REPO ? "your checkout" : b.repo).join(", ");
|
|
18915
|
+
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.`;
|
|
18916
|
+
}
|
|
18917
|
+
const started = [...new Set(blocked.map((b) => b.expected))];
|
|
18918
|
+
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.";
|
|
18919
|
+
}
|
|
18473
18920
|
function landedNotice(landed2) {
|
|
18474
18921
|
if (landed2.length === 0) return "Nothing was merged.";
|
|
18475
18922
|
return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
|
|
@@ -18486,7 +18933,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
|
|
|
18486
18933
|
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
18487
18934
|
}
|
|
18488
18935
|
case "blocked":
|
|
18489
|
-
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}
|
|
18936
|
+
return { level: "warn", message: `Merged nothing, so every tree is as it was: ${result.blocked.map(blockNotice).join("; ")}.${branchFix(result.blocked)}` };
|
|
18490
18937
|
case "conflict":
|
|
18491
18938
|
return {
|
|
18492
18939
|
level: "warn",
|
|
@@ -18850,7 +19297,7 @@ var GitWorktreeIsolation = class {
|
|
|
18850
19297
|
path: repoPath,
|
|
18851
19298
|
root,
|
|
18852
19299
|
baseRef,
|
|
18853
|
-
...branch ? { baseBranch: branch } : {},
|
|
19300
|
+
...branch ? { baseBranch: branch } : { startedDetached: true },
|
|
18854
19301
|
integrationBranch: integrationBranchFor(run.id)
|
|
18855
19302
|
};
|
|
18856
19303
|
if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
|
|
@@ -19203,7 +19650,9 @@ var GitWorktreeIsolation = class {
|
|
|
19203
19650
|
async mergeAll(run) {
|
|
19204
19651
|
const partial = await this.settleLanding(run);
|
|
19205
19652
|
if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
|
|
19653
|
+
const misplaced = await this.offBaseBranch(run.repos);
|
|
19206
19654
|
const repos = await this.reposWithWork(run);
|
|
19655
|
+
if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
|
|
19207
19656
|
if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
|
|
19208
19657
|
const blocked = [];
|
|
19209
19658
|
for (const repo of repos) {
|
|
@@ -19221,6 +19670,27 @@ var GitWorktreeIsolation = class {
|
|
|
19221
19670
|
}
|
|
19222
19671
|
return withWork;
|
|
19223
19672
|
}
|
|
19673
|
+
/**
|
|
19674
|
+
* A merge goes into whatever is checked out, which is only what the user
|
|
19675
|
+
* meant if it is the branch the run started on. A run that started on a
|
|
19676
|
+
* detached HEAD named no branch, so anything is as good as it was.
|
|
19677
|
+
*/
|
|
19678
|
+
async offBaseBranch(repos) {
|
|
19679
|
+
const off = [];
|
|
19680
|
+
for (const repo of repos) {
|
|
19681
|
+
if (!repo.baseBranch) continue;
|
|
19682
|
+
const current = (await this.tryGit(repo.root, ["symbolic-ref", "--short", "-q", "HEAD"])).stdout.trim();
|
|
19683
|
+
if (current === repo.baseBranch) continue;
|
|
19684
|
+
off.push({
|
|
19685
|
+
repo: repo.path,
|
|
19686
|
+
reason: "wrong-branch",
|
|
19687
|
+
files: [],
|
|
19688
|
+
expected: repo.baseBranch,
|
|
19689
|
+
...current ? { actual: current } : await this.rebaseInProgress(repo.root) ? { rebasing: true } : {}
|
|
19690
|
+
});
|
|
19691
|
+
}
|
|
19692
|
+
return off;
|
|
19693
|
+
}
|
|
19224
19694
|
/** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
|
|
19225
19695
|
async canPreflight() {
|
|
19226
19696
|
const version = await this.tryGit(void 0, ["--version"]);
|
|
@@ -19708,6 +20178,14 @@ var GitWorktreeIsolation = class {
|
|
|
19708
20178
|
if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
|
|
19709
20179
|
return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
|
|
19710
20180
|
}
|
|
20181
|
+
/** A rebase leaves HEAD detached, so it is told from a checkout the user detached on purpose. */
|
|
20182
|
+
async rebaseInProgress(cwd) {
|
|
20183
|
+
for (const dir of ["rebase-merge", "rebase-apply"]) {
|
|
20184
|
+
const found = await this.tryGit(cwd, ["rev-parse", "--git-path", dir]);
|
|
20185
|
+
if (found.ok && fs13.existsSync(path28.resolve(cwd, found.stdout.trim()))) return true;
|
|
20186
|
+
}
|
|
20187
|
+
return false;
|
|
20188
|
+
}
|
|
19711
20189
|
async abortMerge(cwd) {
|
|
19712
20190
|
if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
|
|
19713
20191
|
}
|
|
@@ -19728,6 +20206,29 @@ function createWorktreeIsolation(deps) {
|
|
|
19728
20206
|
return new GitWorktreeIsolation(deps);
|
|
19729
20207
|
}
|
|
19730
20208
|
|
|
20209
|
+
// src/services/mergeWording.ts
|
|
20210
|
+
function isRepoGroup(handoff) {
|
|
20211
|
+
return handoff.repos.some((r) => r.path !== ".");
|
|
20212
|
+
}
|
|
20213
|
+
function reposWithWork(handoff) {
|
|
20214
|
+
const withWork = handoff.repos.filter((r) => r.landed.length > 0);
|
|
20215
|
+
return (withWork.length > 0 ? withWork : handoff.repos).map((r) => r.path);
|
|
20216
|
+
}
|
|
20217
|
+
function mergeTarget(handoff) {
|
|
20218
|
+
if (!isRepoGroup(handoff)) return handoff.repos[0]?.baseBranch ?? "the branch you have checked out";
|
|
20219
|
+
const targets = handoff.repos.filter((r) => reposWithWork(handoff).includes(r.path));
|
|
20220
|
+
const branches = new Set(targets.map((r) => r.baseBranch));
|
|
20221
|
+
const [only] = branches;
|
|
20222
|
+
if (branches.size === 1 && only) return `${only} in each of ${targets.map((r) => r.path).join(", ")}`;
|
|
20223
|
+
return `each of ${targets.map((r) => `${r.path} (${r.baseBranch ?? "as checked out"})`).join(", ")}`;
|
|
20224
|
+
}
|
|
20225
|
+
function checkoutRule(handoff) {
|
|
20226
|
+
const named3 = handoff.repos.filter((r) => r.baseBranch);
|
|
20227
|
+
if (named3.length === 0) return "";
|
|
20228
|
+
if (!isRepoGroup(handoff)) return ` Nothing is merged unless ${named3[0].baseBranch} is checked out.`;
|
|
20229
|
+
return " Nothing is merged unless each repository is on the branch it started on.";
|
|
20230
|
+
}
|
|
20231
|
+
|
|
19731
20232
|
// src/interfaces/ILogger.ts
|
|
19732
20233
|
var ConsoleLogger = class {
|
|
19733
20234
|
warn(scope, message, err) {
|
|
@@ -19780,7 +20281,10 @@ async function settleReply(opts) {
|
|
|
19780
20281
|
opts.onProgress({ type: "interrupted" });
|
|
19781
20282
|
return { done: turn };
|
|
19782
20283
|
}
|
|
19783
|
-
if (attempt.failure !== void 0)
|
|
20284
|
+
if (attempt.failure !== void 0) {
|
|
20285
|
+
const { usageLimit } = attempt;
|
|
20286
|
+
return { done: { ...message(attempt.failure), failed: true, ...usageLimit !== void 0 && { usageLimit } } };
|
|
20287
|
+
}
|
|
19784
20288
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
19785
20289
|
const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
|
|
19786
20290
|
switch (reply2.kind) {
|
|
@@ -20711,7 +21215,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
|
|
|
20711
21215
|
function tokenCount(value) {
|
|
20712
21216
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
20713
21217
|
}
|
|
20714
|
-
function usageRecordFrom(usage) {
|
|
21218
|
+
function usageRecordFrom(usage, model) {
|
|
20715
21219
|
if (!usage) return void 0;
|
|
20716
21220
|
const inputTokens = tokenCount(usage.promptTokenCount);
|
|
20717
21221
|
const answerTokens = tokenCount(usage.candidatesTokenCount);
|
|
@@ -20719,6 +21223,7 @@ function usageRecordFrom(usage) {
|
|
|
20719
21223
|
const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
|
|
20720
21224
|
const record = {
|
|
20721
21225
|
source: "google",
|
|
21226
|
+
model,
|
|
20722
21227
|
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
20723
21228
|
...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
|
|
20724
21229
|
...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
|
|
@@ -20727,15 +21232,31 @@ function usageRecordFrom(usage) {
|
|
|
20727
21232
|
return anyReported ? record : void 0;
|
|
20728
21233
|
}
|
|
20729
21234
|
var GeminiResearchChat = class {
|
|
20730
|
-
constructor(
|
|
20731
|
-
this.
|
|
21235
|
+
constructor(getModel, open, hooks = {}) {
|
|
21236
|
+
this.getModel = getModel;
|
|
21237
|
+
this.open = open;
|
|
20732
21238
|
this.hooks = hooks;
|
|
21239
|
+
this.model = getModel();
|
|
21240
|
+
this.chat = open(this.model);
|
|
20733
21241
|
}
|
|
20734
|
-
|
|
21242
|
+
getModel;
|
|
21243
|
+
open;
|
|
20735
21244
|
hooks;
|
|
21245
|
+
chat;
|
|
21246
|
+
/** The model `chat` calls, named on its usage records. */
|
|
21247
|
+
model;
|
|
21248
|
+
/** The SDK binds a chat to one model, so a switch reopens it on the new one over the history so far. */
|
|
21249
|
+
async current() {
|
|
21250
|
+
const model = this.getModel();
|
|
21251
|
+
if (model !== this.model) {
|
|
21252
|
+
this.chat = this.open(model, await this.chat.getHistory());
|
|
21253
|
+
this.model = model;
|
|
21254
|
+
}
|
|
21255
|
+
return this.chat;
|
|
21256
|
+
}
|
|
20736
21257
|
async sendMessage(text, signal) {
|
|
20737
21258
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
20738
|
-
const result = await this.
|
|
21259
|
+
const result = await (await this.current()).sendMessageStream(text);
|
|
20739
21260
|
return this.consume(result.stream);
|
|
20740
21261
|
}
|
|
20741
21262
|
async sendToolResults(results, signal) {
|
|
@@ -20746,7 +21267,7 @@ var GeminiResearchChat = class {
|
|
|
20746
21267
|
response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
|
|
20747
21268
|
}
|
|
20748
21269
|
}));
|
|
20749
|
-
const result = await this.
|
|
21270
|
+
const result = await (await this.current()).sendMessageStream(funcResponses);
|
|
20750
21271
|
return this.consume(result.stream);
|
|
20751
21272
|
}
|
|
20752
21273
|
/**
|
|
@@ -20786,7 +21307,7 @@ var GeminiResearchChat = class {
|
|
|
20786
21307
|
}
|
|
20787
21308
|
}
|
|
20788
21309
|
}
|
|
20789
|
-
const usageRecord3 = usageRecordFrom(usage);
|
|
21310
|
+
const usageRecord3 = usageRecordFrom(usage, this.model);
|
|
20790
21311
|
if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
|
|
20791
21312
|
return {
|
|
20792
21313
|
text,
|
|
@@ -20802,6 +21323,8 @@ var GeminiService = class extends BaseAiService {
|
|
|
20802
21323
|
genAI = null;
|
|
20803
21324
|
genAIKey = "";
|
|
20804
21325
|
model = null;
|
|
21326
|
+
/** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
|
|
21327
|
+
modelId = "";
|
|
20805
21328
|
constructor(config) {
|
|
20806
21329
|
super(config);
|
|
20807
21330
|
}
|
|
@@ -20815,13 +21338,18 @@ var GeminiService = class extends BaseAiService {
|
|
|
20815
21338
|
}
|
|
20816
21339
|
return true;
|
|
20817
21340
|
}
|
|
20818
|
-
|
|
21341
|
+
/** The planner model picked for this session, as the API names it. */
|
|
21342
|
+
plannerModel() {
|
|
21343
|
+
return normalizeGeminiModel(this.config.orchestratorModel || this.config.planningModel);
|
|
21344
|
+
}
|
|
21345
|
+
getPlanningModel(model = this.plannerModel()) {
|
|
20819
21346
|
this.init();
|
|
20820
|
-
if (!this.model || this.
|
|
21347
|
+
if (!this.model || this.modelId !== model) {
|
|
20821
21348
|
this.model = this.genAI.getGenerativeModel({
|
|
20822
|
-
model
|
|
21349
|
+
model,
|
|
20823
21350
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20824
21351
|
});
|
|
21352
|
+
this.modelId = model;
|
|
20825
21353
|
}
|
|
20826
21354
|
return this.model;
|
|
20827
21355
|
}
|
|
@@ -20836,7 +21364,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
20836
21364
|
}
|
|
20837
21365
|
async startConversation(req) {
|
|
20838
21366
|
this.ensureInit();
|
|
20839
|
-
const genModel = this.getPlanningModel();
|
|
20840
21367
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
20841
21368
|
const systemPrompt = buildConversationSystemPrompt(
|
|
20842
21369
|
req.goal,
|
|
@@ -20865,13 +21392,14 @@ ${m.content}` });
|
|
|
20865
21392
|
if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
|
|
20866
21393
|
seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
|
|
20867
21394
|
}
|
|
20868
|
-
const
|
|
20869
|
-
|
|
20870
|
-
|
|
21395
|
+
const tools = [{ functionDeclarations: req.plannerTools ? CONVERSATION_TOOL_DEFINITIONS : TOOL_DEFINITIONS }];
|
|
21396
|
+
const open = (model, history = seededHistory) => this.getPlanningModel(model).startChat({
|
|
21397
|
+
history,
|
|
21398
|
+
tools,
|
|
20871
21399
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20872
21400
|
});
|
|
20873
21401
|
let currentProgress = req.onProgress;
|
|
20874
|
-
const researchChat = new GeminiResearchChat(
|
|
21402
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
20875
21403
|
onReasoning: (text, segmentId) => currentProgress({ type: "thinking", text, segmentId }),
|
|
20876
21404
|
onContent: (text, segmentId) => currentProgress({ type: "text_delta", text, segmentId }),
|
|
20877
21405
|
onUsage: (record) => currentProgress({ type: "usage", record })
|
|
@@ -20944,18 +21472,17 @@ ${repairHint}` }] : parts;
|
|
|
20944
21472
|
async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
20945
21473
|
this.ensureInit();
|
|
20946
21474
|
const { autonomousDefault } = modes;
|
|
20947
|
-
const genModel = this.getPlanningModel();
|
|
20948
21475
|
const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
|
|
20949
21476
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
20950
21477
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
20951
21478
|
|
|
20952
21479
|
Explore the workspace to understand the codebase, then generate the plan.`;
|
|
20953
|
-
const
|
|
20954
|
-
history
|
|
21480
|
+
const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
|
|
21481
|
+
history,
|
|
20955
21482
|
tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
|
|
20956
21483
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20957
21484
|
});
|
|
20958
|
-
const researchChat = new GeminiResearchChat(
|
|
21485
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
20959
21486
|
onUsage: (record) => onProgress({ type: "usage", record })
|
|
20960
21487
|
});
|
|
20961
21488
|
const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
@@ -20991,7 +21518,7 @@ import OpenAI from "openai";
|
|
|
20991
21518
|
import { v4 as uuidv44 } from "uuid";
|
|
20992
21519
|
var KEYLESS_PLACEHOLDER = "not-needed";
|
|
20993
21520
|
var OpenAiResearchChat = class {
|
|
20994
|
-
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage
|
|
21521
|
+
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
|
|
20995
21522
|
this.messages = messages;
|
|
20996
21523
|
this.getClient = getClient;
|
|
20997
21524
|
this.getModel = getModel;
|
|
@@ -21000,7 +21527,6 @@ var OpenAiResearchChat = class {
|
|
|
21000
21527
|
this.onContent = onContent;
|
|
21001
21528
|
this.getSource = getSource;
|
|
21002
21529
|
this.onUsage = onUsage;
|
|
21003
|
-
this.contextWindow = contextWindow;
|
|
21004
21530
|
}
|
|
21005
21531
|
messages;
|
|
21006
21532
|
getClient;
|
|
@@ -21010,7 +21536,6 @@ var OpenAiResearchChat = class {
|
|
|
21010
21536
|
onContent;
|
|
21011
21537
|
getSource;
|
|
21012
21538
|
onUsage;
|
|
21013
|
-
contextWindow;
|
|
21014
21539
|
async sendMessage(text, signal) {
|
|
21015
21540
|
this.answerAbandonedCalls();
|
|
21016
21541
|
this.messages.push({ role: "user", content: text });
|
|
@@ -21125,7 +21650,6 @@ var OpenAiResearchChat = class {
|
|
|
21125
21650
|
record.reportedCost = { amount: reported.cost, currency: "USD" };
|
|
21126
21651
|
hasMeasure = true;
|
|
21127
21652
|
}
|
|
21128
|
-
if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
|
|
21129
21653
|
return hasMeasure ? record : void 0;
|
|
21130
21654
|
}
|
|
21131
21655
|
};
|
|
@@ -21252,8 +21776,7 @@ ${buildResearchToolsPrompt()}` }
|
|
|
21252
21776
|
(delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
|
|
21253
21777
|
(delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
|
|
21254
21778
|
() => this.config.aiProvider,
|
|
21255
|
-
(record) => currentProgress({ type: "usage", record })
|
|
21256
|
-
req.contextWindow
|
|
21779
|
+
(record) => currentProgress({ type: "usage", record })
|
|
21257
21780
|
);
|
|
21258
21781
|
const ctx = {
|
|
21259
21782
|
chat,
|
|
@@ -21815,6 +22338,10 @@ var VerdictEngine = class {
|
|
|
21815
22338
|
this.generations.clear();
|
|
21816
22339
|
}
|
|
21817
22340
|
};
|
|
22341
|
+
var EXIT_CHECK = "exit_code";
|
|
22342
|
+
function endedByExit(verdict) {
|
|
22343
|
+
return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
|
|
22344
|
+
}
|
|
21818
22345
|
function exitVerdict(exitCode) {
|
|
21819
22346
|
const normalized2 = exitCode == null ? 0 : exitCode;
|
|
21820
22347
|
const exitOk = normalized2 === 0;
|
|
@@ -21823,7 +22350,7 @@ function exitVerdict(exitCode) {
|
|
|
21823
22350
|
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.`,
|
|
21824
22351
|
checks: [
|
|
21825
22352
|
{ name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
|
|
21826
|
-
{ name:
|
|
22353
|
+
{ name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
|
|
21827
22354
|
],
|
|
21828
22355
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21829
22356
|
};
|
|
@@ -22022,6 +22549,7 @@ var PlanStore = class {
|
|
|
22022
22549
|
this.rebuild();
|
|
22023
22550
|
for (const task of this._allTasks) {
|
|
22024
22551
|
if (task.status === "failed") task.status = "pending";
|
|
22552
|
+
if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
|
|
22025
22553
|
}
|
|
22026
22554
|
this.validateAssignedRunners();
|
|
22027
22555
|
}
|
|
@@ -22151,9 +22679,17 @@ var PlanStore = class {
|
|
|
22151
22679
|
const task = this._taskMap.get(id);
|
|
22152
22680
|
if (task) task.outputSummary = summary;
|
|
22153
22681
|
}
|
|
22154
|
-
|
|
22682
|
+
/** The owner is saved with the session it belongs to, and dropped with it. */
|
|
22683
|
+
setTaskRunnerSessionId(id, runnerSessionId, owner) {
|
|
22684
|
+
const task = this._taskMap.get(id);
|
|
22685
|
+
if (!task) return;
|
|
22686
|
+
task.runnerSessionId = runnerSessionId;
|
|
22687
|
+
if (runnerSessionId && owner) task.runnerSessionOwner = owner;
|
|
22688
|
+
else delete task.runnerSessionOwner;
|
|
22689
|
+
}
|
|
22690
|
+
setCarriedFrom(id, runner) {
|
|
22155
22691
|
const task = this._taskMap.get(id);
|
|
22156
|
-
if (task) task.
|
|
22692
|
+
if (task) task.carriedFrom = runner;
|
|
22157
22693
|
}
|
|
22158
22694
|
clearCarriedFrom(id) {
|
|
22159
22695
|
const task = this._taskMap.get(id);
|
|
@@ -22167,9 +22703,9 @@ var PlanStore = class {
|
|
|
22167
22703
|
const task = this._taskMap.get(id);
|
|
22168
22704
|
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
|
|
22169
22705
|
}
|
|
22170
|
-
endAttemptUsage(id, attempt, now) {
|
|
22706
|
+
endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
|
|
22171
22707
|
const task = this._taskMap.get(id);
|
|
22172
|
-
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now) : usage);
|
|
22708
|
+
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? endAttemptUsage(usage, now, runnerSessionCost, sessionId) : usage);
|
|
22173
22709
|
}
|
|
22174
22710
|
setTaskAttemptSkills(id, skills) {
|
|
22175
22711
|
const task = this._taskMap.get(id);
|
|
@@ -23027,6 +23563,8 @@ function conflictResolverTask(task, run, plan) {
|
|
|
23027
23563
|
}
|
|
23028
23564
|
|
|
23029
23565
|
// src/services/UsageLimitWaits.ts
|
|
23566
|
+
var RESUME_MARGIN_MS = 6e4;
|
|
23567
|
+
var RELAPSE_MS = 5 * 6e4;
|
|
23030
23568
|
var UsageLimitWaits = class {
|
|
23031
23569
|
constructor(onDue) {
|
|
23032
23570
|
this.onDue = onDue;
|
|
@@ -23149,6 +23687,9 @@ function selectReadyTasks(input) {
|
|
|
23149
23687
|
gated
|
|
23150
23688
|
};
|
|
23151
23689
|
}
|
|
23690
|
+
function isBlocked(task, store) {
|
|
23691
|
+
return blockedIn(task, blockingFailures(store.allTasks));
|
|
23692
|
+
}
|
|
23152
23693
|
var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
|
|
23153
23694
|
function dependencyMet(depId, store, runs) {
|
|
23154
23695
|
return waitingBehind([depId], store, runs).length === 0;
|
|
@@ -23278,6 +23819,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23278
23819
|
* task whose spawn always throws.
|
|
23279
23820
|
*/
|
|
23280
23821
|
onHold = /* @__PURE__ */ new Set();
|
|
23822
|
+
/**
|
|
23823
|
+
* Force starts queued behind dependencies the scheduler will not start them
|
|
23824
|
+
* after: outside a run it drives, or past a failed dependency, which it
|
|
23825
|
+
* never forces. Each starts once those still under way have finished.
|
|
23826
|
+
*/
|
|
23827
|
+
queuedStarts = /* @__PURE__ */ new Set();
|
|
23281
23828
|
/**
|
|
23282
23829
|
* Tasks Mark complete is landing after it ended their attempt: in progress
|
|
23283
23830
|
* with no attempt for as long as that takes, and not orphans for it.
|
|
@@ -23299,6 +23846,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23299
23846
|
observers = [];
|
|
23300
23847
|
skillsAt;
|
|
23301
23848
|
previousAttemptFromLog;
|
|
23849
|
+
lastLoggedAttempt;
|
|
23302
23850
|
runSettings;
|
|
23303
23851
|
isApproved;
|
|
23304
23852
|
constructor(deps) {
|
|
@@ -23314,6 +23862,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23314
23862
|
this.workspaceEnv = deps.workspaceEnv;
|
|
23315
23863
|
this.skillsAt = deps.skillsAt;
|
|
23316
23864
|
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
23865
|
+
this.lastLoggedAttempt = deps.lastLoggedAttempt;
|
|
23317
23866
|
this.runSettings = deps.runSettings;
|
|
23318
23867
|
this.isApproved = deps.isApproved;
|
|
23319
23868
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
@@ -23378,6 +23927,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23378
23927
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
23379
23928
|
skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
|
|
23380
23929
|
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
|
|
23930
|
+
lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
|
|
23381
23931
|
runSettings: options.runSettings ?? (() => void 0),
|
|
23382
23932
|
isApproved: options.isApproved ?? (() => true)
|
|
23383
23933
|
});
|
|
@@ -23634,10 +24184,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23634
24184
|
*/
|
|
23635
24185
|
holdStarts() {
|
|
23636
24186
|
this.startHolds++;
|
|
23637
|
-
let
|
|
24187
|
+
let held2 = true;
|
|
23638
24188
|
return () => {
|
|
23639
|
-
if (!
|
|
23640
|
-
|
|
24189
|
+
if (!held2) return;
|
|
24190
|
+
held2 = false;
|
|
23641
24191
|
this.startHolds--;
|
|
23642
24192
|
};
|
|
23643
24193
|
}
|
|
@@ -23648,24 +24198,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23648
24198
|
*/
|
|
23649
24199
|
admitStart() {
|
|
23650
24200
|
const generation2 = this.stopGeneration;
|
|
23651
|
-
let
|
|
24201
|
+
let held2 = true;
|
|
23652
24202
|
this.admissions++;
|
|
23653
24203
|
return {
|
|
23654
24204
|
current: () => generation2 === this.stopGeneration,
|
|
23655
24205
|
release: () => {
|
|
23656
|
-
if (!
|
|
23657
|
-
|
|
24206
|
+
if (!held2) return;
|
|
24207
|
+
held2 = false;
|
|
23658
24208
|
this.admissions--;
|
|
23659
24209
|
}
|
|
23660
24210
|
};
|
|
23661
24211
|
}
|
|
23662
24212
|
/**
|
|
23663
24213
|
* Something can still start that nobody has to ask for again: a start
|
|
23664
|
-
* admitted and not yet claimed,
|
|
23665
|
-
*
|
|
24214
|
+
* admitted and not yet claimed, a force start queued behind its
|
|
24215
|
+
* dependencies, or a task that resumes on its own at its usage limit's
|
|
24216
|
+
* reset. A Stop is owed to it, whether or not anything runs.
|
|
23666
24217
|
*/
|
|
23667
24218
|
get startsPending() {
|
|
23668
|
-
return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
|
|
24219
|
+
return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
|
|
23669
24220
|
}
|
|
23670
24221
|
/** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
|
|
23671
24222
|
async whileAdmitted(work) {
|
|
@@ -23764,6 +24315,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23764
24315
|
this.retryCounts.clear();
|
|
23765
24316
|
this.spawnCounts.clear();
|
|
23766
24317
|
this.onHold.clear();
|
|
24318
|
+
this.queuedStarts.clear();
|
|
23767
24319
|
this.recoverOrphans();
|
|
23768
24320
|
this.rearmLimitWaits();
|
|
23769
24321
|
}
|
|
@@ -23851,6 +24403,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23851
24403
|
}
|
|
23852
24404
|
this.runs.interrupt();
|
|
23853
24405
|
this.onHold.clear();
|
|
24406
|
+
if (reason === "user") this.queuedStarts.clear();
|
|
23854
24407
|
for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
|
|
23855
24408
|
this.sayStopped(stopped);
|
|
23856
24409
|
this.recoverOrphans();
|
|
@@ -23950,6 +24503,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23950
24503
|
const attempt = this.attempts.get(taskId);
|
|
23951
24504
|
if (!task || !attempt) return;
|
|
23952
24505
|
attempt.decided = true;
|
|
24506
|
+
const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
|
|
23953
24507
|
console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
|
|
23954
24508
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
23955
24509
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
@@ -23971,8 +24525,8 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
23971
24525
|
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.`);
|
|
23972
24526
|
} else if (landing) {
|
|
23973
24527
|
await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
|
|
23974
|
-
} else if (classifyRunnerStop(
|
|
23975
|
-
this.pauseOnLimit(task, attempt,
|
|
24528
|
+
} else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
|
|
24529
|
+
this.pauseOnLimit(task, attempt, stoppedMidTurn);
|
|
23976
24530
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
23977
24531
|
} else {
|
|
23978
24532
|
this.store.markFailed(taskId);
|
|
@@ -23987,16 +24541,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
23987
24541
|
const status = this.store.get(taskId)?.status;
|
|
23988
24542
|
if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
|
|
23989
24543
|
this.emit("onTaskChanged");
|
|
23990
|
-
if (
|
|
23991
|
-
|
|
23992
|
-
if (this.store.isFinished()) this.planStatus = "completed";
|
|
23993
|
-
this.emit("onTick");
|
|
23994
|
-
if (this.settled) await this.closeRun();
|
|
23995
|
-
this.emit("onExecutionComplete");
|
|
23996
|
-
}
|
|
24544
|
+
if (this.running) {
|
|
24545
|
+
await this.tick();
|
|
23997
24546
|
return;
|
|
23998
24547
|
}
|
|
23999
|
-
await this.
|
|
24548
|
+
await this.startQueued();
|
|
24549
|
+
if (this.attempts.size === 0) {
|
|
24550
|
+
if (this.store.isFinished()) this.planStatus = "completed";
|
|
24551
|
+
this.emit("onTick");
|
|
24552
|
+
if (this.settled) await this.closeRun();
|
|
24553
|
+
this.emit("onExecutionComplete");
|
|
24554
|
+
}
|
|
24000
24555
|
}
|
|
24001
24556
|
/**
|
|
24002
24557
|
* A repair's verdict never replaces the verdict the task's own work earned,
|
|
@@ -24264,11 +24819,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24264
24819
|
const resumes = this.awaitsResume;
|
|
24265
24820
|
this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
|
|
24266
24821
|
const ended = this.endAttempt(taskId, "retry");
|
|
24822
|
+
const movedFrom = this.movedFrom(task);
|
|
24267
24823
|
this.store.retry(taskId);
|
|
24824
|
+
if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
|
|
24268
24825
|
this.store.unblockDependents(taskId);
|
|
24269
24826
|
this.onHold.delete(taskId);
|
|
24270
24827
|
this.emit("onTaskChanged");
|
|
24271
|
-
|
|
24828
|
+
if (movedFrom) {
|
|
24829
|
+
await ended?.integration;
|
|
24830
|
+
} else {
|
|
24831
|
+
await this.runs.release(taskId, { keep: false }, ended?.integration);
|
|
24832
|
+
}
|
|
24272
24833
|
if (admittedAt !== this.stopGeneration) return;
|
|
24273
24834
|
if (resumes) await this.start(origin);
|
|
24274
24835
|
else await this.tick();
|
|
@@ -24319,6 +24880,16 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24319
24880
|
this.emit("onTaskChanged");
|
|
24320
24881
|
return true;
|
|
24321
24882
|
}
|
|
24883
|
+
/**
|
|
24884
|
+
* The runner whose attempt left the task's work, when the task has been
|
|
24885
|
+
* moved off it since: a session cannot cross runners, so whatever starts
|
|
24886
|
+
* the task next is a carry-over of that work, the rule a paused task's move
|
|
24887
|
+
* follows (ADR-0018, U1).
|
|
24888
|
+
*/
|
|
24889
|
+
movedFrom(task) {
|
|
24890
|
+
const owner = sessionOwner(task);
|
|
24891
|
+
return owner && owner !== this.store.resolveTaskRunner(task) ? owner : void 0;
|
|
24892
|
+
}
|
|
24322
24893
|
/**
|
|
24323
24894
|
* Run before writing assignments so cancellation sees the old runner's
|
|
24324
24895
|
* shared wait. A session cannot cross runners, so the next attempt is a
|
|
@@ -24376,7 +24947,7 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24376
24947
|
const summary = this.output.finalText(taskId);
|
|
24377
24948
|
this.endAttempt(taskId, "usage-limit");
|
|
24378
24949
|
this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
|
|
24379
|
-
this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError ?? `${attempt.runner} hit its usage limit`, summary));
|
|
24950
|
+
this.store.setTaskOutputSummary(taskId, summarizeOutput(attempt.turnError?.message ?? `${attempt.runner} hit its usage limit`, summary));
|
|
24380
24951
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
24381
24952
|
await this.afterVerdict(taskId);
|
|
24382
24953
|
}
|
|
@@ -24475,10 +25046,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24475
25046
|
return false;
|
|
24476
25047
|
}
|
|
24477
25048
|
/**
|
|
24478
|
-
* Manually start a single AI task right now
|
|
24479
|
-
*
|
|
24480
|
-
* it still needs the user's approval,
|
|
24481
|
-
*
|
|
25049
|
+
* Manually start a single AI task right now (the "force start" affordance on
|
|
25050
|
+
* a task card), past a failed dependency (ADR-0034) or a merge gate
|
|
25051
|
+
* (ADR-0020), but never past the claim: it still needs the user's approval,
|
|
25052
|
+
* a free slot, and no Stop since it was asked for, and throws
|
|
25053
|
+
* {@link TaskControlError} saying which it lacks. A dependency still under
|
|
25054
|
+
* way or yet to run is not forced past: the task is released to the
|
|
25055
|
+
* scheduler instead, which starts it once they finish, and the line saying
|
|
25056
|
+
* so is the answer.
|
|
24482
25057
|
* Reuses the scheduler's own claim so a force-started task gets the same
|
|
24483
25058
|
* augmented prompt, session tracking, and exit handling — callers must not
|
|
24484
25059
|
* re-spawn the runner themselves. No-op if the task is unknown, not an AI
|
|
@@ -24489,8 +25064,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24489
25064
|
if (!task || task.type !== "ai") return;
|
|
24490
25065
|
if (this.attempts.has(taskId)) return;
|
|
24491
25066
|
this.refuseOpsDuringMerge(task);
|
|
25067
|
+
const queued2 = this.queuedBehindDependencies(task);
|
|
25068
|
+
if (queued2) return queued2;
|
|
24492
25069
|
await this.whileAdmitted(async (admittedAt) => {
|
|
24493
|
-
if (!await this.openForAdmitted(taskId, origin, () =>
|
|
25070
|
+
if (!await this.openForAdmitted(taskId, origin, async () => {
|
|
25071
|
+
await this.forceStartTask(taskId, origin);
|
|
25072
|
+
}, admittedAt)) return;
|
|
24494
25073
|
if (this.attempts.has(taskId)) return;
|
|
24495
25074
|
this.refuseOpsDuringMerge(task);
|
|
24496
25075
|
this.refuseClaim(task, admittedAt);
|
|
@@ -24499,6 +25078,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24499
25078
|
await this.claimed(task, admittedAt);
|
|
24500
25079
|
});
|
|
24501
25080
|
}
|
|
25081
|
+
/**
|
|
25082
|
+
* A force start of a task whose dependencies are still under way or yet to
|
|
25083
|
+
* run: started now, it would build on work that is not there, so it waits
|
|
25084
|
+
* for them in the run, off any hold, and says so. Dependencies are never
|
|
25085
|
+
* bypassed silently. A blocked task is forced past its failure as asked.
|
|
25086
|
+
*/
|
|
25087
|
+
queuedBehindDependencies(task) {
|
|
25088
|
+
const waiting = this.underWayBefore(task);
|
|
25089
|
+
if (waiting.length === 0) return null;
|
|
25090
|
+
this.onHold.delete(task.id);
|
|
25091
|
+
if (!this.running || isBlocked(task, this.store)) this.queuedStarts.add(task.id);
|
|
25092
|
+
const names = waiting.map((id) => `task "${this.store.get(id)?.title ?? id}"`);
|
|
25093
|
+
const listed3 = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} and ${names.at(-1)}`;
|
|
25094
|
+
const idle = this.running || waiting.some((id) => this.attempts.has(id)) ? "" : " Nothing is running now: run the plan to start them.";
|
|
25095
|
+
const queued2 = `Task "${task.title}" is approved and starts once ${listed3} ${names.length === 1 ? "finishes" : "finish"}.${idle}`;
|
|
25096
|
+
this.tell("info", queued2);
|
|
25097
|
+
this.emit("onTaskChanged");
|
|
25098
|
+
return queued2;
|
|
25099
|
+
}
|
|
25100
|
+
/**
|
|
25101
|
+
* A task's unmet dependencies, less the failed or blocked ones a force start
|
|
25102
|
+
* goes past. A blocked one is gone past for its failure only: the work under
|
|
25103
|
+
* way or yet to run that it waits on is still waited for, through it.
|
|
25104
|
+
*/
|
|
25105
|
+
underWayBefore(task) {
|
|
25106
|
+
const blocked = blockingFailures(this.store.allTasks);
|
|
25107
|
+
const waiting = /* @__PURE__ */ new Set();
|
|
25108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
25109
|
+
const walk = (of) => {
|
|
25110
|
+
for (const id of unmetDependencies(of, this.store, this.runs)) {
|
|
25111
|
+
if (seen.has(id)) continue;
|
|
25112
|
+
seen.add(id);
|
|
25113
|
+
const dep = this.store.get(id);
|
|
25114
|
+
if (dep?.status === "failed") continue;
|
|
25115
|
+
if (dep && (dep.status === "blocked" || blocked.has(id))) walk(dep);
|
|
25116
|
+
else waiting.add(id);
|
|
25117
|
+
}
|
|
25118
|
+
};
|
|
25119
|
+
walk(task);
|
|
25120
|
+
return [...waiting];
|
|
25121
|
+
}
|
|
25122
|
+
/**
|
|
25123
|
+
* Starts the queued force starts whose dependencies have now finished. One
|
|
25124
|
+
* the scheduler now drives the run for, and nothing blocks, is left to it.
|
|
25125
|
+
*/
|
|
25126
|
+
async startQueued() {
|
|
25127
|
+
for (const id of [...this.queuedStarts]) {
|
|
25128
|
+
const task = this.store.get(id);
|
|
25129
|
+
if (!task || this.attempts.has(id) || task.status === "completed" || task.status === "skipped") {
|
|
25130
|
+
this.queuedStarts.delete(id);
|
|
25131
|
+
continue;
|
|
25132
|
+
}
|
|
25133
|
+
if (this.underWayBefore(task).length > 0) continue;
|
|
25134
|
+
this.queuedStarts.delete(id);
|
|
25135
|
+
if (this.running && !isBlocked(task, this.store)) continue;
|
|
25136
|
+
try {
|
|
25137
|
+
await this.forceStartTask(id);
|
|
25138
|
+
} catch (err) {
|
|
25139
|
+
this.tell("warn", `Task "${task.title}" did not start once its dependencies finished: ${err instanceof Error ? err.message : String(err)}`);
|
|
25140
|
+
}
|
|
25141
|
+
}
|
|
25142
|
+
}
|
|
24502
25143
|
refuseOpsDuringMerge(task) {
|
|
24503
25144
|
if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
|
|
24504
25145
|
throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
|
|
@@ -24563,6 +25204,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24563
25204
|
async tick() {
|
|
24564
25205
|
const admittedAt = this.stopGeneration;
|
|
24565
25206
|
this.recoverOrphans();
|
|
25207
|
+
await this.startQueued();
|
|
24566
25208
|
if (!this.running) {
|
|
24567
25209
|
if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
|
|
24568
25210
|
return;
|
|
@@ -24719,7 +25361,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24719
25361
|
if (refused3) return { refused: refused3 };
|
|
24720
25362
|
const attempt = {
|
|
24721
25363
|
taskId: task.id,
|
|
24722
|
-
attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
|
|
25364
|
+
attempt: Math.max(this.spawnCounts.get(task.id) ?? 0, this.lastLoggedAttempt(task.id), ...task.attemptUsage?.map((u) => u.attempt) ?? []) + 1,
|
|
24723
25365
|
phase: "starting",
|
|
24724
25366
|
session: null,
|
|
24725
25367
|
runner: this.store.resolveTaskRunner(task),
|
|
@@ -24733,6 +25375,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24733
25375
|
skills: [],
|
|
24734
25376
|
autoResumed: opts.autoResumed === true,
|
|
24735
25377
|
turnError: null,
|
|
25378
|
+
turnOutputFrom: 0,
|
|
24736
25379
|
reminded: false
|
|
24737
25380
|
};
|
|
24738
25381
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -24806,6 +25449,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24806
25449
|
title: task.title,
|
|
24807
25450
|
env,
|
|
24808
25451
|
resumeSessionId: kind.kind === "continuation" ? kind.resumeSessionId : void 0,
|
|
25452
|
+
resumeCost: kind.kind === "continuation" ? countedSessionCost(task.attemptUsage, kind.resumeSessionId) : void 0,
|
|
25453
|
+
resumeWindow: kind.kind === "continuation" ? resumedSessionWindow(task.attemptUsage, attempt.runner, attempt.attempt, kind.resumeSessionId) : void 0,
|
|
24809
25454
|
attempt: attempt.attempt,
|
|
24810
25455
|
...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
|
|
24811
25456
|
onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
|
|
@@ -24830,8 +25475,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24830
25475
|
this.output.attach(task.id, session);
|
|
24831
25476
|
this.verifier.watch(task, session);
|
|
24832
25477
|
session.onEvent((event) => {
|
|
24833
|
-
if (event.type === "turn_start")
|
|
24834
|
-
|
|
25478
|
+
if (event.type === "turn_start") {
|
|
25479
|
+
attempt.turnError = null;
|
|
25480
|
+
attempt.turnOutputFrom = session.getOutput().length;
|
|
25481
|
+
} else if (event.type === "error") attempt.turnError = event;
|
|
25482
|
+
else if (event.type === "model_switched" || event.type === "model_drifted") {
|
|
25483
|
+
this.emit("onModelSwitched", {
|
|
25484
|
+
taskId: task.id,
|
|
25485
|
+
sessionId: session.id,
|
|
25486
|
+
from: event.from,
|
|
25487
|
+
model: event.to,
|
|
25488
|
+
...event.type === "model_switched" && event.effort ? { effort: event.effort } : {},
|
|
25489
|
+
asked: event.type === "model_switched",
|
|
25490
|
+
attemptEnded: this.attempts.get(task.id) !== attempt
|
|
25491
|
+
});
|
|
25492
|
+
}
|
|
24835
25493
|
});
|
|
24836
25494
|
session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
|
|
24837
25495
|
return true;
|
|
@@ -24841,6 +25499,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24841
25499
|
return false;
|
|
24842
25500
|
}
|
|
24843
25501
|
this.endAttempt(task.id, "spawn-failed");
|
|
25502
|
+
const refusal = err instanceof Error ? err.message : String(err);
|
|
25503
|
+
if (attempt.kind.kind !== "repair" && classifyRunnerStop(refusal) === "usage-limit") {
|
|
25504
|
+
this.pauseOnLimit(task, attempt, refusal);
|
|
25505
|
+
this.store.setTaskOutputSummary(task.id, summarizeOutput(refusal, ""));
|
|
25506
|
+
await this.runs.release(task.id, { keep: true });
|
|
25507
|
+
this.emit("onTaskSettled", { taskId: task.id });
|
|
25508
|
+
this.emit("onTaskChanged");
|
|
25509
|
+
await this.tick();
|
|
25510
|
+
return false;
|
|
25511
|
+
}
|
|
24844
25512
|
if (attempt.kind.kind === "continuation") {
|
|
24845
25513
|
const refused3 = err instanceof RunnerStartupExit;
|
|
24846
25514
|
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)}`);
|
|
@@ -24960,12 +25628,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24960
25628
|
endAttempt(taskId, reason) {
|
|
24961
25629
|
const attempt = this.attempts.get(taskId);
|
|
24962
25630
|
this.attempts.delete(taskId);
|
|
25631
|
+
const session = attempt?.session ?? null;
|
|
25632
|
+
const nativeSessionId = session?.nativeSessionId() ?? null;
|
|
24963
25633
|
if (attempt) {
|
|
24964
|
-
this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString());
|
|
25634
|
+
this.store.endAttemptUsage(taskId, attempt.attempt, (/* @__PURE__ */ new Date()).toISOString(), session?.sessionCost?.() ?? void 0, nativeSessionId ?? void 0);
|
|
24965
25635
|
this.output.detach(taskId);
|
|
24966
25636
|
}
|
|
24967
|
-
|
|
24968
|
-
if (session) this.saveNativeSession(taskId, session.nativeSessionId());
|
|
25637
|
+
if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
|
|
24969
25638
|
if (stopsRunner(reason)) {
|
|
24970
25639
|
const task = this.store.get(taskId);
|
|
24971
25640
|
if (task) this.verifier.clear(task);
|
|
@@ -24974,8 +25643,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24974
25643
|
return attempt;
|
|
24975
25644
|
}
|
|
24976
25645
|
/** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
|
|
24977
|
-
saveNativeSession(taskId, nativeSessionId) {
|
|
24978
|
-
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
|
|
25646
|
+
saveNativeSession(taskId, nativeSessionId, runner) {
|
|
25647
|
+
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
|
|
24979
25648
|
}
|
|
24980
25649
|
endAllAttempts(reason) {
|
|
24981
25650
|
const ended = [...this.attempts.values()];
|
|
@@ -24985,13 +25654,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24985
25654
|
return ended;
|
|
24986
25655
|
}
|
|
24987
25656
|
};
|
|
24988
|
-
var RESUME_MARGIN_MS = 6e4;
|
|
24989
|
-
var RELAPSE_MS = 5 * 6e4;
|
|
24990
25657
|
var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
|
|
24991
25658
|
var OPS_RETRY_TAIL_LINES = 60;
|
|
24992
25659
|
function unresumedMessage(task, why) {
|
|
24993
25660
|
return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
|
|
24994
25661
|
}
|
|
25662
|
+
function saidInFlight(attempt) {
|
|
25663
|
+
const session = attempt.session;
|
|
25664
|
+
if (!session || session.turnState() !== "working") return null;
|
|
25665
|
+
if (attempt.turnError?.usageLimit !== void 0) return attempt.turnError;
|
|
25666
|
+
return session.getOutput().slice(attempt.turnOutputFrom);
|
|
25667
|
+
}
|
|
24995
25668
|
|
|
24996
25669
|
// src/services/Planner.ts
|
|
24997
25670
|
function plannerModesWith(req) {
|
|
@@ -25149,21 +25822,18 @@ var ModelResolver = class {
|
|
|
25149
25822
|
return this.cachedPickerOptions ?? [];
|
|
25150
25823
|
}
|
|
25151
25824
|
/**
|
|
25152
|
-
*
|
|
25153
|
-
*
|
|
25154
|
-
*
|
|
25155
|
-
*
|
|
25156
|
-
*
|
|
25825
|
+
* A model's context window from whatever catalog is already cached (#49),
|
|
25826
|
+
* for a usage record whose reporter stated none: a vendor's model from the
|
|
25827
|
+
* picker catalog (its own `/models` endpoint), a runner's model from that
|
|
25828
|
+
* runner's own discovered listing. `source` is the record's reporter, and
|
|
25829
|
+
* `model` the id it named. Reads only — never triggers a fetch or discovery
|
|
25830
|
+
* of its own, so an unknown window stays unknown and context fill is
|
|
25831
|
+
* omitted rather than guessed.
|
|
25157
25832
|
*/
|
|
25158
|
-
contextWindowFor(
|
|
25159
|
-
if (!
|
|
25160
|
-
const
|
|
25161
|
-
|
|
25162
|
-
for (const runner of this.config.enabledRunners ?? []) {
|
|
25163
|
-
const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
|
|
25164
|
-
if (found && found > 0) return found;
|
|
25165
|
-
}
|
|
25166
|
-
return void 0;
|
|
25833
|
+
contextWindowFor(source, model) {
|
|
25834
|
+
if (!model) return void 0;
|
|
25835
|
+
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;
|
|
25836
|
+
return window && window > 0 ? window : void 0;
|
|
25167
25837
|
}
|
|
25168
25838
|
invalidate() {
|
|
25169
25839
|
this.discovery.clear();
|
|
@@ -25667,6 +26337,8 @@ var PlannerSession = class {
|
|
|
25667
26337
|
* outlives the session — nothing from one goal may reach the next.
|
|
25668
26338
|
*/
|
|
25669
26339
|
nativeSessionId = null;
|
|
26340
|
+
/** What was counted of that session's running cost, so a resume counts on from it rather than dropping its first turn. */
|
|
26341
|
+
sessionCost = null;
|
|
25670
26342
|
/** Set once the first spawn attached. */
|
|
25671
26343
|
start = null;
|
|
25672
26344
|
/**
|
|
@@ -25857,6 +26529,7 @@ var CliAgentAiService = class {
|
|
|
25857
26529
|
// hit its rate limit, or lost its login must say so in the chat rather
|
|
25858
26530
|
// than leave an empty planner bubble.
|
|
25859
26531
|
failure: turn.error,
|
|
26532
|
+
usageLimit: turn.usageLimit,
|
|
25860
26533
|
fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
|
|
25861
26534
|
};
|
|
25862
26535
|
};
|
|
@@ -25923,6 +26596,7 @@ var CliAgentAiService = class {
|
|
|
25923
26596
|
};
|
|
25924
26597
|
const subagentUsage = /* @__PURE__ */ new Map();
|
|
25925
26598
|
let error;
|
|
26599
|
+
let usageLimit;
|
|
25926
26600
|
let stepIndex = 0;
|
|
25927
26601
|
let backgroundAgents = 0;
|
|
25928
26602
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
@@ -26031,6 +26705,7 @@ var CliAgentAiService = class {
|
|
|
26031
26705
|
}
|
|
26032
26706
|
case "error":
|
|
26033
26707
|
error = event.message;
|
|
26708
|
+
usageLimit = event.usageLimit;
|
|
26034
26709
|
return;
|
|
26035
26710
|
case "turn_end":
|
|
26036
26711
|
return;
|
|
@@ -26049,12 +26724,13 @@ var CliAgentAiService = class {
|
|
|
26049
26724
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
26050
26725
|
}
|
|
26051
26726
|
session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
|
|
26727
|
+
session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
|
|
26052
26728
|
if (error || signal?.aborted) session.stop();
|
|
26053
26729
|
if (error) {
|
|
26054
26730
|
const failure = failureAfterReply(text.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
|
|
26055
26731
|
onProgress({ type: "text_retracted", segmentId: finalSegment });
|
|
26056
26732
|
text = text.slice(0, finalRunStart);
|
|
26057
|
-
return { text, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
|
|
26733
|
+
return { text, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
|
|
26058
26734
|
}
|
|
26059
26735
|
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
26060
26736
|
}
|
|
@@ -26110,7 +26786,7 @@ var CliAgentAiService = class {
|
|
|
26110
26786
|
session.stop();
|
|
26111
26787
|
const start = session.start;
|
|
26112
26788
|
if (!start) throw new Error("No active planner conversation.");
|
|
26113
|
-
return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0 }, start.tools, session);
|
|
26789
|
+
return this.startAdapter({ ...start.startOptions, resumeSessionId: session.nativeSessionId ?? void 0, resumeCost: session.sessionCost ?? void 0 }, start.tools, session);
|
|
26114
26790
|
}
|
|
26115
26791
|
/**
|
|
26116
26792
|
* What the planner gets of the repository's config, its runner being
|
|
@@ -26122,9 +26798,9 @@ var CliAgentAiService = class {
|
|
|
26122
26798
|
const reader = connectorFor(this.runner)?.repoConfig;
|
|
26123
26799
|
if (!reader) return { systemPrompt };
|
|
26124
26800
|
const found = reader.projectServers(cwd);
|
|
26125
|
-
const { started, held } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
|
|
26801
|
+
const { started, held: held2 } = await decideProjectServers(found.servers, { repoRoot: repoRoot(cwd), approval, approvals: this.projectServerApprovals });
|
|
26126
26802
|
return {
|
|
26127
|
-
systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(
|
|
26803
|
+
systemPrompt: [systemPrompt, instructionsBlock(reader.instructions(cwd)), heldServersNote(held2.map((server4) => ({ server: server4, reason: NOT_APPROVED })), found.unreadable)].filter(Boolean).join("\n\n"),
|
|
26128
26804
|
...started.length > 0 ? { projectServers: started } : {}
|
|
26129
26805
|
};
|
|
26130
26806
|
}
|
|
@@ -26289,6 +26965,10 @@ var SessionCatalog = class {
|
|
|
26289
26965
|
allowlist() {
|
|
26290
26966
|
return this.deps.settings().modelAllowlist ?? {};
|
|
26291
26967
|
}
|
|
26968
|
+
/** The enabled runners and allowlist together, for whoever compares an assignment with them. */
|
|
26969
|
+
scope() {
|
|
26970
|
+
return { enabledRunners: this.enabledRunners(), allowlist: this.allowlist(), models: this.models() };
|
|
26971
|
+
}
|
|
26292
26972
|
/** The models the user allows on one runner; unset means no restriction. */
|
|
26293
26973
|
allowlistFor(runner) {
|
|
26294
26974
|
return this.allowlist()[runner];
|
|
@@ -26303,7 +26983,7 @@ var SessionCatalog = class {
|
|
|
26303
26983
|
*/
|
|
26304
26984
|
models() {
|
|
26305
26985
|
const out = { ...this.discovered };
|
|
26306
|
-
for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners()])) {
|
|
26986
|
+
for (const runner of /* @__PURE__ */ new Set([...Object.keys(out), ...this.deps.planRunners(), ...this.enabledRunners()])) {
|
|
26307
26987
|
const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
|
|
26308
26988
|
if (cached2.length > 0) out[runner] = cached2;
|
|
26309
26989
|
}
|
|
@@ -26340,9 +27020,16 @@ var SessionCatalog = class {
|
|
|
26340
27020
|
* asked for.
|
|
26341
27021
|
*/
|
|
26342
27022
|
async live() {
|
|
26343
|
-
|
|
26344
|
-
|
|
26345
|
-
|
|
27023
|
+
await this.discover(this.enabledRunners());
|
|
27024
|
+
return this.current();
|
|
27025
|
+
}
|
|
27026
|
+
/**
|
|
27027
|
+
* What {@link live} answers, from what is cached and without discovering:
|
|
27028
|
+
* the enabled runners, however the plan was started. The per-turn block
|
|
27029
|
+
* reads this, so it can never offer a different set than `list_runners`.
|
|
27030
|
+
*/
|
|
27031
|
+
current() {
|
|
27032
|
+
return this.queryCatalog(this.enabledRunners());
|
|
26346
27033
|
}
|
|
26347
27034
|
queryCatalog(runners) {
|
|
26348
27035
|
const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
|
|
@@ -26368,7 +27055,8 @@ var SessionCatalog = class {
|
|
|
26368
27055
|
modelsByRunner: this.models(),
|
|
26369
27056
|
runnerModes: this.modes(runners),
|
|
26370
27057
|
perRunnerAllowlist: this.allowlist(),
|
|
26371
|
-
autonomousDefault: this.deps.config.autonomousMode
|
|
27058
|
+
autonomousDefault: this.deps.config.autonomousMode,
|
|
27059
|
+
...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
|
|
26372
27060
|
};
|
|
26373
27061
|
}
|
|
26374
27062
|
/** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
|
|
@@ -26433,6 +27121,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
|
|
|
26433
27121
|
};
|
|
26434
27122
|
}
|
|
26435
27123
|
|
|
27124
|
+
// src/services/PlannerResend.ts
|
|
27125
|
+
var HELD = "Send a message once it has reset: what waits goes with it.";
|
|
27126
|
+
var WAIT = "planner";
|
|
27127
|
+
var PlannerResend = class {
|
|
27128
|
+
constructor(host) {
|
|
27129
|
+
this.host = host;
|
|
27130
|
+
}
|
|
27131
|
+
host;
|
|
27132
|
+
waits = new UsageLimitWaits(() => this.due());
|
|
27133
|
+
/** When the last resend at a reset went out, until a turn lands: a limit in that window is a relapse. */
|
|
27134
|
+
resentAt = null;
|
|
27135
|
+
/** Whether turns wait on the limit: until the reset, or for the user's next message. */
|
|
27136
|
+
holds() {
|
|
27137
|
+
return this.host.plan()?.plannerResend !== void 0;
|
|
27138
|
+
}
|
|
27139
|
+
/** A planner turn Ordewell started ended on the planner's usage limit, which it reported as `said`. */
|
|
27140
|
+
limited(said2) {
|
|
27141
|
+
const plan = this.host.plan();
|
|
27142
|
+
const runner = this.host.plannerRunner() ?? void 0;
|
|
27143
|
+
const now = /* @__PURE__ */ new Date();
|
|
27144
|
+
const relapsed = this.resentAt !== null && now.getTime() - this.resentAt < RELAPSE_MS;
|
|
27145
|
+
this.resentAt = null;
|
|
27146
|
+
this.waits.clear();
|
|
27147
|
+
const name = runner ?? "Planner";
|
|
27148
|
+
const reset = runner && !relapsed ? readUsageLimitReset(runner, said2, now) : null;
|
|
27149
|
+
if (!reset) {
|
|
27150
|
+
if (plan) plan.plannerResend = runner ? { runner } : {};
|
|
27151
|
+
const held2 = relapsed ? "hit it again right after the resend" : "did not say when it resets";
|
|
27152
|
+
const text = 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}`;
|
|
27153
|
+
return { text, resendAt: null, held: held2 };
|
|
27154
|
+
}
|
|
27155
|
+
const at = new Date(reset.getTime() + RESUME_MARGIN_MS);
|
|
27156
|
+
if (plan) plan.plannerResend = { runner, at: at.toISOString() };
|
|
27157
|
+
this.waits.arm(WAIT, at);
|
|
27158
|
+
return { text: `${name} usage limit, resets ${resumeTime(reset, now)}. Resending at ${resumeTime(at, now)}; anything you send meanwhile goes with it.`, resendAt: at };
|
|
27159
|
+
}
|
|
27160
|
+
/**
|
|
27161
|
+
* A new input arrived — the user's message, or a wake-up the supervisor
|
|
27162
|
+
* asked for. What was held for one goes with it now; so does what waits on
|
|
27163
|
+
* a runner the planner has left, since nothing it waits for applies to the
|
|
27164
|
+
* planner that will answer. True when it lifted a wait.
|
|
27165
|
+
*/
|
|
27166
|
+
arrived() {
|
|
27167
|
+
const saved = this.host.plan()?.plannerResend;
|
|
27168
|
+
if (!saved || saved.at !== void 0 && !this.switched(saved)) return false;
|
|
27169
|
+
this.lift();
|
|
27170
|
+
return true;
|
|
27171
|
+
}
|
|
27172
|
+
/** Keep what waits from going on its own; it goes with the user's next message. False when nothing waits on a reset. */
|
|
27173
|
+
cancel() {
|
|
27174
|
+
const plan = this.host.plan();
|
|
27175
|
+
const saved = plan?.plannerResend;
|
|
27176
|
+
if (!plan || saved?.at === void 0) return false;
|
|
27177
|
+
this.waits.clear();
|
|
27178
|
+
plan.plannerResend = held(saved);
|
|
27179
|
+
return true;
|
|
27180
|
+
}
|
|
27181
|
+
/** A turn landed: the resend before it got past the limit. */
|
|
27182
|
+
landed() {
|
|
27183
|
+
this.resentAt = null;
|
|
27184
|
+
}
|
|
27185
|
+
/** A plan adopted off disk: re-arm the wait it saved. A time already past resends at once. */
|
|
27186
|
+
restore() {
|
|
27187
|
+
this.reset();
|
|
27188
|
+
const saved = this.host.plan()?.plannerResend;
|
|
27189
|
+
if (saved?.at === void 0) return;
|
|
27190
|
+
const at = Date.parse(saved.at);
|
|
27191
|
+
if (Number.isNaN(at)) delete saved.at;
|
|
27192
|
+
else this.waits.arm(WAIT, new Date(at));
|
|
27193
|
+
}
|
|
27194
|
+
/** The plan is gone: nothing it waited on carries over. */
|
|
27195
|
+
reset() {
|
|
27196
|
+
this.waits.clear();
|
|
27197
|
+
this.resentAt = null;
|
|
27198
|
+
}
|
|
27199
|
+
due() {
|
|
27200
|
+
const plan = this.host.plan();
|
|
27201
|
+
const saved = plan?.plannerResend;
|
|
27202
|
+
if (!plan || saved?.at === void 0) return;
|
|
27203
|
+
if (this.switched(saved)) {
|
|
27204
|
+
plan.plannerResend = held(saved);
|
|
27205
|
+
this.host.changed();
|
|
27206
|
+
this.host.notice(`The planner no longer runs on ${saved.runner}, whose usage limit it waited on, so nothing was resent. ${HELD}`);
|
|
27207
|
+
return;
|
|
27208
|
+
}
|
|
27209
|
+
delete plan.plannerResend;
|
|
27210
|
+
this.resentAt = Date.now();
|
|
27211
|
+
this.host.due();
|
|
27212
|
+
}
|
|
27213
|
+
lift() {
|
|
27214
|
+
this.waits.clear();
|
|
27215
|
+
const plan = this.host.plan();
|
|
27216
|
+
if (plan) delete plan.plannerResend;
|
|
27217
|
+
}
|
|
27218
|
+
switched(saved) {
|
|
27219
|
+
return saved.runner !== void 0 && saved.runner !== (this.host.plannerRunner() ?? void 0);
|
|
27220
|
+
}
|
|
27221
|
+
};
|
|
27222
|
+
function held({ runner }) {
|
|
27223
|
+
return runner ? { runner } : {};
|
|
27224
|
+
}
|
|
27225
|
+
|
|
26436
27226
|
// src/services/PlanTurns.ts
|
|
26437
27227
|
var START_HOLD_TIMEOUT_MS = 5 * 6e4;
|
|
26438
27228
|
function turnPrompt(batch) {
|
|
@@ -26449,6 +27239,16 @@ var PlanTurns = class {
|
|
|
26449
27239
|
constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
|
|
26450
27240
|
this.host = host;
|
|
26451
27241
|
this.holdTimeoutMs = holdTimeoutMs;
|
|
27242
|
+
this.resend = new PlannerResend({
|
|
27243
|
+
plan: () => host.plan(),
|
|
27244
|
+
plannerRunner: () => host.plannerRunner(),
|
|
27245
|
+
due: () => {
|
|
27246
|
+
host.announce();
|
|
27247
|
+
this.pump();
|
|
27248
|
+
},
|
|
27249
|
+
changed: () => host.announce(),
|
|
27250
|
+
notice: (message) => host.notice(message)
|
|
27251
|
+
});
|
|
26452
27252
|
}
|
|
26453
27253
|
host;
|
|
26454
27254
|
holdTimeoutMs;
|
|
@@ -26461,6 +27261,33 @@ var PlanTurns = class {
|
|
|
26461
27261
|
/** The user stopped the turn wake-ups waited behind: they wait for the next input to open one, not for the stop's release. */
|
|
26462
27262
|
quiet = false;
|
|
26463
27263
|
seq = 0;
|
|
27264
|
+
resend;
|
|
27265
|
+
/** A turn ended on the planner's usage limit: arm the wait for its inputs. Saved with the failure the turn records. */
|
|
27266
|
+
usageLimit(said2) {
|
|
27267
|
+
return this.resend.limited(said2);
|
|
27268
|
+
}
|
|
27269
|
+
/** Keep what waits on the planner's usage limit from being sent at the reset; see {@link PlannerResend.cancel}. */
|
|
27270
|
+
cancelResend() {
|
|
27271
|
+
if (!this.resend.cancel()) return false;
|
|
27272
|
+
this.host.announce();
|
|
27273
|
+
return true;
|
|
27274
|
+
}
|
|
27275
|
+
/**
|
|
27276
|
+
* The conversation's opening ended on the planner's usage limit: its goal
|
|
27277
|
+
* waits with the limit, ahead of what was sent behind it, whose senders are
|
|
27278
|
+
* answered now as any sent during the wait are.
|
|
27279
|
+
*/
|
|
27280
|
+
waitOnLimit(input) {
|
|
27281
|
+
const plan = this.host.plan();
|
|
27282
|
+
if (!plan) return;
|
|
27283
|
+
plan.pendingTurns = [this.pending(input, false), ...plan.pendingTurns ?? []];
|
|
27284
|
+
this.host.persist();
|
|
27285
|
+
for (const t of plan.pendingTurns) if (t.source === "user") this.settle(t.id, (w) => w.resolve(plan));
|
|
27286
|
+
}
|
|
27287
|
+
/** A plan adopted off disk: the wait it saved is armed again. */
|
|
27288
|
+
restoreResend() {
|
|
27289
|
+
this.resend.restore();
|
|
27290
|
+
}
|
|
26464
27291
|
/** Whether a turn this module opened is in flight. */
|
|
26465
27292
|
get busy() {
|
|
26466
27293
|
return this.running !== null;
|
|
@@ -26502,25 +27329,33 @@ var PlanTurns = class {
|
|
|
26502
27329
|
admit(input, forced) {
|
|
26503
27330
|
const plan = this.host.plan();
|
|
26504
27331
|
if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
|
|
26505
|
-
const entry =
|
|
26506
|
-
id: `turn-${Date.now()}-${++this.seq}`,
|
|
26507
|
-
source: input.source,
|
|
26508
|
-
text: input.text,
|
|
26509
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26510
|
-
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
26511
|
-
...forced ? { forced: true } : {}
|
|
26512
|
-
};
|
|
27332
|
+
const entry = this.pending(input, forced);
|
|
26513
27333
|
this.quiet = false;
|
|
27334
|
+
if (this.resend.arrived()) this.host.announce();
|
|
26514
27335
|
const pending = plan.pendingTurns ?? [];
|
|
26515
27336
|
const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
|
|
26516
27337
|
plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
|
|
26517
27338
|
this.host.persist();
|
|
27339
|
+
if (input.source === "user" && this.resend.holds()) {
|
|
27340
|
+
this.pump();
|
|
27341
|
+
return Promise.resolve(plan);
|
|
27342
|
+
}
|
|
26518
27343
|
const settled = new Promise((resolve9, reject) => {
|
|
26519
27344
|
this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
|
|
26520
27345
|
});
|
|
26521
27346
|
this.pump();
|
|
26522
27347
|
return settled;
|
|
26523
27348
|
}
|
|
27349
|
+
pending(input, forced) {
|
|
27350
|
+
return {
|
|
27351
|
+
id: `turn-${Date.now()}-${++this.seq}`,
|
|
27352
|
+
source: input.source,
|
|
27353
|
+
text: input.text,
|
|
27354
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
27355
|
+
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
27356
|
+
...forced ? { forced: true } : {}
|
|
27357
|
+
};
|
|
27358
|
+
}
|
|
26524
27359
|
/**
|
|
26525
27360
|
* Start the next turn if one is due: nothing of ours is in flight, the
|
|
26526
27361
|
* conversation is free, and inputs wait. The host calls it when any planner
|
|
@@ -26531,7 +27366,7 @@ var PlanTurns = class {
|
|
|
26531
27366
|
* the plan, what still waits, and the planner in use at that moment.
|
|
26532
27367
|
*/
|
|
26533
27368
|
pump() {
|
|
26534
|
-
if (this.running || this.detecting) return;
|
|
27369
|
+
if (this.running || this.detecting || this.resend.holds()) return;
|
|
26535
27370
|
const plan = this.host.plan();
|
|
26536
27371
|
if (!plan || this.host.conversationBusy()) return;
|
|
26537
27372
|
const batch = this.liveBatch(plan);
|
|
@@ -26611,6 +27446,7 @@ var PlanTurns = class {
|
|
|
26611
27446
|
* conversation's to abandon; whatever it settles lands nothing here.
|
|
26612
27447
|
*/
|
|
26613
27448
|
abandon() {
|
|
27449
|
+
this.resend.reset();
|
|
26614
27450
|
this.running?.release();
|
|
26615
27451
|
this.running = null;
|
|
26616
27452
|
this.quiet = false;
|
|
@@ -26654,8 +27490,10 @@ var PlanTurns = class {
|
|
|
26654
27490
|
clearTimeout(timeout);
|
|
26655
27491
|
if (this.running !== running) return;
|
|
26656
27492
|
const forceStopped = !outcome.ok && running.forceStopped === true;
|
|
27493
|
+
const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
|
|
27494
|
+
if (outcome.ok) this.resend.landed();
|
|
26657
27495
|
try {
|
|
26658
|
-
const dropped = outcome.ok || isInterruption(outcome.err) && !forceStopped ? ids : new Set(batch.filter((t) => t.source === "user").map((t) => t.id));
|
|
27496
|
+
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));
|
|
26659
27497
|
const took = this.take(plan, dropped);
|
|
26660
27498
|
if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
|
|
26661
27499
|
if (took || forceStopped) this.host.persist();
|
|
@@ -26669,7 +27507,11 @@ var PlanTurns = class {
|
|
|
26669
27507
|
const ended = outcome;
|
|
26670
27508
|
const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
|
|
26671
27509
|
for (const t of batch) {
|
|
26672
|
-
if (
|
|
27510
|
+
if (limited && t.source === "user") {
|
|
27511
|
+
this.settle(t.id, (w) => w.resolve(plan));
|
|
27512
|
+
continue;
|
|
27513
|
+
}
|
|
27514
|
+
if ((forceStopped || limited) && waiting.has(t.id)) continue;
|
|
26673
27515
|
const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
|
|
26674
27516
|
if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
|
|
26675
27517
|
}
|
|
@@ -26728,7 +27570,6 @@ var RunSupervision = class {
|
|
|
26728
27570
|
blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
|
|
26729
27571
|
readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
|
|
26730
27572
|
retryLimit: () => this.retryLimit(),
|
|
26731
|
-
plannerRunner: () => host.plannerRunner(),
|
|
26732
27573
|
restartPlanner: () => host.restartPlanner(),
|
|
26733
27574
|
abandonTurn: () => host.abandonTurn(),
|
|
26734
27575
|
goUnsupervised: (reason) => this.goUnsupervised(reason),
|
|
@@ -27209,8 +28050,8 @@ function dropLegacyFile(sessionsDir, meta) {
|
|
|
27209
28050
|
const legacy = path30.join(sessionsDir, legacySessionFilename(meta));
|
|
27210
28051
|
if (legacy === path30.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
|
|
27211
28052
|
try {
|
|
27212
|
-
const
|
|
27213
|
-
if (
|
|
28053
|
+
const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
|
|
28054
|
+
if (held2 === meta.id) fs14.unlinkSync(legacy);
|
|
27214
28055
|
} catch {
|
|
27215
28056
|
}
|
|
27216
28057
|
}
|
|
@@ -27343,24 +28184,25 @@ function listTaskLogAttempts(location, taskId) {
|
|
|
27343
28184
|
}
|
|
27344
28185
|
return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
|
|
27345
28186
|
}
|
|
27346
|
-
function openTaskLog(location, taskId) {
|
|
28187
|
+
function openTaskLog(location, taskId, attempt) {
|
|
27347
28188
|
ensureStateDirIgnored(location.baseDir);
|
|
27348
28189
|
const dir = taskDir(location, taskId);
|
|
27349
28190
|
fs15.mkdirSync(dir, { recursive: true });
|
|
27350
|
-
|
|
28191
|
+
const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
|
|
28192
|
+
let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
|
|
27351
28193
|
for (; ; ) {
|
|
27352
|
-
const file = path31.join(dir, `${
|
|
28194
|
+
const file = path31.join(dir, `${number}${EXTENSION}`);
|
|
27353
28195
|
try {
|
|
27354
28196
|
fs15.writeFileSync(file, "", { flag: "wx" });
|
|
27355
28197
|
} catch (err) {
|
|
27356
28198
|
if (err.code === "EEXIST") {
|
|
27357
|
-
|
|
28199
|
+
number = Math.max(number + 1, next);
|
|
27358
28200
|
continue;
|
|
27359
28201
|
}
|
|
27360
28202
|
throw err;
|
|
27361
28203
|
}
|
|
27362
28204
|
return {
|
|
27363
|
-
attempt,
|
|
28205
|
+
attempt: number,
|
|
27364
28206
|
append(events) {
|
|
27365
28207
|
if (events.length === 0) return;
|
|
27366
28208
|
fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
|
|
@@ -27406,11 +28248,13 @@ var TaskLogRecorder = class {
|
|
|
27406
28248
|
open;
|
|
27407
28249
|
flushMs;
|
|
27408
28250
|
logger;
|
|
28251
|
+
/** Each task's latest attempt, by the session it records: its log takes a {@link note} until the next attempt starts. */
|
|
28252
|
+
latest = /* @__PURE__ */ new Map();
|
|
27409
28253
|
wrap(runner) {
|
|
27410
28254
|
return {
|
|
27411
28255
|
spawn: async (opts) => {
|
|
27412
28256
|
const session = await runner.spawn(opts);
|
|
27413
|
-
this.record(opts.taskId, session, opts.skills);
|
|
28257
|
+
this.record(opts.taskId, session, opts.skills, opts.attempt);
|
|
27414
28258
|
return session;
|
|
27415
28259
|
},
|
|
27416
28260
|
stop: (sessionId) => runner.stop(sessionId),
|
|
@@ -27420,11 +28264,20 @@ var TaskLogRecorder = class {
|
|
|
27420
28264
|
}
|
|
27421
28265
|
};
|
|
27422
28266
|
}
|
|
28267
|
+
/**
|
|
28268
|
+
* An entry the session did not emit, written into the log of the attempt
|
|
28269
|
+
* that runs `sessionId` and sent at once: what Ordewell made of one of its
|
|
28270
|
+
* events once that settled. Dropped once the task's next attempt started.
|
|
28271
|
+
*/
|
|
28272
|
+
note(sessionId, taskId, entry) {
|
|
28273
|
+
const attempt = this.latest.get(taskId);
|
|
28274
|
+
if (attempt?.sessionId === sessionId) attempt.note(entry);
|
|
28275
|
+
}
|
|
27423
28276
|
/** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
|
|
27424
|
-
record(taskId, session, skills = []) {
|
|
28277
|
+
record(taskId, session, skills = [], attemptNumber) {
|
|
27425
28278
|
let file;
|
|
27426
28279
|
try {
|
|
27427
|
-
file = this.open(this.deps.location(), taskId);
|
|
28280
|
+
file = this.open(this.deps.location(), taskId, attemptNumber);
|
|
27428
28281
|
} catch (err) {
|
|
27429
28282
|
this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
|
|
27430
28283
|
file = null;
|
|
@@ -27451,6 +28304,10 @@ var TaskLogRecorder = class {
|
|
|
27451
28304
|
}
|
|
27452
28305
|
this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
|
|
27453
28306
|
};
|
|
28307
|
+
this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
|
|
28308
|
+
pending.push(entry);
|
|
28309
|
+
flush();
|
|
28310
|
+
} });
|
|
27454
28311
|
const openCalls = /* @__PURE__ */ new Set();
|
|
27455
28312
|
session.onEvent((event) => {
|
|
27456
28313
|
const entry = toTaskLogEvent(event);
|
|
@@ -27471,21 +28328,82 @@ var TaskLogRecorder = class {
|
|
|
27471
28328
|
}
|
|
27472
28329
|
};
|
|
27473
28330
|
|
|
28331
|
+
// src/services/ContextWindowFill.ts
|
|
28332
|
+
var ContextWindowFill = class {
|
|
28333
|
+
constructor(catalog, last = {}) {
|
|
28334
|
+
this.catalog = catalog;
|
|
28335
|
+
if (last.contextWindow !== void 0) this.known.set(modelKey(last.source, last.model), last.contextWindow);
|
|
28336
|
+
}
|
|
28337
|
+
catalog;
|
|
28338
|
+
/** The window known for each model of the stream, by reporter and model. */
|
|
28339
|
+
known = /* @__PURE__ */ new Map();
|
|
28340
|
+
fill(record) {
|
|
28341
|
+
if (record.subagentId || record.aggregate) return record;
|
|
28342
|
+
const key = modelKey(record.source, record.model);
|
|
28343
|
+
const reported = record.contextWindow !== void 0 && record.contextWindow > 0 ? record.contextWindow : void 0;
|
|
28344
|
+
const window = reported ?? this.known.get(key) ?? (record.model ? positive(this.catalog(record.source, record.model)) : void 0);
|
|
28345
|
+
if (window !== void 0) this.known.set(key, window);
|
|
28346
|
+
return window === void 0 || window === record.contextWindow ? record : { ...record, contextWindow: window };
|
|
28347
|
+
}
|
|
28348
|
+
};
|
|
28349
|
+
function modelKey(source, model) {
|
|
28350
|
+
return JSON.stringify([source ?? null, model ?? null]);
|
|
28351
|
+
}
|
|
28352
|
+
function withContextWindows(runner, catalog) {
|
|
28353
|
+
return {
|
|
28354
|
+
spawn: async (opts) => windowedSession(await runner.spawn(opts), new ContextWindowFill(catalog, opts.resumeWindow)),
|
|
28355
|
+
stop: (sessionId) => runner.stop(sessionId),
|
|
28356
|
+
stopAll: () => runner.stopAll(),
|
|
28357
|
+
get activeCount() {
|
|
28358
|
+
return runner.activeCount;
|
|
28359
|
+
}
|
|
28360
|
+
};
|
|
28361
|
+
}
|
|
28362
|
+
function windowedSession(session, windows) {
|
|
28363
|
+
const listeners = [];
|
|
28364
|
+
session.onEvent((event) => {
|
|
28365
|
+
const filled = event.type === "usage" ? { ...event, record: windows.fill(event.record) } : event;
|
|
28366
|
+
for (const listener of listeners) listener(filled);
|
|
28367
|
+
});
|
|
28368
|
+
const onEvent = (listener) => {
|
|
28369
|
+
listeners.push(listener);
|
|
28370
|
+
};
|
|
28371
|
+
return new Proxy(session, {
|
|
28372
|
+
get: (target, prop) => {
|
|
28373
|
+
if (prop === "onEvent") return onEvent;
|
|
28374
|
+
const value = Reflect.get(target, prop, target);
|
|
28375
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
28376
|
+
}
|
|
28377
|
+
});
|
|
28378
|
+
}
|
|
28379
|
+
function positive(n) {
|
|
28380
|
+
return n !== void 0 && n > 0 ? n : void 0;
|
|
28381
|
+
}
|
|
28382
|
+
|
|
27474
28383
|
// src/services/PlannerUsage.ts
|
|
27475
28384
|
var PlannerUsageLedger = class {
|
|
28385
|
+
/** `catalogWindow` serves the window of a planner model whose backend reports none. */
|
|
28386
|
+
constructor(catalogWindow = () => void 0) {
|
|
28387
|
+
this.catalogWindow = catalogWindow;
|
|
28388
|
+
this.windows = new ContextWindowFill(catalogWindow);
|
|
28389
|
+
}
|
|
28390
|
+
catalogWindow;
|
|
27476
28391
|
usage = { totals: {} };
|
|
28392
|
+
windows;
|
|
27477
28393
|
/** Fold one model call's record into the totals; returns the running ledger. */
|
|
27478
28394
|
record(record) {
|
|
27479
|
-
this.usage = addPlannerUsage(this.usage, record);
|
|
28395
|
+
this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
|
|
27480
28396
|
return this.usage;
|
|
27481
28397
|
}
|
|
27482
28398
|
/** Adopt a persisted ledger (a reopened session) or start from zero. */
|
|
27483
28399
|
restore(usage) {
|
|
27484
28400
|
this.usage = usage ?? { totals: {} };
|
|
28401
|
+
this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
|
|
27485
28402
|
}
|
|
27486
28403
|
/** Start a fresh session's ledger from zero. */
|
|
27487
28404
|
clear() {
|
|
27488
28405
|
this.usage = { totals: {} };
|
|
28406
|
+
this.windows = new ContextWindowFill(this.catalogWindow);
|
|
27489
28407
|
}
|
|
27490
28408
|
/** Whether anything has been recorded — a plan with no usage says nothing. */
|
|
27491
28409
|
get hasUsage() {
|
|
@@ -27580,19 +28498,54 @@ var PlanEditor = class {
|
|
|
27580
28498
|
* between AI and MAN, are the patches that can leave a task incoherent
|
|
27581
28499
|
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
27582
28500
|
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
27583
|
-
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
27584
|
-
*
|
|
27585
|
-
* say why. Gated on the task existing so an edit to an unknown id still
|
|
28501
|
+
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
28502
|
+
* but not a live runner's hold on its model) rather than a second copy of
|
|
28503
|
+
* the rules — and throws, so the surface can say why. Gated on the task existing so an edit to an unknown id still
|
|
27586
28504
|
* falls through to the no-op `store.update` below instead of throwing.
|
|
27587
28505
|
* Attached skills meet the rule a planner's do: a planner skill is refused,
|
|
27588
|
-
* a name not found yet only warns.
|
|
28506
|
+
* a name not found yet only warns. A runner change is made by
|
|
28507
|
+
* {@link setTaskRunner}, which re-derives what the runner scopes, ahead of
|
|
28508
|
+
* the rest of the patch.
|
|
27589
28509
|
*
|
|
27590
28510
|
* Edits to one task commit in call order: the skill check waits on git, so
|
|
27591
28511
|
* two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
|
|
27592
28512
|
* one last.
|
|
27593
28513
|
*/
|
|
27594
28514
|
updateTask(taskId, changes) {
|
|
27595
|
-
|
|
28515
|
+
return this.inTaskOrder(taskId, () => this.applyTaskUpdate(taskId, changes));
|
|
28516
|
+
}
|
|
28517
|
+
/**
|
|
28518
|
+
* The plan follows a model switch the user made with the runner's own
|
|
28519
|
+
* command (#106), as a direct edit of theirs: saved and announced as
|
|
28520
|
+
* `task_updated`, so every surface shows it, and what a later retry or
|
|
28521
|
+
* continue spawns on. It is the one model edit a live runner allows, since
|
|
28522
|
+
* it says what that runner already runs, and the one that skips the
|
|
28523
|
+
* allowlist: the user picked it in the runner. The runner's own label is
|
|
28524
|
+
* used only for a model discovery does not list. Effort is the runner's
|
|
28525
|
+
* when it reported one, else the plan's, either clamped to what the new
|
|
28526
|
+
* model offers; none for a model discovery does not list. `live` is asked
|
|
28527
|
+
* as the edit lands, after the edits queued ahead of it: by then the plan
|
|
28528
|
+
* may be another one, whose task of the same id the switch says nothing of.
|
|
28529
|
+
*/
|
|
28530
|
+
followRunnerModel(taskId, model, effort, live) {
|
|
28531
|
+
return this.inTaskOrder(taskId, async () => {
|
|
28532
|
+
const task = this.store.get(taskId);
|
|
28533
|
+
if (!task || task.type === "user" || !live()) return null;
|
|
28534
|
+
const listed3 = this.catalog.models()[task.assignedRunner]?.find((m) => m.modelId === model.model);
|
|
28535
|
+
const thinkingEffort = clampThinkingEffort(effort ?? task.assignedModel?.thinkingEffort, listed3?.variants ?? []);
|
|
28536
|
+
if (task.assignedModel?.modelId === model.model && task.assignedModel.thinkingEffort === thinkingEffort) return this.plan();
|
|
28537
|
+
const assignedModel = {
|
|
28538
|
+
modelId: model.model,
|
|
28539
|
+
modelLabel: listed3?.modelLabel ?? model.label ?? model.model,
|
|
28540
|
+
...thinkingEffort ? { thinkingEffort } : {},
|
|
28541
|
+
...listed3 ? { availableVariants: listed3.variants.map((v) => v.id) } : {}
|
|
28542
|
+
};
|
|
28543
|
+
return this.commitTaskUpdate(taskId, { assignedModel });
|
|
28544
|
+
});
|
|
28545
|
+
}
|
|
28546
|
+
/** Edits to one task commit in call order; see {@link updateTask}. */
|
|
28547
|
+
inTaskOrder(taskId, apply) {
|
|
28548
|
+
const edit = (this.taskEdits.get(taskId) ?? Promise.resolve()).then(apply);
|
|
27596
28549
|
const settled = edit.catch(() => {
|
|
27597
28550
|
});
|
|
27598
28551
|
this.taskEdits.set(taskId, settled);
|
|
@@ -27608,22 +28561,31 @@ var PlanEditor = class {
|
|
|
27608
28561
|
const target = this.store.get(taskId);
|
|
27609
28562
|
const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
|
|
27610
28563
|
if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
|
|
27611
|
-
if ((changes.dependencies || changes.type ||
|
|
28564
|
+
if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
|
|
27612
28565
|
const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
|
|
27613
28566
|
if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
|
|
27614
28567
|
if (check2.clear?.length || check2.set) {
|
|
27615
28568
|
changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
|
|
27616
28569
|
}
|
|
27617
28570
|
}
|
|
27618
|
-
const
|
|
28571
|
+
const { assignedRunner, ...rest } = changes;
|
|
28572
|
+
if (target && assignedRunner && assignedRunner !== target.assignedRunner) {
|
|
28573
|
+
const moved = await this.setTaskRunner(taskId, assignedRunner);
|
|
28574
|
+
if (!moved || Object.keys(rest).length === 0) return moved;
|
|
28575
|
+
changes = rest;
|
|
28576
|
+
}
|
|
28577
|
+
const plan = await this.commitTaskUpdate(taskId, changes);
|
|
28578
|
+
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
28579
|
+
return plan;
|
|
28580
|
+
}
|
|
28581
|
+
commitTaskUpdate(taskId, changes) {
|
|
28582
|
+
return this.commit(
|
|
27619
28583
|
() => {
|
|
27620
28584
|
changes = this.scheduler.releaseMovedResume(taskId, changes);
|
|
27621
28585
|
return Boolean(this.store.update(taskId, changes));
|
|
27622
28586
|
},
|
|
27623
28587
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
27624
28588
|
);
|
|
27625
|
-
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
27626
|
-
return plan;
|
|
27627
28589
|
}
|
|
27628
28590
|
/**
|
|
27629
28591
|
* Replace one task's dependency list — the named entry point the surfaces'
|
|
@@ -27640,7 +28602,8 @@ var PlanEditor = class {
|
|
|
27640
28602
|
* because a runner change is never a single-field edit: the task's model,
|
|
27641
28603
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
27642
28604
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
27643
|
-
* That needs discovery, which is why
|
|
28605
|
+
* That needs discovery, which is why `updateTask` hands a runner change here
|
|
28606
|
+
* rather than writing it as a field.
|
|
27644
28607
|
*
|
|
27645
28608
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
27646
28609
|
*/
|
|
@@ -27650,11 +28613,13 @@ var PlanEditor = class {
|
|
|
27650
28613
|
const task = this.store.get(taskId);
|
|
27651
28614
|
if (!task) return null;
|
|
27652
28615
|
if (task.assignedRunner === runner || task.type === "user") return plan;
|
|
28616
|
+
this.refuseLiveRetarget(task);
|
|
27653
28617
|
const catalog = await this.catalog.runner(runner);
|
|
27654
28618
|
const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
|
|
27655
28619
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
|
|
27656
28620
|
if (Object.keys(changes).length === 0) return this.plan();
|
|
27657
28621
|
this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
|
|
28622
|
+
this.refuseLiveRetarget(task);
|
|
27658
28623
|
return this.commit(() => {
|
|
27659
28624
|
if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
|
|
27660
28625
|
this.admitRunner(runner, catalog.models);
|
|
@@ -27688,6 +28653,9 @@ var PlanEditor = class {
|
|
|
27688
28653
|
return true;
|
|
27689
28654
|
});
|
|
27690
28655
|
}
|
|
28656
|
+
refuseLiveRetarget(task) {
|
|
28657
|
+
if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
|
|
28658
|
+
}
|
|
27691
28659
|
/**
|
|
27692
28660
|
* A mode the host cannot run is refused, with its reason, not swapped for
|
|
27693
28661
|
* another: the plan is the source of truth for what runs (ADR-0001), and a
|
|
@@ -28017,6 +28985,10 @@ var ExecutionCommands = class {
|
|
|
28017
28985
|
const all = this.all(tasks);
|
|
28018
28986
|
const live = all.filter(({ task }) => this.host.running(task.id));
|
|
28019
28987
|
if (this.host.hasLiveWork()) {
|
|
28988
|
+
if (actor.kind === "user") {
|
|
28989
|
+
const waiting = all.filter(({ task }) => this.host.waitsForApproval(task.id)).length;
|
|
28990
|
+
return { act: async () => ({ message: await this.host.joinRun(waiting) }) };
|
|
28991
|
+
}
|
|
28020
28992
|
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.` };
|
|
28021
28993
|
}
|
|
28022
28994
|
const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
|
|
@@ -28442,8 +29414,11 @@ function mergeStopped(result) {
|
|
|
28442
29414
|
switch (result.outcome) {
|
|
28443
29415
|
case "merged":
|
|
28444
29416
|
return "it merged";
|
|
28445
|
-
case "blocked":
|
|
28446
|
-
|
|
29417
|
+
case "blocked": {
|
|
29418
|
+
const off = result.blocked.filter((b) => b.reason === "wrong-branch");
|
|
29419
|
+
if (off.length === 0) return "the user's checkout has changes that overlap what it would merge";
|
|
29420
|
+
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("; ");
|
|
29421
|
+
}
|
|
28447
29422
|
case "conflict":
|
|
28448
29423
|
return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
|
|
28449
29424
|
case "failed":
|
|
@@ -28568,11 +29543,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
|
|
|
28568
29543
|
function isPlannerApproval(request) {
|
|
28569
29544
|
return !isRunnerApproval(request);
|
|
28570
29545
|
}
|
|
29546
|
+
function writtenTasks(before, after) {
|
|
29547
|
+
const said2 = ({ subtasks: _subtasks, status: _status, ...task }) => JSON.stringify(task);
|
|
29548
|
+
const prior = new Map(flattenTasks(before).map((t) => [t.id, said2(t)]));
|
|
29549
|
+
return flattenTasks(after).filter((t) => prior.get(t.id) !== said2(t)).map((t) => t.id);
|
|
29550
|
+
}
|
|
28571
29551
|
function createSession(deps) {
|
|
28572
29552
|
const pinnedAiService = deps.aiService;
|
|
28573
29553
|
const mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
28574
29554
|
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
|
|
28575
29555
|
const store = new PlanStore();
|
|
29556
|
+
const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
|
|
28576
29557
|
const taskLogs = new TaskLogRecorder({
|
|
28577
29558
|
broadcast: deps.broadcast,
|
|
28578
29559
|
// Read through the Session, whose id and workspace are the ones it saves
|
|
@@ -28606,7 +29587,7 @@ function createSession(deps) {
|
|
|
28606
29587
|
const orchestrator = TaskOrchestrator.compose({
|
|
28607
29588
|
config: deps.config,
|
|
28608
29589
|
notifications: deps.notifications,
|
|
28609
|
-
runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
|
|
29590
|
+
runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
|
|
28610
29591
|
store,
|
|
28611
29592
|
output: deps.taskOutput,
|
|
28612
29593
|
isolation: deps.isolation,
|
|
@@ -28614,10 +29595,11 @@ function createSession(deps) {
|
|
|
28614
29595
|
workspaceRoot: deps.workspaceRoot,
|
|
28615
29596
|
skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
|
|
28616
29597
|
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
|
|
29598
|
+
lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
|
|
28617
29599
|
runSettings: () => session.planState?.runSettings,
|
|
28618
29600
|
isApproved: (taskId) => session.isApproved(taskId)
|
|
28619
29601
|
});
|
|
28620
|
-
const usage = new PlannerUsageLedger();
|
|
29602
|
+
const usage = new PlannerUsageLedger(catalogWindow);
|
|
28621
29603
|
const events = new SessionEventRelay({
|
|
28622
29604
|
broadcast: deps.broadcast,
|
|
28623
29605
|
onNotice: deps.onNotice,
|
|
@@ -28666,7 +29648,8 @@ function createSession(deps) {
|
|
|
28666
29648
|
modelResolver: deps.modelResolver,
|
|
28667
29649
|
settings: deps.settings,
|
|
28668
29650
|
// Read through the Session, which holds the plan; the catalog is built first.
|
|
28669
|
-
planRunners: () => session.planState?.runners ?? []
|
|
29651
|
+
planRunners: () => session.planState?.runners ?? [],
|
|
29652
|
+
liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
|
|
28670
29653
|
});
|
|
28671
29654
|
const session = new Session({
|
|
28672
29655
|
config: deps.config,
|
|
@@ -28688,6 +29671,7 @@ function createSession(deps) {
|
|
|
28688
29671
|
usage,
|
|
28689
29672
|
approvals,
|
|
28690
29673
|
approvalPolicy,
|
|
29674
|
+
taskLogs,
|
|
28691
29675
|
// Every planner's web reads, carried here or natively, are decided by the
|
|
28692
29676
|
// one policy, which asks through the same channel as paths and commands.
|
|
28693
29677
|
// The response cache and search provider are process-wide, so parallel
|
|
@@ -28738,6 +29722,7 @@ var Session = class {
|
|
|
28738
29722
|
fsAdapter;
|
|
28739
29723
|
approvals;
|
|
28740
29724
|
approvalPolicy;
|
|
29725
|
+
taskLogs;
|
|
28741
29726
|
fetcher;
|
|
28742
29727
|
settingsFn;
|
|
28743
29728
|
save;
|
|
@@ -28773,7 +29758,7 @@ var Session = class {
|
|
|
28773
29758
|
turnId: () => this.conversation.currentTurnId,
|
|
28774
29759
|
recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
|
|
28775
29760
|
liveCatalog: () => this.liveCatalog(),
|
|
28776
|
-
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
|
|
29761
|
+
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
|
|
28777
29762
|
submitPlan: (tasks) => this.submitPlanFromTool(tasks),
|
|
28778
29763
|
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
28779
29764
|
tasks: () => this.store.planTasks,
|
|
@@ -28818,6 +29803,7 @@ var Session = class {
|
|
|
28818
29803
|
this.usage = parts.usage;
|
|
28819
29804
|
this.approvals = parts.approvals;
|
|
28820
29805
|
this.approvalPolicy = parts.approvalPolicy;
|
|
29806
|
+
this.taskLogs = parts.taskLogs;
|
|
28821
29807
|
this.fetcher = parts.fetcher;
|
|
28822
29808
|
this.skillsService = parts.skillsService;
|
|
28823
29809
|
this.save = parts.saveSession;
|
|
@@ -28827,7 +29813,8 @@ var Session = class {
|
|
|
28827
29813
|
aiService: () => this.aiService(),
|
|
28828
29814
|
onProgress: (p) => this.events.progress(p),
|
|
28829
29815
|
opening: (runners) => this.conversationOpening(runners),
|
|
28830
|
-
catalog: () => this.catalog.
|
|
29816
|
+
catalog: () => this.catalog.current(),
|
|
29817
|
+
assignmentScope: () => this.catalog.scope(),
|
|
28831
29818
|
autorun: () => this.userRunSettings().autorun,
|
|
28832
29819
|
takeRunUpdates: () => this.untoldRunUpdates.splice(0),
|
|
28833
29820
|
tasks: () => this.store.planTasks,
|
|
@@ -28847,13 +29834,18 @@ var Session = class {
|
|
|
28847
29834
|
turnAborted: () => this.approvals.clear(isPlannerApproval),
|
|
28848
29835
|
// After the turn has announced its end, so the next one never starts
|
|
28849
29836
|
// ahead of it on a surface.
|
|
28850
|
-
turnReleased: () => queueMicrotask(() => this.turns.pump())
|
|
29837
|
+
turnReleased: () => queueMicrotask(() => this.turns.pump()),
|
|
29838
|
+
usageLimit: (said2) => this.turns.usageLimit(said2)
|
|
28851
29839
|
});
|
|
28852
29840
|
this.turns = new PlanTurns({
|
|
28853
29841
|
plan: () => this.closed ? null : this.plan,
|
|
28854
29842
|
conversationBusy: () => this.conversation.isTurnInFlight,
|
|
28855
29843
|
mayWake: () => this.supervision.mayWake(),
|
|
28856
29844
|
persist: () => this.persist({ background: true }),
|
|
29845
|
+
announce: () => {
|
|
29846
|
+
this.persist({ background: true });
|
|
29847
|
+
this.events.planGenerated(this.plan, this.goal);
|
|
29848
|
+
},
|
|
28857
29849
|
interrupt: () => {
|
|
28858
29850
|
this.conversation.stopTurn("force_send");
|
|
28859
29851
|
},
|
|
@@ -28870,7 +29862,8 @@ var Session = class {
|
|
|
28870
29862
|
},
|
|
28871
29863
|
notice: (message) => this.notice("error", message),
|
|
28872
29864
|
plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
|
|
28873
|
-
assertPlannerReady: () => this.assertPlannerReady()
|
|
29865
|
+
assertPlannerReady: () => this.assertPlannerReady(),
|
|
29866
|
+
plannerRunner: () => runnerForProvider(this.config.aiProvider)
|
|
28874
29867
|
});
|
|
28875
29868
|
this.supervision = new RunSupervision({
|
|
28876
29869
|
plan: () => this.plan,
|
|
@@ -28888,7 +29881,6 @@ var Session = class {
|
|
|
28888
29881
|
},
|
|
28889
29882
|
deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
|
|
28890
29883
|
pump: () => this.turns.pump(),
|
|
28891
|
-
plannerRunner: () => runnerForProvider(this.config.aiProvider),
|
|
28892
29884
|
restartPlanner: () => this.aiService().restart?.(),
|
|
28893
29885
|
abandonTurn: () => this.conversation.abandonHungTurn(),
|
|
28894
29886
|
notice: (level, message) => this.notice(level, message)
|
|
@@ -28970,6 +29962,7 @@ var Session = class {
|
|
|
28970
29962
|
tasks: () => this.plan ? this.store.planTasks : null,
|
|
28971
29963
|
planApproved: () => this.plan?.approved === true,
|
|
28972
29964
|
taskApproved: (taskId) => this.isApproved(taskId),
|
|
29965
|
+
waitsForApproval: (taskId) => this.waitsForApproval(taskId),
|
|
28973
29966
|
approve: (taskIds) => this.approve(taskIds),
|
|
28974
29967
|
autonomous: () => this.config.autonomousMode,
|
|
28975
29968
|
autorun: () => this.userRunSettings().autorun,
|
|
@@ -28991,6 +29984,7 @@ var Session = class {
|
|
|
28991
29984
|
blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
|
|
28992
29985
|
waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
|
|
28993
29986
|
runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
|
|
29987
|
+
joinRun: (approved) => this.joinRun(approved),
|
|
28994
29988
|
startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
|
|
28995
29989
|
stopTask: (taskId) => this.cancelTask(taskId),
|
|
28996
29990
|
stopRun: () => this.stopExecution(),
|
|
@@ -29019,8 +30013,8 @@ var Session = class {
|
|
|
29019
30013
|
* one: nothing it would start is approved until the user executes it.
|
|
29020
30014
|
*/
|
|
29021
30015
|
heldEdit(taskId) {
|
|
29022
|
-
const
|
|
29023
|
-
if (!
|
|
30016
|
+
const held2 = this.conversation.pendingOps();
|
|
30017
|
+
if (!held2 || held2.length === 0 || !this.plan) return false;
|
|
29024
30018
|
if (taskId === void 0) return true;
|
|
29025
30019
|
const before = this.store.get(taskId);
|
|
29026
30020
|
const after = this.asEdited(taskId);
|
|
@@ -29033,10 +30027,10 @@ var Session = class {
|
|
|
29033
30027
|
/** {@link editedTasks}, with the id each of the held batch's handles names in them. */
|
|
29034
30028
|
editedPlan() {
|
|
29035
30029
|
const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
|
|
29036
|
-
const
|
|
29037
|
-
if (!
|
|
30030
|
+
const held2 = this.conversation.pendingOps();
|
|
30031
|
+
if (!held2 || held2.length === 0 || !this.plan) return unedited;
|
|
29038
30032
|
const runners = this.editRunners();
|
|
29039
|
-
const result = applyTaskOps(this.store.planTasks,
|
|
30033
|
+
const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
|
|
29040
30034
|
return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
|
|
29041
30035
|
}
|
|
29042
30036
|
/** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
|
|
@@ -29060,9 +30054,9 @@ var Session = class {
|
|
|
29060
30054
|
* rules, then, for the supervisor, the oscillation guard. `held` are the
|
|
29061
30055
|
* ops the turn already holds, which `ops` join; errors are numbered by `ops`.
|
|
29062
30056
|
*/
|
|
29063
|
-
checkPlannerOps(ops,
|
|
30057
|
+
checkPlannerOps(ops, held2 = []) {
|
|
29064
30058
|
const runners = this.editRunners();
|
|
29065
|
-
const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners),
|
|
30059
|
+
const result = applyTaskOps(this.store.planTasks, ops, runners, this.catalog.edit(runners), held2);
|
|
29066
30060
|
if (!result.ok || !this.supervisorEditing()) return result;
|
|
29067
30061
|
const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
|
|
29068
30062
|
if (!trip) return result;
|
|
@@ -29232,6 +30226,9 @@ var Session = class {
|
|
|
29232
30226
|
onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
|
|
29233
30227
|
if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
|
|
29234
30228
|
},
|
|
30229
|
+
onModelSwitched: (switched) => {
|
|
30230
|
+
void this.followRunnerModel(switched);
|
|
30231
|
+
},
|
|
29235
30232
|
onTaskIdle: ({ taskId, idleSince }) => {
|
|
29236
30233
|
this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
|
|
29237
30234
|
},
|
|
@@ -29352,10 +30349,12 @@ var Session = class {
|
|
|
29352
30349
|
* boundary rewrites `in_progress` to `pending` — nothing is running when a
|
|
29353
30350
|
* session comes off a file — so a surface that re-reads the plan mid-run must
|
|
29354
30351
|
* come here instead, or every task it is watching reads as never started.
|
|
30352
|
+
* An archived snapshot carries the task as it is now: a retry or continue
|
|
30353
|
+
* keeps the archive while the live task runs on and spends more.
|
|
29355
30354
|
*/
|
|
29356
30355
|
get currentPlanState() {
|
|
29357
30356
|
if (!this.plan) return null;
|
|
29358
|
-
const executionLog = this.store.getExecutionLog();
|
|
30357
|
+
const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
|
|
29359
30358
|
const logged = new Set(executionLog.map((s) => s.id));
|
|
29360
30359
|
const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
|
|
29361
30360
|
const record = approvalRecordOf(this.plan);
|
|
@@ -29366,7 +30365,7 @@ var Session = class {
|
|
|
29366
30365
|
phase: "executing",
|
|
29367
30366
|
history: [],
|
|
29368
30367
|
message: "",
|
|
29369
|
-
executionLog
|
|
30368
|
+
executionLog,
|
|
29370
30369
|
pendingTasks,
|
|
29371
30370
|
goal: this.goal,
|
|
29372
30371
|
runners: this.plan.runners,
|
|
@@ -29536,16 +30535,20 @@ var Session = class {
|
|
|
29536
30535
|
opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
|
|
29537
30536
|
} catch (err) {
|
|
29538
30537
|
if (this.opening === pending) this.opening = null;
|
|
29539
|
-
for (const
|
|
30538
|
+
for (const held3 of pending.held ?? []) held3.reject(err);
|
|
29540
30539
|
throw err;
|
|
29541
30540
|
}
|
|
29542
|
-
const
|
|
30541
|
+
const held2 = pending.held ?? [];
|
|
29543
30542
|
pending.held = null;
|
|
29544
|
-
for (const h of
|
|
30543
|
+
for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
|
|
29545
30544
|
try {
|
|
29546
30545
|
const { landed: landed2, effects } = await opened;
|
|
29547
30546
|
this.autorunSubmitted(effects);
|
|
29548
30547
|
return landed2;
|
|
30548
|
+
} catch (err) {
|
|
30549
|
+
if (!(err instanceof PlannerUsageLimitError) || !this.plan) throw err;
|
|
30550
|
+
this.turns.waitOnLimit({ source: "user", ...this.resolveSkillInvocation(goal) });
|
|
30551
|
+
return this.plan;
|
|
29549
30552
|
} finally {
|
|
29550
30553
|
if (this.opening === pending) this.opening = null;
|
|
29551
30554
|
}
|
|
@@ -29560,10 +30563,10 @@ var Session = class {
|
|
|
29560
30563
|
behindOpening(input, forced = false) {
|
|
29561
30564
|
const pending = this.opening;
|
|
29562
30565
|
if (!pending) return null;
|
|
29563
|
-
const
|
|
29564
|
-
if (!
|
|
30566
|
+
const held2 = pending.held;
|
|
30567
|
+
if (!held2) return this.turns.follow(input, forced);
|
|
29565
30568
|
return new Promise((resolve9, reject) => {
|
|
29566
|
-
|
|
30569
|
+
held2.push({ input, forced, resolve: resolve9, reject });
|
|
29567
30570
|
});
|
|
29568
30571
|
}
|
|
29569
30572
|
/**
|
|
@@ -29662,12 +30665,12 @@ var Session = class {
|
|
|
29662
30665
|
await this.liveCatalog();
|
|
29663
30666
|
if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
|
|
29664
30667
|
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
29665
|
-
const
|
|
29666
|
-
if (!
|
|
29667
|
-
const result = this.checkPlannerOps(ops,
|
|
30668
|
+
const held2 = this.conversation.pendingOps();
|
|
30669
|
+
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.");
|
|
30670
|
+
const result = this.checkPlannerOps(ops, held2);
|
|
29668
30671
|
if (!result.ok) return { ok: false, errors: result.errors };
|
|
29669
|
-
if (!this.conversation.submit({ kind: "task_ops", ops: [...
|
|
29670
|
-
const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models());
|
|
30672
|
+
if (!this.conversation.submit({ kind: "task_ops", ops: [...held2, ...ops] })) return refuse("No planning turn is open to take the edit.");
|
|
30673
|
+
const { coerced } = coerceAssignments(result.tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks);
|
|
29671
30674
|
return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
|
|
29672
30675
|
}
|
|
29673
30676
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
@@ -29704,9 +30707,6 @@ var Session = class {
|
|
|
29704
30707
|
autonomousDefault: this.config.autonomousMode,
|
|
29705
30708
|
autorun: this.userRunSettings().autorun,
|
|
29706
30709
|
isolatedExecution: await this.runs.plannerLayout(),
|
|
29707
|
-
// The planner's own model window, when a cached catalog knows it, so the
|
|
29708
|
-
// usage line can show context fill (#49). Unknown stays absent.
|
|
29709
|
-
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
29710
30710
|
fs: this.fsAdapter,
|
|
29711
30711
|
fetcher: this.fetcher,
|
|
29712
30712
|
approval: this.approvalPolicy,
|
|
@@ -29733,7 +30733,7 @@ var Session = class {
|
|
|
29733
30733
|
for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
|
|
29734
30734
|
const runners = this.plan.runners;
|
|
29735
30735
|
const before = this.store.planTasks;
|
|
29736
|
-
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
|
|
30736
|
+
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
|
|
29737
30737
|
if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
|
|
29738
30738
|
const bySupervisor = this.supervisorEditing();
|
|
29739
30739
|
const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
|
|
@@ -29746,6 +30746,7 @@ var Session = class {
|
|
|
29746
30746
|
this.store.resetForRun({ keepScheduledResumes: true });
|
|
29747
30747
|
}
|
|
29748
30748
|
if (bySupervisor) this.approve(added);
|
|
30749
|
+
else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
|
|
29749
30750
|
return coerced.length;
|
|
29750
30751
|
}
|
|
29751
30752
|
preparePlannerTaskUpdates(tasks) {
|
|
@@ -29780,6 +30781,11 @@ var Session = class {
|
|
|
29780
30781
|
if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
|
|
29781
30782
|
}
|
|
29782
30783
|
async executePlan() {
|
|
30784
|
+
if (this.orchestrator.hasLiveWork) {
|
|
30785
|
+
const outcome = await this.execute({ command: "run_plan" }, { kind: "user" });
|
|
30786
|
+
if (!outcome.ok) throw new TaskControlError(outcome.message);
|
|
30787
|
+
return;
|
|
30788
|
+
}
|
|
29783
30789
|
this.idlePlan();
|
|
29784
30790
|
this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
|
|
29785
30791
|
await this.runWholePlan("user");
|
|
@@ -29801,6 +30807,25 @@ var Session = class {
|
|
|
29801
30807
|
await this.orchestrator.approveReview(origin);
|
|
29802
30808
|
});
|
|
29803
30809
|
}
|
|
30810
|
+
/**
|
|
30811
|
+
* Execute while a run is under way (#95): the seam has approved every task,
|
|
30812
|
+
* as executing does, and the run takes them in. A run the scheduler was not
|
|
30813
|
+
* driving, one task started on its own, becomes the plan's run, as Execute
|
|
30814
|
+
* would make it.
|
|
30815
|
+
*/
|
|
30816
|
+
async joinRun(approved) {
|
|
30817
|
+
const plan = this.plan;
|
|
30818
|
+
const scheduling = this.orchestrator.isScheduling;
|
|
30819
|
+
await this.withSave(async () => {
|
|
30820
|
+
plan.approved = true;
|
|
30821
|
+
if (scheduling) await this.orchestrator.tick();
|
|
30822
|
+
else if (this.orchestrator.isReviewApproved) await this.orchestrator.start("user");
|
|
30823
|
+
else await this.orchestrator.approveReview("user");
|
|
30824
|
+
});
|
|
30825
|
+
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.`;
|
|
30826
|
+
this.notice("info", line);
|
|
30827
|
+
return line;
|
|
30828
|
+
}
|
|
29804
30829
|
async approveReview() {
|
|
29805
30830
|
if (!this.plan) throw new NoPlanError("review");
|
|
29806
30831
|
const plan = this.plan;
|
|
@@ -29809,9 +30834,12 @@ var Session = class {
|
|
|
29809
30834
|
await this.withSave(() => this.orchestrator.approveReview("user"));
|
|
29810
30835
|
return plan;
|
|
29811
30836
|
}
|
|
30837
|
+
/** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
|
|
29812
30838
|
async forceStartTask(taskId) {
|
|
29813
30839
|
this.approveTask(taskId);
|
|
29814
|
-
|
|
30840
|
+
if (!this.plan) return;
|
|
30841
|
+
this.requireTask(taskId);
|
|
30842
|
+
return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
|
|
29815
30843
|
}
|
|
29816
30844
|
async startOneTask(taskId, origin) {
|
|
29817
30845
|
if (!this.plan) return;
|
|
@@ -29868,10 +30896,51 @@ var Session = class {
|
|
|
29868
30896
|
async resumeNow(taskId) {
|
|
29869
30897
|
await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
|
|
29870
30898
|
}
|
|
30899
|
+
/**
|
|
30900
|
+
* Keep what waits on the planner's usage limit from being sent at the reset:
|
|
30901
|
+
* it goes with the user's next message instead. False when nothing waits on
|
|
30902
|
+
* a reset.
|
|
30903
|
+
*/
|
|
30904
|
+
cancelPlannerResend() {
|
|
30905
|
+
return this.turns.cancelResend();
|
|
30906
|
+
}
|
|
29871
30907
|
/** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
|
|
29872
30908
|
cancelScheduledResume(taskId) {
|
|
29873
30909
|
return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
|
|
29874
30910
|
}
|
|
30911
|
+
/**
|
|
30912
|
+
* The plan follows a model switch the user made in the runner (#106), and
|
|
30913
|
+
* the task's log says what the plan holds once that settled: a write-back
|
|
30914
|
+
* not made — no model command asked for the change, or the edit failed — is
|
|
30915
|
+
* stated, never shown as made. Once the switch's attempt has ended, the plan
|
|
30916
|
+
* may be another one, so nothing of it is said.
|
|
30917
|
+
*/
|
|
30918
|
+
async followRunnerModel({ taskId, sessionId, from, model, effort, asked, attemptEnded }) {
|
|
30919
|
+
let kept = asked ? void 0 : "no model command in the chat asked for it";
|
|
30920
|
+
const live = () => this.orchestrator.activeSessionMap.get(taskId) === sessionId;
|
|
30921
|
+
const ended = () => this.taskLogs.note(sessionId, taskId, { type: "model_switched", from, to: model, kept: kept ?? "its attempt had already ended" });
|
|
30922
|
+
if (attemptEnded) return ended();
|
|
30923
|
+
if (asked) {
|
|
30924
|
+
try {
|
|
30925
|
+
if (!await this.editor.followRunnerModel(taskId, model, effort, live) && !live()) return ended();
|
|
30926
|
+
} catch (err) {
|
|
30927
|
+
kept = `the plan could not follow it: ${err instanceof Error ? err.message : String(err)}`;
|
|
30928
|
+
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)}`);
|
|
30929
|
+
}
|
|
30930
|
+
}
|
|
30931
|
+
const assigned = this.store.get(taskId)?.assignedModel;
|
|
30932
|
+
if (!kept && assigned?.modelId !== model.model) kept = "the plan did not take the edit";
|
|
30933
|
+
this.taskLogs.note(sessionId, taskId, {
|
|
30934
|
+
type: "model_switched",
|
|
30935
|
+
from,
|
|
30936
|
+
to: model,
|
|
30937
|
+
plan: {
|
|
30938
|
+
...assigned ? { model: { model: assigned.modelId, label: assigned.modelLabel } } : {},
|
|
30939
|
+
...assigned?.thinkingEffort ? { effort: assigned.thinkingEffort } : {}
|
|
30940
|
+
},
|
|
30941
|
+
...kept ? { kept } : {}
|
|
30942
|
+
});
|
|
30943
|
+
}
|
|
29875
30944
|
/** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
|
|
29876
30945
|
notice(level, message) {
|
|
29877
30946
|
this.notifications[level](message);
|
|
@@ -29943,8 +31012,8 @@ var Session = class {
|
|
|
29943
31012
|
if (!this.plan) return EMPTY_RUN;
|
|
29944
31013
|
const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
|
|
29945
31014
|
const view = reduceRun(planned, this.events.statusMessage());
|
|
29946
|
-
const
|
|
29947
|
-
return this.sentHandoff ? reduceRun(
|
|
31015
|
+
const held2 = this.sentHeld ? reduceRun(view, { type: "plan_edit_held", held: this.sentHeld }) : view;
|
|
31016
|
+
return this.sentHandoff ? reduceRun(held2, { type: "isolation_handoff", ...this.sentHandoff }) : held2;
|
|
29948
31017
|
}
|
|
29949
31018
|
/** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
|
|
29950
31019
|
mergeGate(taskId) {
|
|
@@ -30103,6 +31172,7 @@ var Session = class {
|
|
|
30103
31172
|
if (adopting) this.dropPlanScope();
|
|
30104
31173
|
this.plan = plan;
|
|
30105
31174
|
if (adopting) this.supervision.restore(plan.supervision);
|
|
31175
|
+
if (adopting) this.turns.restoreResend();
|
|
30106
31176
|
this.goal = goal;
|
|
30107
31177
|
this.workspace = workspace;
|
|
30108
31178
|
plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
|
|
@@ -30310,6 +31380,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30310
31380
|
messageCount = 0;
|
|
30311
31381
|
interrupting = null;
|
|
30312
31382
|
lastSessionId = null;
|
|
31383
|
+
lastSessionCost = null;
|
|
30313
31384
|
structuredEmitter = new EventEmitter2();
|
|
30314
31385
|
permissionCount = 0;
|
|
30315
31386
|
/**
|
|
@@ -30514,6 +31585,9 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30514
31585
|
nativeSessionId() {
|
|
30515
31586
|
return this.adapter?.nativeSessionId() ?? this.lastSessionId;
|
|
30516
31587
|
}
|
|
31588
|
+
sessionCost() {
|
|
31589
|
+
return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
|
|
31590
|
+
}
|
|
30517
31591
|
answerPermission(id, decision) {
|
|
30518
31592
|
const open = this.permissions.get(id);
|
|
30519
31593
|
if (!open) return false;
|
|
@@ -30561,6 +31635,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30561
31635
|
*/
|
|
30562
31636
|
async restartInterrupted(turn) {
|
|
30563
31637
|
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
31638
|
+
const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
|
|
30564
31639
|
this.generation += 1;
|
|
30565
31640
|
turn.abort.abort();
|
|
30566
31641
|
if (this.adapter) {
|
|
@@ -30570,7 +31645,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30570
31645
|
for (const message of this.queue) this.requeue(message);
|
|
30571
31646
|
try {
|
|
30572
31647
|
this.revokeTools();
|
|
30573
|
-
await this.startAttached({ ...this.startOptions, resumeSessionId });
|
|
31648
|
+
await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
|
|
30574
31649
|
} catch (err) {
|
|
30575
31650
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
30576
31651
|
this.withdrawPermissions();
|
|
@@ -30597,6 +31672,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30597
31672
|
return;
|
|
30598
31673
|
}
|
|
30599
31674
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
31675
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
30600
31676
|
this.withdrawPermissions();
|
|
30601
31677
|
this.baseHandleExit(code);
|
|
30602
31678
|
});
|
|
@@ -30634,6 +31710,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30634
31710
|
this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
30635
31711
|
}).then(() => {
|
|
30636
31712
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
31713
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
30637
31714
|
if (generation2 === this.generation) this.endTurn(turn, turn.reason);
|
|
30638
31715
|
});
|
|
30639
31716
|
this.offerNext();
|
|
@@ -30760,6 +31837,11 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30760
31837
|
case "error":
|
|
30761
31838
|
this.text.failure(event.message);
|
|
30762
31839
|
break;
|
|
31840
|
+
// The plan follows the switch (#106), so a restart after an interrupt
|
|
31841
|
+
// runs what the plan now says rather than what this attempt started on.
|
|
31842
|
+
case "model_switched":
|
|
31843
|
+
this.startOptions = { ...this.startOptions, model: event.to.model };
|
|
31844
|
+
break;
|
|
30763
31845
|
default:
|
|
30764
31846
|
break;
|
|
30765
31847
|
}
|
|
@@ -30840,7 +31922,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
30840
31922
|
model: opts.modelId,
|
|
30841
31923
|
mode: opts.mode ?? "default",
|
|
30842
31924
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
30843
|
-
resumeSessionId: opts.resumeSessionId
|
|
31925
|
+
resumeSessionId: opts.resumeSessionId,
|
|
31926
|
+
resumeCost: opts.resumeCost
|
|
30844
31927
|
},
|
|
30845
31928
|
interruptGraceMs: this.interruptGraceMs,
|
|
30846
31929
|
tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
|
|
@@ -31142,6 +32225,7 @@ export {
|
|
|
31142
32225
|
PlannerNotReadyError,
|
|
31143
32226
|
PlannerTurnDiscardedError,
|
|
31144
32227
|
PlannerTurnStoppedError,
|
|
32228
|
+
PlannerUsageLimitError,
|
|
31145
32229
|
REFUSED_COMMANDS,
|
|
31146
32230
|
RUN_SETTINGS_DEFAULTS,
|
|
31147
32231
|
RunSettingsError,
|
|
@@ -31198,6 +32282,8 @@ export {
|
|
|
31198
32282
|
approvalScopes,
|
|
31199
32283
|
assertWorkspaceExists,
|
|
31200
32284
|
assertWorkspaceIsProject,
|
|
32285
|
+
assignmentIssue,
|
|
32286
|
+
assignmentIssueText,
|
|
31201
32287
|
augmentedPath,
|
|
31202
32288
|
automergeSummary,
|
|
31203
32289
|
autonomyLevelLabel,
|
|
@@ -31214,6 +32300,7 @@ export {
|
|
|
31214
32300
|
canSetDependencies,
|
|
31215
32301
|
canSplitTask,
|
|
31216
32302
|
capConflictFiles,
|
|
32303
|
+
checkoutRule,
|
|
31217
32304
|
classifyCommand,
|
|
31218
32305
|
classifyOutcome,
|
|
31219
32306
|
clearAugmentedPathCache,
|
|
@@ -31227,6 +32314,7 @@ export {
|
|
|
31227
32314
|
commandAllowedBy,
|
|
31228
32315
|
configuredProviders,
|
|
31229
32316
|
continuability,
|
|
32317
|
+
countedSessionCost,
|
|
31230
32318
|
createAiService,
|
|
31231
32319
|
createEmptyPlan,
|
|
31232
32320
|
createSession,
|
|
@@ -31290,6 +32378,7 @@ export {
|
|
|
31290
32378
|
isMeasured,
|
|
31291
32379
|
isOpenAiProvider,
|
|
31292
32380
|
isPlannerBackend,
|
|
32381
|
+
isRepoGroup,
|
|
31293
32382
|
isRunnerApproval,
|
|
31294
32383
|
isSupervisorLevel,
|
|
31295
32384
|
isTaskRunning,
|
|
@@ -31312,6 +32401,7 @@ export {
|
|
|
31312
32401
|
markdownHtml,
|
|
31313
32402
|
markdownTokens,
|
|
31314
32403
|
mcpClientConfig,
|
|
32404
|
+
mergeTarget,
|
|
31315
32405
|
migrateOldConfigDir,
|
|
31316
32406
|
migratePlanIsolation,
|
|
31317
32407
|
migratePlanState,
|
|
@@ -31361,6 +32451,7 @@ export {
|
|
|
31361
32451
|
removedPluginNotice,
|
|
31362
32452
|
renderTaskQueryAnswer,
|
|
31363
32453
|
replayTaskLog,
|
|
32454
|
+
reposWithWork,
|
|
31364
32455
|
researchShellWarning,
|
|
31365
32456
|
researchToolsPath,
|
|
31366
32457
|
resetInstalledPlanner,
|
|
@@ -31374,6 +32465,7 @@ export {
|
|
|
31374
32465
|
resolveWithin,
|
|
31375
32466
|
resumeLabel,
|
|
31376
32467
|
resumeTime,
|
|
32468
|
+
resumedSessionWindow,
|
|
31377
32469
|
runSettingsOf,
|
|
31378
32470
|
runnerForProvider,
|
|
31379
32471
|
runnerModesFrom,
|
|
@@ -31414,6 +32506,7 @@ export {
|
|
|
31414
32506
|
taskRowView,
|
|
31415
32507
|
taskStartedNotice,
|
|
31416
32508
|
taskStatusKind,
|
|
32509
|
+
taskUsageSummary,
|
|
31417
32510
|
textHasTaskOps,
|
|
31418
32511
|
textHasTaskQuery,
|
|
31419
32512
|
titledRefs,
|