@ordewell/core 0.10.3 → 0.10.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{IFileSystem-DCd-rkeH.d.ts → IFileSystem-ClBdp59f.d.ts} +1 -1
- package/dist/{IFileSystem-B8Nkgg4e.d.mts → IFileSystem-Db34maxC.d.mts} +1 -1
- package/dist/{IRunner-CXdYyIZo.d.mts → IRunner-Crkt3tHD.d.ts} +64 -3
- package/dist/{IRunner-QBdYcatb.d.ts → IRunner-lMQIa5Wc.d.mts} +64 -3
- package/dist/{ModeResolver-geqjKkLY.d.mts → ModeResolver-B4hhABNf.d.mts} +1 -1
- package/dist/{ModeResolver-BOU90Nn-.d.ts → ModeResolver-CUR5jCSU.d.ts} +1 -1
- package/dist/{Task-CRVIqSfy.d.mts → Task-D4QHjdGv.d.mts} +93 -11
- package/dist/{Task-CRVIqSfy.d.ts → Task-D4QHjdGv.d.ts} +93 -11
- package/dist/{chunk-A57ITQE7.mjs → chunk-4QLFM73A.mjs} +34 -5
- package/dist/chunk-4QLFM73A.mjs.map +1 -0
- package/dist/{chunk-FH6M5QZG.mjs → chunk-6LEA6WAM.mjs} +88 -7
- package/dist/chunk-6LEA6WAM.mjs.map +1 -0
- package/dist/{chunk-645FALZX.mjs → chunk-F3KIX7WR.mjs} +3 -3
- package/dist/{chunk-OYUSADFM.mjs → chunk-NJGID6BG.mjs} +3 -1
- package/dist/chunk-NJGID6BG.mjs.map +1 -0
- package/dist/{chunk-SM6BV6XS.mjs → chunk-RT4NQ2BZ.mjs} +3 -1
- package/dist/chunk-RT4NQ2BZ.mjs.map +1 -0
- package/dist/index.d.mts +307 -66
- package/dist/index.d.ts +307 -66
- package/dist/index.js +1688 -402
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1567 -388
- package/dist/index.mjs.map +1 -1
- package/dist/parsing.d.mts +2 -2
- package/dist/parsing.d.ts +2 -2
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +3 -3
- package/dist/{plan-utils-oC8bO4pN.d.ts → plan-utils-BVJCzNvm.d.ts} +204 -19
- package/dist/{plan-utils-KU1Rse82.d.mts → plan-utils-wI8MuDBC.d.mts} +204 -19
- package/dist/plan-utils.d.mts +4 -4
- package/dist/plan-utils.d.ts +4 -4
- package/dist/plan-utils.js +94 -5
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +13 -3
- package/dist/testing.d.mts +6 -3
- package/dist/testing.d.ts +6 -3
- package/dist/testing.js +7 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +7 -2
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-A57ITQE7.mjs.map +0 -1
- package/dist/chunk-FH6M5QZG.mjs.map +0 -1
- package/dist/chunk-OYUSADFM.mjs.map +0 -1
- package/dist/chunk-SM6BV6XS.mjs.map +0 -1
- /package/dist/{chunk-645FALZX.mjs.map → chunk-F3KIX7WR.mjs.map} +0 -0
package/dist/index.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
|
|
|
@@ -6406,7 +6417,7 @@ var CODEX_MANIFEST = {
|
|
|
6406
6417
|
}
|
|
6407
6418
|
},
|
|
6408
6419
|
modelDiscovery: {
|
|
6409
|
-
// Codex has no `codex models` subcommand (see
|
|
6420
|
+
// Codex has no `codex models` subcommand (see ADR-0004). Discovery
|
|
6410
6421
|
// tries three sources in order, each falling through to the next:
|
|
6411
6422
|
//
|
|
6412
6423
|
// 1. `codex app-server` stdio JSON-RPC (`initialize` → `model/list`) —
|
|
@@ -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,12 +6522,15 @@ 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
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6529
|
+
return preferredFirst(models, manifest);
|
|
6530
|
+
}
|
|
6531
|
+
function preferredFirst(models, manifest) {
|
|
6532
|
+
const prefOrder = manifest.modelDiscovery.preferredPatterns?.map((p) => p.id) ?? [];
|
|
6533
|
+
return [...models].sort((a, b) => {
|
|
6514
6534
|
const ai = prefOrder.indexOf(a.modelId);
|
|
6515
6535
|
const bi = prefOrder.indexOf(b.modelId);
|
|
6516
6536
|
if (ai >= 0 && bi >= 0) return ai - bi;
|
|
@@ -6518,7 +6538,37 @@ function parseOpenCodeModelsVerbose(stdout, manifest) {
|
|
|
6518
6538
|
if (bi >= 0) return 1;
|
|
6519
6539
|
return a.modelId.localeCompare(b.modelId);
|
|
6520
6540
|
});
|
|
6521
|
-
|
|
6541
|
+
}
|
|
6542
|
+
function parseOpenCodeModelsApi(stdout, manifest) {
|
|
6543
|
+
let data;
|
|
6544
|
+
try {
|
|
6545
|
+
data = JSON.parse(stdout)?.data;
|
|
6546
|
+
} catch {
|
|
6547
|
+
return [];
|
|
6548
|
+
}
|
|
6549
|
+
if (!Array.isArray(data)) return [];
|
|
6550
|
+
const prefLabels = new Map((manifest.modelDiscovery.preferredPatterns ?? []).map((p) => [p.id, p.label]));
|
|
6551
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6552
|
+
const models = [];
|
|
6553
|
+
for (const entry of data) {
|
|
6554
|
+
if (!entry || typeof entry.providerID !== "string" || typeof entry.modelID !== "string") continue;
|
|
6555
|
+
if (entry.enabled === false) continue;
|
|
6556
|
+
const fullId = `${entry.providerID}/${entry.modelID}`;
|
|
6557
|
+
if (seen.has(fullId)) continue;
|
|
6558
|
+
seen.add(fullId);
|
|
6559
|
+
const variantIds = Array.isArray(entry.variants) ? entry.variants.flatMap((v) => {
|
|
6560
|
+
const id = v?.id;
|
|
6561
|
+
return typeof id === "string" && id ? [id] : [];
|
|
6562
|
+
}) : [];
|
|
6563
|
+
models.push({
|
|
6564
|
+
modelId: fullId,
|
|
6565
|
+
modelLabel: prefLabels.get(fullId) || typeof entry.name === "string" && entry.name || entry.modelID,
|
|
6566
|
+
runnerProvider: entry.providerID,
|
|
6567
|
+
variants: variantIds.map((id) => ({ id, label: id.charAt(0).toUpperCase() + id.slice(1) })),
|
|
6568
|
+
...contextLimit(entry.limit)
|
|
6569
|
+
});
|
|
6570
|
+
}
|
|
6571
|
+
return preferredFirst(models, manifest);
|
|
6522
6572
|
}
|
|
6523
6573
|
|
|
6524
6574
|
// src/plugins/builtin/opencode.manifest.ts
|
|
@@ -6534,13 +6584,17 @@ var OPENCODE_MANIFEST = {
|
|
|
6534
6584
|
modelDiscovery: {
|
|
6535
6585
|
method: "command",
|
|
6536
6586
|
discoveryCommands: [
|
|
6587
|
+
// v1 prints per-model variants under --verbose; v2 rejects the flag (a
|
|
6588
|
+
// fast non-zero exit) and serves the same catalog, variants included, over
|
|
6589
|
+
// `api model.list`. v1 is asked first so it never reaches the `api` form.
|
|
6537
6590
|
{ command: "opencode", args: ["models", "--verbose"], parser: parseOpenCodeModelsVerbose },
|
|
6591
|
+
{ command: "opencode", args: ["api", "model.list"], parser: parseOpenCodeModelsApi },
|
|
6538
6592
|
{ command: "opencode", args: ["models"], parser: parseOpenCodeModels }
|
|
6539
6593
|
],
|
|
6540
6594
|
// No fallbackModels: the list must always reflect what `opencode models`
|
|
6541
6595
|
// actually reports for THIS user (their configured providers only). A
|
|
6542
6596
|
// hardcoded fallback previously leaked models the user didn't have.
|
|
6543
|
-
// No static variants — per-model variants
|
|
6597
|
+
// No static variants — per-model variants come from the first two commands above
|
|
6544
6598
|
providerQualifiedIds: true
|
|
6545
6599
|
},
|
|
6546
6600
|
contextFile: "AGENTS.md",
|
|
@@ -8151,6 +8205,7 @@ var LOG_KINDS = {
|
|
|
8151
8205
|
turn_start: true,
|
|
8152
8206
|
command: true,
|
|
8153
8207
|
command_output: true,
|
|
8208
|
+
model_switched: true,
|
|
8154
8209
|
compaction: true,
|
|
8155
8210
|
runner_loaded: true,
|
|
8156
8211
|
turn_end: true,
|
|
@@ -8186,6 +8241,11 @@ ${s.content}` : ""}`).join("\n");
|
|
|
8186
8241
|
return `${event.messageId ? `${event.messageId} ` : ""}/${event.name}${event.args ? ` ${event.args}` : ""}`;
|
|
8187
8242
|
case "command_output":
|
|
8188
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
|
+
}
|
|
8189
8249
|
case "compaction":
|
|
8190
8250
|
return `${event.trigger}${event.preTokens !== void 0 && event.postTokens !== void 0 ? `, ${event.preTokens} \u2192 ${event.postTokens} tokens` : ""}`;
|
|
8191
8251
|
case "runner_loaded":
|
|
@@ -9745,6 +9805,7 @@ function canUseToolAnswer({ input, suggestions }, decision) {
|
|
|
9745
9805
|
...decision.decision === "allowForTask" && suggestions.length > 0 ? { updatedPermissions: suggestions } : {}
|
|
9746
9806
|
};
|
|
9747
9807
|
}
|
|
9808
|
+
var MODEL_COMMAND = "model";
|
|
9748
9809
|
var SUBAGENT_TOOLS = /* @__PURE__ */ new Set(["Agent", "Task"]);
|
|
9749
9810
|
function usageRecord(usage, model, subagentId) {
|
|
9750
9811
|
const record = {
|
|
@@ -9806,9 +9867,9 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9806
9867
|
/** The model answering the planner's current message, as its `message_start` named it. */
|
|
9807
9868
|
plannerModel;
|
|
9808
9869
|
/**
|
|
9809
|
-
* The session's `total_cost_usd` as last
|
|
9810
|
-
*
|
|
9811
|
-
*
|
|
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.
|
|
9812
9873
|
*/
|
|
9813
9874
|
reportedCostUsd = 0;
|
|
9814
9875
|
reportedModelCostUsd = /* @__PURE__ */ new Map();
|
|
@@ -9849,6 +9910,19 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9849
9910
|
terminalOnly = /* @__PURE__ */ new Set();
|
|
9850
9911
|
/** Whether this process's `runner_loaded` went out: its first `init` sends it. */
|
|
9851
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 = [];
|
|
9852
9926
|
spawnSpec(opts, workspace) {
|
|
9853
9927
|
if (opts.kind === "task") return this.taskSpawnSpec(opts);
|
|
9854
9928
|
const args = [
|
|
@@ -9874,12 +9948,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9874
9948
|
if (opts.effort && opts.effort !== "adaptive") args.push("--effort", opts.effort);
|
|
9875
9949
|
if (opts.resumeSessionId) {
|
|
9876
9950
|
args.push("--resume", opts.resumeSessionId);
|
|
9877
|
-
this.
|
|
9951
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
9878
9952
|
this.pendingResume = opts.resumeSessionId;
|
|
9879
9953
|
}
|
|
9880
9954
|
this.configDir = workspace.CLAUDE_CONFIG_DIR ?? process.env.CLAUDE_CONFIG_DIR ?? path13.join(os6.homedir(), ".claude");
|
|
9881
9955
|
return { command: "claude", args, env: NO_AUTO_MEMORY };
|
|
9882
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
|
+
}
|
|
9883
9968
|
savedOutput(abs) {
|
|
9884
9969
|
return this.configDir !== null && this.sessionId !== null && inSavedOutput(abs, this.configDir, this.sessionId);
|
|
9885
9970
|
}
|
|
@@ -9894,6 +9979,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9894
9979
|
*/
|
|
9895
9980
|
taskSpawnSpec(opts) {
|
|
9896
9981
|
this.requestedMode = opts.flags.permissionMode;
|
|
9982
|
+
this.followedModel = opts.model;
|
|
9897
9983
|
const args = [
|
|
9898
9984
|
...TASK_PROTOCOL_ARGS,
|
|
9899
9985
|
"--permission-prompt-tool",
|
|
@@ -9907,7 +9993,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9907
9993
|
if (opts.model) args.push("--model", opts.model);
|
|
9908
9994
|
if (opts.resumeSessionId) {
|
|
9909
9995
|
args.push("--resume", opts.resumeSessionId);
|
|
9910
|
-
this.
|
|
9996
|
+
this.countFrom(opts.resumeSessionId, opts.resumeCost);
|
|
9911
9997
|
this.pendingResume = opts.resumeSessionId;
|
|
9912
9998
|
}
|
|
9913
9999
|
if (opts.mcp) args.push("--append-system-prompt", TASK_COMPLETION_CONTRACT, ...this.ordewellServerArgs(opts.mcp, "task"));
|
|
@@ -9949,6 +10035,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
9949
10035
|
if (opts.kind !== "task") return;
|
|
9950
10036
|
const initialized = (await this.control({ subtype: "initialize" }, MODEL_PIN_TIMEOUT_MS))?.response;
|
|
9951
10037
|
this.listedCommands = initialized?.commands ?? [];
|
|
10038
|
+
this.listedModels = initialized?.models ?? [];
|
|
9952
10039
|
if (opts.flags.permissionMode !== "plan" || !opts.model) return;
|
|
9953
10040
|
const resolved = initialized?.models?.find((m) => m.value === opts.model)?.resolvedModel;
|
|
9954
10041
|
if (resolved && resolved !== opts.model) await this.control({ subtype: "set_model", model: resolved }, MODEL_PIN_TIMEOUT_MS);
|
|
@@ -10002,6 +10089,7 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10002
10089
|
if (this.role !== "task" || !this.process || this.interruptRequested || this.pendingResume) return false;
|
|
10003
10090
|
const uuid = randomUUID();
|
|
10004
10091
|
this.steers.set(uuid, id);
|
|
10092
|
+
this.noteModelCommand(text, 2);
|
|
10005
10093
|
this.writeLine({ type: "user", uuid, message: { role: "user", content: [{ type: "text", text }] } });
|
|
10006
10094
|
return true;
|
|
10007
10095
|
}
|
|
@@ -10053,12 +10141,23 @@ var ClaudeCodeAdapter = class extends StdioAgentAdapter {
|
|
|
10053
10141
|
}
|
|
10054
10142
|
turnPayload(message) {
|
|
10055
10143
|
this.openTurnState();
|
|
10144
|
+
this.noteModelCommand(message, 1);
|
|
10056
10145
|
return `${JSON.stringify({
|
|
10057
10146
|
type: "user",
|
|
10058
10147
|
message: { role: "user", content: [{ type: "text", text: message }] }
|
|
10059
10148
|
})}
|
|
10060
10149
|
`;
|
|
10061
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
|
+
}
|
|
10062
10161
|
openTurnState() {
|
|
10063
10162
|
this.turnHasText = false;
|
|
10064
10163
|
this.interruptRequested = false;
|
|
@@ -10170,6 +10269,7 @@ ${block.text}` : block.text });
|
|
|
10170
10269
|
this.clearFollowOnTimer();
|
|
10171
10270
|
if (this.refuseOtherMode(msg, emit)) return;
|
|
10172
10271
|
this.readLoaded(msg, emit);
|
|
10272
|
+
this.followModel(msg.model, emit);
|
|
10173
10273
|
}
|
|
10174
10274
|
if (msg.subtype === "commands_changed" && Array.isArray(msg.commands)) this.listedCommands = msg.commands;
|
|
10175
10275
|
if (msg.subtype === "compact_boundary") {
|
|
@@ -10192,6 +10292,7 @@ ${block.text}` : block.text });
|
|
|
10192
10292
|
return;
|
|
10193
10293
|
case "result":
|
|
10194
10294
|
this.reportSessionCost(msg, emit);
|
|
10295
|
+
for (const command of this.modelCommands) command.turnsLeft = Math.max(0, command.turnsLeft - 1);
|
|
10195
10296
|
if (this.interruptRequested && (msg.is_error || msg.subtype !== "success")) {
|
|
10196
10297
|
this.interruptRequested = false;
|
|
10197
10298
|
this.closeTurn({ type: "turn_end", interrupted: true }, emit);
|
|
@@ -10211,6 +10312,55 @@ ${block.text}` : block.text });
|
|
|
10211
10312
|
return;
|
|
10212
10313
|
}
|
|
10213
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
|
+
}
|
|
10214
10364
|
/**
|
|
10215
10365
|
* `--permission-mode auto` on a model or account without auto mode is not
|
|
10216
10366
|
* refused: the CLI says nothing and starts in `default`, then asks about every
|
|
@@ -10294,8 +10444,10 @@ ${block.text}` : block.text });
|
|
|
10294
10444
|
* {@link AgentEvent}), so nothing here has to reconcile with it.
|
|
10295
10445
|
*/
|
|
10296
10446
|
handleStreamEvent(event, emit) {
|
|
10297
|
-
if (event.type === "message_start")
|
|
10298
|
-
|
|
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) });
|
|
10299
10451
|
else if (event.type === "content_block_start" && event.content_block?.type === "text") {
|
|
10300
10452
|
this.pendingBreak = this.turnHasText;
|
|
10301
10453
|
} else if (event.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
|
@@ -10310,7 +10462,8 @@ ${block.text}` : block.text });
|
|
|
10310
10462
|
* What only the result line knows: the cost, which covers every call the
|
|
10311
10463
|
* session made — subagents included, since none of their lines carries one —
|
|
10312
10464
|
* and the planner model's window. `total_cost_usd` is a running total, so a
|
|
10313
|
-
* 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
|
|
10314
10467
|
* baseline: its total includes turns counted before, and a turn's own share
|
|
10315
10468
|
* cannot be told apart from them.
|
|
10316
10469
|
*/
|
|
@@ -10890,6 +11043,97 @@ var CodexRpc = class {
|
|
|
10890
11043
|
}
|
|
10891
11044
|
};
|
|
10892
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
|
+
|
|
10893
11137
|
// src/services/harness/CodexAdapter.ts
|
|
10894
11138
|
var HANDSHAKE_TIMEOUT_MS = 3e4;
|
|
10895
11139
|
var MCP_ATTACH_TIMEOUT_MS2 = 1e4;
|
|
@@ -11033,6 +11277,11 @@ var CodexAdapter = class extends StdioAgentAdapter {
|
|
|
11033
11277
|
turn = null;
|
|
11034
11278
|
/** The last turn this adapter ended — so its second end signal, landing late, ends nothing. */
|
|
11035
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();
|
|
11036
11285
|
/** An interrupt was asked for during this turn, so however it ends, it was cut short. */
|
|
11037
11286
|
interruptRequested = false;
|
|
11038
11287
|
/**
|
|
@@ -11428,6 +11677,9 @@ ${this.exitMessage()}`);
|
|
|
11428
11677
|
case "thread/tokenUsage/updated":
|
|
11429
11678
|
this.emitUsage(msg.params, emit);
|
|
11430
11679
|
return;
|
|
11680
|
+
case "account/rateLimits/updated":
|
|
11681
|
+
this.rateLimits = mergeCodexRateLimits(this.rateLimits, msg.params);
|
|
11682
|
+
return;
|
|
11431
11683
|
case "turn/started": {
|
|
11432
11684
|
if (this.subagentOf(msg.params?.threadId)) return;
|
|
11433
11685
|
this.turnHasText = false;
|
|
@@ -11468,7 +11720,7 @@ ${this.exitMessage()}`);
|
|
|
11468
11720
|
const failure = msg.params;
|
|
11469
11721
|
if (failure?.willRetry) return;
|
|
11470
11722
|
this.closeTurn(emit);
|
|
11471
|
-
emit(
|
|
11723
|
+
emit(this.turnFailed(failure?.error));
|
|
11472
11724
|
return;
|
|
11473
11725
|
}
|
|
11474
11726
|
case "turn/completed": {
|
|
@@ -11479,7 +11731,7 @@ ${this.exitMessage()}`);
|
|
|
11479
11731
|
if (this.interruptRequested || turn?.status === "interrupted") {
|
|
11480
11732
|
emit({ type: "turn_end", interrupted: true });
|
|
11481
11733
|
} else if (turn?.status === "failed") {
|
|
11482
|
-
emit(
|
|
11734
|
+
emit(this.turnFailed(turn.error));
|
|
11483
11735
|
} else {
|
|
11484
11736
|
emit({ type: "turn_end" });
|
|
11485
11737
|
}
|
|
@@ -11491,6 +11743,11 @@ ${this.exitMessage()}`);
|
|
|
11491
11743
|
return;
|
|
11492
11744
|
}
|
|
11493
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
|
+
}
|
|
11494
11751
|
turnStarted(id) {
|
|
11495
11752
|
const turn = this.turn;
|
|
11496
11753
|
if (!turn || turn.id || typeof id !== "string") return;
|
|
@@ -11948,9 +12205,9 @@ function recordUnusable(servers, approval) {
|
|
|
11948
12205
|
for (const server4 of servers) approval?.record?.(startRequest(server4), false, "unsupported");
|
|
11949
12206
|
}
|
|
11950
12207
|
var NOT_APPROVED = "not approved for planning";
|
|
11951
|
-
function heldServersNote(
|
|
12208
|
+
function heldServersNote(held2, unreadable = []) {
|
|
11952
12209
|
const lines = [
|
|
11953
|
-
...
|
|
12210
|
+
...held2.map(({ server: server4, reason }) => `- ${server4.name} (${server4.source}): ${reason}`),
|
|
11954
12211
|
...unreadable.map((file) => `- ${file.source}: Ordewell could not read it (${file.reason})`)
|
|
11955
12212
|
];
|
|
11956
12213
|
if (lines.length === 0) return "";
|
|
@@ -12087,8 +12344,8 @@ var PendingSteers = class {
|
|
|
12087
12344
|
* False when it was already delivered, so a late failure does not recall it.
|
|
12088
12345
|
*/
|
|
12089
12346
|
forget(id) {
|
|
12090
|
-
for (const [messageId,
|
|
12091
|
-
if (
|
|
12347
|
+
for (const [messageId, held2] of this.pending) {
|
|
12348
|
+
if (held2 !== id) continue;
|
|
12092
12349
|
this.pending.delete(messageId);
|
|
12093
12350
|
return true;
|
|
12094
12351
|
}
|
|
@@ -12166,9 +12423,9 @@ var ChildSessions = class {
|
|
|
12166
12423
|
adopt(child, callId) {
|
|
12167
12424
|
if (this.owners.get(child)) return null;
|
|
12168
12425
|
this.owners.set(child, callId);
|
|
12169
|
-
const
|
|
12426
|
+
const held2 = this.held.get(child) ?? [];
|
|
12170
12427
|
this.held.delete(child);
|
|
12171
|
-
return
|
|
12428
|
+
return held2;
|
|
12172
12429
|
}
|
|
12173
12430
|
owns(callId) {
|
|
12174
12431
|
return [...this.owners.values()].includes(callId);
|
|
@@ -12692,11 +12949,11 @@ var OpenCodeV2 = class {
|
|
|
12692
12949
|
adoptChild(data, state, onEvent, turn) {
|
|
12693
12950
|
const child = data.metadata?.sessionID;
|
|
12694
12951
|
const callId = data.id;
|
|
12695
|
-
const
|
|
12696
|
-
if (!
|
|
12952
|
+
const held2 = child && callId ? state.children.adopt(child, callId) : null;
|
|
12953
|
+
if (!held2 || !callId) return;
|
|
12697
12954
|
const model = state.stepModels.get(data.assistantMessageID ?? "");
|
|
12698
12955
|
onEvent({ type: "subagent_started", subagentId: callId, brief: state.briefs.get(callId) ?? "", ...model ? { model } : {} });
|
|
12699
|
-
for (const frame of
|
|
12956
|
+
for (const frame of held2) this.onFrame(frame, state, turn, onEvent);
|
|
12700
12957
|
}
|
|
12701
12958
|
finishChild(callId, success, output, state, onEvent) {
|
|
12702
12959
|
if (!state.children.owns(callId)) return;
|
|
@@ -13408,13 +13665,13 @@ var OpenCodeAdapter = class {
|
|
|
13408
13665
|
trackSubagent(part, callId, turn, onEvent) {
|
|
13409
13666
|
const state = part.state;
|
|
13410
13667
|
const child = state?.metadata?.sessionId;
|
|
13411
|
-
const
|
|
13412
|
-
if (
|
|
13668
|
+
const held2 = child ? turn.children.adopt(child, callId) : null;
|
|
13669
|
+
if (held2) {
|
|
13413
13670
|
const input = state?.input ?? {};
|
|
13414
13671
|
const brief2 = typeof input.description === "string" ? input.description : typeof input.prompt === "string" ? input.prompt : "";
|
|
13415
13672
|
const model = flatModelId(state?.metadata?.model?.providerID, state?.metadata?.model?.modelID);
|
|
13416
13673
|
onEvent({ type: "subagent_started", subagentId: callId, brief: brief2, ...model ? { model } : {} });
|
|
13417
|
-
for (const frame of
|
|
13674
|
+
for (const frame of held2) this.onFrame(frame, turn, onEvent);
|
|
13418
13675
|
}
|
|
13419
13676
|
const status = state?.status;
|
|
13420
13677
|
if (status === "completed" || status === "error") {
|
|
@@ -13644,61 +13901,6 @@ var OPENCODE_REPO_CONFIG = {
|
|
|
13644
13901
|
}
|
|
13645
13902
|
};
|
|
13646
13903
|
|
|
13647
|
-
// src/utils/wallTime.ts
|
|
13648
|
-
var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
|
|
13649
|
-
function lastMatch(re, text) {
|
|
13650
|
-
let last = null;
|
|
13651
|
-
for (const m of text.matchAll(re)) last = m;
|
|
13652
|
-
return last;
|
|
13653
|
-
}
|
|
13654
|
-
function hour24(hour, meridiem) {
|
|
13655
|
-
const h = Number(hour) % 12;
|
|
13656
|
-
return meridiem.toLowerCase() === "pm" ? h + 12 : h;
|
|
13657
|
-
}
|
|
13658
|
-
function month(name) {
|
|
13659
|
-
if (!name) return void 0;
|
|
13660
|
-
const at = MONTHS.indexOf(name.toLowerCase());
|
|
13661
|
-
return at < 0 ? void 0 : at;
|
|
13662
|
-
}
|
|
13663
|
-
function placeWallTime(wall, zone, now) {
|
|
13664
|
-
const today = wallFields(now, zone);
|
|
13665
|
-
const year = wall.year ?? today.year;
|
|
13666
|
-
const monthIndex = wall.month ?? today.month;
|
|
13667
|
-
const day = wall.day ?? today.day;
|
|
13668
|
-
const at = fromWall(year, monthIndex, day, wall.hour, wall.minute, zone);
|
|
13669
|
-
if (wall.day !== void 0 || at.getTime() >= now.getTime() - HOUR_MS) return at;
|
|
13670
|
-
return fromWall(year, monthIndex, day + 1, wall.hour, wall.minute, zone);
|
|
13671
|
-
}
|
|
13672
|
-
var HOUR_MS = 60 * 6e4;
|
|
13673
|
-
function wallFields(instant, zone) {
|
|
13674
|
-
if (!zone) {
|
|
13675
|
-
return { year: instant.getFullYear(), month: instant.getMonth(), day: instant.getDate(), hour: instant.getHours(), minute: instant.getMinutes(), second: instant.getSeconds() };
|
|
13676
|
-
}
|
|
13677
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
13678
|
-
timeZone: zone,
|
|
13679
|
-
hourCycle: "h23",
|
|
13680
|
-
year: "numeric",
|
|
13681
|
-
month: "numeric",
|
|
13682
|
-
day: "numeric",
|
|
13683
|
-
hour: "numeric",
|
|
13684
|
-
minute: "numeric",
|
|
13685
|
-
second: "numeric"
|
|
13686
|
-
}).formatToParts(instant);
|
|
13687
|
-
const field = (type) => Number(parts.find((p) => p.type === type)?.value ?? 0);
|
|
13688
|
-
return { year: field("year"), month: field("month") - 1, day: field("day"), hour: field("hour"), minute: field("minute"), second: field("second") };
|
|
13689
|
-
}
|
|
13690
|
-
function zoneOffsetMs(instant, zone) {
|
|
13691
|
-
const f = wallFields(new Date(instant), zone);
|
|
13692
|
-
return Date.UTC(f.year, f.month, f.day, f.hour, f.minute, f.second) - Math.floor(instant / 1e3) * 1e3;
|
|
13693
|
-
}
|
|
13694
|
-
function fromWall(year, monthIndex, day, hour, minute, zone) {
|
|
13695
|
-
if (!zone) return new Date(year, monthIndex, day, hour, minute);
|
|
13696
|
-
const asUtc = Date.UTC(year, monthIndex, day, hour, minute);
|
|
13697
|
-
let instant = asUtc - zoneOffsetMs(asUtc, zone);
|
|
13698
|
-
instant = asUtc - zoneOffsetMs(instant, zone);
|
|
13699
|
-
return new Date(instant);
|
|
13700
|
-
}
|
|
13701
|
-
|
|
13702
13904
|
// src/services/harness/claudeUsageLimit.ts
|
|
13703
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;
|
|
13704
13906
|
function claudeUsageLimitReset(text, now) {
|
|
@@ -13714,21 +13916,6 @@ function claudeUsageLimitReset(text, now) {
|
|
|
13714
13916
|
return placeWallTime(wall, zone, now);
|
|
13715
13917
|
}
|
|
13716
13918
|
|
|
13717
|
-
// src/services/harness/codexUsageLimit.ts
|
|
13718
|
-
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;
|
|
13719
|
-
function codexUsageLimitReset(text, now) {
|
|
13720
|
-
const m = lastMatch(CODEX_RESET_RE, text);
|
|
13721
|
-
if (!m) return null;
|
|
13722
|
-
const [, mon, day, year, hour, minute, meridiem] = m;
|
|
13723
|
-
const wall = { hour: hour24(hour, meridiem), minute: Number(minute) };
|
|
13724
|
-
if (mon) {
|
|
13725
|
-
const at = month(mon);
|
|
13726
|
-
if (at === void 0) return null;
|
|
13727
|
-
Object.assign(wall, { month: at, day: Number(day), year: Number(year) });
|
|
13728
|
-
}
|
|
13729
|
-
return placeWallTime(wall, void 0, now);
|
|
13730
|
-
}
|
|
13731
|
-
|
|
13732
13919
|
// src/services/harness/connectors.ts
|
|
13733
13920
|
var CONNECTORS = {
|
|
13734
13921
|
"claude-code": { create: (deps) => new ClaudeCodeAdapter(deps), ordewellTools: CLAUDE_ORDEWELL, repoConfig: CLAUDE_REPO_CONFIG, usageLimitReset: claudeUsageLimitReset },
|
|
@@ -14059,6 +14246,7 @@ function parseCodexModelsCache(content) {
|
|
|
14059
14246
|
}
|
|
14060
14247
|
}
|
|
14061
14248
|
var GEMINI_MODELS_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
14249
|
+
var NEGATIVE_TTL_MS = 6e4;
|
|
14062
14250
|
function geminiDiscoveryCacheKey(apiKey) {
|
|
14063
14251
|
const dir = globalDataDir();
|
|
14064
14252
|
const hash = apiKey.slice(-8);
|
|
@@ -14126,7 +14314,8 @@ async function discoverGeminiModels(apiKey, baseUrl, fetchImpl) {
|
|
|
14126
14314
|
modelId,
|
|
14127
14315
|
modelLabel: m.displayName || modelId,
|
|
14128
14316
|
runnerProvider: "gemini",
|
|
14129
|
-
variants: []
|
|
14317
|
+
variants: [],
|
|
14318
|
+
...typeof m.inputTokenLimit === "number" && m.inputTokenLimit > 0 ? { contextWindow: m.inputTokenLimit } : {}
|
|
14130
14319
|
});
|
|
14131
14320
|
}
|
|
14132
14321
|
}
|
|
@@ -14188,26 +14377,33 @@ async function discoverFromApi(manifest, config, fetchImpl) {
|
|
|
14188
14377
|
return null;
|
|
14189
14378
|
}
|
|
14190
14379
|
}
|
|
14380
|
+
function wasKilled(err) {
|
|
14381
|
+
return !!err && typeof err === "object" && err.killed === true;
|
|
14382
|
+
}
|
|
14191
14383
|
async function discoverFromCommand(manifest, execImpl) {
|
|
14192
14384
|
const discovery = manifest.modelDiscovery;
|
|
14193
14385
|
const commands = discovery.discoveryCommands ?? [
|
|
14194
14386
|
{ command: discovery.command, args: discovery.args || [], parser: discovery.parser }
|
|
14195
14387
|
];
|
|
14196
14388
|
const runCommands = async () => {
|
|
14389
|
+
let timedOut2 = false;
|
|
14197
14390
|
for (const cmd of commands) {
|
|
14198
14391
|
try {
|
|
14199
14392
|
const { stdout } = await execImpl(`${cmd.command} ${cmd.args.join(" ")}`, { timeout: 45e3 });
|
|
14200
14393
|
const models = parseModelOutput(stdout, cmd.parser, manifest);
|
|
14201
|
-
if (models.length > 0) return applyVariants(mergeCanonicalAliases(models, manifest), manifest);
|
|
14202
|
-
} catch {
|
|
14394
|
+
if (models.length > 0) return { models: applyVariants(mergeCanonicalAliases(models, manifest), manifest), timedOut: timedOut2 };
|
|
14395
|
+
} catch (err) {
|
|
14396
|
+
if (wasKilled(err)) timedOut2 = true;
|
|
14203
14397
|
}
|
|
14204
14398
|
}
|
|
14205
|
-
return null;
|
|
14399
|
+
return { models: null, timedOut: timedOut2 };
|
|
14206
14400
|
};
|
|
14207
14401
|
const first = await runCommands();
|
|
14208
|
-
if (first) return { models: first, fromFallback: false };
|
|
14209
|
-
|
|
14210
|
-
|
|
14402
|
+
if (first.models) return { models: first.models, fromFallback: false };
|
|
14403
|
+
if (first.timedOut) {
|
|
14404
|
+
const second = await runCommands();
|
|
14405
|
+
if (second.models) return { models: second.models, fromFallback: false };
|
|
14406
|
+
}
|
|
14211
14407
|
return { models: applyVariants(buildFallbackModels(manifest), manifest), fromFallback: true };
|
|
14212
14408
|
}
|
|
14213
14409
|
function mergeCanonicalAliases(discovered, manifest) {
|
|
@@ -14244,16 +14440,33 @@ function parseLineByLine(stdout) {
|
|
|
14244
14440
|
}
|
|
14245
14441
|
var ModelDiscovery = class {
|
|
14246
14442
|
cache = /* @__PURE__ */ new Map();
|
|
14443
|
+
/**
|
|
14444
|
+
* Degraded results (a fallback list, or empty) remembered for
|
|
14445
|
+
* {@link NEGATIVE_TTL_MS}. They are deliberately not cached — a transient
|
|
14446
|
+
* failure must heal — but re-running a failing CLI on every lookup is the
|
|
14447
|
+
* other extreme.
|
|
14448
|
+
*/
|
|
14449
|
+
misses = /* @__PURE__ */ new Map();
|
|
14450
|
+
/**
|
|
14451
|
+
* Discoveries still running, so a second lookup of the same runner joins the
|
|
14452
|
+
* first instead of spawning the CLI again. A slow CLI is the case this
|
|
14453
|
+
* exists for: a planner switch and a runner-list refresh land inside one
|
|
14454
|
+
* another's wait, and two `opencode models` calls race each other for the
|
|
14455
|
+
* same background service.
|
|
14456
|
+
*/
|
|
14457
|
+
inflight = /* @__PURE__ */ new Map();
|
|
14247
14458
|
registry;
|
|
14248
14459
|
execImpl;
|
|
14249
14460
|
fetchImpl;
|
|
14250
14461
|
appServerClient;
|
|
14251
14462
|
readFileImpl;
|
|
14252
|
-
|
|
14463
|
+
now;
|
|
14464
|
+
constructor(registry, execImpl = defaultExec2, fetchImpl, appServerClient = defaultAppServerClient, readFileImpl, now = Date.now) {
|
|
14253
14465
|
this.registry = registry;
|
|
14254
14466
|
this.execImpl = execImpl;
|
|
14255
14467
|
this.fetchImpl = fetchImpl;
|
|
14256
14468
|
this.appServerClient = appServerClient;
|
|
14469
|
+
this.now = now;
|
|
14257
14470
|
this.readFileImpl = readFileImpl ?? ((filePath) => {
|
|
14258
14471
|
try {
|
|
14259
14472
|
return fsSync3.readFileSync(filePath.replace(/^~/, osMod.homedir()), "utf8");
|
|
@@ -14297,10 +14510,20 @@ var ModelDiscovery = class {
|
|
|
14297
14510
|
const runnerLabel = manifest.displayName || runner;
|
|
14298
14511
|
return models.map((m) => ({ ...m, runnerId: runner, runnerLabel }));
|
|
14299
14512
|
}
|
|
14300
|
-
|
|
14301
|
-
|
|
14302
|
-
|
|
14303
|
-
|
|
14513
|
+
discoverRaw(runner) {
|
|
14514
|
+
const cached2 = this.cache.get(runner);
|
|
14515
|
+
if (cached2) return Promise.resolve(cached2);
|
|
14516
|
+
const remembered = this.misses.get(runner);
|
|
14517
|
+
if (remembered && this.now() - remembered.at < NEGATIVE_TTL_MS) return Promise.resolve(remembered.models);
|
|
14518
|
+
const running = this.inflight.get(runner);
|
|
14519
|
+
if (running) return running;
|
|
14520
|
+
const started = this.discoverUncached(runner).finally(() => {
|
|
14521
|
+
if (this.inflight.get(runner) === started) this.inflight.delete(runner);
|
|
14522
|
+
});
|
|
14523
|
+
this.inflight.set(runner, started);
|
|
14524
|
+
return started;
|
|
14525
|
+
}
|
|
14526
|
+
async discoverUncached(runner) {
|
|
14304
14527
|
const manifest = this.registry.getManifest(runner);
|
|
14305
14528
|
if (!manifest) {
|
|
14306
14529
|
return [];
|
|
@@ -14332,10 +14555,13 @@ var ModelDiscovery = class {
|
|
|
14332
14555
|
models = buildFallbackModels(manifest);
|
|
14333
14556
|
}
|
|
14334
14557
|
if (cacheable && models.length > 0) this.cache.set(runner, models);
|
|
14558
|
+
else this.misses.set(runner, { models, at: this.now() });
|
|
14335
14559
|
return models;
|
|
14336
14560
|
}
|
|
14337
14561
|
clear() {
|
|
14338
14562
|
this.cache.clear();
|
|
14563
|
+
this.misses.clear();
|
|
14564
|
+
this.inflight.clear();
|
|
14339
14565
|
}
|
|
14340
14566
|
/** Whatever `discover()` has already cached for `runner` — never triggers discovery. */
|
|
14341
14567
|
getCached(runner) {
|
|
@@ -14497,6 +14723,7 @@ function parseMaxParallel(value) {
|
|
|
14497
14723
|
// src/services/SettingsService.ts
|
|
14498
14724
|
import * as fs10 from "fs";
|
|
14499
14725
|
import * as path25 from "path";
|
|
14726
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
14500
14727
|
|
|
14501
14728
|
// src/services/LoopGuards.ts
|
|
14502
14729
|
var DEFAULT_SUPERVISOR_RETRY_LIMIT = 2;
|
|
@@ -14763,23 +14990,34 @@ function createWakePolicy(options) {
|
|
|
14763
14990
|
// src/services/runnerExit.ts
|
|
14764
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;
|
|
14765
14992
|
var USAGE_LIMIT_TAIL = 4096;
|
|
14766
|
-
function classifyRunnerStop(
|
|
14767
|
-
|
|
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;
|
|
14768
14999
|
}
|
|
14769
15000
|
function stopsRunner(reason) {
|
|
14770
15001
|
return reason !== "stop" && reason !== "load";
|
|
14771
15002
|
}
|
|
14772
|
-
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) {
|
|
14773
15014
|
const read2 = connectorFor(runner)?.usageLimitReset;
|
|
14774
15015
|
if (!read2) return null;
|
|
14775
|
-
let reset;
|
|
14776
15016
|
try {
|
|
14777
|
-
|
|
15017
|
+
return read2(output.slice(-USAGE_LIMIT_TAIL), now);
|
|
14778
15018
|
} catch {
|
|
14779
15019
|
return null;
|
|
14780
15020
|
}
|
|
14781
|
-
if (!reset || reset.getTime() < now.getTime() - DAY_MS) return null;
|
|
14782
|
-
return reset;
|
|
14783
15021
|
}
|
|
14784
15022
|
var DAY_MS = 24 * 60 * 6e4;
|
|
14785
15023
|
|
|
@@ -14883,15 +15121,15 @@ var ReplySplitter = class {
|
|
|
14883
15121
|
push(segmentId, text) {
|
|
14884
15122
|
const state = this.segments.get(segmentId);
|
|
14885
15123
|
if (state === "text" || state === "plan") return { route: state, text };
|
|
14886
|
-
const
|
|
14887
|
-
const opensWithObject = opensWithJsonObject(
|
|
15124
|
+
const held2 = (state?.held ?? "") + text;
|
|
15125
|
+
const opensWithObject = opensWithJsonObject(held2);
|
|
14888
15126
|
if (opensWithObject === void 0) {
|
|
14889
|
-
this.segments.set(segmentId, { held });
|
|
15127
|
+
this.segments.set(segmentId, { held: held2 });
|
|
14890
15128
|
return null;
|
|
14891
15129
|
}
|
|
14892
15130
|
const route = opensWithObject ? "plan" : "text";
|
|
14893
15131
|
this.segments.set(segmentId, route);
|
|
14894
|
-
return { route, text:
|
|
15132
|
+
return { route, text: held2 };
|
|
14895
15133
|
}
|
|
14896
15134
|
};
|
|
14897
15135
|
var TurnStream = class {
|
|
@@ -15046,6 +15284,7 @@ function plannerTranscript(history) {
|
|
|
15046
15284
|
const loadsOf = /* @__PURE__ */ new Map();
|
|
15047
15285
|
let owner = -1;
|
|
15048
15286
|
for (const entry of history) {
|
|
15287
|
+
if (entry.kind === "error") continue;
|
|
15049
15288
|
if (entry.kind === "skill_load" && entry.skill) {
|
|
15050
15289
|
const loads = entry.skill.invokedBy === "user" ? loadsOf.get(owner) : void 0;
|
|
15051
15290
|
if (loads) loads.push(entry.skill);
|
|
@@ -16383,6 +16622,45 @@ function markOf(was, now) {
|
|
|
16383
16622
|
return editedFields(was, now).length > 0 || was.status !== now.status ? "changed" : void 0;
|
|
16384
16623
|
}
|
|
16385
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
|
+
|
|
16386
16664
|
// src/services/PlannerConversation.ts
|
|
16387
16665
|
var CATALOG_MODEL_CAP = 100;
|
|
16388
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.";
|
|
@@ -16447,6 +16725,17 @@ var PlannerTurnFailedError = class extends Error {
|
|
|
16447
16725
|
this.name = "PlannerTurnFailedError";
|
|
16448
16726
|
}
|
|
16449
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
|
+
};
|
|
16450
16739
|
var PlannerTurnStoppedError = class extends Error {
|
|
16451
16740
|
constructor(options) {
|
|
16452
16741
|
super("The planner turn was stopped.", options);
|
|
@@ -16472,6 +16761,8 @@ var PlannerConversation = class {
|
|
|
16472
16761
|
savedInBackground = 0;
|
|
16473
16762
|
/** What the planner was last told of autorun: by its opening prompt, or a turn's block since. */
|
|
16474
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;
|
|
16475
16766
|
live = null;
|
|
16476
16767
|
openTurn = null;
|
|
16477
16768
|
get transcript() {
|
|
@@ -16587,8 +16878,8 @@ var PlannerConversation = class {
|
|
|
16587
16878
|
* from the moment it is handed in until it lands, is refused, or the turn
|
|
16588
16879
|
* ends: the backend's reply claims it, but it has not landed by then.
|
|
16589
16880
|
*/
|
|
16590
|
-
showHeld(turn,
|
|
16591
|
-
const result =
|
|
16881
|
+
showHeld(turn, held2) {
|
|
16882
|
+
const result = held2?.kind === "task_ops" ? this.host.validateOps(held2.ops) : void 0;
|
|
16592
16883
|
if (!result?.ok) {
|
|
16593
16884
|
this.clearHeld(turn);
|
|
16594
16885
|
return;
|
|
@@ -16607,9 +16898,9 @@ var PlannerConversation = class {
|
|
|
16607
16898
|
* a whole plan, which an edit cannot be layered on.
|
|
16608
16899
|
*/
|
|
16609
16900
|
pendingOps() {
|
|
16610
|
-
const
|
|
16611
|
-
if (!
|
|
16612
|
-
return
|
|
16901
|
+
const held2 = this.openTurn?.submitted;
|
|
16902
|
+
if (!held2) return [];
|
|
16903
|
+
return held2.kind === "task_ops" ? held2.ops : null;
|
|
16613
16904
|
}
|
|
16614
16905
|
/**
|
|
16615
16906
|
* A read the planner makes through a tool. It spends the same per-turn budget
|
|
@@ -16649,7 +16940,8 @@ var PlannerConversation = class {
|
|
|
16649
16940
|
history: plan.conversationHistory,
|
|
16650
16941
|
researchLog: plan.researchLog,
|
|
16651
16942
|
persisted: this.persisted,
|
|
16652
|
-
savedInBackground: this.savedInBackground
|
|
16943
|
+
savedInBackground: this.savedInBackground,
|
|
16944
|
+
toldScope: this.toldScope
|
|
16653
16945
|
};
|
|
16654
16946
|
}
|
|
16655
16947
|
/**
|
|
@@ -16663,7 +16955,9 @@ var PlannerConversation = class {
|
|
|
16663
16955
|
* the undo is saved in turn, or a reload would bring the writes back.
|
|
16664
16956
|
*/
|
|
16665
16957
|
restore(snapshot) {
|
|
16666
|
-
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;
|
|
16667
16961
|
const undo = () => {
|
|
16668
16962
|
const actions = (snapshot.plan.conversationHistory ?? []).filter((entry) => entry.actor === "Ordewell setting" && !(snapshot.history ?? []).includes(entry));
|
|
16669
16963
|
snapshot.plan.conversationHistory = actions.length > 0 ? [...snapshot.history ?? [], ...actions] : snapshot.history;
|
|
@@ -16698,8 +16992,9 @@ var PlannerConversation = class {
|
|
|
16698
16992
|
* at it.
|
|
16699
16993
|
*/
|
|
16700
16994
|
rewindTargets() {
|
|
16995
|
+
const opener = this.transcript.findIndex((m) => m.kind !== "error");
|
|
16701
16996
|
return this.transcript.flatMap((m, index) => {
|
|
16702
|
-
if (!isUserMessage(m) || index ===
|
|
16997
|
+
if (!isUserMessage(m) || index === opener) return [];
|
|
16703
16998
|
const line = m.content.split("\n")[0];
|
|
16704
16999
|
const preview = line.length > REWIND_PREVIEW_WIDTH ? `${line.slice(0, REWIND_PREVIEW_WIDTH - 1)}\u2026` : line;
|
|
16705
17000
|
return [{ index, preview, content: m.content, timestamp: m.timestamp }];
|
|
@@ -16826,8 +17121,10 @@ var PlannerConversation = class {
|
|
|
16826
17121
|
return this.userTurn("supersede", goal, signal, async (userTurn) => {
|
|
16827
17122
|
const opening = await prepare();
|
|
16828
17123
|
this.assertCurrent(userTurn);
|
|
17124
|
+
userTurn.checkpoint = this.snapshot();
|
|
16829
17125
|
this.recordUser(goal, (/* @__PURE__ */ new Date()).toISOString());
|
|
16830
17126
|
this.toldAutorun = opening.autorun ?? false;
|
|
17127
|
+
this.toldScope = this.host.assignmentScope();
|
|
16831
17128
|
const turn = await this.host.aiService().startConversation({
|
|
16832
17129
|
...opening,
|
|
16833
17130
|
// The system prompt's goal is the text as typed; skill bodies ride the
|
|
@@ -16837,7 +17134,9 @@ var PlannerConversation = class {
|
|
|
16837
17134
|
onProgress: userTurn.stream.sink(),
|
|
16838
17135
|
signal: userTurn.signal
|
|
16839
17136
|
});
|
|
16840
|
-
|
|
17137
|
+
const settleable = await this.drainTaskQueries(turn, userTurn);
|
|
17138
|
+
this.assertNotLimited(settleable, userTurn);
|
|
17139
|
+
return this.settle(settleable, userTurn);
|
|
16841
17140
|
});
|
|
16842
17141
|
}
|
|
16843
17142
|
/**
|
|
@@ -16870,13 +17169,16 @@ var PlannerConversation = class {
|
|
|
16870
17169
|
// anything the turn appends as it runs goes after this point.
|
|
16871
17170
|
at: priorHistory.length
|
|
16872
17171
|
};
|
|
16873
|
-
const
|
|
17172
|
+
const toldBefore = this.toldScope;
|
|
17173
|
+
const contextBlock = [this.catalogBlock(), this.planContextBlock(), this.autorunBlock(), this.runnersChangedBlock(), this.runUpdatesBlock()].filter(Boolean).join("\n\n");
|
|
16874
17174
|
const said2 = fromUser ? plannerMessage(message.text, message.skills) : "";
|
|
16875
17175
|
const outgoing = [contextBlock, ...wakeUps, userTurn.wakeUp?.brief, said2].filter(Boolean).join("\n\n");
|
|
16876
17176
|
const ai = this.host.aiService();
|
|
16877
17177
|
try {
|
|
16878
17178
|
const turn = this.canContinueLive() ? await ai.continueConversation(outgoing, userTurn.stream.sink(), signal) : await this.resume(outgoing, priorHistory, signal, userTurn.stream.sink());
|
|
16879
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);
|
|
16880
17182
|
if (userTurn.wakeUp) this.assertWakeUpAnswered(settleable, userTurn);
|
|
16881
17183
|
this.assertCurrent(userTurn, settleable);
|
|
16882
17184
|
return await this.settle(settleable, userTurn);
|
|
@@ -16895,6 +17197,34 @@ var PlannerConversation = class {
|
|
|
16895
17197
|
if (userTurn.signal.aborted) throw new PlannerTurnStoppedError();
|
|
16896
17198
|
if (turn.kind === "message" && turn.failed) throw new PlannerTurnFailedError(turn.text);
|
|
16897
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
|
+
}
|
|
16898
17228
|
assertIdle(operation) {
|
|
16899
17229
|
if (this.isTurnInFlight) throw new ConversationBusyError(operation);
|
|
16900
17230
|
}
|
|
@@ -17036,6 +17366,7 @@ var PlannerConversation = class {
|
|
|
17036
17366
|
const goal = opener || this.host.goal() || message;
|
|
17037
17367
|
this.reset();
|
|
17038
17368
|
this.toldAutorun = opening.autorun ?? false;
|
|
17369
|
+
this.toldScope = this.host.assignmentScope();
|
|
17039
17370
|
return this.host.aiService().startConversation({
|
|
17040
17371
|
...opening,
|
|
17041
17372
|
runners,
|
|
@@ -17274,6 +17605,24 @@ The plan is unchanged. Rephrase the request, or adjust the tasks manually.`,
|
|
|
17274
17605
|
this.toldAutorun = now;
|
|
17275
17606
|
return now ? AUTORUN_ON : AUTORUN_OFF;
|
|
17276
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
|
+
}
|
|
17277
17626
|
runUpdatesBlock() {
|
|
17278
17627
|
const updates = this.host.takeRunUpdates();
|
|
17279
17628
|
return updates.length > 0 ? ["Run update:", ...updates.map((update) => `- ${update}`)].join("\n") : null;
|
|
@@ -17354,7 +17703,6 @@ var WAKE_DEBOUNCE_MS = 5e3;
|
|
|
17354
17703
|
var WAKE_UP_TURN_TIMEOUT_MS = 10 * 6e4;
|
|
17355
17704
|
var HUNG_TURN_GRACE_MS = 3e4;
|
|
17356
17705
|
var DEFAULT_WAKE_UP_READ_BUDGET = 12;
|
|
17357
|
-
var RESET_MARGIN_MS = 6e4;
|
|
17358
17706
|
var DETAIL_CHARS = 240;
|
|
17359
17707
|
var WakeUpTimeoutError = class extends Error {
|
|
17360
17708
|
constructor(timeoutMs) {
|
|
@@ -17380,8 +17728,6 @@ var Supervisor = class {
|
|
|
17380
17728
|
/** The run's end was reported: the scheduler can announce it more than once. */
|
|
17381
17729
|
finished = false;
|
|
17382
17730
|
runEndPending = false;
|
|
17383
|
-
/** While the planner's usage limit holds wake-ups back: when it lets them go. */
|
|
17384
|
-
limited = null;
|
|
17385
17731
|
/** When each wake-up since the last pass was delivered, for the wake-up rate guard. */
|
|
17386
17732
|
wakeUps = [];
|
|
17387
17733
|
debounceMs;
|
|
@@ -17402,9 +17748,9 @@ var Supervisor = class {
|
|
|
17402
17748
|
get awaitsRunEnd() {
|
|
17403
17749
|
return this.runEndPending;
|
|
17404
17750
|
}
|
|
17405
|
-
/** 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. */
|
|
17406
17752
|
mayWake() {
|
|
17407
|
-
return this.watching
|
|
17753
|
+
return this.watching;
|
|
17408
17754
|
}
|
|
17409
17755
|
/**
|
|
17410
17756
|
* What a wake-up turn is held to under the run's level. The brief is the
|
|
@@ -17471,8 +17817,6 @@ var Supervisor = class {
|
|
|
17471
17817
|
/** The plan these events were about is gone: nothing gathered or waited on carries over. */
|
|
17472
17818
|
reset() {
|
|
17473
17819
|
this.disarm();
|
|
17474
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
17475
|
-
this.limited = null;
|
|
17476
17820
|
this.failures = 0;
|
|
17477
17821
|
this.finished = false;
|
|
17478
17822
|
this.runEndPending = false;
|
|
@@ -17523,9 +17867,13 @@ var Supervisor = class {
|
|
|
17523
17867
|
}
|
|
17524
17868
|
failed(err) {
|
|
17525
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
|
+
}
|
|
17526
17874
|
const reason = err instanceof Error ? err.message : String(err);
|
|
17527
|
-
if (!(err instanceof WakeUpTimeoutError) && classifyRunnerStop(reason) === "usage-limit") {
|
|
17528
|
-
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");
|
|
17529
17877
|
return;
|
|
17530
17878
|
}
|
|
17531
17879
|
this.failures++;
|
|
@@ -17536,22 +17884,6 @@ var Supervisor = class {
|
|
|
17536
17884
|
}
|
|
17537
17885
|
this.giveUp(`its wake-up turn failed twice (${brief(reason)})`);
|
|
17538
17886
|
}
|
|
17539
|
-
waitForReset(said2) {
|
|
17540
|
-
const runner = this.host.plannerRunner();
|
|
17541
|
-
const reset = runner ? readUsageLimitReset(runner, said2, new Date(this.now())) : null;
|
|
17542
|
-
if (!reset) {
|
|
17543
|
-
this.giveUp("the planner hit its usage limit and did not say when it resets");
|
|
17544
|
-
return;
|
|
17545
|
-
}
|
|
17546
|
-
if (this.limited) clearTimeout(this.limited.timer);
|
|
17547
|
-
const until = reset.getTime() + RESET_MARGIN_MS;
|
|
17548
|
-
const timer = setTimeout(() => {
|
|
17549
|
-
this.limited = null;
|
|
17550
|
-
this.host.pump();
|
|
17551
|
-
}, Math.max(0, until - this.now()));
|
|
17552
|
-
this.limited = { until, timer };
|
|
17553
|
-
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.`);
|
|
17554
|
-
}
|
|
17555
17887
|
/** The run goes on without a supervisor, and nothing that happened is lost (see {@link standDown}). */
|
|
17556
17888
|
giveUp(reason) {
|
|
17557
17889
|
this.host.goUnsupervised(reason);
|
|
@@ -17565,8 +17897,8 @@ var Supervisor = class {
|
|
|
17565
17897
|
standDown() {
|
|
17566
17898
|
this.disarm();
|
|
17567
17899
|
this.runEndPending = false;
|
|
17568
|
-
const
|
|
17569
|
-
if (
|
|
17900
|
+
const held2 = this.policy.poll(Number.POSITIVE_INFINITY);
|
|
17901
|
+
if (held2) this.deliver(held2);
|
|
17570
17902
|
}
|
|
17571
17903
|
arm() {
|
|
17572
17904
|
const due = this.policy.dueAt();
|
|
@@ -17757,78 +18089,224 @@ function runSettingsOf(settings) {
|
|
|
17757
18089
|
}
|
|
17758
18090
|
return out;
|
|
17759
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
|
+
}
|
|
17760
18169
|
var SettingsService = class {
|
|
17761
18170
|
filePath;
|
|
17762
18171
|
cache = null;
|
|
17763
|
-
|
|
18172
|
+
cachedStamp = null;
|
|
17764
18173
|
constructor(filePath = getSettingsPath()) {
|
|
17765
18174
|
this.filePath = filePath;
|
|
17766
18175
|
}
|
|
17767
18176
|
// Several processes share the settings file (VS Code extension host, web
|
|
17768
|
-
// 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
|
|
17769
18178
|
// other process invalidates it. A stale cache here previously meant an
|
|
17770
18179
|
// allowlist set in one surface never reached another's planner.
|
|
17771
18180
|
getAll() {
|
|
17772
|
-
const
|
|
17773
|
-
if (this.cache &&
|
|
18181
|
+
const stamp = this.fileStamp();
|
|
18182
|
+
if (this.cache && stamp === this.cachedStamp) return this.cache;
|
|
17774
18183
|
this.cache = this.load();
|
|
17775
|
-
this.
|
|
18184
|
+
this.cachedStamp = stamp;
|
|
17776
18185
|
return this.cache;
|
|
17777
18186
|
}
|
|
17778
|
-
|
|
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) {
|
|
17779
18193
|
try {
|
|
17780
|
-
|
|
18194
|
+
const stat = fs10.statSync(file);
|
|
18195
|
+
return `${stat.ino}:${stat.mtimeMs}:${stat.size}`;
|
|
17781
18196
|
} catch {
|
|
17782
18197
|
return null;
|
|
17783
18198
|
}
|
|
17784
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
|
+
}
|
|
17785
18241
|
getModelAllowlist(runner) {
|
|
17786
18242
|
return this.getAll().modelAllowlist?.[runner];
|
|
17787
18243
|
}
|
|
17788
18244
|
setModelAllowlist(runner, ids) {
|
|
17789
|
-
this.
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
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
|
+
}
|
|
17795
18252
|
}
|
|
18253
|
+
} else {
|
|
18254
|
+
if (!this.cache.modelAllowlist) {
|
|
18255
|
+
this.cache.modelAllowlist = {};
|
|
18256
|
+
}
|
|
18257
|
+
this.cache.modelAllowlist[runner] = ids;
|
|
17796
18258
|
}
|
|
17797
|
-
}
|
|
17798
|
-
if (!this.cache.modelAllowlist) {
|
|
17799
|
-
this.cache.modelAllowlist = {};
|
|
17800
|
-
}
|
|
17801
|
-
this.cache.modelAllowlist[runner] = ids;
|
|
17802
|
-
}
|
|
17803
|
-
this.persist();
|
|
18259
|
+
});
|
|
17804
18260
|
}
|
|
17805
18261
|
getPlannerModel(provider) {
|
|
17806
18262
|
return this.getAll().plannerModels?.[provider];
|
|
17807
18263
|
}
|
|
17808
18264
|
setPlannerModel(provider, entry) {
|
|
17809
|
-
this.
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
|
|
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
|
+
}
|
|
17815
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 };
|
|
17816
18278
|
}
|
|
17817
|
-
}
|
|
17818
|
-
if (!this.cache.plannerModels) {
|
|
17819
|
-
this.cache.plannerModels = {};
|
|
17820
|
-
}
|
|
17821
|
-
this.cache.plannerModels[provider] = entry.effort ? { model: entry.model, effort: entry.effort } : { model: entry.model };
|
|
17822
|
-
}
|
|
17823
|
-
this.persist();
|
|
18279
|
+
});
|
|
17824
18280
|
}
|
|
17825
18281
|
getEnabledRunners() {
|
|
17826
18282
|
return this.getAll().enabledRunners;
|
|
17827
18283
|
}
|
|
17828
18284
|
setEnabledRunners(ids) {
|
|
17829
|
-
this.
|
|
17830
|
-
|
|
17831
|
-
|
|
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
|
+
});
|
|
17832
18310
|
}
|
|
17833
18311
|
/**
|
|
17834
18312
|
* Full or Guarded (ADR-0001), as saved here (ADR-0030). A value set in the
|
|
@@ -17846,9 +18324,9 @@ var SettingsService = class {
|
|
|
17846
18324
|
return env !== void 0 && env.trim() !== "";
|
|
17847
18325
|
}
|
|
17848
18326
|
setAutonomousMode(enabled) {
|
|
17849
|
-
this.
|
|
17850
|
-
|
|
17851
|
-
|
|
18327
|
+
this.save(() => {
|
|
18328
|
+
this.cache.autonomousMode = enabled;
|
|
18329
|
+
});
|
|
17852
18330
|
}
|
|
17853
18331
|
getRunSettings() {
|
|
17854
18332
|
return runSettingsOf(this.getAll());
|
|
@@ -17860,11 +18338,11 @@ var SettingsService = class {
|
|
|
17860
18338
|
throw new RunSettingsError(RUN_SETTING_CHECKS[key].refusal);
|
|
17861
18339
|
}
|
|
17862
18340
|
}
|
|
17863
|
-
this.
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
18341
|
+
this.save(() => {
|
|
18342
|
+
for (const key of RUN_SETTING_KEYS) {
|
|
18343
|
+
if (changes[key] !== void 0) this.cache[key] = changes[key];
|
|
18344
|
+
}
|
|
18345
|
+
});
|
|
17868
18346
|
return this.getRunSettings();
|
|
17869
18347
|
}
|
|
17870
18348
|
load() {
|
|
@@ -17893,13 +18371,43 @@ var SettingsService = class {
|
|
|
17893
18371
|
}
|
|
17894
18372
|
return {};
|
|
17895
18373
|
}
|
|
17896
|
-
|
|
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) {
|
|
17897
18393
|
const dir = path25.dirname(this.filePath);
|
|
17898
18394
|
if (!fs10.existsSync(dir)) {
|
|
17899
18395
|
fs10.mkdirSync(dir, { recursive: true });
|
|
17900
18396
|
}
|
|
17901
|
-
|
|
17902
|
-
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;
|
|
17903
18411
|
}
|
|
17904
18412
|
};
|
|
17905
18413
|
|
|
@@ -18383,7 +18891,7 @@ var EnvConfig = class extends BaseConfig {
|
|
|
18383
18891
|
};
|
|
18384
18892
|
|
|
18385
18893
|
// src/services/mergeResultNotice.ts
|
|
18386
|
-
function blockNotice({ repo, reason, files }) {
|
|
18894
|
+
function blockNotice({ repo, reason, files, expected, actual, rebasing }) {
|
|
18387
18895
|
switch (reason) {
|
|
18388
18896
|
case "merge-in-progress":
|
|
18389
18897
|
return `${repo} has a merge in progress`;
|
|
@@ -18393,10 +18901,22 @@ function blockNotice({ repo, reason, files }) {
|
|
|
18393
18901
|
return `${repo} has uncommitted changes to ${files.join(", ")}`;
|
|
18394
18902
|
case "partial-landing":
|
|
18395
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}`;
|
|
18396
18906
|
case "git-error":
|
|
18397
18907
|
return `git could not check ${repo}`;
|
|
18398
18908
|
}
|
|
18399
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
|
+
}
|
|
18400
18920
|
function landedNotice(landed2) {
|
|
18401
18921
|
if (landed2.length === 0) return "Nothing was merged.";
|
|
18402
18922
|
return landed2.length === 1 ? `${landed2[0]} was merged already and stays merged.` : `${landed2.join(", ")} were merged already and stay merged.`;
|
|
@@ -18413,7 +18933,7 @@ function describeMergeResult(result, branch, group, repaired = []) {
|
|
|
18413
18933
|
return { level: "info", message: repaired.length > 0 ? `${base} ${repairedNotice(repaired)}` : base };
|
|
18414
18934
|
}
|
|
18415
18935
|
case "blocked":
|
|
18416
|
-
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)}` };
|
|
18417
18937
|
case "conflict":
|
|
18418
18938
|
return {
|
|
18419
18939
|
level: "warn",
|
|
@@ -18777,7 +19297,7 @@ var GitWorktreeIsolation = class {
|
|
|
18777
19297
|
path: repoPath,
|
|
18778
19298
|
root,
|
|
18779
19299
|
baseRef,
|
|
18780
|
-
...branch ? { baseBranch: branch } : {},
|
|
19300
|
+
...branch ? { baseBranch: branch } : { startedDetached: true },
|
|
18781
19301
|
integrationBranch: integrationBranchFor(run.id)
|
|
18782
19302
|
};
|
|
18783
19303
|
if (!(await this.tryGit(root, ["branch", repo.integrationBranch, baseRef])).ok) return null;
|
|
@@ -19130,7 +19650,9 @@ var GitWorktreeIsolation = class {
|
|
|
19130
19650
|
async mergeAll(run) {
|
|
19131
19651
|
const partial = await this.settleLanding(run);
|
|
19132
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);
|
|
19133
19654
|
const repos = await this.reposWithWork(run);
|
|
19655
|
+
if (misplaced.length > 0) return { outcome: "blocked", blocked: misplaced };
|
|
19134
19656
|
if (run.repos.length === 1 || !await this.canPreflight()) return this.mergeInTurn(repos);
|
|
19135
19657
|
const blocked = [];
|
|
19136
19658
|
for (const repo of repos) {
|
|
@@ -19148,6 +19670,27 @@ var GitWorktreeIsolation = class {
|
|
|
19148
19670
|
}
|
|
19149
19671
|
return withWork;
|
|
19150
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
|
+
}
|
|
19151
19694
|
/** `git merge-tree --write-tree`, which merges without touching a tree, arrived in git 2.38. */
|
|
19152
19695
|
async canPreflight() {
|
|
19153
19696
|
const version = await this.tryGit(void 0, ["--version"]);
|
|
@@ -19635,6 +20178,14 @@ var GitWorktreeIsolation = class {
|
|
|
19635
20178
|
if ((await this.tryGit(cwd, ["rev-parse", "-q", "--verify", "MERGE_HEAD"])).ok) return true;
|
|
19636
20179
|
return (await this.tryGit(cwd, ["ls-files", "-u"])).stdout.trim() !== "";
|
|
19637
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
|
+
}
|
|
19638
20189
|
async abortMerge(cwd) {
|
|
19639
20190
|
if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
|
|
19640
20191
|
}
|
|
@@ -19655,6 +20206,29 @@ function createWorktreeIsolation(deps) {
|
|
|
19655
20206
|
return new GitWorktreeIsolation(deps);
|
|
19656
20207
|
}
|
|
19657
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
|
+
|
|
19658
20232
|
// src/interfaces/ILogger.ts
|
|
19659
20233
|
var ConsoleLogger = class {
|
|
19660
20234
|
warn(scope, message, err) {
|
|
@@ -19707,7 +20281,10 @@ async function settleReply(opts) {
|
|
|
19707
20281
|
opts.onProgress({ type: "interrupted" });
|
|
19708
20282
|
return { done: turn };
|
|
19709
20283
|
}
|
|
19710
|
-
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
|
+
}
|
|
19711
20288
|
if (!attempt.text.trim()) return { done: message(emptyReplyReport(deniedStep(attempt))) };
|
|
19712
20289
|
const reply2 = opts.classify ? classifyPlannerReply(attempt.text, opts.classify) : { kind: "prose" };
|
|
19713
20290
|
switch (reply2.kind) {
|
|
@@ -20638,7 +21215,7 @@ var CONVERSATION_TOOL_DEFINITIONS = toGeminiToolDeclarations(true);
|
|
|
20638
21215
|
function tokenCount(value) {
|
|
20639
21216
|
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
20640
21217
|
}
|
|
20641
|
-
function usageRecordFrom(usage) {
|
|
21218
|
+
function usageRecordFrom(usage, model) {
|
|
20642
21219
|
if (!usage) return void 0;
|
|
20643
21220
|
const inputTokens = tokenCount(usage.promptTokenCount);
|
|
20644
21221
|
const answerTokens = tokenCount(usage.candidatesTokenCount);
|
|
@@ -20646,6 +21223,7 @@ function usageRecordFrom(usage) {
|
|
|
20646
21223
|
const reportedOutput = [answerTokens, thoughtTokens].filter((t) => t !== void 0);
|
|
20647
21224
|
const record = {
|
|
20648
21225
|
source: "google",
|
|
21226
|
+
model,
|
|
20649
21227
|
...inputTokens !== void 0 ? { inputTokens } : {},
|
|
20650
21228
|
...reportedOutput.length > 0 ? { outputTokens: reportedOutput.reduce((a, b) => a + b, 0) } : {},
|
|
20651
21229
|
...tokenCount(usage.cachedContentTokenCount) !== void 0 ? { cachedInputTokens: tokenCount(usage.cachedContentTokenCount) } : {}
|
|
@@ -20654,15 +21232,31 @@ function usageRecordFrom(usage) {
|
|
|
20654
21232
|
return anyReported ? record : void 0;
|
|
20655
21233
|
}
|
|
20656
21234
|
var GeminiResearchChat = class {
|
|
20657
|
-
constructor(
|
|
20658
|
-
this.
|
|
21235
|
+
constructor(getModel, open, hooks = {}) {
|
|
21236
|
+
this.getModel = getModel;
|
|
21237
|
+
this.open = open;
|
|
20659
21238
|
this.hooks = hooks;
|
|
21239
|
+
this.model = getModel();
|
|
21240
|
+
this.chat = open(this.model);
|
|
20660
21241
|
}
|
|
20661
|
-
|
|
21242
|
+
getModel;
|
|
21243
|
+
open;
|
|
20662
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
|
+
}
|
|
20663
21257
|
async sendMessage(text, signal) {
|
|
20664
21258
|
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
20665
|
-
const result = await this.
|
|
21259
|
+
const result = await (await this.current()).sendMessageStream(text);
|
|
20666
21260
|
return this.consume(result.stream);
|
|
20667
21261
|
}
|
|
20668
21262
|
async sendToolResults(results, signal) {
|
|
@@ -20673,7 +21267,7 @@ var GeminiResearchChat = class {
|
|
|
20673
21267
|
response: { output: r.output, truncated: r.truncated, totalChars: r.totalChars }
|
|
20674
21268
|
}
|
|
20675
21269
|
}));
|
|
20676
|
-
const result = await this.
|
|
21270
|
+
const result = await (await this.current()).sendMessageStream(funcResponses);
|
|
20677
21271
|
return this.consume(result.stream);
|
|
20678
21272
|
}
|
|
20679
21273
|
/**
|
|
@@ -20713,7 +21307,7 @@ var GeminiResearchChat = class {
|
|
|
20713
21307
|
}
|
|
20714
21308
|
}
|
|
20715
21309
|
}
|
|
20716
|
-
const usageRecord3 = usageRecordFrom(usage);
|
|
21310
|
+
const usageRecord3 = usageRecordFrom(usage, this.model);
|
|
20717
21311
|
if (usageRecord3) this.hooks.onUsage?.(usageRecord3);
|
|
20718
21312
|
return {
|
|
20719
21313
|
text,
|
|
@@ -20729,6 +21323,8 @@ var GeminiService = class extends BaseAiService {
|
|
|
20729
21323
|
genAI = null;
|
|
20730
21324
|
genAIKey = "";
|
|
20731
21325
|
model = null;
|
|
21326
|
+
/** The id `model` was made for; the SDK's own `model.model` is its `models/…` resource path. */
|
|
21327
|
+
modelId = "";
|
|
20732
21328
|
constructor(config) {
|
|
20733
21329
|
super(config);
|
|
20734
21330
|
}
|
|
@@ -20742,13 +21338,18 @@ var GeminiService = class extends BaseAiService {
|
|
|
20742
21338
|
}
|
|
20743
21339
|
return true;
|
|
20744
21340
|
}
|
|
20745
|
-
|
|
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()) {
|
|
20746
21346
|
this.init();
|
|
20747
|
-
if (!this.model || this.
|
|
21347
|
+
if (!this.model || this.modelId !== model) {
|
|
20748
21348
|
this.model = this.genAI.getGenerativeModel({
|
|
20749
|
-
model
|
|
21349
|
+
model,
|
|
20750
21350
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20751
21351
|
});
|
|
21352
|
+
this.modelId = model;
|
|
20752
21353
|
}
|
|
20753
21354
|
return this.model;
|
|
20754
21355
|
}
|
|
@@ -20763,7 +21364,6 @@ var GeminiService = class extends BaseAiService {
|
|
|
20763
21364
|
}
|
|
20764
21365
|
async startConversation(req) {
|
|
20765
21366
|
this.ensureInit();
|
|
20766
|
-
const genModel = this.getPlanningModel();
|
|
20767
21367
|
const contextStr = await BaseAiService.collectResearchContext(req.fs, req.runners);
|
|
20768
21368
|
const systemPrompt = buildConversationSystemPrompt(
|
|
20769
21369
|
req.goal,
|
|
@@ -20792,13 +21392,14 @@ ${m.content}` });
|
|
|
20792
21392
|
if ((req.priorHistory?.length ?? 0) > 0 && seededHistory[seededHistory.length - 1].role === "user") {
|
|
20793
21393
|
seededHistory.push({ role: "model", parts: [{ text: "Understood." }] });
|
|
20794
21394
|
}
|
|
20795
|
-
const
|
|
20796
|
-
|
|
20797
|
-
|
|
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,
|
|
20798
21399
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20799
21400
|
});
|
|
20800
21401
|
let currentProgress = req.onProgress;
|
|
20801
|
-
const researchChat = new GeminiResearchChat(
|
|
21402
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
20802
21403
|
onReasoning: (text, segmentId) => currentProgress({ type: "thinking", text, segmentId }),
|
|
20803
21404
|
onContent: (text, segmentId) => currentProgress({ type: "text_delta", text, segmentId }),
|
|
20804
21405
|
onUsage: (record) => currentProgress({ type: "usage", record })
|
|
@@ -20871,18 +21472,17 @@ ${repairHint}` }] : parts;
|
|
|
20871
21472
|
async researchAndPlan(userDescription, runners, modelsByRunner, fs18, onProgress, fetcher, runnerModes, modes = DEFAULT_PLANNER_MODES, signal) {
|
|
20872
21473
|
this.ensureInit();
|
|
20873
21474
|
const { autonomousDefault } = modes;
|
|
20874
|
-
const genModel = this.getPlanningModel();
|
|
20875
21475
|
const contextStr = await BaseAiService.collectResearchContext(fs18, runners);
|
|
20876
21476
|
const systemPrompt = buildResearchPrompt(userDescription, contextStr, modelsByRunner, runners, runnerModes, modes);
|
|
20877
21477
|
const firstMessage = `${buildResearchToolsPrompt()}
|
|
20878
21478
|
|
|
20879
21479
|
Explore the workspace to understand the codebase, then generate the plan.`;
|
|
20880
|
-
const
|
|
20881
|
-
history
|
|
21480
|
+
const open = (model, history = [{ role: "user", parts: [{ text: systemPrompt }] }]) => this.getPlanningModel(model).startChat({
|
|
21481
|
+
history,
|
|
20882
21482
|
tools: [{ functionDeclarations: TOOL_DEFINITIONS }],
|
|
20883
21483
|
generationConfig: { temperature: 0.3, topP: 0.95, maxOutputTokens: 16384 }
|
|
20884
21484
|
});
|
|
20885
|
-
const researchChat = new GeminiResearchChat(
|
|
21485
|
+
const researchChat = new GeminiResearchChat(() => this.plannerModel(), open, {
|
|
20886
21486
|
onUsage: (record) => onProgress({ type: "usage", record })
|
|
20887
21487
|
});
|
|
20888
21488
|
const result = await this.runResearchLoop(researchChat, firstMessage, fs18, onProgress, runners, void 0, fetcher, userDescription, runnerModes, autonomousDefault, signal);
|
|
@@ -20918,7 +21518,7 @@ import OpenAI from "openai";
|
|
|
20918
21518
|
import { v4 as uuidv44 } from "uuid";
|
|
20919
21519
|
var KEYLESS_PLACEHOLDER = "not-needed";
|
|
20920
21520
|
var OpenAiResearchChat = class {
|
|
20921
|
-
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage
|
|
21521
|
+
constructor(messages, getClient, getModel, tools, onReasoning, onContent, getSource = () => "openai", onUsage) {
|
|
20922
21522
|
this.messages = messages;
|
|
20923
21523
|
this.getClient = getClient;
|
|
20924
21524
|
this.getModel = getModel;
|
|
@@ -20927,7 +21527,6 @@ var OpenAiResearchChat = class {
|
|
|
20927
21527
|
this.onContent = onContent;
|
|
20928
21528
|
this.getSource = getSource;
|
|
20929
21529
|
this.onUsage = onUsage;
|
|
20930
|
-
this.contextWindow = contextWindow;
|
|
20931
21530
|
}
|
|
20932
21531
|
messages;
|
|
20933
21532
|
getClient;
|
|
@@ -20937,7 +21536,6 @@ var OpenAiResearchChat = class {
|
|
|
20937
21536
|
onContent;
|
|
20938
21537
|
getSource;
|
|
20939
21538
|
onUsage;
|
|
20940
|
-
contextWindow;
|
|
20941
21539
|
async sendMessage(text, signal) {
|
|
20942
21540
|
this.answerAbandonedCalls();
|
|
20943
21541
|
this.messages.push({ role: "user", content: text });
|
|
@@ -21052,7 +21650,6 @@ var OpenAiResearchChat = class {
|
|
|
21052
21650
|
record.reportedCost = { amount: reported.cost, currency: "USD" };
|
|
21053
21651
|
hasMeasure = true;
|
|
21054
21652
|
}
|
|
21055
|
-
if (this.contextWindow && this.contextWindow > 0) record.contextWindow = this.contextWindow;
|
|
21056
21653
|
return hasMeasure ? record : void 0;
|
|
21057
21654
|
}
|
|
21058
21655
|
};
|
|
@@ -21179,8 +21776,7 @@ ${buildResearchToolsPrompt()}` }
|
|
|
21179
21776
|
(delta, segmentId) => currentProgress({ type: "thinking", text: delta, segmentId }),
|
|
21180
21777
|
(delta, segmentId) => currentProgress({ type: "text_delta", text: delta, segmentId }),
|
|
21181
21778
|
() => this.config.aiProvider,
|
|
21182
|
-
(record) => currentProgress({ type: "usage", record })
|
|
21183
|
-
req.contextWindow
|
|
21779
|
+
(record) => currentProgress({ type: "usage", record })
|
|
21184
21780
|
);
|
|
21185
21781
|
const ctx = {
|
|
21186
21782
|
chat,
|
|
@@ -21742,6 +22338,10 @@ var VerdictEngine = class {
|
|
|
21742
22338
|
this.generations.clear();
|
|
21743
22339
|
}
|
|
21744
22340
|
};
|
|
22341
|
+
var EXIT_CHECK = "exit_code";
|
|
22342
|
+
function endedByExit(verdict) {
|
|
22343
|
+
return verdict.checks.some((check2) => check2.name === EXIT_CHECK);
|
|
22344
|
+
}
|
|
21745
22345
|
function exitVerdict(exitCode) {
|
|
21746
22346
|
const normalized2 = exitCode == null ? 0 : exitCode;
|
|
21747
22347
|
const exitOk = normalized2 === 0;
|
|
@@ -21750,7 +22350,7 @@ function exitVerdict(exitCode) {
|
|
|
21750
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.`,
|
|
21751
22351
|
checks: [
|
|
21752
22352
|
{ name: "task_complete", passed: false, skipped: false, detail: "the runner exited before calling task_complete" },
|
|
21753
|
-
{ name:
|
|
22353
|
+
{ name: EXIT_CHECK, passed: exitOk, skipped: false, detail: exitOk ? "the runner exited cleanly (code 0)" : `the runner exited with code ${normalized2}` }
|
|
21754
22354
|
],
|
|
21755
22355
|
decidedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
21756
22356
|
};
|
|
@@ -21949,6 +22549,7 @@ var PlanStore = class {
|
|
|
21949
22549
|
this.rebuild();
|
|
21950
22550
|
for (const task of this._allTasks) {
|
|
21951
22551
|
if (task.status === "failed") task.status = "pending";
|
|
22552
|
+
if (task.runnerSessionId && !task.runnerSessionOwner) task.runnerSessionOwner = sessionOwner(task) ?? task.assignedRunner;
|
|
21952
22553
|
}
|
|
21953
22554
|
this.validateAssignedRunners();
|
|
21954
22555
|
}
|
|
@@ -22078,9 +22679,17 @@ var PlanStore = class {
|
|
|
22078
22679
|
const task = this._taskMap.get(id);
|
|
22079
22680
|
if (task) task.outputSummary = summary;
|
|
22080
22681
|
}
|
|
22081
|
-
|
|
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) {
|
|
22082
22691
|
const task = this._taskMap.get(id);
|
|
22083
|
-
if (task) task.
|
|
22692
|
+
if (task) task.carriedFrom = runner;
|
|
22084
22693
|
}
|
|
22085
22694
|
clearCarriedFrom(id) {
|
|
22086
22695
|
const task = this._taskMap.get(id);
|
|
@@ -22094,9 +22703,9 @@ var PlanStore = class {
|
|
|
22094
22703
|
const task = this._taskMap.get(id);
|
|
22095
22704
|
if (task?.attemptUsage) task.attemptUsage = task.attemptUsage.map((usage) => usage.attempt === attempt ? addAttemptUsage(usage, record, now) : usage);
|
|
22096
22705
|
}
|
|
22097
|
-
endAttemptUsage(id, attempt, now) {
|
|
22706
|
+
endAttemptUsage(id, attempt, now, runnerSessionCost, sessionId) {
|
|
22098
22707
|
const task = this._taskMap.get(id);
|
|
22099
|
-
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);
|
|
22100
22709
|
}
|
|
22101
22710
|
setTaskAttemptSkills(id, skills) {
|
|
22102
22711
|
const task = this._taskMap.get(id);
|
|
@@ -22954,6 +23563,8 @@ function conflictResolverTask(task, run, plan) {
|
|
|
22954
23563
|
}
|
|
22955
23564
|
|
|
22956
23565
|
// src/services/UsageLimitWaits.ts
|
|
23566
|
+
var RESUME_MARGIN_MS = 6e4;
|
|
23567
|
+
var RELAPSE_MS = 5 * 6e4;
|
|
22957
23568
|
var UsageLimitWaits = class {
|
|
22958
23569
|
constructor(onDue) {
|
|
22959
23570
|
this.onDue = onDue;
|
|
@@ -23076,6 +23687,9 @@ function selectReadyTasks(input) {
|
|
|
23076
23687
|
gated
|
|
23077
23688
|
};
|
|
23078
23689
|
}
|
|
23690
|
+
function isBlocked(task, store) {
|
|
23691
|
+
return blockedIn(task, blockingFailures(store.allTasks));
|
|
23692
|
+
}
|
|
23079
23693
|
var blockedIn = (task, blocked) => task.status === "blocked" || blocked.has(task.id);
|
|
23080
23694
|
function dependencyMet(depId, store, runs) {
|
|
23081
23695
|
return waitingBehind([depId], store, runs).length === 0;
|
|
@@ -23205,6 +23819,12 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23205
23819
|
* task whose spawn always throws.
|
|
23206
23820
|
*/
|
|
23207
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();
|
|
23208
23828
|
/**
|
|
23209
23829
|
* Tasks Mark complete is landing after it ended their attempt: in progress
|
|
23210
23830
|
* with no attempt for as long as that takes, and not orphans for it.
|
|
@@ -23226,6 +23846,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23226
23846
|
observers = [];
|
|
23227
23847
|
skillsAt;
|
|
23228
23848
|
previousAttemptFromLog;
|
|
23849
|
+
lastLoggedAttempt;
|
|
23229
23850
|
runSettings;
|
|
23230
23851
|
isApproved;
|
|
23231
23852
|
constructor(deps) {
|
|
@@ -23241,6 +23862,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23241
23862
|
this.workspaceEnv = deps.workspaceEnv;
|
|
23242
23863
|
this.skillsAt = deps.skillsAt;
|
|
23243
23864
|
this.previousAttemptFromLog = deps.previousAttemptFromLog;
|
|
23865
|
+
this.lastLoggedAttempt = deps.lastLoggedAttempt;
|
|
23244
23866
|
this.runSettings = deps.runSettings;
|
|
23245
23867
|
this.isApproved = deps.isApproved;
|
|
23246
23868
|
this.store.onMutate = () => this.emit("onTaskChanged");
|
|
@@ -23305,6 +23927,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23305
23927
|
workspaceEnv: options.workspaceEnv ?? ((cwd) => resolveWorkspaceEnv(cwd)),
|
|
23306
23928
|
skillsAt: options.skillsAt ?? ((roots) => new SkillsService(roots)),
|
|
23307
23929
|
previousAttemptFromLog: options.previousAttemptFromLog ?? (() => null),
|
|
23930
|
+
lastLoggedAttempt: options.lastLoggedAttempt ?? (() => 0),
|
|
23308
23931
|
runSettings: options.runSettings ?? (() => void 0),
|
|
23309
23932
|
isApproved: options.isApproved ?? (() => true)
|
|
23310
23933
|
});
|
|
@@ -23561,10 +24184,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23561
24184
|
*/
|
|
23562
24185
|
holdStarts() {
|
|
23563
24186
|
this.startHolds++;
|
|
23564
|
-
let
|
|
24187
|
+
let held2 = true;
|
|
23565
24188
|
return () => {
|
|
23566
|
-
if (!
|
|
23567
|
-
|
|
24189
|
+
if (!held2) return;
|
|
24190
|
+
held2 = false;
|
|
23568
24191
|
this.startHolds--;
|
|
23569
24192
|
};
|
|
23570
24193
|
}
|
|
@@ -23575,24 +24198,25 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23575
24198
|
*/
|
|
23576
24199
|
admitStart() {
|
|
23577
24200
|
const generation2 = this.stopGeneration;
|
|
23578
|
-
let
|
|
24201
|
+
let held2 = true;
|
|
23579
24202
|
this.admissions++;
|
|
23580
24203
|
return {
|
|
23581
24204
|
current: () => generation2 === this.stopGeneration,
|
|
23582
24205
|
release: () => {
|
|
23583
|
-
if (!
|
|
23584
|
-
|
|
24206
|
+
if (!held2) return;
|
|
24207
|
+
held2 = false;
|
|
23585
24208
|
this.admissions--;
|
|
23586
24209
|
}
|
|
23587
24210
|
};
|
|
23588
24211
|
}
|
|
23589
24212
|
/**
|
|
23590
24213
|
* Something can still start that nobody has to ask for again: a start
|
|
23591
|
-
* admitted and not yet claimed,
|
|
23592
|
-
*
|
|
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.
|
|
23593
24217
|
*/
|
|
23594
24218
|
get startsPending() {
|
|
23595
|
-
return this.admissions > 0 || this.store.allTasks.some(waitsForReset);
|
|
24219
|
+
return this.admissions > 0 || this.queuedStarts.size > 0 || this.store.allTasks.some(waitsForReset);
|
|
23596
24220
|
}
|
|
23597
24221
|
/** Run `work`, which awaits before it claims, under an admission it holds throughout; it is handed the generation it was admitted under. */
|
|
23598
24222
|
async whileAdmitted(work) {
|
|
@@ -23691,6 +24315,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23691
24315
|
this.retryCounts.clear();
|
|
23692
24316
|
this.spawnCounts.clear();
|
|
23693
24317
|
this.onHold.clear();
|
|
24318
|
+
this.queuedStarts.clear();
|
|
23694
24319
|
this.recoverOrphans();
|
|
23695
24320
|
this.rearmLimitWaits();
|
|
23696
24321
|
}
|
|
@@ -23778,6 +24403,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23778
24403
|
}
|
|
23779
24404
|
this.runs.interrupt();
|
|
23780
24405
|
this.onHold.clear();
|
|
24406
|
+
if (reason === "user") this.queuedStarts.clear();
|
|
23781
24407
|
for (const t of this.store.allTasks.filter(waitsForReset)) this.cancelScheduledResume(t.id);
|
|
23782
24408
|
this.sayStopped(stopped);
|
|
23783
24409
|
this.recoverOrphans();
|
|
@@ -23877,6 +24503,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
23877
24503
|
const attempt = this.attempts.get(taskId);
|
|
23878
24504
|
if (!task || !attempt) return;
|
|
23879
24505
|
attempt.decided = true;
|
|
24506
|
+
const stoppedMidTurn = endedByExit(verdict) ? saidInFlight(attempt) : null;
|
|
23880
24507
|
console.error(`[TaskOrchestrator] Task ${taskRefOf(this.store.planTasks, task.id) ?? `#${task.order}`} "${task.title}" verdict=${verdict.outcome}`);
|
|
23881
24508
|
console.error(`[TaskOrchestrator] Runner: ${task.assignedRunner}, Model: ${task.assignedModel?.modelId ?? "default"}`);
|
|
23882
24509
|
console.error(`[TaskOrchestrator] Prompt preview: ${(task.prompt ?? "").slice(0, 200)}`);
|
|
@@ -23898,8 +24525,8 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
23898
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.`);
|
|
23899
24526
|
} else if (landing) {
|
|
23900
24527
|
await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
|
|
23901
|
-
} else if (classifyRunnerStop(
|
|
23902
|
-
this.pauseOnLimit(task, attempt,
|
|
24528
|
+
} else if (stoppedMidTurn !== null && classifyRunnerStop(stoppedMidTurn) === "usage-limit") {
|
|
24529
|
+
this.pauseOnLimit(task, attempt, stoppedMidTurn);
|
|
23903
24530
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
23904
24531
|
} else {
|
|
23905
24532
|
this.store.markFailed(taskId);
|
|
@@ -23914,16 +24541,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
23914
24541
|
const status = this.store.get(taskId)?.status;
|
|
23915
24542
|
if (status === "completed" || status === "failed" || status === "awaiting_user") this.emit("onTaskSettled", { taskId });
|
|
23916
24543
|
this.emit("onTaskChanged");
|
|
23917
|
-
if (
|
|
23918
|
-
|
|
23919
|
-
if (this.store.isFinished()) this.planStatus = "completed";
|
|
23920
|
-
this.emit("onTick");
|
|
23921
|
-
if (this.settled) await this.closeRun();
|
|
23922
|
-
this.emit("onExecutionComplete");
|
|
23923
|
-
}
|
|
24544
|
+
if (this.running) {
|
|
24545
|
+
await this.tick();
|
|
23924
24546
|
return;
|
|
23925
24547
|
}
|
|
23926
|
-
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
|
+
}
|
|
23927
24555
|
}
|
|
23928
24556
|
/**
|
|
23929
24557
|
* A repair's verdict never replaces the verdict the task's own work earned,
|
|
@@ -24191,11 +24819,17 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24191
24819
|
const resumes = this.awaitsResume;
|
|
24192
24820
|
this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
|
|
24193
24821
|
const ended = this.endAttempt(taskId, "retry");
|
|
24822
|
+
const movedFrom = this.movedFrom(task);
|
|
24194
24823
|
this.store.retry(taskId);
|
|
24824
|
+
if (movedFrom) this.store.setCarriedFrom(taskId, movedFrom);
|
|
24195
24825
|
this.store.unblockDependents(taskId);
|
|
24196
24826
|
this.onHold.delete(taskId);
|
|
24197
24827
|
this.emit("onTaskChanged");
|
|
24198
|
-
|
|
24828
|
+
if (movedFrom) {
|
|
24829
|
+
await ended?.integration;
|
|
24830
|
+
} else {
|
|
24831
|
+
await this.runs.release(taskId, { keep: false }, ended?.integration);
|
|
24832
|
+
}
|
|
24199
24833
|
if (admittedAt !== this.stopGeneration) return;
|
|
24200
24834
|
if (resumes) await this.start(origin);
|
|
24201
24835
|
else await this.tick();
|
|
@@ -24246,6 +24880,16 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24246
24880
|
this.emit("onTaskChanged");
|
|
24247
24881
|
return true;
|
|
24248
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
|
+
}
|
|
24249
24893
|
/**
|
|
24250
24894
|
* Run before writing assignments so cancellation sees the old runner's
|
|
24251
24895
|
* shared wait. A session cannot cross runners, so the next attempt is a
|
|
@@ -24303,7 +24947,7 @@ ${summary || "(empty: no output captured)"}`);
|
|
|
24303
24947
|
const summary = this.output.finalText(taskId);
|
|
24304
24948
|
this.endAttempt(taskId, "usage-limit");
|
|
24305
24949
|
this.pauseOnLimit(task, attempt, attempt.turnError ?? "");
|
|
24306
|
-
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));
|
|
24307
24951
|
if (attempt.worktree) await this.runs.release(taskId, { keep: true });
|
|
24308
24952
|
await this.afterVerdict(taskId);
|
|
24309
24953
|
}
|
|
@@ -24402,10 +25046,14 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24402
25046
|
return false;
|
|
24403
25047
|
}
|
|
24404
25048
|
/**
|
|
24405
|
-
* Manually start a single AI task right now
|
|
24406
|
-
*
|
|
24407
|
-
* it still needs the user's approval,
|
|
24408
|
-
*
|
|
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.
|
|
24409
25057
|
* Reuses the scheduler's own claim so a force-started task gets the same
|
|
24410
25058
|
* augmented prompt, session tracking, and exit handling — callers must not
|
|
24411
25059
|
* re-spawn the runner themselves. No-op if the task is unknown, not an AI
|
|
@@ -24416,8 +25064,12 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24416
25064
|
if (!task || task.type !== "ai") return;
|
|
24417
25065
|
if (this.attempts.has(taskId)) return;
|
|
24418
25066
|
this.refuseOpsDuringMerge(task);
|
|
25067
|
+
const queued2 = this.queuedBehindDependencies(task);
|
|
25068
|
+
if (queued2) return queued2;
|
|
24419
25069
|
await this.whileAdmitted(async (admittedAt) => {
|
|
24420
|
-
if (!await this.openForAdmitted(taskId, origin, () =>
|
|
25070
|
+
if (!await this.openForAdmitted(taskId, origin, async () => {
|
|
25071
|
+
await this.forceStartTask(taskId, origin);
|
|
25072
|
+
}, admittedAt)) return;
|
|
24421
25073
|
if (this.attempts.has(taskId)) return;
|
|
24422
25074
|
this.refuseOpsDuringMerge(task);
|
|
24423
25075
|
this.refuseClaim(task, admittedAt);
|
|
@@ -24426,6 +25078,68 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24426
25078
|
await this.claimed(task, admittedAt);
|
|
24427
25079
|
});
|
|
24428
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
|
+
}
|
|
24429
25143
|
refuseOpsDuringMerge(task) {
|
|
24430
25144
|
if (this.merging && mergeExcludes(this.nextAttemptKind(task.id))) {
|
|
24431
25145
|
throw new TaskControlError(`A Merge all is under way: ops task "${task.title}" can start once it has finished.`);
|
|
@@ -24490,6 +25204,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24490
25204
|
async tick() {
|
|
24491
25205
|
const admittedAt = this.stopGeneration;
|
|
24492
25206
|
this.recoverOrphans();
|
|
25207
|
+
await this.startQueued();
|
|
24493
25208
|
if (!this.running) {
|
|
24494
25209
|
if (this.attempts.size === 0 && this.runs.isOpen && this.settled) await this.closeRun();
|
|
24495
25210
|
return;
|
|
@@ -24646,7 +25361,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24646
25361
|
if (refused3) return { refused: refused3 };
|
|
24647
25362
|
const attempt = {
|
|
24648
25363
|
taskId: task.id,
|
|
24649
|
-
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,
|
|
24650
25365
|
phase: "starting",
|
|
24651
25366
|
session: null,
|
|
24652
25367
|
runner: this.store.resolveTaskRunner(task),
|
|
@@ -24660,6 +25375,7 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24660
25375
|
skills: [],
|
|
24661
25376
|
autoResumed: opts.autoResumed === true,
|
|
24662
25377
|
turnError: null,
|
|
25378
|
+
turnOutputFrom: 0,
|
|
24663
25379
|
reminded: false
|
|
24664
25380
|
};
|
|
24665
25381
|
this.spawnCounts.set(task.id, attempt.attempt);
|
|
@@ -24733,6 +25449,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24733
25449
|
title: task.title,
|
|
24734
25450
|
env,
|
|
24735
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,
|
|
24736
25454
|
attempt: attempt.attempt,
|
|
24737
25455
|
...attempt.skills.length > 0 ? { skills: attempt.skills } : {},
|
|
24738
25456
|
onNotice: (message) => this.tell("warn", `Task "${task.title}": ${message}`)
|
|
@@ -24757,8 +25475,21 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24757
25475
|
this.output.attach(task.id, session);
|
|
24758
25476
|
this.verifier.watch(task, session);
|
|
24759
25477
|
session.onEvent((event) => {
|
|
24760
|
-
if (event.type === "turn_start")
|
|
24761
|
-
|
|
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
|
+
}
|
|
24762
25493
|
});
|
|
24763
25494
|
session.onTurnEnd((reason) => this.onTurnEnd(task.id, attempt, session, reason));
|
|
24764
25495
|
return true;
|
|
@@ -24768,6 +25499,16 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24768
25499
|
return false;
|
|
24769
25500
|
}
|
|
24770
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
|
+
}
|
|
24771
25512
|
if (attempt.kind.kind === "continuation") {
|
|
24772
25513
|
const refused3 = err instanceof RunnerStartupExit;
|
|
24773
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)}`);
|
|
@@ -24887,12 +25628,13 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24887
25628
|
endAttempt(taskId, reason) {
|
|
24888
25629
|
const attempt = this.attempts.get(taskId);
|
|
24889
25630
|
this.attempts.delete(taskId);
|
|
25631
|
+
const session = attempt?.session ?? null;
|
|
25632
|
+
const nativeSessionId = session?.nativeSessionId() ?? null;
|
|
24890
25633
|
if (attempt) {
|
|
24891
|
-
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);
|
|
24892
25635
|
this.output.detach(taskId);
|
|
24893
25636
|
}
|
|
24894
|
-
|
|
24895
|
-
if (session) this.saveNativeSession(taskId, session.nativeSessionId());
|
|
25637
|
+
if (attempt && session) this.saveNativeSession(taskId, nativeSessionId, attempt.runner);
|
|
24896
25638
|
if (stopsRunner(reason)) {
|
|
24897
25639
|
const task = this.store.get(taskId);
|
|
24898
25640
|
if (task) this.verifier.clear(task);
|
|
@@ -24901,8 +25643,8 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24901
25643
|
return attempt;
|
|
24902
25644
|
}
|
|
24903
25645
|
/** Kept on the task once the attempt ends, so a continue can resume it after a reload (ADR-0018, K1). */
|
|
24904
|
-
saveNativeSession(taskId, nativeSessionId) {
|
|
24905
|
-
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId);
|
|
25646
|
+
saveNativeSession(taskId, nativeSessionId, runner) {
|
|
25647
|
+
if (nativeSessionId) this.store.setTaskRunnerSessionId(taskId, nativeSessionId, runner);
|
|
24906
25648
|
}
|
|
24907
25649
|
endAllAttempts(reason) {
|
|
24908
25650
|
const ended = [...this.attempts.values()];
|
|
@@ -24912,13 +25654,17 @@ ${now.message}` : LIMIT_RESET_MESSAGE;
|
|
|
24912
25654
|
return ended;
|
|
24913
25655
|
}
|
|
24914
25656
|
};
|
|
24915
|
-
var RESUME_MARGIN_MS = 6e4;
|
|
24916
|
-
var RELAPSE_MS = 5 * 6e4;
|
|
24917
25657
|
var LIMIT_RESET_MESSAGE = "Your usage limit has reset. Carry on with the task from where you stopped.";
|
|
24918
25658
|
var OPS_RETRY_TAIL_LINES = 60;
|
|
24919
25659
|
function unresumedMessage(task, why) {
|
|
24920
25660
|
return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
|
|
24921
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
|
+
}
|
|
24922
25668
|
|
|
24923
25669
|
// src/services/Planner.ts
|
|
24924
25670
|
function plannerModesWith(req) {
|
|
@@ -25021,15 +25767,15 @@ var ModelResolver = class {
|
|
|
25021
25767
|
return toOrchestratorOptions({}, ORCHESTRATOR_SHORTCUTS);
|
|
25022
25768
|
}
|
|
25023
25769
|
async modelsForRunners(runners) {
|
|
25024
|
-
const
|
|
25025
|
-
|
|
25770
|
+
const unique = [...new Set(runners)];
|
|
25771
|
+
const discovered = await Promise.all(unique.map(async (runner) => {
|
|
25026
25772
|
try {
|
|
25027
|
-
|
|
25773
|
+
return [runner, await this.discovery.discover(runner)];
|
|
25028
25774
|
} catch {
|
|
25029
|
-
|
|
25775
|
+
return [runner, []];
|
|
25030
25776
|
}
|
|
25031
|
-
}
|
|
25032
|
-
return
|
|
25777
|
+
}));
|
|
25778
|
+
return Object.fromEntries(discovered);
|
|
25033
25779
|
}
|
|
25034
25780
|
async pickerOptions() {
|
|
25035
25781
|
if (this.cachedPickerOptions) return this.cachedPickerOptions;
|
|
@@ -25076,21 +25822,18 @@ var ModelResolver = class {
|
|
|
25076
25822
|
return this.cachedPickerOptions ?? [];
|
|
25077
25823
|
}
|
|
25078
25824
|
/**
|
|
25079
|
-
*
|
|
25080
|
-
*
|
|
25081
|
-
*
|
|
25082
|
-
*
|
|
25083
|
-
*
|
|
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.
|
|
25084
25832
|
*/
|
|
25085
|
-
contextWindowFor(
|
|
25086
|
-
if (!
|
|
25087
|
-
const
|
|
25088
|
-
|
|
25089
|
-
for (const runner of this.config.enabledRunners ?? []) {
|
|
25090
|
-
const found = this.discovery.getCached(runner)?.find((m) => m.modelId === modelId)?.contextWindow;
|
|
25091
|
-
if (found && found > 0) return found;
|
|
25092
|
-
}
|
|
25093
|
-
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;
|
|
25094
25837
|
}
|
|
25095
25838
|
invalidate() {
|
|
25096
25839
|
this.discovery.clear();
|
|
@@ -25116,24 +25859,28 @@ var defaultExec3 = async (command, options) => {
|
|
|
25116
25859
|
return { stdout: String(stdout) };
|
|
25117
25860
|
};
|
|
25118
25861
|
var CHECK_TIMEOUT_MS = 8e3;
|
|
25862
|
+
var NEGATIVE_TTL_MS2 = 6e4;
|
|
25119
25863
|
var RunnerInstallation = class {
|
|
25120
25864
|
cache = /* @__PURE__ */ new Map();
|
|
25121
25865
|
inflight = /* @__PURE__ */ new Map();
|
|
25122
25866
|
registry;
|
|
25123
25867
|
execImpl;
|
|
25124
|
-
|
|
25868
|
+
now;
|
|
25869
|
+
constructor(registry, execImpl = defaultExec3, now = Date.now) {
|
|
25125
25870
|
this.registry = registry;
|
|
25126
25871
|
this.execImpl = execImpl;
|
|
25872
|
+
this.now = now;
|
|
25127
25873
|
}
|
|
25128
25874
|
/** True if the runner's CLI responds to `--version`. Cached per process. */
|
|
25129
25875
|
async isInstalled(runner) {
|
|
25130
|
-
|
|
25876
|
+
const cached2 = this.cache.get(runner);
|
|
25877
|
+
if (cached2 && (cached2.installed || this.now() - cached2.at < NEGATIVE_TTL_MS2)) return cached2.installed;
|
|
25131
25878
|
const existing = this.inflight.get(runner);
|
|
25132
25879
|
if (existing) return existing;
|
|
25133
25880
|
const manifest = this.registry.getManifest(runner);
|
|
25134
25881
|
const command = manifest?.runner.command;
|
|
25135
25882
|
if (!command) {
|
|
25136
|
-
this.cache.set(runner, false);
|
|
25883
|
+
this.cache.set(runner, { installed: false, at: this.now() });
|
|
25137
25884
|
return false;
|
|
25138
25885
|
}
|
|
25139
25886
|
const probe = (async () => {
|
|
@@ -25144,13 +25891,22 @@ var RunnerInstallation = class {
|
|
|
25144
25891
|
return false;
|
|
25145
25892
|
}
|
|
25146
25893
|
})().then((installed) => {
|
|
25147
|
-
this.cache.set(runner, installed);
|
|
25894
|
+
this.cache.set(runner, { installed, at: this.now() });
|
|
25148
25895
|
this.inflight.delete(runner);
|
|
25149
25896
|
return installed;
|
|
25150
25897
|
});
|
|
25151
25898
|
this.inflight.set(runner, probe);
|
|
25152
25899
|
return probe;
|
|
25153
25900
|
}
|
|
25901
|
+
/**
|
|
25902
|
+
* Forget cached "not installed" verdicts so the next probe re-runs them. A
|
|
25903
|
+
* runner-list refresh calls this, so a CLI installed while the host runs is
|
|
25904
|
+
* offered without a restart. An installed verdict stays: it cannot have
|
|
25905
|
+
* changed without the binary going away, and re-probing one costs a spawn.
|
|
25906
|
+
*/
|
|
25907
|
+
refresh() {
|
|
25908
|
+
for (const [runner, probe] of this.cache) if (!probe.installed) this.cache.delete(runner);
|
|
25909
|
+
}
|
|
25154
25910
|
/**
|
|
25155
25911
|
* Whether this runner can serve as a harness planner (ADR-0009, T9), and why
|
|
25156
25912
|
* not when it cannot. Surfaces call this to grey out an unusable agent in the
|
|
@@ -25581,6 +26337,8 @@ var PlannerSession = class {
|
|
|
25581
26337
|
* outlives the session — nothing from one goal may reach the next.
|
|
25582
26338
|
*/
|
|
25583
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;
|
|
25584
26342
|
/** Set once the first spawn attached. */
|
|
25585
26343
|
start = null;
|
|
25586
26344
|
/**
|
|
@@ -25771,6 +26529,7 @@ var CliAgentAiService = class {
|
|
|
25771
26529
|
// hit its rate limit, or lost its login must say so in the chat rather
|
|
25772
26530
|
// than leave an empty planner bubble.
|
|
25773
26531
|
failure: turn.error,
|
|
26532
|
+
usageLimit: turn.usageLimit,
|
|
25774
26533
|
fullText: [...carried, turn.text].filter((part) => part.trim()).join("\n\n")
|
|
25775
26534
|
};
|
|
25776
26535
|
};
|
|
@@ -25837,6 +26596,7 @@ var CliAgentAiService = class {
|
|
|
25837
26596
|
};
|
|
25838
26597
|
const subagentUsage = /* @__PURE__ */ new Map();
|
|
25839
26598
|
let error;
|
|
26599
|
+
let usageLimit;
|
|
25840
26600
|
let stepIndex = 0;
|
|
25841
26601
|
let backgroundAgents = 0;
|
|
25842
26602
|
const truncate = (value) => value.length > LOG_MAX_CHARS ? `${value.slice(0, LOG_MAX_CHARS)}
|
|
@@ -25945,6 +26705,7 @@ var CliAgentAiService = class {
|
|
|
25945
26705
|
}
|
|
25946
26706
|
case "error":
|
|
25947
26707
|
error = event.message;
|
|
26708
|
+
usageLimit = event.usageLimit;
|
|
25948
26709
|
return;
|
|
25949
26710
|
case "turn_end":
|
|
25950
26711
|
return;
|
|
@@ -25963,12 +26724,13 @@ var CliAgentAiService = class {
|
|
|
25963
26724
|
settle(id, "The agent ended the turn without reporting this call's result.", false, "not_executed");
|
|
25964
26725
|
}
|
|
25965
26726
|
session.nativeSessionId = adapter.nativeSessionId() ?? session.nativeSessionId;
|
|
26727
|
+
session.sessionCost = adapter.sessionCost?.() ?? session.sessionCost;
|
|
25966
26728
|
if (error || signal?.aborted) session.stop();
|
|
25967
26729
|
if (error) {
|
|
25968
26730
|
const failure = failureAfterReply(text.slice(finalRunStart), Math.max(blockStart, finalRunStart) - finalRunStart, error);
|
|
25969
26731
|
onProgress({ type: "text_retracted", segmentId: finalSegment });
|
|
25970
26732
|
text = text.slice(0, finalRunStart);
|
|
25971
|
-
return { text, researchLog, error: failure, aborted: signal?.aborted, backgroundAgents };
|
|
26733
|
+
return { text, researchLog, error: failure, usageLimit, aborted: signal?.aborted, backgroundAgents };
|
|
25972
26734
|
}
|
|
25973
26735
|
return { text, researchLog, error, aborted: signal?.aborted, backgroundAgents };
|
|
25974
26736
|
}
|
|
@@ -26024,7 +26786,7 @@ var CliAgentAiService = class {
|
|
|
26024
26786
|
session.stop();
|
|
26025
26787
|
const start = session.start;
|
|
26026
26788
|
if (!start) throw new Error("No active planner conversation.");
|
|
26027
|
-
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);
|
|
26028
26790
|
}
|
|
26029
26791
|
/**
|
|
26030
26792
|
* What the planner gets of the repository's config, its runner being
|
|
@@ -26036,9 +26798,9 @@ var CliAgentAiService = class {
|
|
|
26036
26798
|
const reader = connectorFor(this.runner)?.repoConfig;
|
|
26037
26799
|
if (!reader) return { systemPrompt };
|
|
26038
26800
|
const found = reader.projectServers(cwd);
|
|
26039
|
-
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 });
|
|
26040
26802
|
return {
|
|
26041
|
-
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"),
|
|
26042
26804
|
...started.length > 0 ? { projectServers: started } : {}
|
|
26043
26805
|
};
|
|
26044
26806
|
}
|
|
@@ -26203,6 +26965,10 @@ var SessionCatalog = class {
|
|
|
26203
26965
|
allowlist() {
|
|
26204
26966
|
return this.deps.settings().modelAllowlist ?? {};
|
|
26205
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
|
+
}
|
|
26206
26972
|
/** The models the user allows on one runner; unset means no restriction. */
|
|
26207
26973
|
allowlistFor(runner) {
|
|
26208
26974
|
return this.allowlist()[runner];
|
|
@@ -26217,7 +26983,7 @@ var SessionCatalog = class {
|
|
|
26217
26983
|
*/
|
|
26218
26984
|
models() {
|
|
26219
26985
|
const out = { ...this.discovered };
|
|
26220
|
-
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()])) {
|
|
26221
26987
|
const cached2 = this.deps.modelResolver.getCachedRunnerModels(runner);
|
|
26222
26988
|
if (cached2.length > 0) out[runner] = cached2;
|
|
26223
26989
|
}
|
|
@@ -26254,9 +27020,16 @@ var SessionCatalog = class {
|
|
|
26254
27020
|
* asked for.
|
|
26255
27021
|
*/
|
|
26256
27022
|
async live() {
|
|
26257
|
-
|
|
26258
|
-
|
|
26259
|
-
|
|
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());
|
|
26260
27033
|
}
|
|
26261
27034
|
queryCatalog(runners) {
|
|
26262
27035
|
const registered = runners.filter((runner) => this.deps.registry.getManifest(runner) !== void 0);
|
|
@@ -26282,7 +27055,8 @@ var SessionCatalog = class {
|
|
|
26282
27055
|
modelsByRunner: this.models(),
|
|
26283
27056
|
runnerModes: this.modes(runners),
|
|
26284
27057
|
perRunnerAllowlist: this.allowlist(),
|
|
26285
|
-
autonomousDefault: this.deps.config.autonomousMode
|
|
27058
|
+
autonomousDefault: this.deps.config.autonomousMode,
|
|
27059
|
+
...this.deps.liveRunner ? { liveRunner: this.deps.liveRunner } : {}
|
|
26286
27060
|
};
|
|
26287
27061
|
}
|
|
26288
27062
|
/** What a runner offers, as `runnerAssignment` needs it. Spawns the runner's CLI to list models. */
|
|
@@ -26347,6 +27121,108 @@ function forkPlanState(plan, tasks, dialogue, now) {
|
|
|
26347
27121
|
};
|
|
26348
27122
|
}
|
|
26349
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
|
+
|
|
26350
27226
|
// src/services/PlanTurns.ts
|
|
26351
27227
|
var START_HOLD_TIMEOUT_MS = 5 * 6e4;
|
|
26352
27228
|
function turnPrompt(batch) {
|
|
@@ -26363,6 +27239,16 @@ var PlanTurns = class {
|
|
|
26363
27239
|
constructor(host, holdTimeoutMs = START_HOLD_TIMEOUT_MS) {
|
|
26364
27240
|
this.host = host;
|
|
26365
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
|
+
});
|
|
26366
27252
|
}
|
|
26367
27253
|
host;
|
|
26368
27254
|
holdTimeoutMs;
|
|
@@ -26375,6 +27261,33 @@ var PlanTurns = class {
|
|
|
26375
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. */
|
|
26376
27262
|
quiet = false;
|
|
26377
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
|
+
}
|
|
26378
27291
|
/** Whether a turn this module opened is in flight. */
|
|
26379
27292
|
get busy() {
|
|
26380
27293
|
return this.running !== null;
|
|
@@ -26416,25 +27329,33 @@ var PlanTurns = class {
|
|
|
26416
27329
|
admit(input, forced) {
|
|
26417
27330
|
const plan = this.host.plan();
|
|
26418
27331
|
if (!plan) return Promise.reject(new Error("No planning conversation to continue"));
|
|
26419
|
-
const entry =
|
|
26420
|
-
id: `turn-${Date.now()}-${++this.seq}`,
|
|
26421
|
-
source: input.source,
|
|
26422
|
-
text: input.text,
|
|
26423
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
26424
|
-
...input.skills && input.skills.length > 0 ? { skills: [...input.skills] } : {},
|
|
26425
|
-
...forced ? { forced: true } : {}
|
|
26426
|
-
};
|
|
27332
|
+
const entry = this.pending(input, forced);
|
|
26427
27333
|
this.quiet = false;
|
|
27334
|
+
if (this.resend.arrived()) this.host.announce();
|
|
26428
27335
|
const pending = plan.pendingTurns ?? [];
|
|
26429
27336
|
const at = forced ? pending.findIndex((t) => !t.forced && !this.running?.ids.has(t.id)) : -1;
|
|
26430
27337
|
plan.pendingTurns = at === -1 ? [...pending, entry] : [...pending.slice(0, at), entry, ...pending.slice(at)];
|
|
26431
27338
|
this.host.persist();
|
|
27339
|
+
if (input.source === "user" && this.resend.holds()) {
|
|
27340
|
+
this.pump();
|
|
27341
|
+
return Promise.resolve(plan);
|
|
27342
|
+
}
|
|
26432
27343
|
const settled = new Promise((resolve9, reject) => {
|
|
26433
27344
|
this.waiters.set(entry.id, { resolve: resolve9, reject, signal: input.signal });
|
|
26434
27345
|
});
|
|
26435
27346
|
this.pump();
|
|
26436
27347
|
return settled;
|
|
26437
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
|
+
}
|
|
26438
27359
|
/**
|
|
26439
27360
|
* Start the next turn if one is due: nothing of ours is in flight, the
|
|
26440
27361
|
* conversation is free, and inputs wait. The host calls it when any planner
|
|
@@ -26445,7 +27366,7 @@ var PlanTurns = class {
|
|
|
26445
27366
|
* the plan, what still waits, and the planner in use at that moment.
|
|
26446
27367
|
*/
|
|
26447
27368
|
pump() {
|
|
26448
|
-
if (this.running || this.detecting) return;
|
|
27369
|
+
if (this.running || this.detecting || this.resend.holds()) return;
|
|
26449
27370
|
const plan = this.host.plan();
|
|
26450
27371
|
if (!plan || this.host.conversationBusy()) return;
|
|
26451
27372
|
const batch = this.liveBatch(plan);
|
|
@@ -26525,6 +27446,7 @@ var PlanTurns = class {
|
|
|
26525
27446
|
* conversation's to abandon; whatever it settles lands nothing here.
|
|
26526
27447
|
*/
|
|
26527
27448
|
abandon() {
|
|
27449
|
+
this.resend.reset();
|
|
26528
27450
|
this.running?.release();
|
|
26529
27451
|
this.running = null;
|
|
26530
27452
|
this.quiet = false;
|
|
@@ -26568,8 +27490,10 @@ var PlanTurns = class {
|
|
|
26568
27490
|
clearTimeout(timeout);
|
|
26569
27491
|
if (this.running !== running) return;
|
|
26570
27492
|
const forceStopped = !outcome.ok && running.forceStopped === true;
|
|
27493
|
+
const limited = !outcome.ok && outcome.err instanceof PlannerUsageLimitError;
|
|
27494
|
+
if (outcome.ok) this.resend.landed();
|
|
26571
27495
|
try {
|
|
26572
|
-
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));
|
|
26573
27497
|
const took = this.take(plan, dropped);
|
|
26574
27498
|
if (forceStopped) plan.pendingTurns = [...(plan.pendingTurns ?? []).filter((t) => t.forced), ...(plan.pendingTurns ?? []).filter((t) => !t.forced)];
|
|
26575
27499
|
if (took || forceStopped) this.host.persist();
|
|
@@ -26583,7 +27507,11 @@ var PlanTurns = class {
|
|
|
26583
27507
|
const ended = outcome;
|
|
26584
27508
|
const waiting = new Set((plan.pendingTurns ?? []).map((t) => t.id));
|
|
26585
27509
|
for (const t of batch) {
|
|
26586
|
-
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;
|
|
26587
27515
|
const delivered = this.settle(t.id, (w) => ended.ok ? w.resolve(ended.plan) : w.reject(ended.err));
|
|
26588
27516
|
if (!delivered && !ended.ok && !isInterruption(ended.err) && t.source === "user") this.host.notice(replayFailed(t, ended.err));
|
|
26589
27517
|
}
|
|
@@ -26642,7 +27570,6 @@ var RunSupervision = class {
|
|
|
26642
27570
|
blockedBy: (failedId) => [...blockingFailures([...this.host.tasks()])].filter(([, failures]) => failures.includes(failedId)).map(([taskId]) => taskId),
|
|
26643
27571
|
readBudget: () => this.settings()?.wakeUpReadBudget ?? host.userSettings().wakeUpReadBudget,
|
|
26644
27572
|
retryLimit: () => this.retryLimit(),
|
|
26645
|
-
plannerRunner: () => host.plannerRunner(),
|
|
26646
27573
|
restartPlanner: () => host.restartPlanner(),
|
|
26647
27574
|
abandonTurn: () => host.abandonTurn(),
|
|
26648
27575
|
goUnsupervised: (reason) => this.goUnsupervised(reason),
|
|
@@ -27123,8 +28050,8 @@ function dropLegacyFile(sessionsDir, meta) {
|
|
|
27123
28050
|
const legacy = path30.join(sessionsDir, legacySessionFilename(meta));
|
|
27124
28051
|
if (legacy === path30.join(sessionsDir, sessionFilename(meta)) || !fs14.existsSync(legacy)) return;
|
|
27125
28052
|
try {
|
|
27126
|
-
const
|
|
27127
|
-
if (
|
|
28053
|
+
const held2 = JSON.parse(fs14.readFileSync(legacy, "utf-8")).meta?.id;
|
|
28054
|
+
if (held2 === meta.id) fs14.unlinkSync(legacy);
|
|
27128
28055
|
} catch {
|
|
27129
28056
|
}
|
|
27130
28057
|
}
|
|
@@ -27257,24 +28184,25 @@ function listTaskLogAttempts(location, taskId) {
|
|
|
27257
28184
|
}
|
|
27258
28185
|
return names.map((name) => /^([1-9]\d*)\.jsonl$/.exec(name)?.[1]).filter((n) => n !== void 0).map(Number).sort((a, b) => a - b);
|
|
27259
28186
|
}
|
|
27260
|
-
function openTaskLog(location, taskId) {
|
|
28187
|
+
function openTaskLog(location, taskId, attempt) {
|
|
27261
28188
|
ensureStateDirIgnored(location.baseDir);
|
|
27262
28189
|
const dir = taskDir(location, taskId);
|
|
27263
28190
|
fs15.mkdirSync(dir, { recursive: true });
|
|
27264
|
-
|
|
28191
|
+
const next = (listTaskLogAttempts(location, taskId).pop() ?? 0) + 1;
|
|
28192
|
+
let number = attempt !== void 0 && Number.isInteger(attempt) && attempt >= 1 ? attempt : next;
|
|
27265
28193
|
for (; ; ) {
|
|
27266
|
-
const file = path31.join(dir, `${
|
|
28194
|
+
const file = path31.join(dir, `${number}${EXTENSION}`);
|
|
27267
28195
|
try {
|
|
27268
28196
|
fs15.writeFileSync(file, "", { flag: "wx" });
|
|
27269
28197
|
} catch (err) {
|
|
27270
28198
|
if (err.code === "EEXIST") {
|
|
27271
|
-
|
|
28199
|
+
number = Math.max(number + 1, next);
|
|
27272
28200
|
continue;
|
|
27273
28201
|
}
|
|
27274
28202
|
throw err;
|
|
27275
28203
|
}
|
|
27276
28204
|
return {
|
|
27277
|
-
attempt,
|
|
28205
|
+
attempt: number,
|
|
27278
28206
|
append(events) {
|
|
27279
28207
|
if (events.length === 0) return;
|
|
27280
28208
|
fs15.appendFileSync(file, events.map((e) => `${JSON.stringify(e)}
|
|
@@ -27320,11 +28248,13 @@ var TaskLogRecorder = class {
|
|
|
27320
28248
|
open;
|
|
27321
28249
|
flushMs;
|
|
27322
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();
|
|
27323
28253
|
wrap(runner) {
|
|
27324
28254
|
return {
|
|
27325
28255
|
spawn: async (opts) => {
|
|
27326
28256
|
const session = await runner.spawn(opts);
|
|
27327
|
-
this.record(opts.taskId, session, opts.skills);
|
|
28257
|
+
this.record(opts.taskId, session, opts.skills, opts.attempt);
|
|
27328
28258
|
return session;
|
|
27329
28259
|
},
|
|
27330
28260
|
stop: (sessionId) => runner.stop(sessionId),
|
|
@@ -27334,11 +28264,20 @@ var TaskLogRecorder = class {
|
|
|
27334
28264
|
}
|
|
27335
28265
|
};
|
|
27336
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
|
+
}
|
|
27337
28276
|
/** Start a new attempt's log for `session`, opening with the skills it was given, and keep it until the session exits. */
|
|
27338
|
-
record(taskId, session, skills = []) {
|
|
28277
|
+
record(taskId, session, skills = [], attemptNumber) {
|
|
27339
28278
|
let file;
|
|
27340
28279
|
try {
|
|
27341
|
-
file = this.open(this.deps.location(), taskId);
|
|
28280
|
+
file = this.open(this.deps.location(), taskId, attemptNumber);
|
|
27342
28281
|
} catch (err) {
|
|
27343
28282
|
this.logger.warn("taskLog", `could not create the log of task ${taskId}; it streams but is not saved`, err);
|
|
27344
28283
|
file = null;
|
|
@@ -27365,6 +28304,10 @@ var TaskLogRecorder = class {
|
|
|
27365
28304
|
}
|
|
27366
28305
|
this.deps.broadcast({ type: "task_log", taskId, attempt, events: taskLogForSurface(events) });
|
|
27367
28306
|
};
|
|
28307
|
+
this.latest.set(taskId, { sessionId: session.id, note: (entry) => {
|
|
28308
|
+
pending.push(entry);
|
|
28309
|
+
flush();
|
|
28310
|
+
} });
|
|
27368
28311
|
const openCalls = /* @__PURE__ */ new Set();
|
|
27369
28312
|
session.onEvent((event) => {
|
|
27370
28313
|
const entry = toTaskLogEvent(event);
|
|
@@ -27385,21 +28328,82 @@ var TaskLogRecorder = class {
|
|
|
27385
28328
|
}
|
|
27386
28329
|
};
|
|
27387
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
|
+
|
|
27388
28383
|
// src/services/PlannerUsage.ts
|
|
27389
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;
|
|
27390
28391
|
usage = { totals: {} };
|
|
28392
|
+
windows;
|
|
27391
28393
|
/** Fold one model call's record into the totals; returns the running ledger. */
|
|
27392
28394
|
record(record) {
|
|
27393
|
-
this.usage = addPlannerUsage(this.usage, record);
|
|
28395
|
+
this.usage = addPlannerUsage(this.usage, this.windows.fill(record));
|
|
27394
28396
|
return this.usage;
|
|
27395
28397
|
}
|
|
27396
28398
|
/** Adopt a persisted ledger (a reopened session) or start from zero. */
|
|
27397
28399
|
restore(usage) {
|
|
27398
28400
|
this.usage = usage ?? { totals: {} };
|
|
28401
|
+
this.windows = new ContextWindowFill(this.catalogWindow, this.usage);
|
|
27399
28402
|
}
|
|
27400
28403
|
/** Start a fresh session's ledger from zero. */
|
|
27401
28404
|
clear() {
|
|
27402
28405
|
this.usage = { totals: {} };
|
|
28406
|
+
this.windows = new ContextWindowFill(this.catalogWindow);
|
|
27403
28407
|
}
|
|
27404
28408
|
/** Whether anything has been recorded — a plan with no usage says nothing. */
|
|
27405
28409
|
get hasUsage() {
|
|
@@ -27494,19 +28498,54 @@ var PlanEditor = class {
|
|
|
27494
28498
|
* between AI and MAN, are the patches that can leave a task incoherent
|
|
27495
28499
|
* (unschedulable, or carrying fields that mean nothing for its new type),
|
|
27496
28500
|
* so both go through the same {@link validateTaskEdit} guard the planner's
|
|
27497
|
-
* task-ops applier uses (as the 'direct' actor, which skips the lock rule
|
|
27498
|
-
*
|
|
27499
|
-
* 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
|
|
27500
28504
|
* falls through to the no-op `store.update` below instead of throwing.
|
|
27501
28505
|
* Attached skills meet the rule a planner's do: a planner skill is refused,
|
|
27502
|
-
* 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.
|
|
27503
28509
|
*
|
|
27504
28510
|
* Edits to one task commit in call order: the skill check waits on git, so
|
|
27505
28511
|
* two quick chip edits (`[a]` then `[a, b]`) could otherwise land the first
|
|
27506
28512
|
* one last.
|
|
27507
28513
|
*/
|
|
27508
28514
|
updateTask(taskId, changes) {
|
|
27509
|
-
|
|
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);
|
|
27510
28549
|
const settled = edit.catch(() => {
|
|
27511
28550
|
});
|
|
27512
28551
|
this.taskEdits.set(taskId, settled);
|
|
@@ -27522,22 +28561,31 @@ var PlanEditor = class {
|
|
|
27522
28561
|
const target = this.store.get(taskId);
|
|
27523
28562
|
const skills = "skills" in changes && target ? await checkTaskSkillsEdit(target, typedSkills, this.taskSkills()) : void 0;
|
|
27524
28563
|
if (skills?.errors.length) throw new PlanEditError(skills.errors.map((e) => e.message).join(" "));
|
|
27525
|
-
if ((changes.dependencies || changes.type ||
|
|
28564
|
+
if ((changes.dependencies || changes.type || "assignedModel" in changes || "assignedRunner" in changes || changes.taskMode || "ops" in changes) && target) {
|
|
27526
28565
|
const check2 = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.catalog.edit());
|
|
27527
28566
|
if (!check2.ok) throw new PlanEditError(check2.error ?? "Those changes are not valid");
|
|
27528
28567
|
if (check2.clear?.length || check2.set) {
|
|
27529
28568
|
changes = { ...changes, ...Object.fromEntries((check2.clear ?? []).map((f) => [f, void 0])), ...check2.set };
|
|
27530
28569
|
}
|
|
27531
28570
|
}
|
|
27532
|
-
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(
|
|
27533
28583
|
() => {
|
|
27534
28584
|
changes = this.scheduler.releaseMovedResume(taskId, changes);
|
|
27535
28585
|
return Boolean(this.store.update(taskId, changes));
|
|
27536
28586
|
},
|
|
27537
28587
|
() => this.broadcast({ type: "task_updated", taskId, changes })
|
|
27538
28588
|
);
|
|
27539
|
-
if (plan) for (const warning of skills?.warnings ?? []) this.notice("warn", warning);
|
|
27540
|
-
return plan;
|
|
27541
28589
|
}
|
|
27542
28590
|
/**
|
|
27543
28591
|
* Replace one task's dependency list — the named entry point the surfaces'
|
|
@@ -27554,7 +28602,8 @@ var PlanEditor = class {
|
|
|
27554
28602
|
* because a runner change is never a single-field edit: the task's model,
|
|
27555
28603
|
* thinking effort and mode are all scoped to its runner, so they are
|
|
27556
28604
|
* re-derived from the new runner's catalog (see {@link retargetTaskRunner}).
|
|
27557
|
-
* 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.
|
|
27558
28607
|
*
|
|
27559
28608
|
* The runner is also admitted into `plan.runners` — see {@link admitRunner}.
|
|
27560
28609
|
*/
|
|
@@ -27564,11 +28613,13 @@ var PlanEditor = class {
|
|
|
27564
28613
|
const task = this.store.get(taskId);
|
|
27565
28614
|
if (!task) return null;
|
|
27566
28615
|
if (task.assignedRunner === runner || task.type === "user") return plan;
|
|
28616
|
+
this.refuseLiveRetarget(task);
|
|
27567
28617
|
const catalog = await this.catalog.runner(runner);
|
|
27568
28618
|
const from = this.catalog.edit([task.assignedRunner]).runnerModes[task.assignedRunner];
|
|
27569
28619
|
const changes = retargetTaskRunner(task, runner, this.allowedCatalog(catalog, runner), from);
|
|
27570
28620
|
if (Object.keys(changes).length === 0) return this.plan();
|
|
27571
28621
|
this.refuseUnavailableMode(runner, changes.taskMode, catalog.modes);
|
|
28622
|
+
this.refuseLiveRetarget(task);
|
|
27572
28623
|
return this.commit(() => {
|
|
27573
28624
|
if (!this.store.update(taskId, this.scheduler.releaseMovedResume(taskId, changes))) return false;
|
|
27574
28625
|
this.admitRunner(runner, catalog.models);
|
|
@@ -27602,6 +28653,9 @@ var PlanEditor = class {
|
|
|
27602
28653
|
return true;
|
|
27603
28654
|
});
|
|
27604
28655
|
}
|
|
28656
|
+
refuseLiveRetarget(task) {
|
|
28657
|
+
if (this.catalog.edit().liveRunner?.(task.id)) throw new PlanEditError(liveRunnerRefusal(task));
|
|
28658
|
+
}
|
|
27605
28659
|
/**
|
|
27606
28660
|
* A mode the host cannot run is refused, with its reason, not swapped for
|
|
27607
28661
|
* another: the plan is the source of truth for what runs (ADR-0001), and a
|
|
@@ -27931,6 +28985,10 @@ var ExecutionCommands = class {
|
|
|
27931
28985
|
const all = this.all(tasks);
|
|
27932
28986
|
const live = all.filter(({ task }) => this.host.running(task.id));
|
|
27933
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
|
+
}
|
|
27934
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.` };
|
|
27935
28993
|
}
|
|
27936
28994
|
const toRun = all.filter(({ task }) => task.type === "ai" && task.status !== "completed" && task.status !== "skipped");
|
|
@@ -28356,8 +29414,11 @@ function mergeStopped(result) {
|
|
|
28356
29414
|
switch (result.outcome) {
|
|
28357
29415
|
case "merged":
|
|
28358
29416
|
return "it merged";
|
|
28359
|
-
case "blocked":
|
|
28360
|
-
|
|
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
|
+
}
|
|
28361
29422
|
case "conflict":
|
|
28362
29423
|
return `it conflicts in ${result.repo}${result.files?.length ? `: ${result.files.slice(0, 5).join(", ")}` : ""}`;
|
|
28363
29424
|
case "failed":
|
|
@@ -28482,11 +29543,17 @@ function liveAiService(config, workspaceRoot, mcpServer) {
|
|
|
28482
29543
|
function isPlannerApproval(request) {
|
|
28483
29544
|
return !isRunnerApproval(request);
|
|
28484
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
|
+
}
|
|
28485
29551
|
function createSession(deps) {
|
|
28486
29552
|
const pinnedAiService = deps.aiService;
|
|
28487
29553
|
const mcpServer = deps.mcpServer ?? sharedMcpServer();
|
|
28488
29554
|
const aiService = pinnedAiService ? () => pinnedAiService : liveAiService(deps.config, deps.workspaceRoot, mcpServer);
|
|
28489
29555
|
const store = new PlanStore();
|
|
29556
|
+
const catalogWindow = (source, model) => deps.modelResolver.contextWindowFor?.(source, model);
|
|
28490
29557
|
const taskLogs = new TaskLogRecorder({
|
|
28491
29558
|
broadcast: deps.broadcast,
|
|
28492
29559
|
// Read through the Session, whose id and workspace are the ones it saves
|
|
@@ -28520,7 +29587,7 @@ function createSession(deps) {
|
|
|
28520
29587
|
const orchestrator = TaskOrchestrator.compose({
|
|
28521
29588
|
config: deps.config,
|
|
28522
29589
|
notifications: deps.notifications,
|
|
28523
|
-
runner: taskLogs.wrap(runnerApprovals.wrap(deps.runner)),
|
|
29590
|
+
runner: taskLogs.wrap(runnerApprovals.wrap(withContextWindows(deps.runner, catalogWindow))),
|
|
28524
29591
|
store,
|
|
28525
29592
|
output: deps.taskOutput,
|
|
28526
29593
|
isolation: deps.isolation,
|
|
@@ -28528,10 +29595,11 @@ function createSession(deps) {
|
|
|
28528
29595
|
workspaceRoot: deps.workspaceRoot,
|
|
28529
29596
|
skillsAt: (roots) => (deps.skillsService ?? new SkillsService(roots)).forRoot(roots),
|
|
28530
29597
|
previousAttemptFromLog: (taskId) => lastAttemptDigest(session.taskLogLocation, taskId),
|
|
29598
|
+
lastLoggedAttempt: (taskId) => session.taskLogAttempts(taskId).at(-1) ?? 0,
|
|
28531
29599
|
runSettings: () => session.planState?.runSettings,
|
|
28532
29600
|
isApproved: (taskId) => session.isApproved(taskId)
|
|
28533
29601
|
});
|
|
28534
|
-
const usage = new PlannerUsageLedger();
|
|
29602
|
+
const usage = new PlannerUsageLedger(catalogWindow);
|
|
28535
29603
|
const events = new SessionEventRelay({
|
|
28536
29604
|
broadcast: deps.broadcast,
|
|
28537
29605
|
onNotice: deps.onNotice,
|
|
@@ -28580,7 +29648,8 @@ function createSession(deps) {
|
|
|
28580
29648
|
modelResolver: deps.modelResolver,
|
|
28581
29649
|
settings: deps.settings,
|
|
28582
29650
|
// Read through the Session, which holds the plan; the catalog is built first.
|
|
28583
|
-
planRunners: () => session.planState?.runners ?? []
|
|
29651
|
+
planRunners: () => session.planState?.runners ?? [],
|
|
29652
|
+
liveRunner: (taskId) => orchestrator.getAttempt(taskId) !== void 0
|
|
28584
29653
|
});
|
|
28585
29654
|
const session = new Session({
|
|
28586
29655
|
config: deps.config,
|
|
@@ -28602,6 +29671,7 @@ function createSession(deps) {
|
|
|
28602
29671
|
usage,
|
|
28603
29672
|
approvals,
|
|
28604
29673
|
approvalPolicy,
|
|
29674
|
+
taskLogs,
|
|
28605
29675
|
// Every planner's web reads, carried here or natively, are decided by the
|
|
28606
29676
|
// one policy, which asks through the same channel as paths and commands.
|
|
28607
29677
|
// The response cache and search provider are process-wide, so parallel
|
|
@@ -28652,6 +29722,7 @@ var Session = class {
|
|
|
28652
29722
|
fsAdapter;
|
|
28653
29723
|
approvals;
|
|
28654
29724
|
approvalPolicy;
|
|
29725
|
+
taskLogs;
|
|
28655
29726
|
fetcher;
|
|
28656
29727
|
settingsFn;
|
|
28657
29728
|
save;
|
|
@@ -28687,7 +29758,7 @@ var Session = class {
|
|
|
28687
29758
|
turnId: () => this.conversation.currentTurnId,
|
|
28688
29759
|
recordSkillLoad: (skill) => this.conversation.recordPlannerSkill(skill),
|
|
28689
29760
|
liveCatalog: () => this.liveCatalog(),
|
|
28690
|
-
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()),
|
|
29761
|
+
coerce: (tasks) => coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), this.store.planTasks),
|
|
28691
29762
|
submitPlan: (tasks) => this.submitPlanFromTool(tasks),
|
|
28692
29763
|
editPlan: (ops) => this.editPlanFromTool(ops),
|
|
28693
29764
|
tasks: () => this.store.planTasks,
|
|
@@ -28732,6 +29803,7 @@ var Session = class {
|
|
|
28732
29803
|
this.usage = parts.usage;
|
|
28733
29804
|
this.approvals = parts.approvals;
|
|
28734
29805
|
this.approvalPolicy = parts.approvalPolicy;
|
|
29806
|
+
this.taskLogs = parts.taskLogs;
|
|
28735
29807
|
this.fetcher = parts.fetcher;
|
|
28736
29808
|
this.skillsService = parts.skillsService;
|
|
28737
29809
|
this.save = parts.saveSession;
|
|
@@ -28741,7 +29813,8 @@ var Session = class {
|
|
|
28741
29813
|
aiService: () => this.aiService(),
|
|
28742
29814
|
onProgress: (p) => this.events.progress(p),
|
|
28743
29815
|
opening: (runners) => this.conversationOpening(runners),
|
|
28744
|
-
catalog: () => this.catalog.
|
|
29816
|
+
catalog: () => this.catalog.current(),
|
|
29817
|
+
assignmentScope: () => this.catalog.scope(),
|
|
28745
29818
|
autorun: () => this.userRunSettings().autorun,
|
|
28746
29819
|
takeRunUpdates: () => this.untoldRunUpdates.splice(0),
|
|
28747
29820
|
tasks: () => this.store.planTasks,
|
|
@@ -28761,13 +29834,18 @@ var Session = class {
|
|
|
28761
29834
|
turnAborted: () => this.approvals.clear(isPlannerApproval),
|
|
28762
29835
|
// After the turn has announced its end, so the next one never starts
|
|
28763
29836
|
// ahead of it on a surface.
|
|
28764
|
-
turnReleased: () => queueMicrotask(() => this.turns.pump())
|
|
29837
|
+
turnReleased: () => queueMicrotask(() => this.turns.pump()),
|
|
29838
|
+
usageLimit: (said2) => this.turns.usageLimit(said2)
|
|
28765
29839
|
});
|
|
28766
29840
|
this.turns = new PlanTurns({
|
|
28767
29841
|
plan: () => this.closed ? null : this.plan,
|
|
28768
29842
|
conversationBusy: () => this.conversation.isTurnInFlight,
|
|
28769
29843
|
mayWake: () => this.supervision.mayWake(),
|
|
28770
29844
|
persist: () => this.persist({ background: true }),
|
|
29845
|
+
announce: () => {
|
|
29846
|
+
this.persist({ background: true });
|
|
29847
|
+
this.events.planGenerated(this.plan, this.goal);
|
|
29848
|
+
},
|
|
28771
29849
|
interrupt: () => {
|
|
28772
29850
|
this.conversation.stopTurn("force_send");
|
|
28773
29851
|
},
|
|
@@ -28784,7 +29862,8 @@ var Session = class {
|
|
|
28784
29862
|
},
|
|
28785
29863
|
notice: (message) => this.notice("error", message),
|
|
28786
29864
|
plannerDetecting: () => installedPlannerAnswered() ? null : installedPlannerDetected(),
|
|
28787
|
-
assertPlannerReady: () => this.assertPlannerReady()
|
|
29865
|
+
assertPlannerReady: () => this.assertPlannerReady(),
|
|
29866
|
+
plannerRunner: () => runnerForProvider(this.config.aiProvider)
|
|
28788
29867
|
});
|
|
28789
29868
|
this.supervision = new RunSupervision({
|
|
28790
29869
|
plan: () => this.plan,
|
|
@@ -28802,7 +29881,6 @@ var Session = class {
|
|
|
28802
29881
|
},
|
|
28803
29882
|
deliver: (digest) => this.turns.enqueue({ source: "supervisor", text: digest }),
|
|
28804
29883
|
pump: () => this.turns.pump(),
|
|
28805
|
-
plannerRunner: () => runnerForProvider(this.config.aiProvider),
|
|
28806
29884
|
restartPlanner: () => this.aiService().restart?.(),
|
|
28807
29885
|
abandonTurn: () => this.conversation.abandonHungTurn(),
|
|
28808
29886
|
notice: (level, message) => this.notice(level, message)
|
|
@@ -28884,6 +29962,7 @@ var Session = class {
|
|
|
28884
29962
|
tasks: () => this.plan ? this.store.planTasks : null,
|
|
28885
29963
|
planApproved: () => this.plan?.approved === true,
|
|
28886
29964
|
taskApproved: (taskId) => this.isApproved(taskId),
|
|
29965
|
+
waitsForApproval: (taskId) => this.waitsForApproval(taskId),
|
|
28887
29966
|
approve: (taskIds) => this.approve(taskIds),
|
|
28888
29967
|
autonomous: () => this.config.autonomousMode,
|
|
28889
29968
|
autorun: () => this.userRunSettings().autorun,
|
|
@@ -28905,6 +29984,7 @@ var Session = class {
|
|
|
28905
29984
|
blockedBy: (taskId) => this.orchestrator.getBlockedBy(taskId),
|
|
28906
29985
|
waitingRequests: (taskId) => this.approvals.outstanding().filter(({ request }) => isRunnerApproval(request) && request.taskId === taskId).map(({ id, request }) => ({ id, subject: request.subject })),
|
|
28907
29986
|
runPlan: (actor) => this.runWholePlan(runOrigin(actor)),
|
|
29987
|
+
joinRun: (approved) => this.joinRun(approved),
|
|
28908
29988
|
startTask: (taskId, actor) => this.startOneTask(taskId, runOrigin(actor)),
|
|
28909
29989
|
stopTask: (taskId) => this.cancelTask(taskId),
|
|
28910
29990
|
stopRun: () => this.stopExecution(),
|
|
@@ -28933,8 +30013,8 @@ var Session = class {
|
|
|
28933
30013
|
* one: nothing it would start is approved until the user executes it.
|
|
28934
30014
|
*/
|
|
28935
30015
|
heldEdit(taskId) {
|
|
28936
|
-
const
|
|
28937
|
-
if (!
|
|
30016
|
+
const held2 = this.conversation.pendingOps();
|
|
30017
|
+
if (!held2 || held2.length === 0 || !this.plan) return false;
|
|
28938
30018
|
if (taskId === void 0) return true;
|
|
28939
30019
|
const before = this.store.get(taskId);
|
|
28940
30020
|
const after = this.asEdited(taskId);
|
|
@@ -28947,10 +30027,10 @@ var Session = class {
|
|
|
28947
30027
|
/** {@link editedTasks}, with the id each of the held batch's handles names in them. */
|
|
28948
30028
|
editedPlan() {
|
|
28949
30029
|
const unedited = { tasks: this.store.planTasks, handles: /* @__PURE__ */ new Map() };
|
|
28950
|
-
const
|
|
28951
|
-
if (!
|
|
30030
|
+
const held2 = this.conversation.pendingOps();
|
|
30031
|
+
if (!held2 || held2.length === 0 || !this.plan) return unedited;
|
|
28952
30032
|
const runners = this.editRunners();
|
|
28953
|
-
const result = applyTaskOps(this.store.planTasks,
|
|
30033
|
+
const result = applyTaskOps(this.store.planTasks, held2, runners, this.catalog.edit(runners));
|
|
28954
30034
|
return result.ok ? { tasks: result.tasks, handles: result.handles ?? /* @__PURE__ */ new Map() } : unedited;
|
|
28955
30035
|
}
|
|
28956
30036
|
/** The task as the open turn's held edit leaves it; undefined when that edit removes it. */
|
|
@@ -28974,9 +30054,9 @@ var Session = class {
|
|
|
28974
30054
|
* rules, then, for the supervisor, the oscillation guard. `held` are the
|
|
28975
30055
|
* ops the turn already holds, which `ops` join; errors are numbered by `ops`.
|
|
28976
30056
|
*/
|
|
28977
|
-
checkPlannerOps(ops,
|
|
30057
|
+
checkPlannerOps(ops, held2 = []) {
|
|
28978
30058
|
const runners = this.editRunners();
|
|
28979
|
-
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);
|
|
28980
30060
|
if (!result.ok || !this.supervisorEditing()) return result;
|
|
28981
30061
|
const trip = this.supervision.checkEdit(this.store.planTasks, result.tasks);
|
|
28982
30062
|
if (!trip) return result;
|
|
@@ -29146,6 +30226,9 @@ var Session = class {
|
|
|
29146
30226
|
onUsageLimitWait: ({ taskId, runner, resumeAt, cancelled }) => {
|
|
29147
30227
|
if (!cancelled) this.supervision.observe({ kind: "usage_limit", taskId, runner, resumeAt });
|
|
29148
30228
|
},
|
|
30229
|
+
onModelSwitched: (switched) => {
|
|
30230
|
+
void this.followRunnerModel(switched);
|
|
30231
|
+
},
|
|
29149
30232
|
onTaskIdle: ({ taskId, idleSince }) => {
|
|
29150
30233
|
this.supervision.observe({ kind: "task_idle", taskId, detail: `no output since ${idleSince}` });
|
|
29151
30234
|
},
|
|
@@ -29266,10 +30349,12 @@ var Session = class {
|
|
|
29266
30349
|
* boundary rewrites `in_progress` to `pending` — nothing is running when a
|
|
29267
30350
|
* session comes off a file — so a surface that re-reads the plan mid-run must
|
|
29268
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.
|
|
29269
30354
|
*/
|
|
29270
30355
|
get currentPlanState() {
|
|
29271
30356
|
if (!this.plan) return null;
|
|
29272
|
-
const executionLog = this.store.getExecutionLog();
|
|
30357
|
+
const executionLog = this.store.getExecutionLog().map((s) => ({ ...s, ...this.store.get(s.id) }));
|
|
29273
30358
|
const logged = new Set(executionLog.map((s) => s.id));
|
|
29274
30359
|
const pendingTasks = this.store.planTasks.filter((t) => !logged.has(t.id));
|
|
29275
30360
|
const record = approvalRecordOf(this.plan);
|
|
@@ -29280,7 +30365,7 @@ var Session = class {
|
|
|
29280
30365
|
phase: "executing",
|
|
29281
30366
|
history: [],
|
|
29282
30367
|
message: "",
|
|
29283
|
-
executionLog
|
|
30368
|
+
executionLog,
|
|
29284
30369
|
pendingTasks,
|
|
29285
30370
|
goal: this.goal,
|
|
29286
30371
|
runners: this.plan.runners,
|
|
@@ -29450,16 +30535,20 @@ var Session = class {
|
|
|
29450
30535
|
opened = this.openTurn(() => this.conversation.start(this.resolveSkillInvocation(goal), () => this.conversationOpening(chosenRunners), options?.signal));
|
|
29451
30536
|
} catch (err) {
|
|
29452
30537
|
if (this.opening === pending) this.opening = null;
|
|
29453
|
-
for (const
|
|
30538
|
+
for (const held3 of pending.held ?? []) held3.reject(err);
|
|
29454
30539
|
throw err;
|
|
29455
30540
|
}
|
|
29456
|
-
const
|
|
30541
|
+
const held2 = pending.held ?? [];
|
|
29457
30542
|
pending.held = null;
|
|
29458
|
-
for (const h of
|
|
30543
|
+
for (const h of held2) this.turns.follow(h.input, h.forced).then(h.resolve, h.reject);
|
|
29459
30544
|
try {
|
|
29460
30545
|
const { landed: landed2, effects } = await opened;
|
|
29461
30546
|
this.autorunSubmitted(effects);
|
|
29462
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;
|
|
29463
30552
|
} finally {
|
|
29464
30553
|
if (this.opening === pending) this.opening = null;
|
|
29465
30554
|
}
|
|
@@ -29474,10 +30563,10 @@ var Session = class {
|
|
|
29474
30563
|
behindOpening(input, forced = false) {
|
|
29475
30564
|
const pending = this.opening;
|
|
29476
30565
|
if (!pending) return null;
|
|
29477
|
-
const
|
|
29478
|
-
if (!
|
|
30566
|
+
const held2 = pending.held;
|
|
30567
|
+
if (!held2) return this.turns.follow(input, forced);
|
|
29479
30568
|
return new Promise((resolve9, reject) => {
|
|
29480
|
-
|
|
30569
|
+
held2.push({ input, forced, resolve: resolve9, reject });
|
|
29481
30570
|
});
|
|
29482
30571
|
}
|
|
29483
30572
|
/**
|
|
@@ -29576,12 +30665,12 @@ var Session = class {
|
|
|
29576
30665
|
await this.liveCatalog();
|
|
29577
30666
|
if (this.conversation.currentTurnId !== turn) return refuse(PLANNER_TURN_ENDED);
|
|
29578
30667
|
if (!this.plan || this.store.planTasks.length === 0) return refuse("There is no plan to edit yet: submit one with submit_plan.");
|
|
29579
|
-
const
|
|
29580
|
-
if (!
|
|
29581
|
-
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);
|
|
29582
30671
|
if (!result.ok) return { ok: false, errors: result.errors };
|
|
29583
|
-
if (!this.conversation.submit({ kind: "task_ops", ops: [...
|
|
29584
|
-
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);
|
|
29585
30674
|
return { ok: true, summary: result.summary, coerced: coerced.map(({ taskId: _id, ...rest }) => rest) };
|
|
29586
30675
|
}
|
|
29587
30676
|
/** Whether the planner conversation is live (started and not yet committed to a plan). */
|
|
@@ -29618,9 +30707,6 @@ var Session = class {
|
|
|
29618
30707
|
autonomousDefault: this.config.autonomousMode,
|
|
29619
30708
|
autorun: this.userRunSettings().autorun,
|
|
29620
30709
|
isolatedExecution: await this.runs.plannerLayout(),
|
|
29621
|
-
// The planner's own model window, when a cached catalog knows it, so the
|
|
29622
|
-
// usage line can show context fill (#49). Unknown stays absent.
|
|
29623
|
-
contextWindow: this.modelResolver.contextWindowFor?.(this.config.orchestratorModel),
|
|
29624
30710
|
fs: this.fsAdapter,
|
|
29625
30711
|
fetcher: this.fetcher,
|
|
29626
30712
|
approval: this.approvalPolicy,
|
|
@@ -29647,7 +30733,7 @@ var Session = class {
|
|
|
29647
30733
|
for (const runner of runnersOf(tasks)) this.editor.admitRunner(runner, this.catalog.known(runner));
|
|
29648
30734
|
const runners = this.plan.runners;
|
|
29649
30735
|
const before = this.store.planTasks;
|
|
29650
|
-
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models()).tasks;
|
|
30736
|
+
let coerced = coerceAssignments(tasks, this.catalog.allowlist(), this.catalog.models(), before).tasks;
|
|
29651
30737
|
if (how === "commit") coerced = keepExecutionState(before, carryPlannedModes(before, coerced, this.catalog.edit(runners)));
|
|
29652
30738
|
const bySupervisor = this.supervisorEditing();
|
|
29653
30739
|
const { tasks: stamped, added } = stampOrigin(before, coerced, bySupervisor);
|
|
@@ -29660,6 +30746,7 @@ var Session = class {
|
|
|
29660
30746
|
this.store.resetForRun({ keepScheduledResumes: true });
|
|
29661
30747
|
}
|
|
29662
30748
|
if (bySupervisor) this.approve(added);
|
|
30749
|
+
else if (this.conversation.currentWakeUp === void 0 && this.userRunSettings().autorun) this.approve(writtenTasks(before, coerced));
|
|
29663
30750
|
return coerced.length;
|
|
29664
30751
|
}
|
|
29665
30752
|
preparePlannerTaskUpdates(tasks) {
|
|
@@ -29694,6 +30781,11 @@ var Session = class {
|
|
|
29694
30781
|
if (changes.mergeAtGatesAutomatically) this.orchestrator.runSettingsChanged();
|
|
29695
30782
|
}
|
|
29696
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
|
+
}
|
|
29697
30789
|
this.idlePlan();
|
|
29698
30790
|
this.approve(flattenTasks(this.store.planTasks).map((t) => t.id));
|
|
29699
30791
|
await this.runWholePlan("user");
|
|
@@ -29715,6 +30807,25 @@ var Session = class {
|
|
|
29715
30807
|
await this.orchestrator.approveReview(origin);
|
|
29716
30808
|
});
|
|
29717
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
|
+
}
|
|
29718
30829
|
async approveReview() {
|
|
29719
30830
|
if (!this.plan) throw new NoPlanError("review");
|
|
29720
30831
|
const plan = this.plan;
|
|
@@ -29723,9 +30834,12 @@ var Session = class {
|
|
|
29723
30834
|
await this.withSave(() => this.orchestrator.approveReview("user"));
|
|
29724
30835
|
return plan;
|
|
29725
30836
|
}
|
|
30837
|
+
/** Answers the line saying the task waits for its dependencies, when it was queued behind them rather than started. */
|
|
29726
30838
|
async forceStartTask(taskId) {
|
|
29727
30839
|
this.approveTask(taskId);
|
|
29728
|
-
|
|
30840
|
+
if (!this.plan) return;
|
|
30841
|
+
this.requireTask(taskId);
|
|
30842
|
+
return this.withSave(() => this.orchestrator.forceStartTask(taskId, "user"));
|
|
29729
30843
|
}
|
|
29730
30844
|
async startOneTask(taskId, origin) {
|
|
29731
30845
|
if (!this.plan) return;
|
|
@@ -29782,10 +30896,51 @@ var Session = class {
|
|
|
29782
30896
|
async resumeNow(taskId) {
|
|
29783
30897
|
await this.withSave(() => this.orchestrator.resumeNow(taskId, "user"));
|
|
29784
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
|
+
}
|
|
29785
30907
|
/** Keep a task paused on a usage limit from resuming on its own; see {@link TaskOrchestrator.cancelScheduledResume}. */
|
|
29786
30908
|
cancelScheduledResume(taskId) {
|
|
29787
30909
|
return this.saved(() => this.orchestrator.cancelScheduledResume(taskId));
|
|
29788
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
|
+
}
|
|
29789
30944
|
/** Said through both channels: a host shows one or the other, never both (the daemon's toasts are no-ops). */
|
|
29790
30945
|
notice(level, message) {
|
|
29791
30946
|
this.notifications[level](message);
|
|
@@ -29857,8 +31012,8 @@ var Session = class {
|
|
|
29857
31012
|
if (!this.plan) return EMPTY_RUN;
|
|
29858
31013
|
const planned = reduceRun(EMPTY_RUN, { type: "plan_snapshot", plan: surfacePlan({ ...this.plan, tasks: this.store.snapshot() }) });
|
|
29859
31014
|
const view = reduceRun(planned, this.events.statusMessage());
|
|
29860
|
-
const
|
|
29861
|
-
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;
|
|
29862
31017
|
}
|
|
29863
31018
|
/** The ancestors a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
|
|
29864
31019
|
mergeGate(taskId) {
|
|
@@ -30017,6 +31172,7 @@ var Session = class {
|
|
|
30017
31172
|
if (adopting) this.dropPlanScope();
|
|
30018
31173
|
this.plan = plan;
|
|
30019
31174
|
if (adopting) this.supervision.restore(plan.supervision);
|
|
31175
|
+
if (adopting) this.turns.restoreResend();
|
|
30020
31176
|
this.goal = goal;
|
|
30021
31177
|
this.workspace = workspace;
|
|
30022
31178
|
plan.approved ??= plan.status !== "draft" && plan.status !== "rejected";
|
|
@@ -30224,6 +31380,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30224
31380
|
messageCount = 0;
|
|
30225
31381
|
interrupting = null;
|
|
30226
31382
|
lastSessionId = null;
|
|
31383
|
+
lastSessionCost = null;
|
|
30227
31384
|
structuredEmitter = new EventEmitter2();
|
|
30228
31385
|
permissionCount = 0;
|
|
30229
31386
|
/**
|
|
@@ -30428,6 +31585,9 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30428
31585
|
nativeSessionId() {
|
|
30429
31586
|
return this.adapter?.nativeSessionId() ?? this.lastSessionId;
|
|
30430
31587
|
}
|
|
31588
|
+
sessionCost() {
|
|
31589
|
+
return this.adapter?.sessionCost?.() ?? this.lastSessionCost;
|
|
31590
|
+
}
|
|
30431
31591
|
answerPermission(id, decision) {
|
|
30432
31592
|
const open = this.permissions.get(id);
|
|
30433
31593
|
if (!open) return false;
|
|
@@ -30475,6 +31635,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30475
31635
|
*/
|
|
30476
31636
|
async restartInterrupted(turn) {
|
|
30477
31637
|
const resumeSessionId = this.nativeSessionId() ?? this.startOptions.resumeSessionId;
|
|
31638
|
+
const resumeCost = this.sessionCost() ?? this.startOptions.resumeCost;
|
|
30478
31639
|
this.generation += 1;
|
|
30479
31640
|
turn.abort.abort();
|
|
30480
31641
|
if (this.adapter) {
|
|
@@ -30484,7 +31645,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30484
31645
|
for (const message of this.queue) this.requeue(message);
|
|
30485
31646
|
try {
|
|
30486
31647
|
this.revokeTools();
|
|
30487
|
-
await this.startAttached({ ...this.startOptions, resumeSessionId });
|
|
31648
|
+
await this.startAttached({ ...this.startOptions, resumeSessionId, resumeCost });
|
|
30488
31649
|
} catch (err) {
|
|
30489
31650
|
this.text.line(`Could not restart ${this.launch.runner} after the interrupt: ${err instanceof Error ? err.message : String(err)}`);
|
|
30490
31651
|
this.withdrawPermissions();
|
|
@@ -30511,6 +31672,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30511
31672
|
return;
|
|
30512
31673
|
}
|
|
30513
31674
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
31675
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
30514
31676
|
this.withdrawPermissions();
|
|
30515
31677
|
this.baseHandleExit(code);
|
|
30516
31678
|
});
|
|
@@ -30548,6 +31710,7 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30548
31710
|
this.handleEvent({ type: "error", message: err instanceof Error ? err.message : String(err) });
|
|
30549
31711
|
}).then(() => {
|
|
30550
31712
|
this.lastSessionId = adapter.nativeSessionId() ?? this.lastSessionId;
|
|
31713
|
+
this.lastSessionCost = adapter.sessionCost?.() ?? this.lastSessionCost;
|
|
30551
31714
|
if (generation2 === this.generation) this.endTurn(turn, turn.reason);
|
|
30552
31715
|
});
|
|
30553
31716
|
this.offerNext();
|
|
@@ -30674,6 +31837,11 @@ var StructuredSession = class extends AbstractRunnerSession {
|
|
|
30674
31837
|
case "error":
|
|
30675
31838
|
this.text.failure(event.message);
|
|
30676
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;
|
|
30677
31845
|
default:
|
|
30678
31846
|
break;
|
|
30679
31847
|
}
|
|
@@ -30754,7 +31922,8 @@ var StructuredRunner = class extends AbstractRunner {
|
|
|
30754
31922
|
model: opts.modelId,
|
|
30755
31923
|
mode: opts.mode ?? "default",
|
|
30756
31924
|
flags: resolveTaskRunnerFlags(manifest, { mode: opts.mode ?? "default", model: opts.modelId, thinkingEffort: opts.thinkingEffort }),
|
|
30757
|
-
resumeSessionId: opts.resumeSessionId
|
|
31925
|
+
resumeSessionId: opts.resumeSessionId,
|
|
31926
|
+
resumeCost: opts.resumeCost
|
|
30758
31927
|
},
|
|
30759
31928
|
interruptGraceMs: this.interruptGraceMs,
|
|
30760
31929
|
tools: { server: this.mcp, scope: { sessionId: opts.planSessionId ?? "", taskId: opts.taskId, attempt: opts.attempt ?? 1 } },
|
|
@@ -31056,6 +32225,7 @@ export {
|
|
|
31056
32225
|
PlannerNotReadyError,
|
|
31057
32226
|
PlannerTurnDiscardedError,
|
|
31058
32227
|
PlannerTurnStoppedError,
|
|
32228
|
+
PlannerUsageLimitError,
|
|
31059
32229
|
REFUSED_COMMANDS,
|
|
31060
32230
|
RUN_SETTINGS_DEFAULTS,
|
|
31061
32231
|
RunSettingsError,
|
|
@@ -31112,6 +32282,8 @@ export {
|
|
|
31112
32282
|
approvalScopes,
|
|
31113
32283
|
assertWorkspaceExists,
|
|
31114
32284
|
assertWorkspaceIsProject,
|
|
32285
|
+
assignmentIssue,
|
|
32286
|
+
assignmentIssueText,
|
|
31115
32287
|
augmentedPath,
|
|
31116
32288
|
automergeSummary,
|
|
31117
32289
|
autonomyLevelLabel,
|
|
@@ -31128,6 +32300,7 @@ export {
|
|
|
31128
32300
|
canSetDependencies,
|
|
31129
32301
|
canSplitTask,
|
|
31130
32302
|
capConflictFiles,
|
|
32303
|
+
checkoutRule,
|
|
31131
32304
|
classifyCommand,
|
|
31132
32305
|
classifyOutcome,
|
|
31133
32306
|
clearAugmentedPathCache,
|
|
@@ -31141,6 +32314,7 @@ export {
|
|
|
31141
32314
|
commandAllowedBy,
|
|
31142
32315
|
configuredProviders,
|
|
31143
32316
|
continuability,
|
|
32317
|
+
countedSessionCost,
|
|
31144
32318
|
createAiService,
|
|
31145
32319
|
createEmptyPlan,
|
|
31146
32320
|
createSession,
|
|
@@ -31204,6 +32378,7 @@ export {
|
|
|
31204
32378
|
isMeasured,
|
|
31205
32379
|
isOpenAiProvider,
|
|
31206
32380
|
isPlannerBackend,
|
|
32381
|
+
isRepoGroup,
|
|
31207
32382
|
isRunnerApproval,
|
|
31208
32383
|
isSupervisorLevel,
|
|
31209
32384
|
isTaskRunning,
|
|
@@ -31226,6 +32401,7 @@ export {
|
|
|
31226
32401
|
markdownHtml,
|
|
31227
32402
|
markdownTokens,
|
|
31228
32403
|
mcpClientConfig,
|
|
32404
|
+
mergeTarget,
|
|
31229
32405
|
migrateOldConfigDir,
|
|
31230
32406
|
migratePlanIsolation,
|
|
31231
32407
|
migratePlanState,
|
|
@@ -31275,6 +32451,7 @@ export {
|
|
|
31275
32451
|
removedPluginNotice,
|
|
31276
32452
|
renderTaskQueryAnswer,
|
|
31277
32453
|
replayTaskLog,
|
|
32454
|
+
reposWithWork,
|
|
31278
32455
|
researchShellWarning,
|
|
31279
32456
|
researchToolsPath,
|
|
31280
32457
|
resetInstalledPlanner,
|
|
@@ -31288,6 +32465,7 @@ export {
|
|
|
31288
32465
|
resolveWithin,
|
|
31289
32466
|
resumeLabel,
|
|
31290
32467
|
resumeTime,
|
|
32468
|
+
resumedSessionWindow,
|
|
31291
32469
|
runSettingsOf,
|
|
31292
32470
|
runnerForProvider,
|
|
31293
32471
|
runnerModesFrom,
|
|
@@ -31328,6 +32506,7 @@ export {
|
|
|
31328
32506
|
taskRowView,
|
|
31329
32507
|
taskStartedNotice,
|
|
31330
32508
|
taskStatusKind,
|
|
32509
|
+
taskUsageSummary,
|
|
31331
32510
|
textHasTaskOps,
|
|
31332
32511
|
textHasTaskQuery,
|
|
31333
32512
|
titledRefs,
|