@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.7
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/README.i18n.yaml +2 -2
- package/README.md +34 -18
- package/README.zh.md +34 -18
- package/lib/client.js +2804 -1024
- package/lib/client.js.map +1 -1
- package/lib/index.js +2576 -416
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +81 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +41 -17
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +163 -14
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +91 -11
- package/lib/types/host-runner.d.ts +111 -13
- package/lib/types/host-service.d.ts +199 -8
- package/lib/types/index.d.ts +141 -0
- package/lib/types/protocol.d.ts +63 -12
- package/package.json +29 -33
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +288 -17
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +237 -60
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +261 -24
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/status-key.ts +3 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +105 -108
- package/src/client/locales.ts +171 -6
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +55 -16
- package/src/core/launch-width.ts +38 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +69 -15
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +213 -39
- package/src/core/use-cases/task-create.ts +57 -25
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +9 -4
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +127 -29
- package/src/host/launch-admission.ts +140 -0
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +346 -48
- package/src/host-runner.ts +234 -35
- package/src/host-service.ts +539 -52
- package/src/index.ts +250 -7
- package/src/protocol.ts +160 -32
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
package/lib/index.js
CHANGED
|
@@ -5,8 +5,193 @@ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readF
|
|
|
5
5
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
8
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
8
9
|
import { ReasoningEffortId, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
9
10
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
|
+
/**
|
|
12
|
+
* Clamp a configured width into the supported range. A value the schema would
|
|
13
|
+
* refuse (non-finite, below the floor, above the ceiling) still leaves the
|
|
14
|
+
* board launching: an out-of-range setting never stalls a run.
|
|
15
|
+
* @param width - configured width, or undefined for the default.
|
|
16
|
+
* @returns the effective width.
|
|
17
|
+
*/
|
|
18
|
+
function normalizeRunLaunchWidth(width) {
|
|
19
|
+
if (width === void 0 || !Number.isFinite(width)) return 3;
|
|
20
|
+
return Math.min(Math.max(Math.trunc(width), 1), 16);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Clamp a configured cadence into the supported range. A value the schema
|
|
24
|
+
* would refuse (non-finite, below the floor, above the ceiling) still leaves
|
|
25
|
+
* the board polling: an out-of-range setting never disables the timer.
|
|
26
|
+
* @param seconds - configured cadence in seconds, or undefined for the default.
|
|
27
|
+
* @returns the effective cadence in seconds.
|
|
28
|
+
*/
|
|
29
|
+
function normalizeSessionPollSeconds(seconds) {
|
|
30
|
+
if (seconds === void 0 || !Number.isFinite(seconds)) return 5;
|
|
31
|
+
return Math.min(Math.max(Math.trunc(seconds), 1), 300);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The cadence in the milliseconds the Host timer face takes.
|
|
35
|
+
* @param seconds - configured cadence in seconds, or undefined for the default.
|
|
36
|
+
* @returns the effective cadence in milliseconds.
|
|
37
|
+
*/
|
|
38
|
+
function sessionPollMs(seconds) {
|
|
39
|
+
return normalizeSessionPollSeconds(seconds) * 1e3;
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/core/session-reuse.ts
|
|
43
|
+
/**
|
|
44
|
+
* Pick the session a new execution of this task may continue in, or undefined
|
|
45
|
+
* to mint a fresh conversation. Reuse requires ALL of:
|
|
46
|
+
* - the task opted in (`reuseSession === true`);
|
|
47
|
+
* - the task's newest SETTLED execution carries a session id (issue #1587: the
|
|
48
|
+
* launch path calls this after `startExecution` appended this run's own open
|
|
49
|
+
* record, so reading the array tail always found an unsettled row and reuse
|
|
50
|
+
* never happened);
|
|
51
|
+
* - the roster is known and that session is present and idle.
|
|
52
|
+
*
|
|
53
|
+
* An unknown roster (session/list unavailable) never reuses: minting a fresh
|
|
54
|
+
* conversation is always safe, prompting into a session we cannot see is not.
|
|
55
|
+
* @param task - the task about to run.
|
|
56
|
+
* @param idleSessionIds - ids the last roster saw as present and not running;
|
|
57
|
+
* undefined when that roster is unknown.
|
|
58
|
+
* @returns the session id to continue in, or undefined for a fresh session.
|
|
59
|
+
*/
|
|
60
|
+
function reusableSessionId(task, idleSessionIds) {
|
|
61
|
+
if (task.reuseSession !== true || idleSessionIds === void 0) return void 0;
|
|
62
|
+
let last;
|
|
63
|
+
for (let index = task.executions.length - 1; index >= 0; index -= 1) {
|
|
64
|
+
const candidate = task.executions[index];
|
|
65
|
+
if (candidate.sessionId !== void 0 && candidate.endedAt !== void 0) {
|
|
66
|
+
last = candidate;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (last === void 0 || last.sessionId === void 0) return void 0;
|
|
71
|
+
return idleSessionIds.has(last.sessionId) ? last.sessionId : void 0;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/host/launch-admission.ts
|
|
75
|
+
var LaunchAdmissionQueue = class {
|
|
76
|
+
width;
|
|
77
|
+
/** Waiting launches per run group, in arrival order. */
|
|
78
|
+
waiting = /* @__PURE__ */ new Map();
|
|
79
|
+
/** Run groups in round-robin order. */
|
|
80
|
+
rotation = [];
|
|
81
|
+
/** Slot holders of the launches in flight, one per admitted launch. */
|
|
82
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
83
|
+
/** Highest number of launches in flight at once; observability and tests. */
|
|
84
|
+
peakInFlight = 0;
|
|
85
|
+
/**
|
|
86
|
+
* @param width - live admission width (already normalized by the caller).
|
|
87
|
+
*/
|
|
88
|
+
constructor(width) {
|
|
89
|
+
this.width = width;
|
|
90
|
+
}
|
|
91
|
+
/** Launches the queue has not admitted yet. */
|
|
92
|
+
get pendingCount() {
|
|
93
|
+
let total = 0;
|
|
94
|
+
for (const queue of this.waiting.values()) total += queue.length;
|
|
95
|
+
return total;
|
|
96
|
+
}
|
|
97
|
+
/** Launches currently holding a slot. */
|
|
98
|
+
get inFlightCount() {
|
|
99
|
+
return this.inFlight.size;
|
|
100
|
+
}
|
|
101
|
+
/** Admit one launch, immediately if a slot is free. */
|
|
102
|
+
enqueue(launch) {
|
|
103
|
+
const queue = this.waiting.get(launch.groupId);
|
|
104
|
+
if (queue === void 0) {
|
|
105
|
+
this.waiting.set(launch.groupId, [launch]);
|
|
106
|
+
this.rotation.push(launch.groupId);
|
|
107
|
+
} else queue.push(launch);
|
|
108
|
+
this.pump();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Abandon every waiting launch: the queue is being torn down, so an entry
|
|
112
|
+
* that never started can never start. Launches already in flight are left
|
|
113
|
+
* alone — their session may already exist, and the callers that own them
|
|
114
|
+
* settle them through the normal path.
|
|
115
|
+
* @returns the abandoned launches, in abandonment order.
|
|
116
|
+
*/
|
|
117
|
+
drain() {
|
|
118
|
+
const abandoned = [];
|
|
119
|
+
for (const groupId of this.rotation) for (const launch of this.waiting.get(groupId) ?? []) abandoned.push(launch);
|
|
120
|
+
this.rotation = [];
|
|
121
|
+
this.waiting.clear();
|
|
122
|
+
for (const launch of abandoned) launch.abandon();
|
|
123
|
+
return abandoned;
|
|
124
|
+
}
|
|
125
|
+
pump() {
|
|
126
|
+
while (this.inFlight.size < this.width()) {
|
|
127
|
+
const launch = this.takeNext();
|
|
128
|
+
if (launch === void 0) return;
|
|
129
|
+
this.admit(launch);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/** The next launch by round-robin across groups and FIFO inside one. */
|
|
133
|
+
takeNext() {
|
|
134
|
+
while (this.rotation.length > 0) {
|
|
135
|
+
const groupId = this.rotation.shift();
|
|
136
|
+
if (groupId === void 0) return void 0;
|
|
137
|
+
const queue = this.waiting.get(groupId);
|
|
138
|
+
if (queue === void 0 || queue.length === 0) {
|
|
139
|
+
this.waiting.delete(groupId);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
const launch = queue.shift();
|
|
143
|
+
if (launch === void 0) continue;
|
|
144
|
+
if (queue.length === 0) this.waiting.delete(groupId);
|
|
145
|
+
else this.rotation.push(groupId);
|
|
146
|
+
return launch;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
admit(launch) {
|
|
150
|
+
const release = () => {
|
|
151
|
+
this.inFlight.delete(release);
|
|
152
|
+
this.pump();
|
|
153
|
+
};
|
|
154
|
+
this.inFlight.add(release);
|
|
155
|
+
if (this.inFlight.size > this.peakInFlight) this.peakInFlight = this.inFlight.size;
|
|
156
|
+
Promise.resolve().then(() => launch.start()).catch(() => {}).then(release);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/dsh-home.ts
|
|
161
|
+
/**
|
|
162
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
163
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
164
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
165
|
+
*/
|
|
166
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
167
|
+
function expandHome(path, home = homedir()) {
|
|
168
|
+
const j = home.startsWith("/") ? join$1 : join;
|
|
169
|
+
if (path === "~") return home;
|
|
170
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return j(home, path.slice(2));
|
|
171
|
+
return path;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Resolve the DSH home directory.
|
|
175
|
+
* @param env - process environment to read DSH_HOME from.
|
|
176
|
+
* @param home - platform home directory fallback (test seam).
|
|
177
|
+
* @returns the absolute DSH home path.
|
|
178
|
+
*/
|
|
179
|
+
function resolveDshHome(env = process.env, home = homedir()) {
|
|
180
|
+
const isPosix = home.startsWith("/");
|
|
181
|
+
const j = isPosix ? join$1 : join;
|
|
182
|
+
const isAbs = isPosix ? isAbsolute$1 : isAbsolute;
|
|
183
|
+
const raw = env.DSH_HOME;
|
|
184
|
+
if (raw !== void 0 && raw.trim() !== "") {
|
|
185
|
+
const expanded = expandHome(raw.trim(), home);
|
|
186
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
|
|
187
|
+
}
|
|
188
|
+
return j(home, ".dsh");
|
|
189
|
+
}
|
|
190
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
191
|
+
function dshHome() {
|
|
192
|
+
return resolveDshHome();
|
|
193
|
+
}
|
|
194
|
+
//#endregion
|
|
10
195
|
//#region src/core/schedule.ts
|
|
11
196
|
/** Inclusive ranges per field, in cron order. */
|
|
12
197
|
const FIELD_RANGES = [
|
|
@@ -283,73 +468,6 @@ function isDigits(value) {
|
|
|
283
468
|
return /^\d+$/.test(value);
|
|
284
469
|
}
|
|
285
470
|
//#endregion
|
|
286
|
-
//#region src/core/session-reuse.ts
|
|
287
|
-
/**
|
|
288
|
-
* Pick the session a new execution of this task may continue in, or undefined
|
|
289
|
-
* to mint a fresh conversation. Reuse requires ALL of:
|
|
290
|
-
* - the task opted in (`reuseSession === true`);
|
|
291
|
-
* - the task's newest SETTLED execution carries a session id (issue #1587: the
|
|
292
|
-
* launch path calls this after `startExecution` appended this run's own open
|
|
293
|
-
* record, so reading the array tail always found an unsettled row and reuse
|
|
294
|
-
* never happened);
|
|
295
|
-
* - the roster is known and that session is present and idle.
|
|
296
|
-
*
|
|
297
|
-
* An unknown roster (session/list unavailable) never reuses: minting a fresh
|
|
298
|
-
* conversation is always safe, prompting into a session we cannot see is not.
|
|
299
|
-
* @param task - the task about to run.
|
|
300
|
-
* @param idleSessionIds - ids the last roster saw as present and not running;
|
|
301
|
-
* undefined when that roster is unknown.
|
|
302
|
-
* @returns the session id to continue in, or undefined for a fresh session.
|
|
303
|
-
*/
|
|
304
|
-
function reusableSessionId(task, idleSessionIds) {
|
|
305
|
-
if (task.reuseSession !== true || idleSessionIds === void 0) return void 0;
|
|
306
|
-
let last;
|
|
307
|
-
for (let index = task.executions.length - 1; index >= 0; index -= 1) {
|
|
308
|
-
const candidate = task.executions[index];
|
|
309
|
-
if (candidate.sessionId !== void 0 && candidate.endedAt !== void 0) {
|
|
310
|
-
last = candidate;
|
|
311
|
-
break;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
if (last === void 0 || last.sessionId === void 0) return void 0;
|
|
315
|
-
return idleSessionIds.has(last.sessionId) ? last.sessionId : void 0;
|
|
316
|
-
}
|
|
317
|
-
//#endregion
|
|
318
|
-
//#region src/dsh-home.ts
|
|
319
|
-
/**
|
|
320
|
-
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
321
|
-
* environment override wins, the platform home fallback follows. Mirrors
|
|
322
|
-
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
323
|
-
*/
|
|
324
|
-
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
325
|
-
function expandHome(path, home = homedir()) {
|
|
326
|
-
const j = home.startsWith("/") ? join$1 : join;
|
|
327
|
-
if (path === "~") return home;
|
|
328
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return j(home, path.slice(2));
|
|
329
|
-
return path;
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Resolve the DSH home directory.
|
|
333
|
-
* @param env - process environment to read DSH_HOME from.
|
|
334
|
-
* @param home - platform home directory fallback (test seam).
|
|
335
|
-
* @returns the absolute DSH home path.
|
|
336
|
-
*/
|
|
337
|
-
function resolveDshHome(env = process.env, home = homedir()) {
|
|
338
|
-
const isPosix = home.startsWith("/");
|
|
339
|
-
const j = isPosix ? join$1 : join;
|
|
340
|
-
const isAbs = isPosix ? isAbsolute$1 : isAbsolute;
|
|
341
|
-
const raw = env.DSH_HOME;
|
|
342
|
-
if (raw !== void 0 && raw.trim() !== "") {
|
|
343
|
-
const expanded = expandHome(raw.trim(), home);
|
|
344
|
-
return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
|
|
345
|
-
}
|
|
346
|
-
return j(home, ".dsh");
|
|
347
|
-
}
|
|
348
|
-
/** Resolve the DSH home directory from the live environment. */
|
|
349
|
-
function dshHome() {
|
|
350
|
-
return resolveDshHome();
|
|
351
|
-
}
|
|
352
|
-
//#endregion
|
|
353
471
|
//#region src/core/extension.ts
|
|
354
472
|
/** Cordis service name the host and browser halves each publish. */
|
|
355
473
|
const TASK_BOARD_SERVICE_NAME = "taskBoard";
|
|
@@ -437,6 +555,37 @@ function retainRecentExecutions(executions) {
|
|
|
437
555
|
const keepSettled = Math.max(20 - open.length, 0);
|
|
438
556
|
return [...settled.slice(Math.max(settled.length - keepSettled, 0)), ...open];
|
|
439
557
|
}
|
|
558
|
+
/** Every stop reason this build understands; the repair and wire gates share it. */
|
|
559
|
+
const SCHEDULE_STOP_REASONS = [
|
|
560
|
+
"fired",
|
|
561
|
+
"limit",
|
|
562
|
+
"no-target",
|
|
563
|
+
"missed",
|
|
564
|
+
"busy",
|
|
565
|
+
"permission",
|
|
566
|
+
"launch-failed"
|
|
567
|
+
];
|
|
568
|
+
/**
|
|
569
|
+
* The rule's total run budget: one for a one-shot, the stored cap for a
|
|
570
|
+
* recurring rule, and undefined when the rule is unlimited.
|
|
571
|
+
* @param rule - the schedule rule.
|
|
572
|
+
* @returns the budget, or undefined for an uncapped rule.
|
|
573
|
+
*/
|
|
574
|
+
function scheduleRunBudget(rule) {
|
|
575
|
+
if (rule.mode === "once") return 1;
|
|
576
|
+
return rule.maxRuns;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Whether a rule has no runs left. The scheduler refuses to open another
|
|
580
|
+
* execution for an exhausted rule, which is what makes the budget terminal
|
|
581
|
+
* regardless of restarts or repeated timer fires.
|
|
582
|
+
* @param rule - the schedule rule.
|
|
583
|
+
* @returns true when the run budget is spent.
|
|
584
|
+
*/
|
|
585
|
+
function scheduleExhausted(rule) {
|
|
586
|
+
const budget = scheduleRunBudget(rule);
|
|
587
|
+
return budget !== void 0 && rule.runCount >= budget;
|
|
588
|
+
}
|
|
440
589
|
/** Whether an unknown value is a well-formed tag (strict: the wire gate). */
|
|
441
590
|
function isTaskTag(value) {
|
|
442
591
|
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
@@ -608,9 +757,10 @@ function createTask(input, now, id) {
|
|
|
608
757
|
mode: normalizeTargetId(input.mode),
|
|
609
758
|
permission: isTaskPermission(input.permission) ? input.permission : void 0,
|
|
610
759
|
model: normalizeTargetId(input.model),
|
|
760
|
+
reasoningEffort: normalizeTargetId(input.reasoningEffort),
|
|
611
761
|
reuseSession: input.reuseSession === true ? true : void 0,
|
|
612
762
|
teamRun: input.teamRun === true ? true : void 0,
|
|
613
|
-
goalRun: input.goalRun ===
|
|
763
|
+
goalRun: input.goalRun === true ? true : void 0,
|
|
614
764
|
skipVerification: input.skipVerification === true ? true : void 0,
|
|
615
765
|
...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
|
|
616
766
|
...input.handover === void 0 ? {} : { handover: {
|
|
@@ -630,27 +780,47 @@ function withStatus(task, status, now) {
|
|
|
630
780
|
updatedAt: now
|
|
631
781
|
};
|
|
632
782
|
}
|
|
783
|
+
/** Every field a schedule patch may carry; the rest of the rule is carried over. */
|
|
784
|
+
const SCHEDULE_PATCH_KEYS = [
|
|
785
|
+
"enabled",
|
|
786
|
+
"mode",
|
|
787
|
+
"cron",
|
|
788
|
+
"at",
|
|
789
|
+
"timeZone",
|
|
790
|
+
"nextRunAt",
|
|
791
|
+
"lastTriggeredAt",
|
|
792
|
+
"runCount",
|
|
793
|
+
"maxRuns",
|
|
794
|
+
"endedAt",
|
|
795
|
+
"endedReason",
|
|
796
|
+
"skippedAt",
|
|
797
|
+
"skippedReason"
|
|
798
|
+
];
|
|
633
799
|
/**
|
|
634
800
|
* Merge a schedule patch into a task's schedule rule (creating it when
|
|
635
801
|
* absent), with a fresh updatedAt. Keys present in the patch overwrite the
|
|
636
802
|
* current value — including explicit `undefined`, which clears a field (used
|
|
637
|
-
* to disarm `nextRunAt`); absent keys keep
|
|
803
|
+
* to disarm `nextRunAt` or to drop an ended/skip record); absent keys keep
|
|
804
|
+
* their current value.
|
|
638
805
|
*/
|
|
639
806
|
function withSchedule(task, patch, now) {
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
807
|
+
const schedule = task.schedule === void 0 ? {
|
|
808
|
+
enabled: false,
|
|
809
|
+
mode: "cron",
|
|
810
|
+
nextRunAt: void 0,
|
|
811
|
+
lastTriggeredAt: void 0,
|
|
812
|
+
runCount: 0
|
|
813
|
+
} : { ...task.schedule };
|
|
814
|
+
const fields = schedule;
|
|
815
|
+
const next = patch;
|
|
816
|
+
for (const key of SCHEDULE_PATCH_KEYS) {
|
|
817
|
+
if (!(key in patch)) continue;
|
|
818
|
+
if (next[key] === void 0) delete fields[key];
|
|
819
|
+
else fields[key] = next[key];
|
|
820
|
+
}
|
|
821
|
+
if (schedule.enabled !== true) schedule.enabled = false;
|
|
822
|
+
if (schedule.mode !== "once") schedule.mode = "cron";
|
|
823
|
+
if (!Number.isFinite(schedule.runCount) || schedule.runCount < 0) schedule.runCount = 0;
|
|
654
824
|
return {
|
|
655
825
|
...task,
|
|
656
826
|
updatedAt: now,
|
|
@@ -689,6 +859,20 @@ function startExecution(task, now, executionId, initiatedBy, runGroupId) {
|
|
|
689
859
|
};
|
|
690
860
|
}
|
|
691
861
|
/**
|
|
862
|
+
* The column a settled outcome puts a card in. The ONE derivation shared by
|
|
863
|
+
* every settlement path (issue #1826): a Host-run execution and an outcome an
|
|
864
|
+
* outside agent reported both land in the same column from the same rule, so
|
|
865
|
+
* the column never becomes a second, independent piece of state.
|
|
866
|
+
@param task - the card being settled.
|
|
867
|
+
@param outcome - the settled outcome.
|
|
868
|
+
@returns the status the card shows afterwards.
|
|
869
|
+
*/
|
|
870
|
+
function settledStatus(task, outcome) {
|
|
871
|
+
if (outcome === "succeeded") return task.schedule?.enabled === true ? "todo" : "done";
|
|
872
|
+
if (outcome === "failed") return "failed";
|
|
873
|
+
return task.status === "running" ? "todo" : task.status;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
692
876
|
* Settle a running execution: record the outcome and move the task into the
|
|
693
877
|
* matching column. No-op (returns the input task) when the execution is not
|
|
694
878
|
* the task's latest or is already settled.
|
|
@@ -706,10 +890,9 @@ function settleExecution(task, executionId, outcome, now, error) {
|
|
|
706
890
|
};
|
|
707
891
|
const executions = [...task.executions];
|
|
708
892
|
executions[index] = settled;
|
|
709
|
-
const status = outcome === "succeeded" ? task.schedule?.enabled ? "todo" : "done" : outcome === "failed" ? "failed" : task.status === "running" ? "todo" : task.status;
|
|
710
893
|
return {
|
|
711
894
|
...task,
|
|
712
|
-
status,
|
|
895
|
+
status: settledStatus(task, outcome),
|
|
713
896
|
updatedAt: now,
|
|
714
897
|
executions
|
|
715
898
|
};
|
|
@@ -837,15 +1020,38 @@ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth, lineage) {
|
|
|
837
1020
|
}
|
|
838
1021
|
/**
|
|
839
1022
|
* The tasks one run request opens executions for: the requested task first,
|
|
840
|
-
* then its on-board descendants within the configured depth.
|
|
841
|
-
*
|
|
842
|
-
* the
|
|
1023
|
+
* then its on-board descendants within the configured depth. This is the
|
|
1024
|
+
* SHAPE of a cascade — every walk that must see the whole tree (settlement,
|
|
1025
|
+
* archive/restore, the running lock) reads it. The run path filters it with
|
|
1026
|
+
* {@link joinsCascadeRun}: archived tasks never run, a participant that already
|
|
1027
|
+
* has an open execution is skipped by the caller, which cannot start it twice,
|
|
1028
|
+
* and a finished descendant is left alone.
|
|
843
1029
|
*/
|
|
844
1030
|
function cascadeTargets(tasks, rootId, maxSubtaskDepth) {
|
|
845
1031
|
const root = tasks.find((task) => task.id === rootId);
|
|
846
1032
|
if (root === void 0) return [];
|
|
847
1033
|
return [root, ...descendantTasks(tasks, rootId, maxSubtaskDepth)];
|
|
848
1034
|
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Whether a cascade member takes part in a new run.
|
|
1037
|
+
*
|
|
1038
|
+
* The task the user ran is always a member: the request names that card, and
|
|
1039
|
+
* the operator asked for it to run whatever its column says. A descendant does
|
|
1040
|
+
* not join once it sits in the `done` column — its work is finished, so running
|
|
1041
|
+
* the parent again is a run of the parent, never a second execution of work
|
|
1042
|
+
* that is already done. The rule keys on the column alone, so it covers both a
|
|
1043
|
+
* subtask that ran and one an operator moved to done by hand, and it holds for
|
|
1044
|
+
* every trigger: a manual run, a re-run, and a due occurrence of a schedule.
|
|
1045
|
+
* A card whose own schedule is enabled settles back into `todo`, so a scheduled
|
|
1046
|
+
* subtask stays a participant; to make a finished one run again, move it back
|
|
1047
|
+
* to `todo` or run it on its own.
|
|
1048
|
+
* @param task - the candidate member.
|
|
1049
|
+
* @param rootId - the task the run request named.
|
|
1050
|
+
* @returns false when this member is left alone by the run.
|
|
1051
|
+
*/
|
|
1052
|
+
function joinsCascadeRun(task, rootId) {
|
|
1053
|
+
return task.id === rootId || task.status !== "done";
|
|
1054
|
+
}
|
|
849
1055
|
/** The open execution of a task inside a run group, if it still exists. */
|
|
850
1056
|
function openGroupExecution(task, runGroupId) {
|
|
851
1057
|
return task.executions.find((execution) => execution.runGroupId === runGroupId && execution.endedAt === void 0);
|
|
@@ -905,11 +1111,12 @@ function effectiveTaskPermission(task) {
|
|
|
905
1111
|
/**
|
|
906
1112
|
* Resolve the execution targets a (sub)task actually runs with: its own value
|
|
907
1113
|
* wins, and an unset target inherits the nearest ancestor up to the root.
|
|
908
|
-
* Workspace, agent preset and
|
|
909
|
-
* handover bundle is a per-card continuation artefact and is never
|
|
910
|
-
* The permission inherits the ancestor's EFFECTIVE binding, and its
|
|
911
|
-
* confirmation travels with it, so a subtask of an already-confirmed
|
|
912
|
-
* card is runnable while a subtask's own unconfirmed binding stays
|
|
1114
|
+
* Workspace, agent preset, model and reasoning effort inherit the ancestor's
|
|
1115
|
+
* PIN only — a handover bundle is a per-card continuation artefact and is never
|
|
1116
|
+
* inherited. The permission inherits the ancestor's EFFECTIVE binding, and its
|
|
1117
|
+
* human confirmation travels with it, so a subtask of an already-confirmed
|
|
1118
|
+
* elevated card is runnable while a subtask's own unconfirmed binding stays
|
|
1119
|
+
* gated.
|
|
913
1120
|
*/
|
|
914
1121
|
/**
|
|
915
1122
|
* The immutable teammate name for one team-mode member. Teammate names are
|
|
@@ -955,19 +1162,22 @@ function stableTag(value) {
|
|
|
955
1162
|
function resolveExecutionTargets(task, tasks, maxSubtaskDepth = 3) {
|
|
956
1163
|
if (task.parentId === void 0) return task;
|
|
957
1164
|
const chain = [task, ...ancestorChain(tasks, task).slice(0, maxSubtaskDepth)];
|
|
958
|
-
const first = (key) => {
|
|
959
|
-
for (const candidate of
|
|
1165
|
+
const first = (key, candidates) => {
|
|
1166
|
+
for (const candidate of candidates) {
|
|
960
1167
|
const value = candidate[key];
|
|
961
1168
|
if (value !== void 0) return value;
|
|
962
1169
|
}
|
|
963
1170
|
};
|
|
1171
|
+
const boundary = chain.findIndex((candidate) => candidate.model !== void 0);
|
|
1172
|
+
const effortChain = boundary === -1 ? chain : chain.slice(0, boundary + 1);
|
|
964
1173
|
const own = effectiveTaskPermission(task);
|
|
965
1174
|
const owner = own === void 0 ? chain.slice(1).find((candidate) => effectiveTaskPermission(candidate) !== void 0) : void 0;
|
|
966
1175
|
return {
|
|
967
1176
|
...task,
|
|
968
|
-
workspaceId: first("workspaceId"),
|
|
969
|
-
mode: first("mode"),
|
|
970
|
-
model: first("model"),
|
|
1177
|
+
workspaceId: first("workspaceId", chain),
|
|
1178
|
+
mode: first("mode", chain),
|
|
1179
|
+
model: first("model", chain),
|
|
1180
|
+
reasoningEffort: first("reasoningEffort", effortChain),
|
|
971
1181
|
permission: own ?? (owner === void 0 ? void 0 : effectiveTaskPermission(owner)),
|
|
972
1182
|
permissionConfirmedAt: own === void 0 ? owner?.permissionConfirmedAt : task.permissionConfirmedAt
|
|
973
1183
|
};
|
|
@@ -1205,6 +1415,20 @@ const SCALE_DESCRIPTION = [
|
|
|
1205
1415
|
" Q-S = failed with some partial progress",
|
|
1206
1416
|
" T = clearly and completely failed (worst)"
|
|
1207
1417
|
].join("\n");
|
|
1418
|
+
/**
|
|
1419
|
+
* Which trajectory slot holds the WORK during one round.
|
|
1420
|
+
*
|
|
1421
|
+
* The even round (0) puts the work in slot A and the empty-work baseline in
|
|
1422
|
+
* slot B; the odd round (1) swaps them, which is how the acceptance cancels
|
|
1423
|
+
* position bias. The judge prompt ALWAYS introduces the work as
|
|
1424
|
+
* `location="TRAJECTORY_B"`, because that is the slot the work occupies in the
|
|
1425
|
+
* prompt's own framing; the parser maps the cited slot back through this table.
|
|
1426
|
+
* @param round - the round index (0-based).
|
|
1427
|
+
* @returns the slot letter the work occupies in that round.
|
|
1428
|
+
*/
|
|
1429
|
+
function workSlotOfRound(round) {
|
|
1430
|
+
return round % 2 === 1 ? "B" : "A";
|
|
1431
|
+
}
|
|
1208
1432
|
Array.from({ length: 20 }, (_, index) => String.fromCharCode(65 + index));
|
|
1209
1433
|
/**
|
|
1210
1434
|
* Normalize one score token to an A-T letter.
|
|
@@ -1286,8 +1510,9 @@ function evidenceNonce(...parts) {
|
|
|
1286
1510
|
* byte-identical for a deployment that serves no change service.
|
|
1287
1511
|
* @returns the rendered prompt.
|
|
1288
1512
|
*/
|
|
1289
|
-
function buildAcceptancePrompt(problem, traceA, traceB, criterion, groundTruthNote = DEFAULT_GROUND_TRUTH_NOTE, context) {
|
|
1513
|
+
function buildAcceptancePrompt(problem, traceA, traceB, criterion, groundTruthNote = DEFAULT_GROUND_TRUTH_NOTE, context, workSlot = "A") {
|
|
1290
1514
|
const reference = context?.trim() ?? "";
|
|
1515
|
+
const workSlotTag = workSlot === "A" ? "Trajectory A" : "Trajectory B";
|
|
1291
1516
|
const token = reference === "" ? evidenceNonce(problem, traceA, traceB) : evidenceNonce(problem, reference, traceA, traceB);
|
|
1292
1517
|
return [
|
|
1293
1518
|
EVALUATOR_ROLE,
|
|
@@ -1300,6 +1525,10 @@ function buildAcceptancePrompt(problem, traceA, traceB, criterion, groundTruthNo
|
|
|
1300
1525
|
"**Rating Scale:**\n" + SCALE_DESCRIPTION,
|
|
1301
1526
|
"**Evaluation Guideline — " + criterion.name + ":**\n" + criterion.description,
|
|
1302
1527
|
"Score each trajectory ONLY on this specific criterion (\"" + criterion.name + "\"). Ignore other aspects that are not relevant to it.",
|
|
1528
|
+
"If and only if the score you give the WORK under this criterion makes it fail the criterion, you MUST justify it with one finding element per failing observation, in exactly this form (replace the placeholders, keep the attribute names and the quoting):",
|
|
1529
|
+
"<finding criterion=\"" + criterion.name + "\" location=\"" + workSlot + "\" requirement=\"the task requirement this criterion demands\" observation=\"what you actually observed\" gap=\"why the observation does not satisfy the requirement\" quote=\"a verbatim passage copied exactly from the block named by location\">optional suggested action</finding>",
|
|
1530
|
+
"Before citing the task you MUST name the failing criterion, restate the requirement it imposes, describe the observation that falls short, explain the difference, and cite a verbatim passage that occurs in the evidence. " + workSlotTag + " is the WORK under review and is the only trajectory a veto may cite: a passage quoted from the other trajectory, which is the empty-work baseline (No useful work or verification was performed.), is not evidence about the work and must never be used.",
|
|
1531
|
+
"A failure with no such citation is not a usable verdict: if the evidence you were given is truncated, incomplete, or simply does not contain the output needed to judge this criterion, say so in your prose and cite nothing rather than inventing a basis to veto.",
|
|
1303
1532
|
"Reason it through first, then END your reply with exactly these two lines and nothing after them. Replace each placeholder with a single letter A-T, keeping the spaces around the letter exactly as shown:\n<score_A> LETTER_A_TO_T </score_A>\n<score_B> LETTER_A_TO_T </score_B>",
|
|
1304
1533
|
"Begin your analysis now."
|
|
1305
1534
|
].join("\n\n");
|
|
@@ -1312,6 +1541,116 @@ function qualityAttempts(verification) {
|
|
|
1312
1541
|
function exceptionAttempts(verification) {
|
|
1313
1542
|
return verification === void 0 ? [] : verification.attempts.filter((attempt) => attempt.stage === "exception");
|
|
1314
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Invalid acceptances spent by this cycle: a veto whose required evidence was
|
|
1546
|
+
* missing, unlocatable, or aimed at the empty-work baseline. They are NOT
|
|
1547
|
+
* quality verdicts, so they never close the cycle and never fail the card; the
|
|
1548
|
+
* bound is what keeps an evidence-free judge from re-running forever.
|
|
1549
|
+
*/
|
|
1550
|
+
function invalidAttempts(verification) {
|
|
1551
|
+
return verification === void 0 ? [] : verification.attempts.filter((attempt) => attempt.stage === "invalid");
|
|
1552
|
+
}
|
|
1553
|
+
/** Whether this cycle still has invalid-acceptance budget left. */
|
|
1554
|
+
function hasInvalidBudget(verification) {
|
|
1555
|
+
return invalidAttempts(verification).length < 2;
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* The acceptance block with every non-quality attempt dropped: the explicit
|
|
1559
|
+
* reset a user performs after fixing the environment (issue #1828).
|
|
1560
|
+
*
|
|
1561
|
+
* Only attempts that are not a quality verdict are dropped. A quality verdict
|
|
1562
|
+
* is the board's own judgement of the work and survives every reset, so this
|
|
1563
|
+
* can never buy an extra acceptance or re-open a judged failure; it only lets
|
|
1564
|
+
* the environment be judged again.
|
|
1565
|
+
* @param verification - the persisted acceptance state.
|
|
1566
|
+
* @returns the cleared block, or undefined when there is nothing to clear.
|
|
1567
|
+
*/
|
|
1568
|
+
function withoutAcceptanceAnomalies(verification) {
|
|
1569
|
+
const kept = verification.attempts.filter((attempt) => attempt.stage === "quality");
|
|
1570
|
+
if (kept.length === verification.attempts.length) return void 0;
|
|
1571
|
+
const cleared = {
|
|
1572
|
+
...verification,
|
|
1573
|
+
attempts: kept
|
|
1574
|
+
};
|
|
1575
|
+
delete cleared.inFlight;
|
|
1576
|
+
delete cleared.failedReason;
|
|
1577
|
+
delete cleared.failedAt;
|
|
1578
|
+
delete cleared.cleanup;
|
|
1579
|
+
return cleared;
|
|
1580
|
+
}
|
|
1581
|
+
/**
|
|
1582
|
+
* The one-line problem summary a cleaned execution keeps: the failing criteria
|
|
1583
|
+
* with their scores, plus how many locatable findings were removed. This is the
|
|
1584
|
+
* lightweight audit material that survives the detail cleanup.
|
|
1585
|
+
* @param attempt - the attempt whose detail is being removed.
|
|
1586
|
+
* @returns the bounded summary line.
|
|
1587
|
+
*/
|
|
1588
|
+
function findingSummaryOf(attempt) {
|
|
1589
|
+
const failed = attempt.criteria.filter((criterion) => !criterion.passed);
|
|
1590
|
+
const parts = [];
|
|
1591
|
+
if (failed.length > 0) parts.push("未达标判据:" + failed.map((criterion) => criterion.name + " " + percent(criterion.score)).join("、"));
|
|
1592
|
+
const located = (attempt.criterionFindings ?? []).length;
|
|
1593
|
+
if (located > 0) parts.push("已移除的可定位问题 " + located + " 条");
|
|
1594
|
+
if (attempt.error !== void 0 && attempt.error !== "") parts.push(attempt.error);
|
|
1595
|
+
if (parts.length === 0) return void 0;
|
|
1596
|
+
return parts.join(";");
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* The audit credential of one attempt: everything the completion gate, the
|
|
1600
|
+
* report and a later review need, and nothing that makes the ledger heavy.
|
|
1601
|
+
* @param attempt - the attempt being condensed.
|
|
1602
|
+
* @returns the lightweight audit record.
|
|
1603
|
+
*/
|
|
1604
|
+
function auditOf(attempt) {
|
|
1605
|
+
const summary = findingSummaryOf(attempt);
|
|
1606
|
+
return {
|
|
1607
|
+
stage: attempt.stage,
|
|
1608
|
+
passed: attempt.passed,
|
|
1609
|
+
at: attempt.at,
|
|
1610
|
+
score: attempt.score,
|
|
1611
|
+
baseline: attempt.baseline,
|
|
1612
|
+
criteria: attempt.criteria.map((criterion) => ({ ...criterion })),
|
|
1613
|
+
evidenceHash: attempt.evidence.hash,
|
|
1614
|
+
route: { ...attempt.route },
|
|
1615
|
+
...attempt.error === void 0 ? {} : { error: attempt.error },
|
|
1616
|
+
...summary === void 0 ? {} : { findingSummary: summary },
|
|
1617
|
+
usage: { ...attempt.usage }
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* Remove this execution's OWN acceptance-only detail while keeping the light
|
|
1622
|
+
* audit record — the acceptance mechanism's side of requirement 2.
|
|
1623
|
+
*
|
|
1624
|
+
* Only material the acceptance mechanism itself created is removed: the
|
|
1625
|
+
* per-attempt findings, the invalid diagnostics and the token/evidence detail
|
|
1626
|
+
* beyond the summary. Everything a human needs to see the verdict survives
|
|
1627
|
+
* (stage, pass flag, time, scores, evidence hash, judge route, usage totals),
|
|
1628
|
+
* so the completion gate's pass record and a later review are unaffected, and
|
|
1629
|
+
* NO user file, task artifact, raw session history or other execution's record
|
|
1630
|
+
* is ever touched by this function.
|
|
1631
|
+
* @param verification - the execution's acceptance block.
|
|
1632
|
+
* @param now - cleanup instant (ms epoch).
|
|
1633
|
+
* @returns the cleaned block, or undefined when there is nothing to remove.
|
|
1634
|
+
*/
|
|
1635
|
+
function withAcceptanceDetailCleared(verification, now) {
|
|
1636
|
+
if (verification.cleanup?.state === "cleaned") return void 0;
|
|
1637
|
+
const audit = verification.attempts.map(auditOf);
|
|
1638
|
+
return {
|
|
1639
|
+
...verification,
|
|
1640
|
+
attempts: verification.attempts.map((attempt) => ({
|
|
1641
|
+
...attempt,
|
|
1642
|
+
findings: [],
|
|
1643
|
+
criterionFindings: []
|
|
1644
|
+
})),
|
|
1645
|
+
cleanup: {
|
|
1646
|
+
state: "cleaned",
|
|
1647
|
+
attempts: (verification.cleanup?.attempts ?? 0) + 1,
|
|
1648
|
+
startedAt: verification.cleanup?.startedAt ?? now,
|
|
1649
|
+
cleanedAt: now,
|
|
1650
|
+
audit
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1315
1654
|
/** The quality attempt that passed, when one did. */
|
|
1316
1655
|
function passedAttempt(verification) {
|
|
1317
1656
|
return qualityAttempts(verification).find((attempt) => attempt.passed);
|
|
@@ -1328,6 +1667,33 @@ function passedAttempt(verification) {
|
|
|
1328
1667
|
function verificationRequired(verification) {
|
|
1329
1668
|
return verification !== void 0 && verification.applicability === "enforced";
|
|
1330
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Whether this execution's acceptance gate was never opened: acceptance was
|
|
1672
|
+
* enforced, the judge recorded nothing at all, and no cycle ever closed. Zero
|
|
1673
|
+
* attempts is therefore a fact about the RUN, not a verdict about the work — a
|
|
1674
|
+
* session that narrated completion instead of calling
|
|
1675
|
+
* `update_goal(action: complete)` produces exactly this shape, and its failure
|
|
1676
|
+
* reason must stay distinguishable from a quality verdict (issue #1837).
|
|
1677
|
+
* @param verification - the execution's persisted acceptance state.
|
|
1678
|
+
* @returns true when no acceptance attempt ever ran.
|
|
1679
|
+
*/
|
|
1680
|
+
function verificationNeverInvoked(verification) {
|
|
1681
|
+
return verification !== void 0 && verification.applicability === "enforced" && verification.attempts.length === 0 && verification.failedReason === void 0;
|
|
1682
|
+
}
|
|
1683
|
+
/**
|
|
1684
|
+
* Whether this execution's acceptance produced no usable veto at all: it was
|
|
1685
|
+
* enforced, the judge recorded attempts, and every one of them is INVALID.
|
|
1686
|
+
*
|
|
1687
|
+
* Such a run must never be described as an evidence-backed quality veto (issue
|
|
1688
|
+
* #1837's sibling): the judge could not locate its own rejection, so the run
|
|
1689
|
+
* ended for lack of a matching pass rather than because the work was judged
|
|
1690
|
+
* and rejected. The settlement reason names that difference.
|
|
1691
|
+
* @param verification - the execution's persisted acceptance state.
|
|
1692
|
+
* @returns true when nothing but invalid acceptances were recorded.
|
|
1693
|
+
*/
|
|
1694
|
+
function verificationOnlyInvalid(verification) {
|
|
1695
|
+
return verification !== void 0 && verification.applicability === "enforced" && verification.failedReason === void 0 && passedAttempt(verification) === void 0 && verification.attempts.length > 0 && invalidAttempts(verification).length === verification.attempts.length;
|
|
1696
|
+
}
|
|
1331
1697
|
/** Whether this cycle still has quality budget left. */
|
|
1332
1698
|
function hasQualityBudget(verification) {
|
|
1333
1699
|
return qualityAttempts(verification).length < 2;
|
|
@@ -1336,6 +1702,99 @@ function hasQualityBudget(verification) {
|
|
|
1336
1702
|
function hasExceptionBudget(verification) {
|
|
1337
1703
|
return exceptionAttempts(verification).length < 2;
|
|
1338
1704
|
}
|
|
1705
|
+
/** Whether a text carries a real statement rather than filler. */
|
|
1706
|
+
function substantive(text) {
|
|
1707
|
+
return text.trim().length >= 4;
|
|
1708
|
+
}
|
|
1709
|
+
/**
|
|
1710
|
+
* Program-level validity of a REJECTING acceptance, decided before any quality
|
|
1711
|
+
* verdict is booked (requirement 1: the rule cannot live in a prompt alone).
|
|
1712
|
+
*
|
|
1713
|
+
* A veto is only usable when every criterion that failed has its own structured
|
|
1714
|
+
* finding carrying the requirement, the observation, the difference and a
|
|
1715
|
+
* citation that really occurs in the evidence the judge saw. A citation aimed at
|
|
1716
|
+
* the empty-work baseline, a citation that is not in the reviewed evidence, an
|
|
1717
|
+
* empty or generic statement, and a truncated review that cannot substantiate
|
|
1718
|
+
* the veto are all invalid acceptances: no quality verdict, no quality budget,
|
|
1719
|
+
* and no card failure.
|
|
1720
|
+
* @param input - the attempt's scores, findings and the evidence it judged.
|
|
1721
|
+
* @returns valid, or the reason and detail of the invalidity.
|
|
1722
|
+
*/
|
|
1723
|
+
function assessAcceptanceValidity(input) {
|
|
1724
|
+
const failed = input.criteria.filter((criterion) => !criterion.passed);
|
|
1725
|
+
const truncated = input.omittedCharacters > 0;
|
|
1726
|
+
if (failed.length === 0) {
|
|
1727
|
+
if (input.findings.filter((finding) => validFinding(finding, input.evidence) === void 0).length > 0) return { valid: true };
|
|
1728
|
+
return truncated ? {
|
|
1729
|
+
valid: false,
|
|
1730
|
+
reason: "insufficient-evidence",
|
|
1731
|
+
detail: "the review was truncated and no verifiable citation supports the total-score veto"
|
|
1732
|
+
} : {
|
|
1733
|
+
valid: false,
|
|
1734
|
+
reason: "missing-finding",
|
|
1735
|
+
detail: "the total score was rejected without any locatable finding"
|
|
1736
|
+
};
|
|
1737
|
+
}
|
|
1738
|
+
for (const criterion of failed) {
|
|
1739
|
+
const own = input.findings.filter((finding) => finding.criterionId === criterion.id);
|
|
1740
|
+
if (own.length === 0) return truncated ? {
|
|
1741
|
+
valid: false,
|
|
1742
|
+
reason: "insufficient-evidence",
|
|
1743
|
+
detail: criterion.name + ": the evidence was truncated and the judge reported no citation of its own, so this criterion cannot be judged"
|
|
1744
|
+
} : {
|
|
1745
|
+
valid: false,
|
|
1746
|
+
reason: "missing-finding",
|
|
1747
|
+
detail: criterion.name + ": the criterion failed with no structured finding of its own"
|
|
1748
|
+
};
|
|
1749
|
+
let usable = false;
|
|
1750
|
+
let lastReason;
|
|
1751
|
+
for (const finding of own) {
|
|
1752
|
+
const problem = validFinding(finding, input.evidence);
|
|
1753
|
+
if (problem === void 0) {
|
|
1754
|
+
usable = true;
|
|
1755
|
+
break;
|
|
1756
|
+
}
|
|
1757
|
+
lastReason = problem;
|
|
1758
|
+
}
|
|
1759
|
+
if (!usable) {
|
|
1760
|
+
const detail = (lastReason?.detail ?? criterion.name) + " (" + criterion.name + ")";
|
|
1761
|
+
return {
|
|
1762
|
+
valid: false,
|
|
1763
|
+
reason: lastReason?.reason ?? "vacuous-finding",
|
|
1764
|
+
detail
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return { valid: true };
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Why one finding cannot support a veto, or undefined when it can.
|
|
1772
|
+
*
|
|
1773
|
+
* The citation is checked against the evidence text of the LOCATION the finding
|
|
1774
|
+
* names, so a quote invented by the judge, a quote taken from an A/B slot that
|
|
1775
|
+
* held the empty-work baseline, and a quote the reviewed window never contained
|
|
1776
|
+
* are all rejected.
|
|
1777
|
+
*/
|
|
1778
|
+
function validFinding(finding, evidence) {
|
|
1779
|
+
if (!substantive(finding.requirement) || !substantive(finding.observation) || !substantive(finding.gap)) return {
|
|
1780
|
+
reason: "vacuous-finding",
|
|
1781
|
+
detail: "the finding states no requirement, observation or difference"
|
|
1782
|
+
};
|
|
1783
|
+
if (finding.location === "baseline") return {
|
|
1784
|
+
reason: "baseline-finding",
|
|
1785
|
+
detail: "the citation is taken from the empty-work baseline, which is not the work under review"
|
|
1786
|
+
};
|
|
1787
|
+
const quote = finding.quote.trim();
|
|
1788
|
+
if (quote.length < 12) return {
|
|
1789
|
+
reason: "unlocatable-quote",
|
|
1790
|
+
detail: "the citation is too short to locate anything in the evidence"
|
|
1791
|
+
};
|
|
1792
|
+
const haystack = finding.location === "task" ? evidence.task : finding.location === "workspace" ? evidence.workspace : finding.location === "trajectory" ? evidence.trajectory : "";
|
|
1793
|
+
if (haystack === "" || !haystack.includes(quote)) return {
|
|
1794
|
+
reason: "unlocatable-quote",
|
|
1795
|
+
detail: "the citation does not occur in the evidence the judge reviewed"
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1339
1798
|
/**
|
|
1340
1799
|
* The acceptance rule, mirrored from the verifier's `sessionAccepted`:
|
|
1341
1800
|
* the work must BEAT the empty-work baseline, reach the total threshold, and
|
|
@@ -1357,7 +1816,7 @@ function readAttempt(value) {
|
|
|
1357
1816
|
if (typeof value !== "object" || value === null) return void 0;
|
|
1358
1817
|
const row = value;
|
|
1359
1818
|
if (typeof row.index !== "number" || typeof row.at !== "number") return void 0;
|
|
1360
|
-
if (row.stage !== "quality" && row.stage !== "exception") return void 0;
|
|
1819
|
+
if (row.stage !== "quality" && row.stage !== "exception" && row.stage !== "budget" && row.stage !== "invalid") return void 0;
|
|
1361
1820
|
if (typeof row.passed !== "boolean") return void 0;
|
|
1362
1821
|
if (typeof row.score !== "number" || !Number.isFinite(row.score)) return void 0;
|
|
1363
1822
|
if (typeof row.baseline !== "number" || !Number.isFinite(row.baseline)) return void 0;
|
|
@@ -1392,7 +1851,18 @@ function readAttempt(value) {
|
|
|
1392
1851
|
const routeRow = route;
|
|
1393
1852
|
if (typeof routeRow.provider !== "string" || typeof routeRow.model !== "string") return void 0;
|
|
1394
1853
|
if (routeRow.reasoningEffort !== void 0 && typeof routeRow.reasoningEffort !== "string") return void 0;
|
|
1395
|
-
const findings = Array.isArray(row.findings) ? row.findings.filter((item) => typeof item === "string")
|
|
1854
|
+
const findings = (Array.isArray(row.findings) ? row.findings : []).filter((item) => typeof item === "string").slice(0, 12).map((item) => clipRead(item, 400));
|
|
1855
|
+
const criterionFindings = [];
|
|
1856
|
+
if (Array.isArray(row.criterionFindings)) {
|
|
1857
|
+
if (row.criterionFindings.length > 12) return void 0;
|
|
1858
|
+
for (const entry of row.criterionFindings) {
|
|
1859
|
+
const finding = readCriterionFinding(entry);
|
|
1860
|
+
if (finding === void 0) return void 0;
|
|
1861
|
+
criterionFindings.push(finding);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const invalidReason = row.invalidReason;
|
|
1865
|
+
if (invalidReason !== void 0 && invalidReason !== "missing-finding" && invalidReason !== "unlocatable-quote" && invalidReason !== "baseline-finding" && invalidReason !== "vacuous-finding" && invalidReason !== "insufficient-evidence") return void 0;
|
|
1396
1866
|
return {
|
|
1397
1867
|
index: row.index,
|
|
1398
1868
|
at: row.at,
|
|
@@ -1425,7 +1895,51 @@ function readAttempt(value) {
|
|
|
1425
1895
|
},
|
|
1426
1896
|
channel: "explicit-tag",
|
|
1427
1897
|
rounds: typeof row.rounds === "number" ? row.rounds : 2,
|
|
1428
|
-
...typeof row.error === "string" ? { error: row.error } : {}
|
|
1898
|
+
...typeof row.error === "string" ? { error: row.error } : {},
|
|
1899
|
+
...criterionFindings.length === 0 ? {} : { criterionFindings },
|
|
1900
|
+
...typeof invalidReason === "string" ? { invalidReason } : {}
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Bound one persisted finding's text to what the acceptance itself would have
|
|
1905
|
+
* written.
|
|
1906
|
+
*
|
|
1907
|
+
* The reader is the only other door into the ledger: a hand-edited or
|
|
1908
|
+
* corrupted document could otherwise carry a megabyte of prose in a field the
|
|
1909
|
+
* writer caps at a few hundred characters, and every later render, audit
|
|
1910
|
+
* summary and cleanup pass would pay for it. Clipping on READ (never rejecting)
|
|
1911
|
+
* keeps the block usable and its capacity bounded: the acceptance-only material
|
|
1912
|
+
* the cleanup removes is bounded by construction on both sides.
|
|
1913
|
+
* @param value - the raw persisted text.
|
|
1914
|
+
* @param max - the cap the writer applies to that field.
|
|
1915
|
+
* @returns the bounded text.
|
|
1916
|
+
*/
|
|
1917
|
+
function clipRead(value, max) {
|
|
1918
|
+
return value.length <= max ? value : value.slice(0, max);
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Repair one persisted structured finding, or undefined when it is unusable.
|
|
1922
|
+
* A malformed finding drops the WHOLE acceptance block (fail closed): a
|
|
1923
|
+
* persisted veto that cannot be read back could otherwise be counted as
|
|
1924
|
+
* evidence it no longer holds.
|
|
1925
|
+
*/
|
|
1926
|
+
function readCriterionFinding(value) {
|
|
1927
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1928
|
+
const row = value;
|
|
1929
|
+
if (typeof row.criterionId !== "string" || row.criterionId === "") return void 0;
|
|
1930
|
+
if (typeof row.requirement !== "string" || typeof row.observation !== "string" || typeof row.gap !== "string") return void 0;
|
|
1931
|
+
if (typeof row.quote !== "string") return void 0;
|
|
1932
|
+
if (row.criterionId.length > 64) return void 0;
|
|
1933
|
+
const location = row.location;
|
|
1934
|
+
if (location !== "task" && location !== "trajectory" && location !== "workspace" && location !== "baseline" && location !== "unknown") return void 0;
|
|
1935
|
+
return {
|
|
1936
|
+
criterionId: row.criterionId,
|
|
1937
|
+
requirement: clipRead(row.requirement, 400),
|
|
1938
|
+
observation: clipRead(row.observation, 400),
|
|
1939
|
+
gap: clipRead(row.gap, 400),
|
|
1940
|
+
quote: clipRead(row.quote, 600),
|
|
1941
|
+
location,
|
|
1942
|
+
...typeof row.action === "string" ? { action: clipRead(row.action, 400) } : {}
|
|
1429
1943
|
};
|
|
1430
1944
|
}
|
|
1431
1945
|
/**
|
|
@@ -1473,7 +1987,7 @@ function normalizeVerification(value) {
|
|
|
1473
1987
|
};
|
|
1474
1988
|
}
|
|
1475
1989
|
const applicability = row.applicability;
|
|
1476
|
-
if (applicability !== "enforced" && applicability !== "disabled" && applicability !== "skipped" && applicability !== "goal-unavailable" && applicability !== "team-member") return void 0;
|
|
1990
|
+
if (applicability !== "enforced" && applicability !== "disabled" && applicability !== "skipped" && applicability !== "goal-unavailable" && applicability !== "team-member" && applicability !== "goal-disabled") return void 0;
|
|
1477
1991
|
const attempts = [];
|
|
1478
1992
|
if (!Array.isArray(row.attempts)) return void 0;
|
|
1479
1993
|
for (const entry of row.attempts) {
|
|
@@ -1482,6 +1996,8 @@ function normalizeVerification(value) {
|
|
|
1482
1996
|
attempts.push(attempt);
|
|
1483
1997
|
}
|
|
1484
1998
|
if (typeof row.failedReason === "string" && row.failedReason === "") return void 0;
|
|
1999
|
+
const cleanup = readCleanupRecord(row.cleanup);
|
|
2000
|
+
if (row.cleanup !== void 0 && cleanup === void 0) return void 0;
|
|
1485
2001
|
return {
|
|
1486
2002
|
contract: {
|
|
1487
2003
|
enabled: contractRow.enabled,
|
|
@@ -1496,7 +2012,98 @@ function normalizeVerification(value) {
|
|
|
1496
2012
|
...row.inFlight === true ? { inFlight: true } : {},
|
|
1497
2013
|
applicability,
|
|
1498
2014
|
...typeof row.failedReason === "string" ? { failedReason: row.failedReason } : {},
|
|
1499
|
-
...typeof row.failedAt === "number" && Number.isFinite(row.failedAt) ? { failedAt: row.failedAt } : {}
|
|
2015
|
+
...typeof row.failedAt === "number" && Number.isFinite(row.failedAt) ? { failedAt: row.failedAt } : {},
|
|
2016
|
+
...cleanup === void 0 ? {} : { cleanup }
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Repair the persisted cleanup record, or undefined when it is unusable. The
|
|
2021
|
+
* record is the audit trail the UI reads to tell "the judge gave its evidence
|
|
2022
|
+
* and it was cleaned up" apart from "the judge produced no evidence at all",
|
|
2023
|
+
* so a malformed one drops the whole block (fail closed).
|
|
2024
|
+
*/
|
|
2025
|
+
function readCleanupRecord(value) {
|
|
2026
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2027
|
+
const row = value;
|
|
2028
|
+
if (row.state !== "pending" && row.state !== "cleaned" && row.state !== "failed") return void 0;
|
|
2029
|
+
if (typeof row.attempts !== "number" || !Number.isInteger(row.attempts) || row.attempts < 0) return void 0;
|
|
2030
|
+
if (typeof row.startedAt !== "number" || !Number.isFinite(row.startedAt)) return void 0;
|
|
2031
|
+
if (row.cleanedAt !== void 0 && (typeof row.cleanedAt !== "number" || !Number.isFinite(row.cleanedAt))) return void 0;
|
|
2032
|
+
if (row.lastError !== void 0 && typeof row.lastError !== "string") return void 0;
|
|
2033
|
+
if (!Array.isArray(row.audit)) return void 0;
|
|
2034
|
+
const audit = [];
|
|
2035
|
+
for (const entry of row.audit) {
|
|
2036
|
+
const parsed = readAuditRecord(entry);
|
|
2037
|
+
if (parsed === void 0) return void 0;
|
|
2038
|
+
audit.push(parsed);
|
|
2039
|
+
}
|
|
2040
|
+
return {
|
|
2041
|
+
state: row.state,
|
|
2042
|
+
attempts: row.attempts,
|
|
2043
|
+
startedAt: row.startedAt,
|
|
2044
|
+
...typeof row.cleanedAt === "number" ? { cleanedAt: row.cleanedAt } : {},
|
|
2045
|
+
...typeof row.lastError === "string" ? { lastError: row.lastError } : {},
|
|
2046
|
+
audit
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
/** Repair one persisted audit credential, or undefined when it is unusable. */
|
|
2050
|
+
function readAuditRecord(value) {
|
|
2051
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
2052
|
+
const row = value;
|
|
2053
|
+
if (row.stage !== "quality" && row.stage !== "exception" && row.stage !== "budget" && row.stage !== "invalid") return void 0;
|
|
2054
|
+
if (typeof row.passed !== "boolean") return void 0;
|
|
2055
|
+
if (typeof row.at !== "number" || !Number.isFinite(row.at)) return void 0;
|
|
2056
|
+
if (typeof row.score !== "number" || !Number.isFinite(row.score)) return void 0;
|
|
2057
|
+
if (typeof row.baseline !== "number" || !Number.isFinite(row.baseline)) return void 0;
|
|
2058
|
+
if (!Array.isArray(row.criteria)) return void 0;
|
|
2059
|
+
const criteria = [];
|
|
2060
|
+
for (const entry of row.criteria) {
|
|
2061
|
+
if (typeof entry !== "object" || entry === null) return void 0;
|
|
2062
|
+
const criterion = entry;
|
|
2063
|
+
if (typeof criterion.id !== "string" || typeof criterion.name !== "string") return void 0;
|
|
2064
|
+
if (typeof criterion.score !== "number" || !Number.isFinite(criterion.score)) return void 0;
|
|
2065
|
+
if (typeof criterion.baseline !== "number" || !Number.isFinite(criterion.baseline)) return void 0;
|
|
2066
|
+
if (typeof criterion.threshold !== "number" || !Number.isFinite(criterion.threshold)) return void 0;
|
|
2067
|
+
criteria.push({
|
|
2068
|
+
id: criterion.id,
|
|
2069
|
+
name: criterion.name,
|
|
2070
|
+
score: criterion.score,
|
|
2071
|
+
baseline: criterion.baseline,
|
|
2072
|
+
threshold: criterion.threshold,
|
|
2073
|
+
passed: criterion.passed === true
|
|
2074
|
+
});
|
|
2075
|
+
}
|
|
2076
|
+
if (typeof row.evidenceHash !== "string") return void 0;
|
|
2077
|
+
const route = row.route;
|
|
2078
|
+
if (typeof route !== "object" || route === null) return void 0;
|
|
2079
|
+
const routeRow = route;
|
|
2080
|
+
if (typeof routeRow.provider !== "string" || typeof routeRow.model !== "string") return void 0;
|
|
2081
|
+
const usage = row.usage;
|
|
2082
|
+
if (typeof usage !== "object" || usage === null) return void 0;
|
|
2083
|
+
const usageRow = usage;
|
|
2084
|
+
if (typeof usageRow.calls !== "number" || typeof usageRow.inputTokens !== "number" || typeof usageRow.outputTokens !== "number" || typeof usageRow.reasoningTokens !== "number") return void 0;
|
|
2085
|
+
return {
|
|
2086
|
+
stage: row.stage,
|
|
2087
|
+
passed: row.passed,
|
|
2088
|
+
at: row.at,
|
|
2089
|
+
score: row.score,
|
|
2090
|
+
baseline: row.baseline,
|
|
2091
|
+
criteria,
|
|
2092
|
+
evidenceHash: row.evidenceHash,
|
|
2093
|
+
route: {
|
|
2094
|
+
provider: routeRow.provider,
|
|
2095
|
+
model: routeRow.model,
|
|
2096
|
+
...typeof routeRow.reasoningEffort === "string" ? { reasoningEffort: routeRow.reasoningEffort } : {}
|
|
2097
|
+
},
|
|
2098
|
+
...typeof row.error === "string" ? { error: row.error } : {},
|
|
2099
|
+
...typeof row.findingSummary === "string" ? { findingSummary: row.findingSummary } : {},
|
|
2100
|
+
usage: {
|
|
2101
|
+
calls: usageRow.calls,
|
|
2102
|
+
inputTokens: usageRow.inputTokens,
|
|
2103
|
+
outputTokens: usageRow.outputTokens,
|
|
2104
|
+
reasoningTokens: usageRow.reasoningTokens,
|
|
2105
|
+
...usageRow.usageIncomplete === true ? { usageIncomplete: true } : {}
|
|
2106
|
+
}
|
|
1500
2107
|
};
|
|
1501
2108
|
}
|
|
1502
2109
|
/**
|
|
@@ -1515,14 +2122,68 @@ function qualityFeedback(attempt, remaining) {
|
|
|
1515
2122
|
"总分 " + percent(attempt.score) + "(阈值 " + percent(attempt.criteria[0]?.threshold ?? .65) + "),空工作基线 " + percent(attempt.baseline) + "。",
|
|
1516
2123
|
"未达标判据:" + (failed.length === 0 ? "无(总分或基线比较未通过)" : failed.map((criterion) => criterion.name + " " + percent(criterion.score)).join("、"))
|
|
1517
2124
|
];
|
|
1518
|
-
|
|
2125
|
+
const located = locatedFindingLines(attempt);
|
|
2126
|
+
if (located.length > 0) {
|
|
1519
2127
|
lines.push("可定位问题:");
|
|
1520
|
-
for (const
|
|
2128
|
+
for (const line of located) lines.push("- " + line);
|
|
1521
2129
|
} else lines.push("本次裁判没有报告可定位的问题;请直接对照任务要求核对证据中的真实输出。");
|
|
1522
2130
|
lines.push("请按上述问题修复,然后再次调用 update_goal(action: complete) 触发第 " + (attempt.index + 1) + " 次验收;本执行周期的验收额度还剩 " + remaining + " 次。");
|
|
1523
2131
|
return lines.join("\n");
|
|
1524
2132
|
}
|
|
1525
2133
|
/**
|
|
2134
|
+
* The feedback one INVALID acceptance returns.
|
|
2135
|
+
*
|
|
2136
|
+
* The claim is refused (the board still has no pass record, so completion may
|
|
2137
|
+
* not take effect) but the agent is NOT handed a repair instruction: there is
|
|
2138
|
+
* no located problem to repair, and telling it to "fix" an evidence-free veto
|
|
2139
|
+
* would be exactly the blind repair the requirement forbids. The text says what
|
|
2140
|
+
* was missing, what happens next, and what the agent should do instead (make
|
|
2141
|
+
* the missing output itself observable, or let a human look).
|
|
2142
|
+
* @param attempt - the invalid attempt.
|
|
2143
|
+
* @param remaining - invalid-acceptance attempts left after this one.
|
|
2144
|
+
* @returns the model-facing text.
|
|
2145
|
+
*/
|
|
2146
|
+
function invalidAcceptanceFeedback(attempt, remaining) {
|
|
2147
|
+
const lines = [
|
|
2148
|
+
"[任务看板 · 验收无效] 第 " + attempt.index + " 次验收没有产生可用的质量判定。",
|
|
2149
|
+
"原因:" + invalidReasonText(attempt.invalidReason) + (attempt.error === void 0 || attempt.error === "" ? "" : "(" + attempt.error + ")"),
|
|
2150
|
+
"本次未消耗质量验收额度,也不把本次执行判为失败。"
|
|
2151
|
+
];
|
|
2152
|
+
if (attempt.invalidReason === "insufficient-evidence") {
|
|
2153
|
+
lines.push("裁判看到的证据不完整(本次轨迹被截断 " + attempt.evidence.omittedCharacters + " 字符),无法据此否决;这属于验收无效,不代表执行者没有完成工作。");
|
|
2154
|
+
lines.push("请把关键结论的证据直接放进你能控制的输出里(例如把最终验证命令的输出贴出来),或说明还需要哪些证据;再次调用 update_goal(action: complete) 会重新验收。");
|
|
2155
|
+
} else {
|
|
2156
|
+
lines.push("验收没有拿到任务要求、实际观测、差异与可定位引用俱全的证据,因此本次否决无效。");
|
|
2157
|
+
lines.push("请不要据此盲目修改:先确认你的交付是否真的满足了任务要求,并确保关键产物/输出可见;再次调用 update_goal(action: complete) 会重新验收。");
|
|
2158
|
+
}
|
|
2159
|
+
if (remaining > 0) lines.push("无效验收重试额度还剩 " + remaining + " 次;用尽后看板会停止调用裁判并等待人工处理。");
|
|
2160
|
+
else lines.push("无效验收重试额度已用尽:看板将停止调用裁判并把本次执行留在等待人工处理的状态。");
|
|
2161
|
+
return lines.join("\n");
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* The reason one execution records when its invalid-acceptance budget is spent.
|
|
2165
|
+
*
|
|
2166
|
+
* Deliberately NOT a quality failure: no veto was ever substantiated, so the
|
|
2167
|
+
* card stays open for a human and the reason says the acceptance is INVALID
|
|
2168
|
+
* rather than rejected.
|
|
2169
|
+
* @param used - invalid attempts spent.
|
|
2170
|
+
* @returns the user- and model-facing text.
|
|
2171
|
+
*/
|
|
2172
|
+
function invalidHoldReason(used) {
|
|
2173
|
+
return "goal 验收无效(连续 " + used + " 次未产生可用判定):验收没有给出与任务要求对应、且可定位到本次证据的否决依据,本次执行未判失败、未消耗质量验收额度。看板已停止调用裁判,等待人工处理:修复验收环境或证据可见性后,用 task_board_manage(action=reset-verification) 清除记录的无效验收,再重新完成目标;也可以直接重跑该卡片。";
|
|
2174
|
+
}
|
|
2175
|
+
/** Human-readable text of one invalid reason. */
|
|
2176
|
+
function invalidReasonText(reason) {
|
|
2177
|
+
switch (reason) {
|
|
2178
|
+
case "missing-finding": return "未达标的判据没有对应的结构化问题反馈";
|
|
2179
|
+
case "unlocatable-quote": return "问题反馈引用的位置不存在于本次裁判所见的证据中";
|
|
2180
|
+
case "baseline-finding": return "问题反馈指向空工作基线,而不是本次执行的工作";
|
|
2181
|
+
case "vacuous-finding": return "问题反馈为空或空泛评价,没有说明实际观测与差异";
|
|
2182
|
+
case "insufficient-evidence": return "裁判看到的证据不足(例如轨迹被截断),无法支撑否决";
|
|
2183
|
+
default: return "未产生可用的质量判定";
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
/**
|
|
1526
2187
|
* The reason one exhausted acceptance cycle records as the execution's final
|
|
1527
2188
|
* failure. Unlike {@link qualityFeedback} — which the fixing agent reads while
|
|
1528
2189
|
* budget remains — this reason is also persisted on the ledger, echoed on every
|
|
@@ -1538,12 +2199,36 @@ function qualityFeedback(attempt, remaining) {
|
|
|
1538
2199
|
function finalFailureReason(attempt, used, threshold) {
|
|
1539
2200
|
const failed = attempt.criteria.filter((criterion) => !criterion.passed);
|
|
1540
2201
|
const lines = ["goal 验收第 " + used + " 次仍未通过(总分 " + percent(attempt.score) + ",阈值 " + percent(threshold) + ",空工作基线 " + percent(attempt.baseline) + ";未达标判据:" + (failed.length === 0 ? "总分或基线比较未通过" : failed.map((criterion) => criterion.name + " " + percent(criterion.score)).join("、")) + "),本次 execution 判失败。"];
|
|
1541
|
-
|
|
2202
|
+
const located = locatedFindingLines(attempt);
|
|
2203
|
+
if (located.length > 0) {
|
|
1542
2204
|
lines.push("验收报告记录的可定位问题:");
|
|
1543
|
-
for (const
|
|
2205
|
+
for (const line of located) lines.push("- " + line);
|
|
1544
2206
|
} else lines.push("本次验收没有记录可定位的问题;请对照任务要求与验收面板中的判据得分核对证据中的真实输出。");
|
|
1545
2207
|
return lines.join("\n");
|
|
1546
2208
|
}
|
|
2209
|
+
/**
|
|
2210
|
+
* The locatable problem lines of one attempt, structured findings first.
|
|
2211
|
+
*
|
|
2212
|
+
* A structured finding renders the requirement, the observed fact, the gap and
|
|
2213
|
+
* the citation in a fixed order, so the fixing agent reads WHAT was required,
|
|
2214
|
+
* WHAT was seen, WHY it falls short and WHERE to look — never a bare complaint.
|
|
2215
|
+
* @param attempt - the attempt whose findings are rendered.
|
|
2216
|
+
* @returns one line per finding, bounded.
|
|
2217
|
+
*/
|
|
2218
|
+
function locatedFindingLines(attempt) {
|
|
2219
|
+
const structured = attempt.criterionFindings ?? [];
|
|
2220
|
+
if (structured.length > 0) return structured.map((finding) => {
|
|
2221
|
+
const locator = "[" + finding.criterionId + " " + finding.location + "] \"" + clip(finding.quote, 200) + "\"";
|
|
2222
|
+
const action = finding.action === void 0 || finding.action === "" ? "" : " → 建议核实:" + finding.action;
|
|
2223
|
+
return locator + " 要求:" + finding.requirement + " 观测:" + finding.observation + " 差异:" + finding.gap + action;
|
|
2224
|
+
});
|
|
2225
|
+
return [...attempt.findings];
|
|
2226
|
+
}
|
|
2227
|
+
/** Bound one text for a single-line rendering. */
|
|
2228
|
+
function clip(text, max) {
|
|
2229
|
+
const value = text.replace(/s+/g, " ").trim();
|
|
2230
|
+
return value.length <= max ? value : value.slice(0, max) + "…";
|
|
2231
|
+
}
|
|
1547
2232
|
/** `0.42` style percentage. */
|
|
1548
2233
|
function percent(value) {
|
|
1549
2234
|
return (value * 100).toFixed(1) + "%";
|
|
@@ -1718,6 +2403,7 @@ function isTaskRecordShape(value) {
|
|
|
1718
2403
|
if (record.parentId !== void 0 && typeof record.parentId !== "string") return false;
|
|
1719
2404
|
if (record.workspaceId !== void 0 && typeof record.workspaceId !== "string") return false;
|
|
1720
2405
|
if (record.mode !== void 0 && typeof record.mode !== "string") return false;
|
|
2406
|
+
if (record.reasoningEffort !== void 0 && typeof record.reasoningEffort !== "string") return false;
|
|
1721
2407
|
if (record.permission !== void 0 && typeof record.permission !== "string") return false;
|
|
1722
2408
|
if (record.integrations !== void 0 && (typeof record.integrations !== "object" || record.integrations === null || Array.isArray(record.integrations))) return false;
|
|
1723
2409
|
if (record.hidden !== void 0 && typeof record.hidden !== "boolean") return false;
|
|
@@ -1737,6 +2423,7 @@ function isTaskRecordShape(value) {
|
|
|
1737
2423
|
if (entry.initiatedBy !== void 0 && typeof entry.initiatedBy !== "string") return false;
|
|
1738
2424
|
if (entry.frozenBy !== void 0 && typeof entry.frozenBy !== "string") return false;
|
|
1739
2425
|
if (entry.frozenAt !== void 0 && typeof entry.frozenAt !== "number") return false;
|
|
2426
|
+
if (entry.external !== void 0 && typeof entry.external !== "boolean") return false;
|
|
1740
2427
|
if (entry.verification !== void 0 && normalizeVerification(entry.verification) === void 0) return false;
|
|
1741
2428
|
}
|
|
1742
2429
|
return true;
|
|
@@ -1749,23 +2436,62 @@ function isTaskRecord(value) {
|
|
|
1749
2436
|
function normalizeStatus(status) {
|
|
1750
2437
|
return isTaskStatus(status) ? status : "todo";
|
|
1751
2438
|
}
|
|
2439
|
+
/** A persisted ms timestamp, kept only when it is a usable finite number. */
|
|
2440
|
+
function optionalInstant(value) {
|
|
2441
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
2442
|
+
}
|
|
2443
|
+
/** A persisted positive-integer run cap, or undefined for unlimited/repair. */
|
|
2444
|
+
function optionalRunCap(value) {
|
|
2445
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : void 0;
|
|
2446
|
+
}
|
|
2447
|
+
/** A persisted stop reason, kept only when it names a reason this build knows. */
|
|
2448
|
+
function optionalStopReason(value) {
|
|
2449
|
+
return typeof value === "string" && SCHEDULE_STOP_REASONS.includes(value) ? value : void 0;
|
|
2450
|
+
}
|
|
1752
2451
|
/**
|
|
1753
|
-
* Repair a persisted schedule rule
|
|
1754
|
-
*
|
|
1755
|
-
*
|
|
2452
|
+
* Repair a persisted schedule rule. Both modes are repaired field by field and
|
|
2453
|
+
* a rule that carries no usable plan is dropped (the task row survives):
|
|
2454
|
+
* a recurring rule without a well-formed 5-field cron, or a one-shot without a
|
|
2455
|
+
* finite planned instant. A rule written before `mode` existed is a cron rule.
|
|
1756
2456
|
*/
|
|
1757
2457
|
function normalizeSchedule(schedule) {
|
|
1758
2458
|
if (typeof schedule !== "object" || schedule === null) return void 0;
|
|
1759
2459
|
const rule = schedule;
|
|
2460
|
+
const timeZone = typeof rule.timeZone === "string" && isValidTimeZone(rule.timeZone) ? rule.timeZone : void 0;
|
|
2461
|
+
const runCountValue = rule.runCount;
|
|
2462
|
+
const runCount = typeof runCountValue === "number" && Number.isInteger(runCountValue) && runCountValue >= 0 ? runCountValue : 0;
|
|
2463
|
+
const endedAt = optionalInstant(rule.endedAt);
|
|
2464
|
+
const endedReason = optionalStopReason(rule.endedReason);
|
|
2465
|
+
const skippedAt = optionalInstant(rule.skippedAt);
|
|
2466
|
+
const skippedReason = optionalStopReason(rule.skippedReason);
|
|
2467
|
+
const common = {
|
|
2468
|
+
enabled: rule.enabled === true,
|
|
2469
|
+
...timeZone === void 0 ? {} : { timeZone },
|
|
2470
|
+
nextRunAt: optionalInstant(rule.nextRunAt),
|
|
2471
|
+
lastTriggeredAt: optionalInstant(rule.lastTriggeredAt),
|
|
2472
|
+
runCount,
|
|
2473
|
+
...endedAt === void 0 ? {} : { endedAt },
|
|
2474
|
+
...endedReason === void 0 ? {} : { endedReason },
|
|
2475
|
+
...skippedAt === void 0 ? {} : { skippedAt },
|
|
2476
|
+
...skippedReason === void 0 ? {} : { skippedReason }
|
|
2477
|
+
};
|
|
2478
|
+
if (rule.mode === "once") {
|
|
2479
|
+
const at = optionalInstant(rule.at);
|
|
2480
|
+
if (at === void 0) return void 0;
|
|
2481
|
+
return {
|
|
2482
|
+
...common,
|
|
2483
|
+
mode: "once",
|
|
2484
|
+
at
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
1760
2487
|
if (typeof rule.cron !== "string") return void 0;
|
|
1761
2488
|
if (rule.cron.trim() === "" || !isValidCron(rule.cron)) return void 0;
|
|
1762
|
-
const
|
|
2489
|
+
const maxRuns = optionalRunCap(rule.maxRuns);
|
|
1763
2490
|
return {
|
|
1764
|
-
|
|
2491
|
+
...common,
|
|
2492
|
+
mode: "cron",
|
|
1765
2493
|
cron: rule.cron,
|
|
1766
|
-
...
|
|
1767
|
-
nextRunAt: typeof rule.nextRunAt === "number" ? rule.nextRunAt : void 0,
|
|
1768
|
-
lastTriggeredAt: typeof rule.lastTriggeredAt === "number" ? rule.lastTriggeredAt : void 0
|
|
2494
|
+
...maxRuns === void 0 ? {} : { maxRuns }
|
|
1769
2495
|
};
|
|
1770
2496
|
}
|
|
1771
2497
|
/**
|
|
@@ -1842,14 +2568,16 @@ function parseLedger(raw) {
|
|
|
1842
2568
|
runGroupId: normalizeTargetId(execution.runGroupId),
|
|
1843
2569
|
ownResult: isExecutionOutcome(execution.ownResult) ? execution.ownResult : void 0,
|
|
1844
2570
|
ownError: typeof execution.ownError === "string" ? execution.ownError : void 0,
|
|
1845
|
-
verification: normalizeVerification(execution.verification)
|
|
2571
|
+
verification: normalizeVerification(execution.verification),
|
|
2572
|
+
external: execution.external === true ? true : void 0
|
|
1846
2573
|
}));
|
|
1847
2574
|
task.workspaceId = normalizeTargetId(row.workspaceId);
|
|
1848
2575
|
task.mode = normalizeTargetId(row.mode);
|
|
2576
|
+
task.reasoningEffort = normalizeTargetId(row.reasoningEffort);
|
|
1849
2577
|
task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
|
|
1850
2578
|
task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
|
|
1851
2579
|
task.reuseSession = row.reuseSession === true ? true : void 0;
|
|
1852
|
-
task.goalRun = row.goalRun ===
|
|
2580
|
+
task.goalRun = row.goalRun === true ? true : void 0;
|
|
1853
2581
|
task.skipVerification = row.skipVerification === true ? true : void 0;
|
|
1854
2582
|
task.freeze = normalizeFreeze(row.freeze);
|
|
1855
2583
|
task.handover = normalizeHandover(row.handover);
|
|
@@ -1946,6 +2674,57 @@ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
|
1946
2674
|
};
|
|
1947
2675
|
}
|
|
1948
2676
|
//#endregion
|
|
2677
|
+
//#region src/core/card-defaults.ts
|
|
2678
|
+
/** Trim one optional target: blank text means "not set", like every stored pin. */
|
|
2679
|
+
function textOf(value) {
|
|
2680
|
+
return value?.trim() ?? "";
|
|
2681
|
+
}
|
|
2682
|
+
/**
|
|
2683
|
+
* Resolve the execution targets a new card starts from.
|
|
2684
|
+
* @param defaults - the board's configured new-card defaults, if any.
|
|
2685
|
+
* @param source - the card this one is created from (a duplicate or a parent), if any.
|
|
2686
|
+
* @param picked - what the caller chose explicitly; a non-blank value wins.
|
|
2687
|
+
* @returns the four targets, blank where nothing supplies a value.
|
|
2688
|
+
*/
|
|
2689
|
+
function resolveCardTargets(defaults, source, picked) {
|
|
2690
|
+
const defaultMode = textOf(defaults?.mode);
|
|
2691
|
+
const defaultPermission = textOf(defaults?.permission);
|
|
2692
|
+
const defaultModel = textOf(defaults?.model);
|
|
2693
|
+
const defaultEffort = defaultModel === "" ? "" : textOf(defaults?.reasoningEffort);
|
|
2694
|
+
const chosen = {
|
|
2695
|
+
mode: textOf(picked?.mode),
|
|
2696
|
+
permission: textOf(picked?.permission),
|
|
2697
|
+
model: textOf(picked?.model),
|
|
2698
|
+
reasoningEffort: textOf(picked?.reasoningEffort)
|
|
2699
|
+
};
|
|
2700
|
+
const copied = source === void 0 ? void 0 : {
|
|
2701
|
+
mode: textOf(source.task.mode),
|
|
2702
|
+
permission: textOf(source.task.permission),
|
|
2703
|
+
model: textOf(source.task.model),
|
|
2704
|
+
reasoningEffort: textOf(source.task.reasoningEffort)
|
|
2705
|
+
};
|
|
2706
|
+
/** The value carried over from the source card, or the board default. */
|
|
2707
|
+
const inherited = (key) => {
|
|
2708
|
+
const fallback = key === "mode" ? defaultMode : key === "model" ? defaultModel : defaultPermission;
|
|
2709
|
+
if (copied === void 0 || key === "permission" && source?.kind === "parent") return fallback;
|
|
2710
|
+
return copied[key] === "" ? fallback : copied[key];
|
|
2711
|
+
};
|
|
2712
|
+
const mode = chosen.mode === "" ? inherited("mode") : chosen.mode;
|
|
2713
|
+
const permission = chosen.permission === "" ? inherited("permission") : chosen.permission;
|
|
2714
|
+
const model = chosen.model === "" ? inherited("model") : chosen.model;
|
|
2715
|
+
let reasoningEffort = chosen.reasoningEffort;
|
|
2716
|
+
if (reasoningEffort === "" && chosen.model === "") {
|
|
2717
|
+
const sourced = copied === void 0 ? "" : copied.reasoningEffort;
|
|
2718
|
+
reasoningEffort = sourced !== "" ? sourced : model !== "" && model === defaultModel ? defaultEffort : "";
|
|
2719
|
+
}
|
|
2720
|
+
return {
|
|
2721
|
+
mode,
|
|
2722
|
+
permission,
|
|
2723
|
+
model,
|
|
2724
|
+
reasoningEffort
|
|
2725
|
+
};
|
|
2726
|
+
}
|
|
2727
|
+
//#endregion
|
|
1949
2728
|
//#region src/core/use-cases/task-create.ts
|
|
1950
2729
|
/**
|
|
1951
2730
|
* Create-task use case: mint a new task from user input, rejecting a blank
|
|
@@ -1958,18 +2737,22 @@ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
|
1958
2737
|
* appended ledger, or the unchanged ledger when the title is blank or the
|
|
1959
2738
|
* requested parent link is refused.
|
|
1960
2739
|
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1964
|
-
*
|
|
2740
|
+
* The execution targets resolve through {@link resolveCardTargets}: what the
|
|
2741
|
+
* caller sent wins, then the parent's own pins, then - only when the caller
|
|
2742
|
+
* opted in with `applyCardDefaults` - the deployment's configured new-card
|
|
2743
|
+
* defaults. Without the flag the resolution sees no defaults at all, which is
|
|
2744
|
+
* exactly the inline parent inheritance this path has always applied. The
|
|
2745
|
+
* workspace is not part of that resolution: a subtask still takes the parent's
|
|
2746
|
+
* workspace, and the defaults never name one.
|
|
1965
2747
|
* @param tasks - current ledger.
|
|
1966
2748
|
* @param input - raw user input (title/description/prompt + optional schedule/parent).
|
|
1967
2749
|
* @param now - clock instant (ms epoch).
|
|
1968
2750
|
* @param id - minted task id.
|
|
1969
2751
|
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
1970
2752
|
* @param hostTimeZone - zone a rule that stores no zone of its own follows.
|
|
2753
|
+
* @param cardDefaults - the deployment's new-card defaults, read live at creation.
|
|
1971
2754
|
*/
|
|
1972
|
-
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone()) {
|
|
2755
|
+
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone(), cardDefaults) {
|
|
1973
2756
|
if (input.title.trim() === "") return {
|
|
1974
2757
|
task: void 0,
|
|
1975
2758
|
tasks,
|
|
@@ -1995,28 +2778,67 @@ function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZon
|
|
|
1995
2778
|
error: `subtask depth limit (${limit}) exceeded`
|
|
1996
2779
|
};
|
|
1997
2780
|
}
|
|
1998
|
-
|
|
2781
|
+
const seed = resolveCardTargets(input.applyCardDefaults === true ? cardDefaults : void 0, parent === void 0 ? void 0 : {
|
|
2782
|
+
kind: "parent",
|
|
2783
|
+
task: parent
|
|
2784
|
+
}, {
|
|
2785
|
+
mode: input.mode,
|
|
2786
|
+
permission: input.permission,
|
|
2787
|
+
model: input.model,
|
|
2788
|
+
reasoningEffort: input.reasoningEffort
|
|
2789
|
+
});
|
|
2790
|
+
let task = createTask({
|
|
1999
2791
|
...input,
|
|
2000
|
-
workspaceId: input.workspaceId ?? parent
|
|
2001
|
-
mode:
|
|
2002
|
-
|
|
2792
|
+
workspaceId: input.workspaceId ?? parent?.workspaceId,
|
|
2793
|
+
mode: seed.mode,
|
|
2794
|
+
permission: isTaskPermission(seed.permission) ? seed.permission : void 0,
|
|
2795
|
+
model: seed.model,
|
|
2796
|
+
reasoningEffort: seed.reasoningEffort
|
|
2003
2797
|
}, now, id);
|
|
2004
2798
|
const requested = input.schedule;
|
|
2005
|
-
if (requested?.enabled === true
|
|
2006
|
-
const cron = requested.cron.trim();
|
|
2799
|
+
if (requested?.enabled === true) {
|
|
2007
2800
|
const requestedZone = requested.timeZone;
|
|
2008
2801
|
if (requestedZone !== void 0 && !isValidTimeZone(requestedZone)) return {
|
|
2009
2802
|
task: void 0,
|
|
2010
2803
|
tasks,
|
|
2011
2804
|
error: "invalid schedule time zone"
|
|
2012
2805
|
};
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2806
|
+
if (requested.mode === "once") {
|
|
2807
|
+
const at = requested.at;
|
|
2808
|
+
if (at === void 0 || !Number.isInteger(at) || at <= now) return {
|
|
2809
|
+
task: void 0,
|
|
2810
|
+
tasks,
|
|
2811
|
+
error: "invalid schedule instant"
|
|
2812
|
+
};
|
|
2813
|
+
task = withSchedule(task, {
|
|
2814
|
+
enabled: true,
|
|
2815
|
+
mode: "once",
|
|
2816
|
+
at,
|
|
2817
|
+
timeZone: requestedZone,
|
|
2818
|
+
nextRunAt: at,
|
|
2819
|
+
runCount: 0
|
|
2820
|
+
}, now);
|
|
2821
|
+
} else {
|
|
2822
|
+
const cron = (requested.cron ?? "").trim();
|
|
2823
|
+
if (cron !== "" && isValidCron(cron)) {
|
|
2824
|
+
const maxRuns = requested.maxRuns;
|
|
2825
|
+
if (maxRuns !== void 0 && (!Number.isInteger(maxRuns) || maxRuns < 1)) return {
|
|
2826
|
+
task: void 0,
|
|
2827
|
+
tasks,
|
|
2828
|
+
error: "invalid schedule run cap"
|
|
2829
|
+
};
|
|
2830
|
+
const nextRunAt = nextRunAtMs(cron, now, requestedZone ?? hostTimeZone);
|
|
2831
|
+
if (nextRunAt !== void 0) task = withSchedule(task, {
|
|
2832
|
+
enabled: true,
|
|
2833
|
+
mode: "cron",
|
|
2834
|
+
cron,
|
|
2835
|
+
maxRuns,
|
|
2836
|
+
timeZone: requestedZone,
|
|
2837
|
+
nextRunAt,
|
|
2838
|
+
runCount: 0
|
|
2839
|
+
}, now);
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2020
2842
|
}
|
|
2021
2843
|
return {
|
|
2022
2844
|
task,
|
|
@@ -2047,16 +2869,30 @@ function applyDeleteTask(tasks, selectedTaskId, id) {
|
|
|
2047
2869
|
//#endregion
|
|
2048
2870
|
//#region src/core/use-cases/task-schedule.ts
|
|
2049
2871
|
/**
|
|
2050
|
-
* Schedule use
|
|
2051
|
-
* Pure ledger transitions (no persistence or notify
|
|
2052
|
-
* orchestrates those). Validation and next-run computation
|
|
2053
|
-
* the core cron parser (schedule.ts) and the withSchedule
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
*
|
|
2057
|
-
*
|
|
2058
|
-
*
|
|
2059
|
-
*
|
|
2872
|
+
* Schedule use cases: arm/disarm a task's scheduling rule, and record what a
|
|
2873
|
+
* due occurrence did to it. Pure ledger transitions (no persistence or notify
|
|
2874
|
+
* — the controller orchestrates those). Validation and next-run computation
|
|
2875
|
+
* live here, sharing the core cron parser (schedule.ts) and the withSchedule
|
|
2876
|
+
* transition.
|
|
2877
|
+
*
|
|
2878
|
+
* A rule is either a recurring cron rule or a single planned instant, and
|
|
2879
|
+
* either kind may be capped by a run budget. The counter (`runCount`) moves
|
|
2880
|
+
* only when the Host actually opened an execution; a skipped occurrence
|
|
2881
|
+
* (busy, unconfirmed permission, missed instant, unreachable plan) moves the
|
|
2882
|
+
* rule without consuming its budget.
|
|
2883
|
+
*/
|
|
2884
|
+
/**
|
|
2885
|
+
* Set an on-board task's scheduling rule. An unknown or archived task, an
|
|
2886
|
+
* unusable zone, a blank or invalid cron, a one-shot with no usable instant,
|
|
2887
|
+
* a one-shot armed in the past, and a run cap that is not a positive integer
|
|
2888
|
+
* are all rejected (state untouched). An enabled rule computes its next run
|
|
2889
|
+
* instant immediately — a one-shot's next instant IS its planned instant — and
|
|
2890
|
+
* a disabled rule carries none.
|
|
2891
|
+
*
|
|
2892
|
+
* Arming a rule that was not enabled, or switching its kind, starts a fresh
|
|
2893
|
+
* budget: the run counter and any previous stop/skip record are cleared. Edits
|
|
2894
|
+
* to a live rule (expression, zone, cap) keep the consumed count, so an
|
|
2895
|
+
* already-spent budget can never silently reset and overrun.
|
|
2060
2896
|
* @param tasks - current ledger.
|
|
2061
2897
|
* @param id - the task to schedule.
|
|
2062
2898
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
@@ -2070,47 +2906,168 @@ function applySetSchedule(tasks, id, patch, now, hostTimeZone) {
|
|
|
2070
2906
|
applied: false
|
|
2071
2907
|
};
|
|
2072
2908
|
const current = task.schedule;
|
|
2909
|
+
const mode = patch.mode ?? current?.mode ?? "cron";
|
|
2910
|
+
const timeZone = patch.timeZone === void 0 ? current?.timeZone : patch.timeZone ?? void 0;
|
|
2911
|
+
if (timeZone !== void 0 && !isValidTimeZone(timeZone)) return {
|
|
2912
|
+
tasks,
|
|
2913
|
+
applied: false
|
|
2914
|
+
};
|
|
2915
|
+
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
2916
|
+
const restart = enabled && current?.enabled !== true || (current?.mode ?? "cron") !== mode;
|
|
2917
|
+
const runCount = restart ? 0 : current?.runCount ?? 0;
|
|
2918
|
+
const effectiveZone = timeZone ?? hostTimeZone;
|
|
2919
|
+
const cleared = restart ? {
|
|
2920
|
+
endedAt: void 0,
|
|
2921
|
+
endedReason: void 0,
|
|
2922
|
+
skippedAt: void 0,
|
|
2923
|
+
skippedReason: void 0
|
|
2924
|
+
} : {
|
|
2925
|
+
endedAt: current?.endedAt,
|
|
2926
|
+
endedReason: current?.endedReason,
|
|
2927
|
+
skippedAt: current?.skippedAt,
|
|
2928
|
+
skippedReason: current?.skippedReason
|
|
2929
|
+
};
|
|
2930
|
+
if (mode === "once") {
|
|
2931
|
+
const at = patch.at ?? current?.at;
|
|
2932
|
+
if (at === void 0 || !Number.isInteger(at)) return {
|
|
2933
|
+
tasks,
|
|
2934
|
+
applied: false
|
|
2935
|
+
};
|
|
2936
|
+
if (enabled && at <= now) return {
|
|
2937
|
+
tasks,
|
|
2938
|
+
applied: false
|
|
2939
|
+
};
|
|
2940
|
+
return {
|
|
2941
|
+
tasks: tasks.map((candidate) => candidate.id !== id ? candidate : withSchedule(candidate, {
|
|
2942
|
+
enabled,
|
|
2943
|
+
mode: "once",
|
|
2944
|
+
at,
|
|
2945
|
+
cron: void 0,
|
|
2946
|
+
maxRuns: void 0,
|
|
2947
|
+
timeZone: timeZone ?? void 0,
|
|
2948
|
+
nextRunAt: enabled ? at : void 0,
|
|
2949
|
+
runCount,
|
|
2950
|
+
...cleared
|
|
2951
|
+
}, now)),
|
|
2952
|
+
applied: true
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2073
2955
|
const cron = (patch.cron ?? current?.cron ?? "").trim();
|
|
2074
2956
|
if (cron === "" || !isValidCron(cron)) return {
|
|
2075
2957
|
tasks,
|
|
2076
2958
|
applied: false
|
|
2077
2959
|
};
|
|
2078
|
-
const
|
|
2079
|
-
if (
|
|
2960
|
+
const maxRuns = ("maxRuns" in patch ? patch.maxRuns : current?.maxRuns) ?? void 0;
|
|
2961
|
+
if (maxRuns !== void 0 && (!Number.isInteger(maxRuns) || maxRuns < 1)) return {
|
|
2080
2962
|
tasks,
|
|
2081
2963
|
applied: false
|
|
2082
2964
|
};
|
|
2083
|
-
const
|
|
2084
|
-
const
|
|
2085
|
-
|
|
2965
|
+
const spent = maxRuns !== void 0 && runCount >= maxRuns;
|
|
2966
|
+
const armed = enabled && !spent;
|
|
2967
|
+
const nextRunAt = armed ? nextRunAtMs(cron, now, effectiveZone) : void 0;
|
|
2968
|
+
if (armed && nextRunAt === void 0) return {
|
|
2086
2969
|
tasks,
|
|
2087
2970
|
applied: false
|
|
2088
2971
|
};
|
|
2972
|
+
const exhausted = enabled && spent;
|
|
2089
2973
|
return {
|
|
2090
|
-
tasks: tasks.map((candidate) => candidate.id
|
|
2091
|
-
enabled,
|
|
2974
|
+
tasks: tasks.map((candidate) => candidate.id !== id ? candidate : withSchedule(candidate, {
|
|
2975
|
+
enabled: armed,
|
|
2976
|
+
mode: "cron",
|
|
2092
2977
|
cron,
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2978
|
+
at: void 0,
|
|
2979
|
+
maxRuns,
|
|
2980
|
+
timeZone: timeZone ?? void 0,
|
|
2981
|
+
nextRunAt,
|
|
2982
|
+
runCount,
|
|
2983
|
+
...exhausted ? {
|
|
2984
|
+
endedAt: now,
|
|
2985
|
+
endedReason: "limit"
|
|
2986
|
+
} : cleared
|
|
2987
|
+
}, now)),
|
|
2096
2988
|
applied: true
|
|
2097
2989
|
};
|
|
2098
2990
|
}
|
|
2099
2991
|
/**
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2102
|
-
*
|
|
2992
|
+
* Refund the one scheduled run an occurrence consumed before its launch failed
|
|
2993
|
+
* without ever creating a session. Nothing ran, so the run budget is restored
|
|
2994
|
+
* and the rule is left able to serve its next occurrence — a launch failure
|
|
2995
|
+
* must never silently spend the user's limited budget.
|
|
2996
|
+
*
|
|
2997
|
+
* A recurring rule keeps its already-rolled-forward target; a fire that ended
|
|
2998
|
+
* the rule at its cap (no target left) is restored and re-armed at the next
|
|
2999
|
+
* occurrence, so a failed launch consumes nothing. A one-shot has no later
|
|
3000
|
+
* occurrence, so it is left stopped with `launch-failed` and its run returned.
|
|
3001
|
+
* A rule the user disabled in the meantime is not resurrected.
|
|
3002
|
+
* @param tasks - current ledger.
|
|
3003
|
+
* @param id - the task whose occurrence is being refunded.
|
|
3004
|
+
* @param now - clock instant (ms epoch).
|
|
3005
|
+
* @param hostTimeZone - zone a rule with no stored zone follows.
|
|
3006
|
+
* @returns the next ledger.
|
|
3007
|
+
*/
|
|
3008
|
+
function applyScheduleRefund(tasks, id, now, hostTimeZone) {
|
|
3009
|
+
return tasks.map((task) => {
|
|
3010
|
+
if (task.id !== id || task.archivedAt !== void 0 || task.schedule === void 0) return task;
|
|
3011
|
+
const rule = task.schedule;
|
|
3012
|
+
const runCount = Math.max(rule.runCount - 1, 0);
|
|
3013
|
+
if (rule.mode === "once") return withSchedule(task, {
|
|
3014
|
+
enabled: false,
|
|
3015
|
+
runCount,
|
|
3016
|
+
nextRunAt: void 0,
|
|
3017
|
+
endedAt: now,
|
|
3018
|
+
endedReason: "launch-failed",
|
|
3019
|
+
skippedAt: void 0,
|
|
3020
|
+
skippedReason: void 0
|
|
3021
|
+
}, now);
|
|
3022
|
+
const refunded = withSchedule(task, {
|
|
3023
|
+
runCount,
|
|
3024
|
+
skippedAt: now,
|
|
3025
|
+
skippedReason: "launch-failed"
|
|
3026
|
+
}, now);
|
|
3027
|
+
if (rule.nextRunAt !== void 0 || rule.endedReason !== "limit") return refunded;
|
|
3028
|
+
const cron = rule.cron ?? "";
|
|
3029
|
+
const next = cron === "" ? void 0 : nextRunAtMs(cron, now, rule.timeZone ?? hostTimeZone);
|
|
3030
|
+
return withSchedule(refunded, next === void 0 ? {
|
|
3031
|
+
enabled: false,
|
|
3032
|
+
endedAt: now,
|
|
3033
|
+
endedReason: "no-target"
|
|
3034
|
+
} : {
|
|
3035
|
+
enabled: true,
|
|
3036
|
+
nextRunAt: next,
|
|
3037
|
+
endedAt: void 0,
|
|
3038
|
+
endedReason: void 0
|
|
3039
|
+
}, now);
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
/**
|
|
3043
|
+
* Record what a due occurrence did to a task's rule: advance the run counter
|
|
3044
|
+
* by the executions it opened, set the next target (or stop the rule), and
|
|
3045
|
+
* note a terminal reason or a skipped occurrence. Nothing is written for a
|
|
3046
|
+
* task that has no rule or has been archived mid-tick.
|
|
2103
3047
|
* @param tasks - current ledger.
|
|
2104
|
-
* @param id - the task
|
|
2105
|
-
* @param
|
|
2106
|
-
* @param lastTriggeredAt - the trigger instant of this run.
|
|
3048
|
+
* @param id - the task whose rule fired.
|
|
3049
|
+
* @param progress - the occurrence's effect.
|
|
2107
3050
|
* @param now - clock instant (ms epoch).
|
|
3051
|
+
* @returns the next ledger.
|
|
2108
3052
|
*/
|
|
2109
|
-
function
|
|
2110
|
-
return tasks.map((task) =>
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
3053
|
+
function applyScheduleProgress(tasks, id, progress, now) {
|
|
3054
|
+
return tasks.map((task) => {
|
|
3055
|
+
if (task.id !== id || task.archivedAt !== void 0 || task.schedule === void 0) return task;
|
|
3056
|
+
return withSchedule(task, {
|
|
3057
|
+
runCount: task.schedule.runCount + progress.consumed,
|
|
3058
|
+
nextRunAt: progress.nextRunAt,
|
|
3059
|
+
...progress.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: progress.lastTriggeredAt },
|
|
3060
|
+
...progress.endedReason === void 0 ? {} : {
|
|
3061
|
+
enabled: false,
|
|
3062
|
+
endedAt: now,
|
|
3063
|
+
endedReason: progress.endedReason
|
|
3064
|
+
},
|
|
3065
|
+
...progress.skippedReason === void 0 ? {} : {
|
|
3066
|
+
skippedAt: now,
|
|
3067
|
+
skippedReason: progress.skippedReason
|
|
3068
|
+
}
|
|
3069
|
+
}, now);
|
|
3070
|
+
});
|
|
2114
3071
|
}
|
|
2115
3072
|
//#endregion
|
|
2116
3073
|
//#region src/core/use-cases/task-parent.ts
|
|
@@ -2356,6 +3313,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
2356
3313
|
const mode = "mode" in patch ? normalizeTargetId(patch.mode) : void 0;
|
|
2357
3314
|
const permission = "permission" in patch ? normalizePermission(task.permission, patch.permission) : void 0;
|
|
2358
3315
|
const model = "model" in patch ? normalizeTargetId(patch.model) : void 0;
|
|
3316
|
+
const reasoningEffort = "reasoningEffort" in patch ? normalizeTargetId(patch.reasoningEffort) : void 0;
|
|
2359
3317
|
const next = {
|
|
2360
3318
|
...task,
|
|
2361
3319
|
...rest,
|
|
@@ -2375,27 +3333,31 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
2375
3333
|
if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
|
|
2376
3334
|
if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
|
|
2377
3335
|
if ("teamRun" in patch) next.teamRun = patch.teamRun === true ? true : void 0;
|
|
2378
|
-
if ("goalRun" in patch) next.goalRun = patch.goalRun ===
|
|
3336
|
+
if ("goalRun" in patch) next.goalRun = patch.goalRun === true ? true : void 0;
|
|
2379
3337
|
if ("skipVerification" in patch) next.skipVerification = patch.skipVerification === true ? true : void 0;
|
|
2380
3338
|
if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
|
|
2381
3339
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
2382
3340
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
2383
3341
|
if (model !== void 0 || "model" in patch) next.model = model;
|
|
3342
|
+
if (reasoningEffort !== void 0 || "reasoningEffort" in patch) next.reasoningEffort = reasoningEffort;
|
|
2384
3343
|
return next;
|
|
2385
3344
|
});
|
|
2386
3345
|
}
|
|
2387
3346
|
/**
|
|
2388
|
-
* Every older generation the loader migrates in place. v5 only
|
|
2389
|
-
* per-execution acceptance block
|
|
2390
|
-
*
|
|
2391
|
-
* losslessly.
|
|
3347
|
+
* Every older generation the loader migrates in place. v5 only added the
|
|
3348
|
+
* per-execution acceptance block and v6 only added the scheduling mode and run
|
|
3349
|
+
* budget, so the migration is the normalization pass that re-validates each
|
|
3350
|
+
* row; a document from any listed generation upgrades losslessly.
|
|
2392
3351
|
*/
|
|
2393
3352
|
const TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS = [
|
|
3353
|
+
5,
|
|
2394
3354
|
4,
|
|
2395
3355
|
3,
|
|
2396
3356
|
2
|
|
2397
3357
|
];
|
|
2398
3358
|
const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
3359
|
+
/** Longest accepted free-text summary on an external outcome record. */
|
|
3360
|
+
const EXTERNAL_SUMMARY_MAX_LENGTH = 1e3;
|
|
2399
3361
|
/** Strict parse of a `/parse` request body; undefined rejects the request. */
|
|
2400
3362
|
function parseTaskParseRequest(value) {
|
|
2401
3363
|
if (typeof value !== "object" || value === null) return void 0;
|
|
@@ -2433,15 +3395,39 @@ function hasForbiddenImportField(value) {
|
|
|
2433
3395
|
function optionalFiniteNumber(value) {
|
|
2434
3396
|
return value === void 0 || typeof value === "number" && Number.isFinite(value);
|
|
2435
3397
|
}
|
|
3398
|
+
/** Whether a stop-reason field names a reason this build understands. */
|
|
3399
|
+
function validStopReason(value) {
|
|
3400
|
+
return value === void 0 || typeof value === "string" && SCHEDULE_STOP_REASONS.includes(value);
|
|
3401
|
+
}
|
|
3402
|
+
/**
|
|
3403
|
+
* Whether an imported schedule rule is structurally usable. A one-shot must
|
|
3404
|
+
* carry its planned instant and a recurring rule its cron expression; the
|
|
3405
|
+
* counter and stop record are optional (a rule written before them simply has
|
|
3406
|
+
* none), but a value that IS present must be usable so the import cannot smuggle
|
|
3407
|
+
* a rule the scheduler would misread.
|
|
3408
|
+
*/
|
|
3409
|
+
function validImportedSchedule(value) {
|
|
3410
|
+
const schedule = record(value);
|
|
3411
|
+
if (schedule === void 0 || typeof schedule.enabled !== "boolean") return false;
|
|
3412
|
+
if (schedule.mode !== void 0 && schedule.mode !== "cron" && schedule.mode !== "once") return false;
|
|
3413
|
+
if (schedule.mode === "once") {
|
|
3414
|
+
if (typeof schedule.at !== "number" || !Number.isFinite(schedule.at)) return false;
|
|
3415
|
+
} else if (typeof schedule.cron !== "string") return false;
|
|
3416
|
+
if (schedule.cron !== void 0 && typeof schedule.cron !== "string") return false;
|
|
3417
|
+
if (schedule.at !== void 0 && (typeof schedule.at !== "number" || !Number.isFinite(schedule.at))) return false;
|
|
3418
|
+
if (schedule.maxRuns !== void 0 && (typeof schedule.maxRuns !== "number" || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false;
|
|
3419
|
+
if (schedule.runCount !== void 0 && (typeof schedule.runCount !== "number" || !Number.isInteger(schedule.runCount) || schedule.runCount < 0)) return false;
|
|
3420
|
+
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false;
|
|
3421
|
+
if (!optionalFiniteNumber(schedule.endedAt) || !optionalFiniteNumber(schedule.skippedAt)) return false;
|
|
3422
|
+
if (!validStopReason(schedule.endedReason) || !validStopReason(schedule.skippedReason)) return false;
|
|
3423
|
+
if (schedule.timeZone !== void 0 && (typeof schedule.timeZone !== "string" || !isValidTimeZone(schedule.timeZone))) return false;
|
|
3424
|
+
return true;
|
|
3425
|
+
}
|
|
2436
3426
|
function validImportedKnownFields(value) {
|
|
2437
3427
|
if (value.tags !== void 0 && !isTaskTagList(value.tags)) return false;
|
|
3428
|
+
if (value.model !== void 0 && typeof value.model !== "string") return false;
|
|
2438
3429
|
if (value.integrations !== void 0 && normalizeTaskIntegrations(value.integrations) === void 0) return false;
|
|
2439
|
-
if (value.schedule !== void 0)
|
|
2440
|
-
const schedule = record(value.schedule);
|
|
2441
|
-
if (schedule === void 0 || typeof schedule.enabled !== "boolean" || typeof schedule.cron !== "string") return false;
|
|
2442
|
-
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false;
|
|
2443
|
-
if (schedule.timeZone !== void 0 && (typeof schedule.timeZone !== "string" || !isValidTimeZone(schedule.timeZone))) return false;
|
|
2444
|
-
}
|
|
3430
|
+
if (value.schedule !== void 0 && !validImportedSchedule(value.schedule)) return false;
|
|
2445
3431
|
if (value.executions !== void 0) {
|
|
2446
3432
|
if (!Array.isArray(value.executions)) return false;
|
|
2447
3433
|
for (const item of value.executions) {
|
|
@@ -2455,6 +3441,7 @@ function validImportedKnownFields(value) {
|
|
|
2455
3441
|
"cancelled"
|
|
2456
3442
|
].includes(String(execution.result))) return false;
|
|
2457
3443
|
if (execution.initiatedBy !== void 0 && typeof execution.initiatedBy !== "string") return false;
|
|
3444
|
+
if (execution.external !== void 0 && typeof execution.external !== "boolean") return false;
|
|
2458
3445
|
if (execution.frozenBy !== void 0 && typeof execution.frozenBy !== "string") return false;
|
|
2459
3446
|
if (execution.frozenAt !== void 0 && typeof execution.frozenAt !== "number") return false;
|
|
2460
3447
|
if (execution.verification !== void 0 && normalizeVerification(execution.verification) === void 0) return false;
|
|
@@ -2483,20 +3470,17 @@ function importedTask(value) {
|
|
|
2483
3470
|
result: execution.result,
|
|
2484
3471
|
error: execution.error,
|
|
2485
3472
|
...execution.initiatedBy === void 0 ? {} : { initiatedBy: execution.initiatedBy },
|
|
3473
|
+
...execution.external === true ? { external: true } : {},
|
|
2486
3474
|
...execution.frozenAt === void 0 ? {} : { frozenAt: execution.frozenAt },
|
|
2487
3475
|
...execution.frozenBy === void 0 ? {} : { frozenBy: execution.frozenBy }
|
|
2488
3476
|
})),
|
|
2489
|
-
...task.schedule === void 0 ? {} : { schedule: {
|
|
2490
|
-
enabled: task.schedule.enabled,
|
|
2491
|
-
cron: task.schedule.cron,
|
|
2492
|
-
...task.schedule.timeZone === void 0 ? {} : { timeZone: task.schedule.timeZone },
|
|
2493
|
-
nextRunAt: task.schedule.nextRunAt,
|
|
2494
|
-
lastTriggeredAt: task.schedule.lastTriggeredAt
|
|
2495
|
-
} },
|
|
3477
|
+
...task.schedule === void 0 ? {} : { schedule: { ...task.schedule } },
|
|
2496
3478
|
...task.parentId === void 0 ? {} : { parentId: task.parentId },
|
|
2497
3479
|
...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
|
|
2498
3480
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
3481
|
+
...typeof task.model !== "string" || task.model.trim() === "" ? {} : { model: task.model },
|
|
2499
3482
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
3483
|
+
...task.reasoningEffort === void 0 ? {} : { reasoningEffort: task.reasoningEffort },
|
|
2500
3484
|
...task.reuseSession === void 0 ? {} : { reuseSession: task.reuseSession },
|
|
2501
3485
|
...task.goalRun === void 0 ? {} : { goalRun: task.goalRun },
|
|
2502
3486
|
...task.skipVerification === void 0 ? {} : { skipVerification: task.skipVerification },
|
|
@@ -2545,6 +3529,8 @@ function createInput(value) {
|
|
|
2545
3529
|
"freeze",
|
|
2546
3530
|
"handover",
|
|
2547
3531
|
"model",
|
|
3532
|
+
"reasoningEffort",
|
|
3533
|
+
"applyCardDefaults",
|
|
2548
3534
|
"reuseSession",
|
|
2549
3535
|
"teamRun",
|
|
2550
3536
|
"goalRun",
|
|
@@ -2554,7 +3540,8 @@ function createInput(value) {
|
|
|
2554
3540
|
])) return false;
|
|
2555
3541
|
if (input.parentId !== void 0 && (typeof input.parentId !== "string" || input.parentId.trim() === "")) return false;
|
|
2556
3542
|
if (typeof input.title !== "string" || typeof input.description !== "string" || typeof input.prompt !== "string") return false;
|
|
2557
|
-
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false;
|
|
3543
|
+
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model) || !optionalString(input.reasoningEffort)) return false;
|
|
3544
|
+
if (input.applyCardDefaults !== void 0 && typeof input.applyCardDefaults !== "boolean") return false;
|
|
2558
3545
|
if (input.reuseSession !== void 0 && typeof input.reuseSession !== "boolean") return false;
|
|
2559
3546
|
if (input.teamRun !== void 0 && typeof input.teamRun !== "boolean") return false;
|
|
2560
3547
|
if (input.goalRun !== void 0 && typeof input.goalRun !== "boolean") return false;
|
|
@@ -2568,10 +3555,17 @@ function createInput(value) {
|
|
|
2568
3555
|
const schedule = record(input.schedule);
|
|
2569
3556
|
if (schedule === void 0 || !exactKeys(schedule, [
|
|
2570
3557
|
"enabled",
|
|
3558
|
+
"mode",
|
|
2571
3559
|
"cron",
|
|
2572
|
-
"
|
|
3560
|
+
"at",
|
|
3561
|
+
"timeZone",
|
|
3562
|
+
"maxRuns"
|
|
2573
3563
|
])) return false;
|
|
2574
|
-
if (typeof schedule.enabled !== "boolean"
|
|
3564
|
+
if (typeof schedule.enabled !== "boolean") return false;
|
|
3565
|
+
if (schedule.mode !== void 0 && schedule.mode !== "cron" && schedule.mode !== "once") return false;
|
|
3566
|
+
if (schedule.cron !== void 0 && typeof schedule.cron !== "string") return false;
|
|
3567
|
+
if (schedule.at !== void 0 && (typeof schedule.at !== "number" || !Number.isInteger(schedule.at))) return false;
|
|
3568
|
+
if (schedule.maxRuns !== void 0 && (typeof schedule.maxRuns !== "number" || !Number.isInteger(schedule.maxRuns) || schedule.maxRuns < 1)) return false;
|
|
2575
3569
|
if (schedule.timeZone !== void 0 && (typeof schedule.timeZone !== "string" || !isValidTimeZone(schedule.timeZone))) return false;
|
|
2576
3570
|
}
|
|
2577
3571
|
return true;
|
|
@@ -2588,6 +3582,7 @@ function updatePatch(value) {
|
|
|
2588
3582
|
"freeze",
|
|
2589
3583
|
"handover",
|
|
2590
3584
|
"model",
|
|
3585
|
+
"reasoningEffort",
|
|
2591
3586
|
"reuseSession",
|
|
2592
3587
|
"teamRun",
|
|
2593
3588
|
"goalRun",
|
|
@@ -2604,7 +3599,8 @@ function updatePatch(value) {
|
|
|
2604
3599
|
"prompt",
|
|
2605
3600
|
"workspaceId",
|
|
2606
3601
|
"mode",
|
|
2607
|
-
"model"
|
|
3602
|
+
"model",
|
|
3603
|
+
"reasoningEffort"
|
|
2608
3604
|
]) if (!optionalString(patch[key])) return false;
|
|
2609
3605
|
if (patch.permission !== void 0 && !isTaskPermission(patch.permission)) return false;
|
|
2610
3606
|
if (patch.tags !== void 0 && patch.tags !== null && !isTaskTagList(patch.tags)) return false;
|
|
@@ -2623,9 +3619,12 @@ function schedulePatch(value) {
|
|
|
2623
3619
|
const patch = record(value);
|
|
2624
3620
|
return patch !== void 0 && exactKeys(patch, [
|
|
2625
3621
|
"enabled",
|
|
3622
|
+
"mode",
|
|
2626
3623
|
"cron",
|
|
2627
|
-
"
|
|
2628
|
-
|
|
3624
|
+
"at",
|
|
3625
|
+
"timeZone",
|
|
3626
|
+
"maxRuns"
|
|
3627
|
+
]) && (patch.enabled === void 0 || typeof patch.enabled === "boolean") && (patch.mode === void 0 || patch.mode === "cron" || patch.mode === "once") && (patch.cron === void 0 || typeof patch.cron === "string") && (patch.at === void 0 || typeof patch.at === "number" && Number.isInteger(patch.at)) && (patch.timeZone === void 0 || patch.timeZone === null || typeof patch.timeZone === "string" && isValidTimeZone(patch.timeZone)) && (patch.maxRuns === void 0 || patch.maxRuns === null || typeof patch.maxRuns === "number" && Number.isInteger(patch.maxRuns) && patch.maxRuns >= 1);
|
|
2629
3628
|
}
|
|
2630
3629
|
function parseActionEnvelope(value) {
|
|
2631
3630
|
const parsed = parseEnvelopeAction(value);
|
|
@@ -2804,11 +3803,38 @@ function parseEnvelopeAction(value) {
|
|
|
2804
3803
|
...action.payload === void 0 ? {} : { payload: action.payload }
|
|
2805
3804
|
}
|
|
2806
3805
|
};
|
|
3806
|
+
case "record-external-outcome": {
|
|
3807
|
+
if (!exactKeys(action, [
|
|
3808
|
+
"kind",
|
|
3809
|
+
"taskId",
|
|
3810
|
+
"result",
|
|
3811
|
+
"initiatedBy",
|
|
3812
|
+
"summary"
|
|
3813
|
+
])) return void 0;
|
|
3814
|
+
if (taskId === void 0) return void 0;
|
|
3815
|
+
if (action.result !== "succeeded" && action.result !== "failed") return void 0;
|
|
3816
|
+
if (typeof action.initiatedBy !== "string") return void 0;
|
|
3817
|
+
const initiatedBy = action.initiatedBy.trim();
|
|
3818
|
+
if (initiatedBy === "" || initiatedBy.length > 200) return void 0;
|
|
3819
|
+
if (action.summary !== void 0 && typeof action.summary !== "string") return void 0;
|
|
3820
|
+
const summary = typeof action.summary === "string" ? action.summary.trim().slice(0, EXTERNAL_SUMMARY_MAX_LENGTH) : "";
|
|
3821
|
+
return {
|
|
3822
|
+
requestId: envelope.requestId,
|
|
3823
|
+
action: {
|
|
3824
|
+
kind: "record-external-outcome",
|
|
3825
|
+
taskId,
|
|
3826
|
+
result: action.result,
|
|
3827
|
+
initiatedBy,
|
|
3828
|
+
...summary === "" ? {} : { summary }
|
|
3829
|
+
}
|
|
3830
|
+
};
|
|
3831
|
+
}
|
|
2807
3832
|
case "confirm-permission":
|
|
2808
3833
|
case "delete":
|
|
2809
3834
|
case "archive":
|
|
2810
3835
|
case "restore":
|
|
2811
3836
|
case "settle":
|
|
3837
|
+
case "reset-verification":
|
|
2812
3838
|
case "run":
|
|
2813
3839
|
case "rerun":
|
|
2814
3840
|
if (!exactKeys(action, ["kind", "taskId"])) return void 0;
|
|
@@ -2836,8 +3862,25 @@ function timeZone() {
|
|
|
2836
3862
|
function scheduleZone(schedule) {
|
|
2837
3863
|
return schedule.timeZone ?? timeZone();
|
|
2838
3864
|
}
|
|
2839
|
-
|
|
2840
|
-
|
|
3865
|
+
/**
|
|
3866
|
+
* The task collection one board read hands out.
|
|
3867
|
+
*
|
|
3868
|
+
* A read must not hand a consumer the ledger's own array: the state route, every
|
|
3869
|
+
* action response and the agent tools all receive this value in-process, so an
|
|
3870
|
+
* in-place rewrite of that array (splice, sort, push, `length = 0`) would edit
|
|
3871
|
+
* the authoritative document from outside the write path. One level of copying
|
|
3872
|
+
* is what that contract needs, and it is all it needs: every write in this class
|
|
3873
|
+
* replaces the document's array (`this.document.tasks = ...`) and replaces the
|
|
3874
|
+
* records it changes rather than mutating them, so the records a caller receives
|
|
3875
|
+
* are stable values. Copying them as well only rebuilt the entire document on
|
|
3876
|
+
* every board read - measured at 0.43 ms per read for 400 cards with 2
|
|
3877
|
+
* executions each and 2.72 ms for 400 cards with 20 - and bought nothing a
|
|
3878
|
+
* consumer relies on. Callers still must not mutate a record in place; that is
|
|
3879
|
+
* what the shared record objects now make possible, and no consumer in this
|
|
3880
|
+
* package does it.
|
|
3881
|
+
*/
|
|
3882
|
+
function tasksForRead(tasks) {
|
|
3883
|
+
return [...tasks];
|
|
2841
3884
|
}
|
|
2842
3885
|
/**
|
|
2843
3886
|
* Process states that are dead but still occupy the PID table: `Z` (zombie)
|
|
@@ -3103,15 +4146,18 @@ function parseHostTasks(values) {
|
|
|
3103
4146
|
const rawSchedule = rawById.get(task.id)?.schedule;
|
|
3104
4147
|
if (typeof rawSchedule !== "object" || rawSchedule === null) return task;
|
|
3105
4148
|
const schedule = rawSchedule;
|
|
4149
|
+
if (schedule.mode === "once") return task;
|
|
3106
4150
|
if (typeof schedule.cron !== "string" || isValidCron(schedule.cron)) return task;
|
|
3107
4151
|
return {
|
|
3108
4152
|
...task,
|
|
3109
4153
|
schedule: {
|
|
3110
4154
|
enabled: false,
|
|
4155
|
+
mode: "cron",
|
|
3111
4156
|
cron: schedule.cron,
|
|
3112
4157
|
...typeof schedule.timeZone === "string" && isValidTimeZone(schedule.timeZone) ? { timeZone: schedule.timeZone } : {},
|
|
3113
4158
|
nextRunAt: void 0,
|
|
3114
|
-
lastTriggeredAt: typeof schedule.lastTriggeredAt === "number" && Number.isFinite(schedule.lastTriggeredAt) ? schedule.lastTriggeredAt : void 0
|
|
4159
|
+
lastTriggeredAt: typeof schedule.lastTriggeredAt === "number" && Number.isFinite(schedule.lastTriggeredAt) ? schedule.lastTriggeredAt : void 0,
|
|
4160
|
+
runCount: typeof schedule.runCount === "number" && Number.isInteger(schedule.runCount) && schedule.runCount >= 0 ? schedule.runCount : 0
|
|
3115
4161
|
}
|
|
3116
4162
|
};
|
|
3117
4163
|
});
|
|
@@ -3143,11 +4189,14 @@ var HostTaskLedger = class {
|
|
|
3143
4189
|
* instead of remounting the row.
|
|
3144
4190
|
*/
|
|
3145
4191
|
depthLimit;
|
|
4192
|
+
/** Live reader of the deployment's new-card defaults (a volatile config read). */
|
|
4193
|
+
cardDefaults;
|
|
3146
4194
|
constructor(dir = join(dshHome(), "task-board"), now = Date.now, options = {}) {
|
|
3147
4195
|
this.now = now;
|
|
3148
4196
|
const baseline = options.sessionDefaultPermission;
|
|
3149
4197
|
this.sessionDefault = typeof baseline === "function" ? baseline : () => baseline ?? "read-only";
|
|
3150
4198
|
this.depthLimit = normalizeSubtaskDepth(options.maxSubtaskDepth ?? 1);
|
|
4199
|
+
this.cardDefaults = options.cardDefaults ?? (() => ({}));
|
|
3151
4200
|
mkdirSync(dir, { recursive: true });
|
|
3152
4201
|
this.file = join(dir, "ledger-v2.json");
|
|
3153
4202
|
this.lockFile = join(dir, "ledger-v2.lock");
|
|
@@ -3202,27 +4251,31 @@ var HostTaskLedger = class {
|
|
|
3202
4251
|
const { revision, scheduler } = this.summary();
|
|
3203
4252
|
return {
|
|
3204
4253
|
revision,
|
|
3205
|
-
tasks:
|
|
4254
|
+
tasks: tasksForRead(this.document.tasks),
|
|
3206
4255
|
scheduler
|
|
3207
4256
|
};
|
|
3208
4257
|
}
|
|
3209
4258
|
/**
|
|
3210
|
-
* Runtime-only projection for the
|
|
4259
|
+
* Runtime-only projection for the Host poll. It copies just primitive
|
|
3211
4260
|
* identifiers and timestamps, never the complete task/execution history or
|
|
3212
4261
|
* an authoritative mutable object from the ledger.
|
|
3213
4262
|
*/
|
|
3214
4263
|
runtimeView() {
|
|
3215
4264
|
let armedSchedules = 0;
|
|
4265
|
+
let runningCards = 0;
|
|
3216
4266
|
const teamGroups = /* @__PURE__ */ new Set();
|
|
3217
4267
|
for (const task of this.document.tasks) {
|
|
3218
4268
|
if (task.teamRun !== true) continue;
|
|
3219
4269
|
for (const execution of task.executions) if (execution.runGroupId !== void 0) teamGroups.add(execution.runGroupId);
|
|
3220
4270
|
}
|
|
3221
4271
|
const openExecutions = [];
|
|
4272
|
+
const openSessionIds = [];
|
|
3222
4273
|
for (const task of this.document.tasks) {
|
|
3223
4274
|
if (task.archivedAt === void 0 && task.schedule?.enabled === true) armedSchedules += 1;
|
|
4275
|
+
if (task.status === "running") runningCards += 1;
|
|
3224
4276
|
for (const execution of task.executions) {
|
|
3225
4277
|
if (execution.endedAt !== void 0) continue;
|
|
4278
|
+
if (execution.sessionId !== void 0) openSessionIds.push(execution.sessionId);
|
|
3226
4279
|
if (execution.ownResult !== void 0) continue;
|
|
3227
4280
|
openExecutions.push({
|
|
3228
4281
|
taskId: task.id,
|
|
@@ -3235,7 +4288,9 @@ var HostTaskLedger = class {
|
|
|
3235
4288
|
}
|
|
3236
4289
|
return {
|
|
3237
4290
|
armedSchedules,
|
|
3238
|
-
openExecutions
|
|
4291
|
+
openExecutions,
|
|
4292
|
+
openSessionIds,
|
|
4293
|
+
needsSessionState: openExecutions.length > 0 || runningCards > 0
|
|
3239
4294
|
};
|
|
3240
4295
|
}
|
|
3241
4296
|
/** Count armed, non-archived schedules without cloning task histories. */
|
|
@@ -3272,7 +4327,8 @@ var HostTaskLedger = class {
|
|
|
3272
4327
|
if (schedule === void 0 || !schedule.enabled || schedule.nextRunAt === void 0 || schedule.nextRunAt > now) continue;
|
|
3273
4328
|
due.push({
|
|
3274
4329
|
taskId: task.id,
|
|
3275
|
-
|
|
4330
|
+
mode: schedule.mode,
|
|
4331
|
+
...schedule.cron === void 0 ? {} : { cron: schedule.cron },
|
|
3276
4332
|
timeZone: scheduleZone(schedule),
|
|
3277
4333
|
nextRunAt: schedule.nextRunAt
|
|
3278
4334
|
});
|
|
@@ -3315,41 +4371,144 @@ var HostTaskLedger = class {
|
|
|
3315
4371
|
/**
|
|
3316
4372
|
* Open the cascade one due schedule triggers: an empty array means nothing
|
|
3317
4373
|
* ran (already running, or a participant whose elevated permission is still
|
|
3318
|
-
* unconfirmed
|
|
4374
|
+
* unconfirmed, a one-shot whose instant was missed, or an exhausted budget),
|
|
4375
|
+
* and the rule's own state says which.
|
|
4376
|
+
*
|
|
4377
|
+
* The run budget is enforced HERE, inside the single ledger writer: a rule
|
|
4378
|
+
* whose counter already reached its cap is stopped instead of opened, so no
|
|
4379
|
+
* restart, repeated timer fire or re-arm can overrun it.
|
|
4380
|
+
* @param taskId - the task whose rule came due.
|
|
4381
|
+
* @param triggeredAt - the instant the occurrence came due.
|
|
4382
|
+
* @returns the executions this occurrence opened.
|
|
3319
4383
|
*/
|
|
3320
|
-
openScheduled(taskId,
|
|
4384
|
+
openScheduled(taskId, triggeredAt) {
|
|
3321
4385
|
const task = this.document.tasks.find((item) => item.id === taskId);
|
|
3322
4386
|
if (task === void 0 || task.archivedAt !== void 0) return [];
|
|
3323
|
-
const
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
4387
|
+
const schedule = task.schedule;
|
|
4388
|
+
if (schedule === void 0 || !schedule.enabled) return [];
|
|
4389
|
+
if (scheduleExhausted(schedule)) {
|
|
4390
|
+
this.progressSchedule(taskId, {
|
|
4391
|
+
consumed: 0,
|
|
4392
|
+
endedReason: schedule.mode === "once" ? "fired" : "limit"
|
|
4393
|
+
}, triggeredAt);
|
|
4394
|
+
return [];
|
|
4395
|
+
}
|
|
3327
4396
|
const refusal = this.bindingRefusal(task);
|
|
3328
4397
|
if (refusal !== void 0) {
|
|
3329
4398
|
this.document.scheduler.error = `scheduled run refused for task ${taskId}: ${bindingRefusalMessage(refusal, this.sessionDefaultPermission, "schedule")}`;
|
|
3330
|
-
|
|
4399
|
+
this.skipOccurrence(taskId, "permission", triggeredAt);
|
|
3331
4400
|
return [];
|
|
3332
4401
|
}
|
|
3333
4402
|
if (hasOpenExecution(task)) {
|
|
3334
|
-
|
|
4403
|
+
this.skipOccurrence(taskId, "busy", triggeredAt);
|
|
3335
4404
|
return [];
|
|
3336
4405
|
}
|
|
3337
|
-
return this.startCascade(task, triggeredAt, void 0, false,
|
|
4406
|
+
if (schedule.mode === "once") return this.startCascade(task, triggeredAt, void 0, false, {
|
|
4407
|
+
consumed: 1,
|
|
4408
|
+
lastTriggeredAt: triggeredAt,
|
|
4409
|
+
endedReason: "fired"
|
|
4410
|
+
});
|
|
4411
|
+
const next = nextRunAtMs(schedule.cron ?? "", schedule.nextRunAt ?? triggeredAt, scheduleZone(schedule));
|
|
4412
|
+
if (next === void 0) {
|
|
4413
|
+
this.progressSchedule(taskId, {
|
|
4414
|
+
consumed: 0,
|
|
4415
|
+
endedReason: "no-target"
|
|
4416
|
+
}, triggeredAt);
|
|
4417
|
+
return [];
|
|
4418
|
+
}
|
|
4419
|
+
const consumed = schedule.runCount + 1;
|
|
4420
|
+
const budget = scheduleRunBudget(schedule);
|
|
4421
|
+
const spent = budget !== void 0 && consumed >= budget;
|
|
4422
|
+
return this.startCascade(task, triggeredAt, void 0, false, {
|
|
4423
|
+
consumed: 1,
|
|
4424
|
+
nextRunAt: spent ? void 0 : next,
|
|
4425
|
+
lastTriggeredAt: triggeredAt,
|
|
4426
|
+
...spent ? { endedReason: "limit" } : {}
|
|
4427
|
+
});
|
|
4428
|
+
}
|
|
4429
|
+
/**
|
|
4430
|
+
* Record a due occurrence that opened no execution: roll a recurring rule to
|
|
4431
|
+
* its next occurrence (budget untouched), or stop it when there is none. A
|
|
4432
|
+
* one-shot has no second occurrence, so it always stops here with the reason.
|
|
4433
|
+
*/
|
|
4434
|
+
skipOccurrence(taskId, reason, now) {
|
|
4435
|
+
const schedule = this.document.tasks.find((item) => item.id === taskId)?.schedule;
|
|
4436
|
+
if (schedule === void 0) return;
|
|
4437
|
+
if (schedule.mode === "once") {
|
|
4438
|
+
this.progressSchedule(taskId, {
|
|
4439
|
+
consumed: 0,
|
|
4440
|
+
endedReason: reason
|
|
4441
|
+
}, now);
|
|
4442
|
+
return;
|
|
4443
|
+
}
|
|
4444
|
+
const next = nextRunAtMs(schedule.cron ?? "", schedule.nextRunAt ?? now, scheduleZone(schedule));
|
|
4445
|
+
if (next === void 0) this.progressSchedule(taskId, {
|
|
4446
|
+
consumed: 0,
|
|
4447
|
+
endedReason: "no-target"
|
|
4448
|
+
}, now);
|
|
4449
|
+
else this.progressSchedule(taskId, {
|
|
4450
|
+
consumed: 0,
|
|
4451
|
+
nextRunAt: next,
|
|
4452
|
+
skippedReason: reason
|
|
4453
|
+
}, now);
|
|
4454
|
+
}
|
|
4455
|
+
/** Apply a schedule-occurrence transition and persist it atomically. */
|
|
4456
|
+
progressSchedule(taskId, progress, now) {
|
|
4457
|
+
this.document.tasks = [...applyScheduleProgress(this.document.tasks, taskId, progress, now)];
|
|
4458
|
+
this.commit();
|
|
3338
4459
|
}
|
|
4460
|
+
/**
|
|
4461
|
+
* Refund the one scheduled run an occurrence consumed before its launch
|
|
4462
|
+
* failed without creating a session (see
|
|
4463
|
+
* {@link applyScheduleRefund}). Called by the service on the failed-launch
|
|
4464
|
+
* path only; a manual run and a launch that did reach a session are never
|
|
4465
|
+
* refunded. A no-op for a task with no rule, so a failed cascade member does
|
|
4466
|
+
* not touch anything.
|
|
4467
|
+
* @param taskId - the task whose occurrence is being refunded.
|
|
4468
|
+
* @param now - clock instant (ms epoch).
|
|
4469
|
+
*/
|
|
4470
|
+
refundScheduledOccurrence(taskId, now) {
|
|
4471
|
+
const before = this.document.tasks.find((item) => item.id === taskId)?.schedule;
|
|
4472
|
+
if (before === void 0) return;
|
|
4473
|
+
const next = applyScheduleRefund(this.document.tasks, taskId, now, timeZone());
|
|
4474
|
+
const after = next.find((item) => item.id === taskId)?.schedule;
|
|
4475
|
+
if (JSON.stringify(before) === JSON.stringify(after)) return;
|
|
4476
|
+
this.document.tasks = [...next];
|
|
4477
|
+
this.commit();
|
|
4478
|
+
}
|
|
4479
|
+
/**
|
|
4480
|
+
* Boot / resume recovery: skip every occurrence that came due while the board
|
|
4481
|
+
* was not running through it. A recurring rule rolls to its next future
|
|
4482
|
+
* target; a one-shot whose instant has passed is stopped as missed, because
|
|
4483
|
+
* there is no later occurrence left to run. Rendering the missed occurrence
|
|
4484
|
+
* is deliberately not attempted: the ACL of a card that fired hours ago is
|
|
4485
|
+
* stale, and the board's own recovery contract is "missed triggers are
|
|
4486
|
+
* skipped, never replayed".
|
|
4487
|
+
*/
|
|
3339
4488
|
skipMissed(now) {
|
|
3340
4489
|
let changed = false;
|
|
3341
4490
|
this.document.tasks = this.document.tasks.map((task) => {
|
|
3342
4491
|
const schedule = task.schedule;
|
|
3343
4492
|
if (schedule === void 0 || !schedule.enabled || schedule.nextRunAt === void 0 || schedule.nextRunAt > now) return task;
|
|
3344
4493
|
changed = true;
|
|
3345
|
-
return {
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3351
|
-
|
|
3352
|
-
|
|
4494
|
+
if (schedule.mode === "once") return withSchedule(task, {
|
|
4495
|
+
enabled: false,
|
|
4496
|
+
nextRunAt: void 0,
|
|
4497
|
+
endedAt: now,
|
|
4498
|
+
endedReason: "missed"
|
|
4499
|
+
}, now);
|
|
4500
|
+
const next = nextRunAtMs(schedule.cron ?? "", now, scheduleZone(schedule));
|
|
4501
|
+
if (next === void 0) return withSchedule(task, {
|
|
4502
|
+
enabled: false,
|
|
4503
|
+
nextRunAt: void 0,
|
|
4504
|
+
endedAt: now,
|
|
4505
|
+
endedReason: "no-target"
|
|
4506
|
+
}, now);
|
|
4507
|
+
return withSchedule(task, {
|
|
4508
|
+
nextRunAt: next,
|
|
4509
|
+
skippedAt: now,
|
|
4510
|
+
skippedReason: "missed"
|
|
4511
|
+
}, now);
|
|
3353
4512
|
});
|
|
3354
4513
|
if (changed) this.commit();
|
|
3355
4514
|
}
|
|
@@ -3434,7 +4593,7 @@ var HostTaskLedger = class {
|
|
|
3434
4593
|
case "import": {
|
|
3435
4594
|
const sources = new Set(this.document.scheduler.importedSources ?? []);
|
|
3436
4595
|
if (sources.has(action.sourceId)) return { state: this.state() };
|
|
3437
|
-
const invalidScheduleIds = action.tasks.filter((task) => task.schedule !== void 0 && !isValidCron(task.schedule.cron)).map((task) => task.id);
|
|
4596
|
+
const invalidScheduleIds = action.tasks.filter((task) => task.schedule !== void 0 && task.schedule.mode !== "once" && !isValidCron(task.schedule.cron ?? "")).map((task) => task.id);
|
|
3438
4597
|
const incoming = parseHostTasks(action.tasks);
|
|
3439
4598
|
const merged = new Map(this.document.tasks.map((task) => [task.id, task]));
|
|
3440
4599
|
for (const task of incoming) merged.set(task.id, merged.has(task.id) ? mergeTask(merged.get(task.id), task) : task);
|
|
@@ -3448,7 +4607,8 @@ var HostTaskLedger = class {
|
|
|
3448
4607
|
}
|
|
3449
4608
|
case "create": {
|
|
3450
4609
|
if (this.document.tasks.some((task) => task.id === action.id)) throw new Error("task id already exists");
|
|
3451
|
-
|
|
4610
|
+
const requested = action.input.schedule;
|
|
4611
|
+
if (requested?.enabled === true && !this.validScheduleRequest(requested, now)) throw new Error("invalid schedule");
|
|
3452
4612
|
const input = action.input.freeze === void 0 || initiator === void 0 || initiator === "" ? action.input : {
|
|
3453
4613
|
...action.input,
|
|
3454
4614
|
freeze: {
|
|
@@ -3456,7 +4616,7 @@ var HostTaskLedger = class {
|
|
|
3456
4616
|
frozenBy: initiator
|
|
3457
4617
|
}
|
|
3458
4618
|
};
|
|
3459
|
-
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth, timeZone());
|
|
4619
|
+
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth, timeZone(), this.cardDefaults());
|
|
3460
4620
|
if (result.task === void 0) throw new Error(result.error ?? "invalid task");
|
|
3461
4621
|
this.document.tasks = [...result.tasks];
|
|
3462
4622
|
break;
|
|
@@ -3507,6 +4667,29 @@ var HostTaskLedger = class {
|
|
|
3507
4667
|
this.document.tasks = [...result.tasks];
|
|
3508
4668
|
break;
|
|
3509
4669
|
}
|
|
4670
|
+
case "record-external-outcome": {
|
|
4671
|
+
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
4672
|
+
if (task === void 0) throw new Error("task not found");
|
|
4673
|
+
if (task.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
4674
|
+
if (hasOpenExecution(task)) throw new Error("running task cannot receive an external outcome");
|
|
4675
|
+
const execution = {
|
|
4676
|
+
id: crypto.randomUUID(),
|
|
4677
|
+
sessionId: void 0,
|
|
4678
|
+
startedAt: now,
|
|
4679
|
+
endedAt: now,
|
|
4680
|
+
result: action.result,
|
|
4681
|
+
error: action.summary,
|
|
4682
|
+
initiatedBy: action.initiatedBy,
|
|
4683
|
+
external: true
|
|
4684
|
+
};
|
|
4685
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== action.taskId ? item : {
|
|
4686
|
+
...item,
|
|
4687
|
+
status: settledStatus(item, action.result),
|
|
4688
|
+
updatedAt: now,
|
|
4689
|
+
executions: retainRecentExecutions([...item.executions, execution])
|
|
4690
|
+
});
|
|
4691
|
+
break;
|
|
4692
|
+
}
|
|
3510
4693
|
case "settle": {
|
|
3511
4694
|
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
3512
4695
|
if (task === void 0) throw new Error("task not found");
|
|
@@ -3520,6 +4703,26 @@ var HostTaskLedger = class {
|
|
|
3520
4703
|
this.finalizeReadyRuns(false);
|
|
3521
4704
|
break;
|
|
3522
4705
|
}
|
|
4706
|
+
case "reset-verification": {
|
|
4707
|
+
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
4708
|
+
if (task === void 0) throw new Error("task not found");
|
|
4709
|
+
if (task.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
4710
|
+
const execution = [...task.executions].reverse().find((entry) => entry.endedAt === void 0);
|
|
4711
|
+
if (execution === void 0) throw new Error("task has no open execution");
|
|
4712
|
+
const verification = execution.verification;
|
|
4713
|
+
if (verification === void 0 || verification.applicability !== "enforced") throw new Error("this execution is not gated by task acceptance");
|
|
4714
|
+
const cleared = withoutAcceptanceAnomalies(verification);
|
|
4715
|
+
if (cleared === void 0) throw new Error("this execution has no acceptance anomaly to reset");
|
|
4716
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== action.taskId ? item : {
|
|
4717
|
+
...item,
|
|
4718
|
+
updatedAt: now,
|
|
4719
|
+
executions: item.executions.map((entry) => entry.id === execution.id ? {
|
|
4720
|
+
...entry,
|
|
4721
|
+
verification: cleared
|
|
4722
|
+
} : entry)
|
|
4723
|
+
});
|
|
4724
|
+
break;
|
|
4725
|
+
}
|
|
3523
4726
|
case "restore": {
|
|
3524
4727
|
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be restored");
|
|
3525
4728
|
const result = applyRestoreTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
@@ -3575,6 +4778,21 @@ var HostTaskLedger = class {
|
|
|
3575
4778
|
};
|
|
3576
4779
|
}
|
|
3577
4780
|
/**
|
|
4781
|
+
* Whether a creation-time schedule request is usable. A one-shot must carry
|
|
4782
|
+
* a whole-millisecond instant in the future; a recurring rule must carry a
|
|
4783
|
+
* valid expression with a reachable occurrence and, when capped, a positive
|
|
4784
|
+
* whole run budget. An unusable zone is refused rather than reinterpreted as
|
|
4785
|
+
* the Host zone.
|
|
4786
|
+
*/
|
|
4787
|
+
validScheduleRequest(request, now) {
|
|
4788
|
+
if (request.timeZone !== void 0 && !isValidTimeZone(request.timeZone)) return false;
|
|
4789
|
+
if (request.mode === "once") return typeof request.at === "number" && Number.isInteger(request.at) && request.at > now;
|
|
4790
|
+
const cron = (request.cron ?? "").trim();
|
|
4791
|
+
if (cron === "" || !isValidCron(cron)) return false;
|
|
4792
|
+
if (request.maxRuns !== void 0 && (!Number.isInteger(request.maxRuns) || request.maxRuns < 1)) return false;
|
|
4793
|
+
return nextRunAtMs(cron, now, request.timeZone ?? timeZone()) !== void 0;
|
|
4794
|
+
}
|
|
4795
|
+
/**
|
|
3578
4796
|
* The binding that makes a run illegal, if any.
|
|
3579
4797
|
*
|
|
3580
4798
|
* A plain cascade launches one session per participant, so every participant
|
|
@@ -3628,17 +4846,20 @@ var HostTaskLedger = class {
|
|
|
3628
4846
|
return cascadeTargets(this.document.tasks, id, this.maxSubtaskDepth).some((task) => hasOpenExecution(task));
|
|
3629
4847
|
}
|
|
3630
4848
|
/**
|
|
3631
|
-
* Open one execution per cascade participant
|
|
3632
|
-
* on-board descendants, within the depth limit) under
|
|
3633
|
-
* commit the ledger once.
|
|
3634
|
-
*
|
|
4849
|
+
* Open one execution per cascade participant that still has work (the
|
|
4850
|
+
* requested task and its on-board descendants, within the depth limit) under
|
|
4851
|
+
* a single run group, and commit the ledger once. A descendant in 'done' is
|
|
4852
|
+
* not a participant ({@link joinsCascadeRun}). The returned runs are ordered
|
|
4853
|
+
* root-first, so the Host launches the parent before its subtasks.
|
|
3635
4854
|
* @param root - the task the user ran.
|
|
3636
4855
|
* @param now - clock instant (ms epoch).
|
|
3637
4856
|
* @param initiator - the DSH session that asked for the run (audit only).
|
|
3638
4857
|
* @param rerun - true to reset the root to 'todo' before starting it.
|
|
3639
|
-
* @param
|
|
4858
|
+
* @param progress - when set, this run was a due occurrence and the root's
|
|
4859
|
+
* rule is advanced (counter, next target, stop/skip record) in the same
|
|
4860
|
+
* atomic commit that opened the executions.
|
|
3640
4861
|
*/
|
|
3641
|
-
startCascade(root, now, initiator, rerun = false,
|
|
4862
|
+
startCascade(root, now, initiator, rerun = false, progress) {
|
|
3642
4863
|
const before = this.document.tasks;
|
|
3643
4864
|
const groupId = crypto.randomUUID();
|
|
3644
4865
|
const team = root.teamRun === true;
|
|
@@ -3646,6 +4867,7 @@ var HostTaskLedger = class {
|
|
|
3646
4867
|
const runs = [];
|
|
3647
4868
|
for (const task of cascadeTargets(before, root.id, this.maxSubtaskDepth)) {
|
|
3648
4869
|
if (task.archivedAt !== void 0) continue;
|
|
4870
|
+
if (!joinsCascadeRun(task, root.id)) continue;
|
|
3649
4871
|
if (hasOpenExecution(task)) continue;
|
|
3650
4872
|
const opened = startExecution(rerun && task.id === root.id ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator, groupId);
|
|
3651
4873
|
started.set(task.id, opened.task);
|
|
@@ -3657,7 +4879,7 @@ var HostTaskLedger = class {
|
|
|
3657
4879
|
}
|
|
3658
4880
|
if (runs.length === 0) return [];
|
|
3659
4881
|
let tasks = before.map((task) => started.get(task.id) ?? task);
|
|
3660
|
-
if (
|
|
4882
|
+
if (progress !== void 0) tasks = applyScheduleProgress(tasks, root.id, progress, now);
|
|
3661
4883
|
this.document.tasks = [...tasks];
|
|
3662
4884
|
this.commit();
|
|
3663
4885
|
return runs;
|
|
@@ -3779,6 +5001,14 @@ var HostTaskLedger = class {
|
|
|
3779
5001
|
for (const memberId of members) if (this.settleCascade(memberId, groupId, now)) changed = true;
|
|
3780
5002
|
return changed;
|
|
3781
5003
|
}
|
|
5004
|
+
/**
|
|
5005
|
+
* Re-align every armed rule at load/import. A recurring rule is recomputed
|
|
5006
|
+
* from the current instant; a one-shot is armed at its planned instant when
|
|
5007
|
+
* that is still in the future, and stopped as missed once it has passed
|
|
5008
|
+
* (there is no later occurrence to run). A recurring rule recomputed past an
|
|
5009
|
+
* occurrence records the skip, so the board can show that a fire was passed
|
|
5010
|
+
* over rather than silently rewinding.
|
|
5011
|
+
*/
|
|
3782
5012
|
repairSchedules(skipPast, persist = true) {
|
|
3783
5013
|
const now = this.now();
|
|
3784
5014
|
let changed = false;
|
|
@@ -3786,30 +5016,49 @@ var HostTaskLedger = class {
|
|
|
3786
5016
|
const schedule = task.schedule;
|
|
3787
5017
|
if (schedule === void 0 || !schedule.enabled) return task;
|
|
3788
5018
|
if (!skipPast && schedule.nextRunAt !== void 0) return task;
|
|
3789
|
-
|
|
5019
|
+
if (schedule.mode === "once") {
|
|
5020
|
+
const at = schedule.at;
|
|
5021
|
+
if (at === void 0) {
|
|
5022
|
+
changed = true;
|
|
5023
|
+
return withSchedule(task, {
|
|
5024
|
+
enabled: false,
|
|
5025
|
+
nextRunAt: void 0,
|
|
5026
|
+
endedAt: now,
|
|
5027
|
+
endedReason: "no-target"
|
|
5028
|
+
}, now);
|
|
5029
|
+
}
|
|
5030
|
+
if (at <= now) {
|
|
5031
|
+
changed = true;
|
|
5032
|
+
return withSchedule(task, {
|
|
5033
|
+
enabled: false,
|
|
5034
|
+
nextRunAt: void 0,
|
|
5035
|
+
endedAt: now,
|
|
5036
|
+
endedReason: "missed"
|
|
5037
|
+
}, now);
|
|
5038
|
+
}
|
|
5039
|
+
if (schedule.nextRunAt === at) return task;
|
|
5040
|
+
changed = true;
|
|
5041
|
+
return withSchedule(task, { nextRunAt: at }, now);
|
|
5042
|
+
}
|
|
5043
|
+
const next = nextRunAtMs(schedule.cron ?? "", now, scheduleZone(schedule));
|
|
3790
5044
|
if (next === void 0) {
|
|
3791
5045
|
changed = true;
|
|
3792
|
-
this.document.scheduler.error = `invalid cron disabled for task: ${task.id}`;
|
|
3793
|
-
return {
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
},
|
|
3800
|
-
updatedAt: now
|
|
3801
|
-
};
|
|
5046
|
+
this.document.scheduler.error = `invalid cron disabled for task: ${task.id}`;
|
|
5047
|
+
return withSchedule(task, {
|
|
5048
|
+
enabled: false,
|
|
5049
|
+
nextRunAt: void 0,
|
|
5050
|
+
endedAt: now,
|
|
5051
|
+
endedReason: "no-target"
|
|
5052
|
+
}, now);
|
|
3802
5053
|
}
|
|
3803
|
-
|
|
5054
|
+
const passed = schedule.nextRunAt !== void 0 && schedule.nextRunAt <= now;
|
|
5055
|
+
if (schedule.nextRunAt === next && !passed) return task;
|
|
3804
5056
|
changed = true;
|
|
3805
|
-
return {
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
},
|
|
3811
|
-
updatedAt: now
|
|
3812
|
-
};
|
|
5057
|
+
return withSchedule(task, passed ? {
|
|
5058
|
+
nextRunAt: next,
|
|
5059
|
+
skippedAt: now,
|
|
5060
|
+
skippedReason: "missed"
|
|
5061
|
+
} : { nextRunAt: next }, now);
|
|
3813
5062
|
});
|
|
3814
5063
|
if (changed && persist) this.commit();
|
|
3815
5064
|
}
|
|
@@ -3854,7 +5103,7 @@ var HostTaskLedger = class {
|
|
|
3854
5103
|
*/
|
|
3855
5104
|
migrateLegacyDocument(parsed) {
|
|
3856
5105
|
if (!Array.isArray(parsed.tasks) || !parsed.tasks.every((row) => isTaskRecord(row))) throw new Error(`v${String(parsed.schemaVersion)} document contains structurally invalid task rows`);
|
|
3857
|
-
const rows = parsed.tasks.map((value) => {
|
|
5106
|
+
const rows = typeof parsed.schemaVersion === "number" && parsed.schemaVersion <= 4 ? parsed.tasks.map((value) => {
|
|
3858
5107
|
const schedule = value.schedule;
|
|
3859
5108
|
if (typeof schedule !== "object" || schedule === null) return value;
|
|
3860
5109
|
if (typeof schedule.timeZone === "string") return value;
|
|
@@ -3865,7 +5114,7 @@ var HostTaskLedger = class {
|
|
|
3865
5114
|
timeZone: timeZone()
|
|
3866
5115
|
}
|
|
3867
5116
|
};
|
|
3868
|
-
});
|
|
5117
|
+
}) : parsed.tasks;
|
|
3869
5118
|
return this.normalizeDocument({
|
|
3870
5119
|
...parsed,
|
|
3871
5120
|
tasks: rows
|
|
@@ -3931,10 +5180,10 @@ var HostTaskLedger = class {
|
|
|
3931
5180
|
if (typeof parsed.schemaVersion === "number" && TASK_BOARD_MIGRATABLE_SCHEMA_VERSIONS.includes(parsed.schemaVersion)) try {
|
|
3932
5181
|
return this.migrateLegacyDocument(parsed);
|
|
3933
5182
|
} catch (error) {
|
|
3934
|
-
throw new Error(`ledger v${String(parsed.schemaVersion)} to
|
|
5183
|
+
throw new Error(`ledger v${String(parsed.schemaVersion)} to v6 migration failed; original file kept at ${this.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
3935
5184
|
}
|
|
3936
5185
|
try {
|
|
3937
|
-
if (parsed.schemaVersion !==
|
|
5186
|
+
if (parsed.schemaVersion !== 6 || !Array.isArray(parsed.tasks)) throw new Error("unsupported ledger schema");
|
|
3938
5187
|
return this.normalizeDocument(parsed);
|
|
3939
5188
|
} catch (error) {
|
|
3940
5189
|
return this.recoverCorrupt(dir, existed, error);
|
|
@@ -3949,14 +5198,15 @@ var HostTaskLedger = class {
|
|
|
3949
5198
|
if (typeof value !== "object" || value === null) return [];
|
|
3950
5199
|
const row = value;
|
|
3951
5200
|
if (typeof row.schedule !== "object" || row.schedule === null) return [];
|
|
3952
|
-
const
|
|
3953
|
-
return typeof
|
|
5201
|
+
const rule = row.schedule;
|
|
5202
|
+
if (rule.mode === "once") return typeof rule.at === "number" && Number.isFinite(rule.at) ? [] : [typeof row.id === "string" ? row.id : "unknown"];
|
|
5203
|
+
return typeof rule.cron !== "string" || !isValidCron(rule.cron) ? [typeof row.id === "string" ? row.id : "unknown"] : [];
|
|
3954
5204
|
});
|
|
3955
5205
|
const documentLastTickAt = typeof parsed.scheduler?.lastTickAt === "number" ? parsed.scheduler.lastTickAt : void 0;
|
|
3956
5206
|
const sidecarLastTickAt = this.readSchedulerSidecar();
|
|
3957
5207
|
const lastTickAt = sidecarLastTickAt === void 0 || documentLastTickAt !== void 0 && documentLastTickAt >= sidecarLastTickAt ? documentLastTickAt : sidecarLastTickAt;
|
|
3958
5208
|
return {
|
|
3959
|
-
schemaVersion:
|
|
5209
|
+
schemaVersion: 6,
|
|
3960
5210
|
revision: Number.isSafeInteger(parsed.revision) && parsed.revision >= 0 ? parsed.revision : 0,
|
|
3961
5211
|
tasks,
|
|
3962
5212
|
scheduler: {
|
|
@@ -3982,7 +5232,7 @@ var HostTaskLedger = class {
|
|
|
3982
5232
|
if (existed) renameSync(this.file, `${this.file}.corrupt-${this.now()}-${process.pid}-${crypto.randomUUID()}`);
|
|
3983
5233
|
mkdirSync(dir, { recursive: true });
|
|
3984
5234
|
return {
|
|
3985
|
-
schemaVersion:
|
|
5235
|
+
schemaVersion: 6,
|
|
3986
5236
|
revision: 0,
|
|
3987
5237
|
tasks: [],
|
|
3988
5238
|
scheduler: {
|
|
@@ -4171,7 +5421,96 @@ function mostRecentWorkspaceId(items) {
|
|
|
4171
5421
|
return best?.id ?? items[0]?.id;
|
|
4172
5422
|
}
|
|
4173
5423
|
//#endregion
|
|
5424
|
+
//#region src/core/reasoning-effort.ts
|
|
5425
|
+
/** Trim one optional effort: blank text means "not set", like every other pinned target. */
|
|
5426
|
+
function normalizeReasoningEffort(value) {
|
|
5427
|
+
const trimmed = value?.trim();
|
|
5428
|
+
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
5429
|
+
}
|
|
5430
|
+
/**
|
|
5431
|
+
* Complete one route through the host catalog: a qualified route must match its
|
|
5432
|
+
* provider group, and a bare model id matches the first group carrying it —
|
|
5433
|
+
* which is the provider the request then names.
|
|
5434
|
+
* @param catalog - the host model catalog, when this deployment serves one.
|
|
5435
|
+
* @param route - the route as the card pins it.
|
|
5436
|
+
* @returns the completed route with its reasoning metadata, or undefined when
|
|
5437
|
+
* the catalog declares no such model.
|
|
5438
|
+
*/
|
|
5439
|
+
function declaredRoute(catalog, route) {
|
|
5440
|
+
const groups = catalog?.groups ?? [];
|
|
5441
|
+
const matched = route.provider === void 0 || route.provider === "" ? groups.find((group) => group.models.some((model) => model.id === route.model)) : groups.find((group) => group.id === route.provider);
|
|
5442
|
+
const model = matched?.models.find((candidate) => candidate.id === route.model);
|
|
5443
|
+
if (matched === void 0 || model === void 0) return void 0;
|
|
5444
|
+
return {
|
|
5445
|
+
provider: matched.id,
|
|
5446
|
+
model: model.id,
|
|
5447
|
+
...model.reasoning === void 0 ? {} : { reasoning: model.reasoning }
|
|
5448
|
+
};
|
|
5449
|
+
}
|
|
5450
|
+
/**
|
|
5451
|
+
* Resolve the effort one execution sends.
|
|
5452
|
+
* @param requested - the card's stored effort (absent or blank = inherit).
|
|
5453
|
+
* @param catalog - the host model catalog, when this deployment serves one.
|
|
5454
|
+
* @param route - the route the execution will actually run at.
|
|
5455
|
+
* @returns the effort to send, or the dropped value that will not be sent.
|
|
5456
|
+
*/
|
|
5457
|
+
function resolveReasoningEffort(requested, catalog, route) {
|
|
5458
|
+
const effort = normalizeReasoningEffort(requested);
|
|
5459
|
+
if (effort === void 0) return {};
|
|
5460
|
+
if (route === void 0) return { dropped: effort };
|
|
5461
|
+
return declaredRoute(catalog, route)?.reasoning?.efforts.some((option) => option.id === effort) === true ? { effort } : { dropped: effort };
|
|
5462
|
+
}
|
|
5463
|
+
//#endregion
|
|
4174
5464
|
//#region src/host-runner.ts
|
|
5465
|
+
/**
|
|
5466
|
+
* Mint the in-process owner peer this plugin's Host calls speak for.
|
|
5467
|
+
*
|
|
5468
|
+
* The 0.2.1 gateway made `peer` mandatory on every request, and the
|
|
5469
|
+
* authorization layer reads only `peer.principal`: a principal of
|
|
5470
|
+
* `{ kind: 'owner' }` is how host-local code states that the call IS the local
|
|
5471
|
+
* user driving their own Host, so the gateway answers it directly instead of
|
|
5472
|
+
* running a remote-access decision (only non-owner principals are judged).
|
|
5473
|
+
* That is exactly the task board's position — it creates, renames, prompts,
|
|
5474
|
+
* and reads sessions as the Host itself, which before 0.2.1 was the implicit
|
|
5475
|
+
* privilege of every in-process caller. The board claims owner explicitly, and
|
|
5476
|
+
* only ever inside its own Host process: no wire field can set a principal,
|
|
5477
|
+
* and this plugin never forwards a browser-supplied one.
|
|
5478
|
+
*
|
|
5479
|
+
* `interactions` stays `'none'`: the board only invokes methods and opens the
|
|
5480
|
+
* short `session/follow` stream. The gateway's interactions check gates
|
|
5481
|
+
* exactly the forwarded-event endpoints (opening an event stream, answering a
|
|
5482
|
+
* waterfall), which this caller never touches; an `'answer'` peer would
|
|
5483
|
+
* additionally receive forwarded Host events, a capability the board neither
|
|
5484
|
+
* needs nor wants.
|
|
5485
|
+
*
|
|
5486
|
+
* The peer's signal aborts once `ctx` disposes, so streams opened for it end
|
|
5487
|
+
* with the plugin fiber, and `dispose()` is idempotent with racing calls
|
|
5488
|
+
* sharing one completion. The id carries no credential — the protocol fixes
|
|
5489
|
+
* `principal` as the only identity persisted in a session log — so a fresh
|
|
5490
|
+
* unique id per mount is sufficient.
|
|
5491
|
+
*/
|
|
5492
|
+
function createOwnerPeer(ctx) {
|
|
5493
|
+
const controller = new AbortController();
|
|
5494
|
+
let completion;
|
|
5495
|
+
const peer = {
|
|
5496
|
+
id: `task-board-owner:${crypto.randomUUID()}`,
|
|
5497
|
+
ctx,
|
|
5498
|
+
principal: { kind: "owner" },
|
|
5499
|
+
signal: controller.signal,
|
|
5500
|
+
interactions: "none",
|
|
5501
|
+
dispose() {
|
|
5502
|
+
if (completion === void 0) {
|
|
5503
|
+
controller.abort();
|
|
5504
|
+
completion = Promise.resolve();
|
|
5505
|
+
}
|
|
5506
|
+
return completion;
|
|
5507
|
+
}
|
|
5508
|
+
};
|
|
5509
|
+
ctx.effect(() => () => {
|
|
5510
|
+
peer.dispose();
|
|
5511
|
+
}, "task-board owner peer");
|
|
5512
|
+
return peer;
|
|
5513
|
+
}
|
|
4175
5514
|
function sessionAddress(sessionId) {
|
|
4176
5515
|
return {
|
|
4177
5516
|
kind: "session",
|
|
@@ -4261,16 +5600,19 @@ function peerPromptPreamble(context) {
|
|
|
4261
5600
|
return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join("\n")}`;
|
|
4262
5601
|
}
|
|
4263
5602
|
/**
|
|
4264
|
-
* The scheduled-run section: this execution was triggered by the card's
|
|
4265
|
-
*
|
|
4266
|
-
* and the rule text, so the run's own clock is unambiguous — the board's
|
|
5603
|
+
* The scheduled-run section: this execution was triggered by the card's
|
|
5604
|
+
* schedule rather than by a person. It states the firing instant, the rule's
|
|
5605
|
+
* zone, and the rule text, so the run's own clock is unambiguous — the board's
|
|
4267
5606
|
* scheduler fires on a wall clock in a specific IANA zone, which is not
|
|
4268
|
-
* necessarily the zone of whatever session the run lands in.
|
|
5607
|
+
* necessarily the zone of whatever session the run lands in. A one-shot says
|
|
5608
|
+
* so explicitly, so the run does not expect a recurrence.
|
|
4269
5609
|
*/
|
|
4270
5610
|
function schedulePromptPreamble(context) {
|
|
4271
5611
|
const schedule = context.schedule;
|
|
4272
5612
|
if (schedule === void 0) return void 0;
|
|
4273
|
-
|
|
5613
|
+
const stamp = new Date(schedule.triggeredAt).toISOString();
|
|
5614
|
+
if (schedule.mode === "once") return `本次执行由任务看板的单次定时自动触发:计划时间 ${stamp}(UTC),时区 ${schedule.timeZone}。这是一次性计划,本次执行之后该卡片不会再自动执行。需要判断「今天」「现在」或计算时间窗口时,以该时区的触发时间为准。`;
|
|
5615
|
+
return `本次执行由任务看板的定时规则自动触发:触发时间 ${stamp}(UTC),规则时区 ${schedule.timeZone},cron 表达式 ${schedule.cron}。需要判断「今天」「现在」或计算时间窗口时,以该时区的触发时间为准。`;
|
|
4274
5616
|
}
|
|
4275
5617
|
/**
|
|
4276
5618
|
* Build the execution prompt for one task.
|
|
@@ -4369,27 +5711,39 @@ function invokeWireArgs(namespace, method, request) {
|
|
|
4369
5711
|
if (namespace === "session" && method === "list") return { _request: request };
|
|
4370
5712
|
return { request };
|
|
4371
5713
|
}
|
|
4372
|
-
var HostExecutionRunner = class {
|
|
5714
|
+
var HostExecutionRunner = class HostExecutionRunner {
|
|
4373
5715
|
gateway;
|
|
4374
5716
|
commands;
|
|
4375
5717
|
workspaceRegistry;
|
|
4376
|
-
/**
|
|
5718
|
+
/**
|
|
5719
|
+
* Newest event sequence per session whose COMPLETE history walk (reaching
|
|
5720
|
+
* the execution boundary) found no matching execution end. A later walk may
|
|
5721
|
+
* trust everything at or below it and examine only newer records.
|
|
5722
|
+
*/
|
|
4377
5723
|
scanMemos = /* @__PURE__ */ new Map();
|
|
4378
5724
|
unavailableAttempts;
|
|
4379
5725
|
unavailableBackoffMs;
|
|
5726
|
+
/**
|
|
5727
|
+
* The peer every request speaks for. Production passes the peer bound to the
|
|
5728
|
+
* plugin fiber; a bare runner (unit tests around fake gateways) gets one
|
|
5729
|
+
* bound to a fresh root context instead.
|
|
5730
|
+
*/
|
|
5731
|
+
ownerPeer;
|
|
4380
5732
|
unsupportedSessionListWarned = false;
|
|
4381
|
-
constructor(gateway, commands, workspaceRegistry, unavailableRetry) {
|
|
5733
|
+
constructor(gateway, commands, workspaceRegistry, unavailableRetry, ownerPeer) {
|
|
4382
5734
|
this.gateway = gateway;
|
|
4383
5735
|
this.commands = commands;
|
|
4384
5736
|
this.workspaceRegistry = workspaceRegistry;
|
|
4385
5737
|
this.unavailableAttempts = unavailableRetry?.attempts ?? SERVICE_UNAVAILABLE_ATTEMPTS;
|
|
4386
5738
|
this.unavailableBackoffMs = unavailableRetry?.backoffMs ?? SERVICE_UNAVAILABLE_BACKOFF_MS;
|
|
5739
|
+
this.ownerPeer = ownerPeer ?? createOwnerPeer(new Context());
|
|
4387
5740
|
}
|
|
4388
5741
|
invoke(namespace, method, request, signal) {
|
|
4389
5742
|
return this.gateway.invoke({
|
|
4390
5743
|
namespace,
|
|
4391
5744
|
method,
|
|
4392
5745
|
args: invokeWireArgs(namespace, method, request),
|
|
5746
|
+
peer: this.ownerPeer,
|
|
4393
5747
|
...signal === void 0 ? {} : { signal }
|
|
4394
5748
|
});
|
|
4395
5749
|
}
|
|
@@ -4399,6 +5753,7 @@ var HostExecutionRunner = class {
|
|
|
4399
5753
|
namespace,
|
|
4400
5754
|
method,
|
|
4401
5755
|
args: { request },
|
|
5756
|
+
peer: this.ownerPeer,
|
|
4402
5757
|
...signal === void 0 ? {} : { signal }
|
|
4403
5758
|
});
|
|
4404
5759
|
}
|
|
@@ -4440,7 +5795,7 @@ var HostExecutionRunner = class {
|
|
|
4440
5795
|
try {
|
|
4441
5796
|
options.onSession?.(reused);
|
|
4442
5797
|
await this.assertReusedPreset(reused, mode);
|
|
4443
|
-
await this.pinAndPrompt(reused, task, permission, options.promptContext, options.onGoalArmed);
|
|
5798
|
+
await this.pinAndPrompt(reused, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true, options.catalog);
|
|
4444
5799
|
} catch (error) {
|
|
4445
5800
|
throw new SessionLaunchError(reused, error);
|
|
4446
5801
|
}
|
|
@@ -4456,7 +5811,7 @@ var HostExecutionRunner = class {
|
|
|
4456
5811
|
sessionId,
|
|
4457
5812
|
title: task.title
|
|
4458
5813
|
});
|
|
4459
|
-
await this.pinAndPrompt(sessionId, task, permission, options.promptContext, options.onGoalArmed);
|
|
5814
|
+
await this.pinAndPrompt(sessionId, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true, options.catalog);
|
|
4460
5815
|
} catch (error) {
|
|
4461
5816
|
throw new SessionLaunchError(sessionId, error);
|
|
4462
5817
|
}
|
|
@@ -4483,30 +5838,85 @@ var HostExecutionRunner = class {
|
|
|
4483
5838
|
throw new Error("reused session " + sessionId + " was composed from agent preset " + (typeof recorded === "string" ? "\"" + recorded + "\"" : "an unreadable value") + ", but the task pins \"" + pinned + "\"");
|
|
4484
5839
|
}
|
|
4485
5840
|
/**
|
|
5841
|
+
* Split a `provider/model` pin. A pin without a provider keeps only the model
|
|
5842
|
+
* id: the provider is the one the host catalog declares for it, resolved at
|
|
5843
|
+
* launch because the session wire contract requires a provider.
|
|
5844
|
+
*/
|
|
5845
|
+
static pinnedRoute(model) {
|
|
5846
|
+
const raw = model?.trim();
|
|
5847
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
5848
|
+
const slash = raw.indexOf("/");
|
|
5849
|
+
if (slash < 0) return { model: raw };
|
|
5850
|
+
const provider = raw.slice(0, slash).trim();
|
|
5851
|
+
const modelId = raw.slice(slash + 1).trim();
|
|
5852
|
+
if (provider === "" || modelId === "") return void 0;
|
|
5853
|
+
return {
|
|
5854
|
+
provider,
|
|
5855
|
+
model: modelId
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
/**
|
|
5859
|
+
* The route a card that pins no model actually runs at: the session's own
|
|
5860
|
+
* recorded selection (the durable fold the GUI reads; `next` is the pending
|
|
5861
|
+
* user choice, `lastUsed` what the last request consumed), or — for a session
|
|
5862
|
+
* that has selected none yet, which is exactly a session that would start at
|
|
5863
|
+
* the deployment default — the host default from the catalog.
|
|
5864
|
+
*
|
|
5865
|
+
* A route that cannot be READ is undefined and the effort is not sent:
|
|
5866
|
+
* falling back to the host default there would silently switch the model of a
|
|
5867
|
+
* session this run only meant to continue.
|
|
5868
|
+
*/
|
|
5869
|
+
async sessionRoute(sessionId, catalog) {
|
|
5870
|
+
let selection;
|
|
5871
|
+
try {
|
|
5872
|
+
const projections = await this.invoke("session", "projections", { sessionId });
|
|
5873
|
+
selection = projections?.values?.modelSelection?.next ?? projections?.values?.modelSelection?.lastUsed;
|
|
5874
|
+
} catch (error) {
|
|
5875
|
+
console.warn("[dsh-task-board] session/projections failed while resolving the execution model route", error);
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
if (typeof selection?.provider === "string" && selection.provider !== "" && typeof selection.model === "string" && selection.model !== "") return {
|
|
5879
|
+
provider: selection.provider,
|
|
5880
|
+
model: selection.model
|
|
5881
|
+
};
|
|
5882
|
+
return catalog?.default === void 0 ? void 0 : {
|
|
5883
|
+
provider: catalog.default.provider,
|
|
5884
|
+
model: catalog.default.model
|
|
5885
|
+
};
|
|
5886
|
+
}
|
|
5887
|
+
/**
|
|
4486
5888
|
* Re-assert the pinned execution contract on a session and queue the task
|
|
4487
5889
|
* prompt. Shared by the fresh-session and reuse paths so both apply exactly
|
|
4488
|
-
* the same permission/model pins before the prompt.
|
|
5890
|
+
* the same permission/model/effort pins before the prompt. A session that
|
|
5891
|
+
* refuses a selection carrying a catalog-confirmed effort throws here, so the
|
|
5892
|
+
* prompt is never queued behind a run at the wrong level.
|
|
4489
5893
|
*/
|
|
4490
|
-
async pinAndPrompt(sessionId, task, permission, context = {}, onGoalArmed) {
|
|
5894
|
+
async pinAndPrompt(sessionId, task, permission, context = {}, onGoalArmed, goalEnabled = false, catalog) {
|
|
4491
5895
|
if (permission !== void 0) {
|
|
4492
5896
|
if (this.commands === void 0) throw new Error("permission command dispatcher is unavailable");
|
|
4493
5897
|
const command = await this.commands.execute(sessionId, "/permission " + permission, AbortSignal.timeout(3e4));
|
|
4494
5898
|
if (command === void 0) throw new Error("permission command was not acknowledged");
|
|
4495
5899
|
if (command.kind !== "success") throw new Error(command.text ?? "permission command failed");
|
|
4496
5900
|
}
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
const
|
|
4501
|
-
const
|
|
5901
|
+
const requestedEffort = normalizeReasoningEffort(task.reasoningEffort);
|
|
5902
|
+
const route = HostExecutionRunner.pinnedRoute(task.model) ?? (requestedEffort === void 0 ? void 0 : await this.sessionRoute(sessionId, catalog));
|
|
5903
|
+
if (route !== void 0) {
|
|
5904
|
+
const routeLabel = `${route.provider === void 0 ? "" : route.provider + "/"}${route.model}`;
|
|
5905
|
+
const decision = resolveReasoningEffort(requestedEffort, catalog, route);
|
|
5906
|
+
if (decision.dropped !== void 0) console.warn(`[dsh-task-board] model route ${routeLabel} does not declare reasoning effort "${decision.dropped}"; session ${sessionId} runs at the model default`);
|
|
5907
|
+
const declared = declaredRoute(catalog, route);
|
|
5908
|
+
const provider = declared?.provider ?? route.provider;
|
|
5909
|
+
const modelId = declared?.model ?? route.model;
|
|
4502
5910
|
try {
|
|
4503
5911
|
await this.invoke("session", "selectModel", {
|
|
4504
5912
|
sessionId,
|
|
4505
|
-
...provider ? {
|
|
4506
|
-
model: modelId
|
|
5913
|
+
...provider === void 0 ? {} : { provider },
|
|
5914
|
+
model: modelId,
|
|
5915
|
+
...decision.effort === void 0 ? {} : { reasoningEffort: decision.effort }
|
|
4507
5916
|
});
|
|
4508
5917
|
} catch (modelError) {
|
|
4509
|
-
|
|
5918
|
+
if (decision.effort !== void 0) throw new Error(`session ${sessionId} refused the model selection "${provider === void 0 ? modelId : provider + "/" + modelId}" with reasoning effort "${decision.effort}": ` + (modelError instanceof Error ? modelError.message : String(modelError)));
|
|
5919
|
+
console.warn(`[dsh-task-board] failed to select model "${provider === void 0 ? modelId : provider + "/" + modelId}" for session ${sessionId}, falling back to default:`, modelError);
|
|
4510
5920
|
}
|
|
4511
5921
|
}
|
|
4512
5922
|
await this.invoke("session", "prompt", {
|
|
@@ -4518,7 +5928,7 @@ var HostExecutionRunner = class {
|
|
|
4518
5928
|
text: promptText(task, context)
|
|
4519
5929
|
}]
|
|
4520
5930
|
});
|
|
4521
|
-
const armed = await this.armGoal(sessionId, task, context);
|
|
5931
|
+
const armed = await this.armGoal(sessionId, task, context, goalEnabled);
|
|
4522
5932
|
onGoalArmed?.(armed);
|
|
4523
5933
|
}
|
|
4524
5934
|
/**
|
|
@@ -4544,14 +5954,25 @@ var HostExecutionRunner = class {
|
|
|
4544
5954
|
* dsh's goal-round driver keeps starting continuation rounds until the agent
|
|
4545
5955
|
* marks it complete (see {@link goalVerdict} for how the run settles).
|
|
4546
5956
|
*
|
|
4547
|
-
*
|
|
5957
|
+
* Two switches gate the arming, and BOTH must ask for it:
|
|
5958
|
+
* - the GLOBAL native-/goal switch (`goalEnabled`, default OFF), frozen when
|
|
5959
|
+
* this execution started; and
|
|
5960
|
+
* - the task's own `goalRun` (absent means off, only an explicit true opts
|
|
5961
|
+
* the card into a goal run).
|
|
5962
|
+
*
|
|
4548
5963
|
* A refusal (no command dispatcher, no `/goal` command in this cohort, an
|
|
4549
|
-
* objective the command rejects) is reported and the run continues as
|
|
4550
|
-
*
|
|
4551
|
-
*
|
|
5964
|
+
* objective the command rejects) is reported and the run continues as a plain
|
|
5965
|
+
* turn: the task was asked to run, and a missing goal mode must not lose the
|
|
5966
|
+
* work.
|
|
5967
|
+
* @param sessionId - the session to arm in.
|
|
5968
|
+
* @param task - the task being run.
|
|
5969
|
+
* @param context - the run's prompt context.
|
|
5970
|
+
* @param goalEnabled - the GLOBAL switch, frozen for this execution.
|
|
5971
|
+
* @returns whether `/goal` was really armed.
|
|
4552
5972
|
*/
|
|
4553
|
-
async armGoal(sessionId, task, context) {
|
|
4554
|
-
if (
|
|
5973
|
+
async armGoal(sessionId, task, context, goalEnabled) {
|
|
5974
|
+
if (!goalEnabled) return false;
|
|
5975
|
+
if (task.goalRun !== true) return false;
|
|
4555
5976
|
if (this.commands === void 0) {
|
|
4556
5977
|
console.warn("[dsh-task-board] no command dispatcher is available; task " + task.id + " runs without /goal");
|
|
4557
5978
|
return false;
|
|
@@ -4710,10 +6131,13 @@ var HostExecutionRunner = class {
|
|
|
4710
6131
|
}
|
|
4711
6132
|
const openingEvents = opening.records.map((record) => ({ event: recordEvent(record) }));
|
|
4712
6133
|
const newestSeq = openingEvents.reduce((newest, entry) => newest === void 0 ? entry.event.seq : Math.max(newest, entry.event.seq), void 0);
|
|
4713
|
-
|
|
6134
|
+
const scannedThrough = this.scanMemos.get(sessionId);
|
|
6135
|
+
if (newestSeq !== void 0 && scannedThrough === newestSeq) return { outcome: "pending" };
|
|
6136
|
+
const oldestOpeningSeq = openingEvents.reduce((oldest, entry) => oldest === void 0 ? entry.event.seq : Math.min(oldest, entry.event.seq), void 0);
|
|
6137
|
+
const coveredThrough = scannedThrough !== void 0 && oldestOpeningSeq !== void 0 && oldestOpeningSeq <= scannedThrough;
|
|
4714
6138
|
const events = [...openingEvents];
|
|
4715
6139
|
let beforeSeq;
|
|
4716
|
-
let reachedExecutionBoundary = !opening.hasMore;
|
|
6140
|
+
let reachedExecutionBoundary = !opening.hasMore || coveredThrough;
|
|
4717
6141
|
for (let page = 0; page < HISTORY_PAGE_LIMIT && !reachedExecutionBoundary; page += 1) {
|
|
4718
6142
|
let history;
|
|
4719
6143
|
try {
|
|
@@ -4734,11 +6158,11 @@ var HostExecutionRunner = class {
|
|
|
4734
6158
|
const pageEntries = pageEvents(history);
|
|
4735
6159
|
events.push(...pageEntries);
|
|
4736
6160
|
const oldestTime = pageEntries.reduce((oldest, entry) => oldest === void 0 ? entry.event.time : Math.min(oldest, entry.event.time), void 0);
|
|
4737
|
-
|
|
6161
|
+
const oldestSeq = pageEntries.reduce((oldest, entry) => oldest === void 0 ? entry.event.seq : Math.min(oldest, entry.event.seq), void 0);
|
|
6162
|
+
if (!history.hasMore || oldestTime !== void 0 && oldestTime <= startedAt || scannedThrough !== void 0 && oldestSeq !== void 0 && oldestSeq <= scannedThrough) {
|
|
4738
6163
|
reachedExecutionBoundary = true;
|
|
4739
6164
|
break;
|
|
4740
6165
|
}
|
|
4741
|
-
const oldestSeq = pageEntries.reduce((oldest, entry) => oldest === void 0 ? entry.event.seq : Math.min(oldest, entry.event.seq), void 0);
|
|
4742
6166
|
if (oldestSeq === void 0 || oldestSeq === beforeSeq) return { outcome: "pending" };
|
|
4743
6167
|
beforeSeq = oldestSeq;
|
|
4744
6168
|
}
|
|
@@ -5473,8 +6897,8 @@ var TaskBoardExtensionRegistry = class {
|
|
|
5473
6897
|
};
|
|
5474
6898
|
//#endregion
|
|
5475
6899
|
//#region src/host-service.ts
|
|
5476
|
-
/**
|
|
5477
|
-
const
|
|
6900
|
+
/** Ceiling for the roster-poll failure backoff: a broken session tree is retried at most once a minute. */
|
|
6901
|
+
const POLL_FAILURE_MAX_BACKOFF_MS = 6e4;
|
|
5478
6902
|
/**
|
|
5479
6903
|
* How late an armed schedule fire may be before it counts as a resume rather
|
|
5480
6904
|
* than a normal occurrence. The schedule timer is armed AT the next due
|
|
@@ -5495,10 +6919,51 @@ const MAX_TIMER_DELAY_MS = 2147483647;
|
|
|
5495
6919
|
*/
|
|
5496
6920
|
const UNREADABLE_SETTLE_POLLS = 24;
|
|
5497
6921
|
/**
|
|
5498
|
-
*
|
|
5499
|
-
* an
|
|
6922
|
+
* Why a member of a run never started: the Host stopped while its launch was
|
|
6923
|
+
* still waiting for an admission slot. Recorded rather than left open, so the
|
|
6924
|
+
* card leaves the running column instead of waiting for the next Host start's
|
|
6925
|
+
* interrupted-start recovery.
|
|
6926
|
+
*/
|
|
6927
|
+
const ABANDONED_LAUNCH_REASON = "host stopped before this execution started";
|
|
6928
|
+
/**
|
|
6929
|
+
* Why an enforced goal execution settled failed while its acceptance DID run:
|
|
6930
|
+
* attempts were recorded and none of them passed. The judge answered about the
|
|
6931
|
+
* work and the work did not pass.
|
|
6932
|
+
*/
|
|
6933
|
+
const NO_MATCHING_PASS_VERIFICATION_REASON = "goal 验收:本次执行没有匹配的验收通过记录(验收未运行、未通过或报告来自其他执行),按未验收判失败。";
|
|
6934
|
+
/**
|
|
6935
|
+
* Why an enforced goal execution settled failed with ZERO acceptance records:
|
|
6936
|
+
* the gate was never opened, so the judge never ran and nothing was ever judged
|
|
6937
|
+
* (issue #1837). The root cause is a session that declared completion in prose
|
|
6938
|
+
* instead of calling `update_goal(action: complete)` — a tool-adherence problem,
|
|
6939
|
+
* NOT a quality verdict on the delivery — so this reason says so and keeps the
|
|
6940
|
+
* two investigation directions apart.
|
|
6941
|
+
*/
|
|
6942
|
+
const NEVER_INVOKED_VERIFICATION_REASON = "goal 验收未触发:本次执行没有任何验收记录,验收门从未打开——执行会话没有调用 update_goal(action: complete) 来完成目标(常见于只在回复里宣告完成)。这不是质量判负:交付从未被裁判评估。";
|
|
6943
|
+
/**
|
|
6944
|
+
* Why an enforced goal execution settled failed while its ONLY acceptance
|
|
6945
|
+
* records are INVALID: the judge answered but could not locate its own
|
|
6946
|
+
* rejection, so the board stopped calling it. This is NOT an evidence-backed
|
|
6947
|
+
* quality veto — the requirement is explicit that such a run must not be
|
|
6948
|
+
* described as one — but the execution still has no matching pass, so it ends
|
|
6949
|
+
* for lack of one, and the reason names that precisely.
|
|
6950
|
+
*/
|
|
6951
|
+
const INVALID_ONLY_VERIFICATION_REASON = "goal 验收无效:本次执行只记录了无效验收(裁判未能给出与任务要求对应、且可定位到本次证据的否决依据),看板已停止调用裁判。这**不是**有证据的质量判负:验收本身不可用,本次运行因没有匹配的通过记录而结束。请检查验收环境与证据可见性,或用 task_board_manage(action=reset-verification) 清除记录的无效验收后重新完成目标。";
|
|
6952
|
+
/**
|
|
6953
|
+
* Actions that can change the instant the schedule timer is armed at. Only
|
|
6954
|
+
* these re-arm the native timer; an unrelated card edit leaves the pending fire
|
|
6955
|
+
* untouched.
|
|
6956
|
+
*
|
|
6957
|
+
* The set is the write surface of a rule itself rather than a guess at the UI
|
|
6958
|
+
* flows that reach it: `create` and `import` land a card that already carries
|
|
6959
|
+
* an enabled rule (the new-task dialog and the one-time browser import),
|
|
6960
|
+
* `set-schedule` edits one, and `delete`/`archive` drop one. `update` is
|
|
6961
|
+
* deliberately absent — the update patch cannot carry a schedule — and neither
|
|
6962
|
+
* can the run, move, lineage, permission, verification or label actions.
|
|
5500
6963
|
*/
|
|
5501
6964
|
const SCHEDULE_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
6965
|
+
"create",
|
|
6966
|
+
"import",
|
|
5502
6967
|
"set-schedule",
|
|
5503
6968
|
"delete",
|
|
5504
6969
|
"archive"
|
|
@@ -5518,6 +6983,26 @@ const PROCESS_TIMERS = {
|
|
|
5518
6983
|
};
|
|
5519
6984
|
}
|
|
5520
6985
|
};
|
|
6986
|
+
/**
|
|
6987
|
+
* The new-card defaults a snapshot publishes, or undefined when this
|
|
6988
|
+
* deployment configures none. A target the board configures nothing for is
|
|
6989
|
+
* omitted instead of published blank, so the browser can tell "no default
|
|
6990
|
+
* model" from "a blank one" (only a hand-edited settings document can express
|
|
6991
|
+
* the latter).
|
|
6992
|
+
*/
|
|
6993
|
+
function cardDefaultsOf(defaults) {
|
|
6994
|
+
const mode = normalizeTargetId(defaults.mode);
|
|
6995
|
+
const permission = normalizeTargetId(defaults.permission);
|
|
6996
|
+
const model = normalizeTargetId(defaults.model);
|
|
6997
|
+
const reasoningEffort = normalizeTargetId(defaults.reasoningEffort);
|
|
6998
|
+
const published = {
|
|
6999
|
+
...mode === void 0 ? {} : { mode },
|
|
7000
|
+
...isTaskPermission(permission) ? { permission } : {},
|
|
7001
|
+
...model === void 0 ? {} : { model },
|
|
7002
|
+
...reasoningEffort === void 0 ? {} : { reasoningEffort }
|
|
7003
|
+
};
|
|
7004
|
+
return Object.keys(published).length === 0 ? void 0 : published;
|
|
7005
|
+
}
|
|
5521
7006
|
var TaskBoardHostService = class {
|
|
5522
7007
|
ledger;
|
|
5523
7008
|
runner;
|
|
@@ -5525,6 +7010,12 @@ var TaskBoardHostService = class {
|
|
|
5525
7010
|
listeners = /* @__PURE__ */ new Set();
|
|
5526
7011
|
/** The one recurring timer: the session-roster poll. */
|
|
5527
7012
|
pollTimer;
|
|
7013
|
+
/** Cadence of the roster poll, in seconds (a settings field; see the config schema). */
|
|
7014
|
+
sessionPollSeconds = 5;
|
|
7015
|
+
/** Pending delay that follows a failed poll pass, if one is armed. */
|
|
7016
|
+
pollBackoffTimer;
|
|
7017
|
+
/** Consecutive failed poll passes; doubles the next retry delay. */
|
|
7018
|
+
consecutivePollFailures = 0;
|
|
5528
7019
|
/** The armed schedule timer, if a trigger is pending. */
|
|
5529
7020
|
scheduleTimer;
|
|
5530
7021
|
/** The instant the armed schedule timer targets (ms epoch), for resume detection. */
|
|
@@ -5533,12 +7024,11 @@ var TaskBoardHostService = class {
|
|
|
5533
7024
|
pollInFlight = false;
|
|
5534
7025
|
active = true;
|
|
5535
7026
|
/**
|
|
5536
|
-
*
|
|
5537
|
-
*
|
|
5538
|
-
*
|
|
5539
|
-
* conversation instead of prompting into a session it cannot see.
|
|
7027
|
+
* Admission control for the session-creating half of a run: one board action
|
|
7028
|
+
* opens a whole subtree, and this is what keeps the Host from asking DSH for
|
|
7029
|
+
* every member's session at once (see {@link LaunchAdmissionQueue}).
|
|
5540
7030
|
*/
|
|
5541
|
-
|
|
7031
|
+
launches;
|
|
5542
7032
|
/**
|
|
5543
7033
|
* Consecutive unreadable-history polls per open execution (see
|
|
5544
7034
|
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
@@ -5550,6 +7040,16 @@ var TaskBoardHostService = class {
|
|
|
5550
7040
|
verificationCatalog;
|
|
5551
7041
|
team;
|
|
5552
7042
|
timers;
|
|
7043
|
+
/** Live reader of the global native-/goal switch; frozen per execution. */
|
|
7044
|
+
goalRunEnabled;
|
|
7045
|
+
/** Live reader of the deployment's new-card defaults (published and applied per create). */
|
|
7046
|
+
cardDefaults;
|
|
7047
|
+
/**
|
|
7048
|
+
* Live reader of the run-launch admission width: how many launches may be in
|
|
7049
|
+
* flight at once. Read at every admission, so a settings edit bounds the next
|
|
7050
|
+
* launch without remounting the row. Absent means the default.
|
|
7051
|
+
*/
|
|
7052
|
+
runLaunchWidth;
|
|
5553
7053
|
lastPowerJson = "";
|
|
5554
7054
|
now;
|
|
5555
7055
|
/** The deployment's workspaces, consulted to resolve a card that pins none. */
|
|
@@ -5563,14 +7063,19 @@ var TaskBoardHostService = class {
|
|
|
5563
7063
|
constructor(gateway, options = {}) {
|
|
5564
7064
|
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
5565
7065
|
sessionDefaultPermission: options.sessionDefaultPermission,
|
|
5566
|
-
maxSubtaskDepth: options.maxSubtaskDepth
|
|
7066
|
+
maxSubtaskDepth: options.maxSubtaskDepth,
|
|
7067
|
+
cardDefaults: options.cardDefaults
|
|
5567
7068
|
});
|
|
5568
|
-
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
|
|
7069
|
+
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry, void 0, options.ownerPeer);
|
|
5569
7070
|
this.workspaceRegistry = options.workspaceRegistry;
|
|
5570
7071
|
this.team = options.team;
|
|
5571
7072
|
this.timers = options.timers ?? PROCESS_TIMERS;
|
|
5572
7073
|
this.power = options.power ?? new PowerInhibitor();
|
|
5573
7074
|
this.now = options.now ?? Date.now;
|
|
7075
|
+
this.goalRunEnabled = options.goalRunEnabled ?? (() => false);
|
|
7076
|
+
this.cardDefaults = options.cardDefaults ?? (() => ({}));
|
|
7077
|
+
this.runLaunchWidth = options.runLaunchWidth ?? (() => 3);
|
|
7078
|
+
this.launches = new LaunchAdmissionQueue(() => normalizeRunLaunchWidth(this.runLaunchWidth()));
|
|
5574
7079
|
this.verificationSettings = options.verificationSettings ?? (() => ({
|
|
5575
7080
|
enabled: false,
|
|
5576
7081
|
model: "",
|
|
@@ -5599,14 +7104,35 @@ var TaskBoardHostService = class {
|
|
|
5599
7104
|
this.syncPowerReasons();
|
|
5600
7105
|
this.pollTimer = this.timers.interval(() => {
|
|
5601
7106
|
this.schedulePoll();
|
|
5602
|
-
},
|
|
7107
|
+
}, sessionPollMs(this.sessionPollSeconds));
|
|
5603
7108
|
this.schedulePoll();
|
|
7109
|
+
this.cleanupPendingAcceptanceDetail();
|
|
5604
7110
|
this.recoverSchedule();
|
|
5605
7111
|
}
|
|
5606
|
-
|
|
7112
|
+
/**
|
|
7113
|
+
* Apply the live configuration. The roster cadence is re-read on every
|
|
7114
|
+
* commit, so a settings edit takes effect without a restart; the poll is
|
|
7115
|
+
* scheduled at the new cadence immediately rather than waiting out the old
|
|
7116
|
+
* interval.
|
|
7117
|
+
* @param active - the board's master switch.
|
|
7118
|
+
* @param preventIdleSleep - whether the board holds an idle-sleep assertion.
|
|
7119
|
+
* @param sessionPollSeconds - roster-poll cadence in seconds (clamped).
|
|
7120
|
+
*/
|
|
7121
|
+
setConfiguration(active, preventIdleSleep, sessionPollSeconds) {
|
|
5607
7122
|
const resumed = !this.active && active;
|
|
5608
7123
|
this.active = active;
|
|
5609
7124
|
this.preventIdleSleep = preventIdleSleep;
|
|
7125
|
+
const cadence = normalizeSessionPollSeconds(sessionPollSeconds);
|
|
7126
|
+
const retimed = this.pollTimer !== void 0 && cadence !== this.sessionPollSeconds;
|
|
7127
|
+
this.sessionPollSeconds = cadence;
|
|
7128
|
+
if (retimed) {
|
|
7129
|
+
this.pollTimer?.();
|
|
7130
|
+
this.pollTimer = this.timers.interval(() => {
|
|
7131
|
+
this.schedulePoll();
|
|
7132
|
+
}, sessionPollMs(this.sessionPollSeconds));
|
|
7133
|
+
this.clearPollBackoff();
|
|
7134
|
+
this.schedulePoll();
|
|
7135
|
+
}
|
|
5610
7136
|
if (resumed) {
|
|
5611
7137
|
const current = this.power.snapshot();
|
|
5612
7138
|
this.power.updateReasons({
|
|
@@ -5626,8 +7152,9 @@ var TaskBoardHostService = class {
|
|
|
5626
7152
|
snapshot() {
|
|
5627
7153
|
const state = this.ledger.state();
|
|
5628
7154
|
const published = this.extensions.published();
|
|
7155
|
+
const cardDefaults = cardDefaultsOf(this.cardDefaults());
|
|
5629
7156
|
return {
|
|
5630
|
-
schemaVersion:
|
|
7157
|
+
schemaVersion: 6,
|
|
5631
7158
|
revision: state.revision,
|
|
5632
7159
|
tasks: state.tasks,
|
|
5633
7160
|
scheduler: state.scheduler,
|
|
@@ -5635,6 +7162,8 @@ var TaskBoardHostService = class {
|
|
|
5635
7162
|
sessionDefaultPermission: this.ledger.sessionDefaultPermission,
|
|
5636
7163
|
maxSubtaskDepth: this.ledger.maxSubtaskDepth,
|
|
5637
7164
|
teamRunAvailable: this.team !== void 0,
|
|
7165
|
+
goalRunEnabled: this.goalRunEnabled(),
|
|
7166
|
+
...cardDefaults === void 0 ? {} : { cardDefaults },
|
|
5638
7167
|
...Object.keys(published).length === 0 ? {} : { extensions: published }
|
|
5639
7168
|
};
|
|
5640
7169
|
}
|
|
@@ -5731,7 +7260,7 @@ var TaskBoardHostService = class {
|
|
|
5731
7260
|
this.emitStatusChanges(before, initiator);
|
|
5732
7261
|
if (action.kind === "delete") this.extensions.emitTaskDeleted({ taskId: action.taskId });
|
|
5733
7262
|
return {
|
|
5734
|
-
schemaVersion:
|
|
7263
|
+
schemaVersion: 6,
|
|
5735
7264
|
revision: result.state.revision,
|
|
5736
7265
|
tasks: result.state.tasks,
|
|
5737
7266
|
scheduler: result.state.scheduler,
|
|
@@ -5767,18 +7296,23 @@ var TaskBoardHostService = class {
|
|
|
5767
7296
|
}
|
|
5768
7297
|
dispose() {
|
|
5769
7298
|
this.disposed = true;
|
|
7299
|
+
const abandoned = this.launches.drain();
|
|
7300
|
+
if (abandoned.length > 0) safeConsoleError(`[dsh-task-board] ${abandoned.length} run member(s) were still waiting for a launch slot when the Host stopped; each is settled as cancelled`);
|
|
5770
7301
|
this.extensions.dispose();
|
|
5771
7302
|
this.clearScheduleTimer();
|
|
7303
|
+
this.clearPollBackoff();
|
|
5772
7304
|
this.pollTimer?.();
|
|
5773
7305
|
this.pollTimer = void 0;
|
|
5774
7306
|
this.power.dispose();
|
|
5775
7307
|
this.ledger.dispose();
|
|
5776
7308
|
this.listeners.clear();
|
|
5777
7309
|
}
|
|
5778
|
-
async launch(opened, others = [], schedule) {
|
|
7310
|
+
async launch(opened, others = [], schedule, freezeContract) {
|
|
7311
|
+
/** The session this launch attached to the execution, once it exists. */
|
|
7312
|
+
let attached;
|
|
5779
7313
|
try {
|
|
5780
7314
|
const team = opened.task.teamRun === true;
|
|
5781
|
-
const reuseSessionId = team ? void 0 :
|
|
7315
|
+
const reuseSessionId = team ? void 0 : await this.reuseSessionFor(opened.task);
|
|
5782
7316
|
const peers = others.length === 0 ? void 0 : others.map((other) => ({
|
|
5783
7317
|
id: other.task.id,
|
|
5784
7318
|
title: other.task.title,
|
|
@@ -5789,41 +7323,84 @@ var TaskBoardHostService = class {
|
|
|
5789
7323
|
...team ? { team: true } : {},
|
|
5790
7324
|
...schedule === void 0 ? {} : { schedule }
|
|
5791
7325
|
};
|
|
5792
|
-
const settings = this.
|
|
7326
|
+
const { settings, globalGoalEnabled, catalog } = await (freezeContract ?? (() => this.freezeRunContract()))();
|
|
7327
|
+
const goalEnabled = globalGoalEnabled && opened.task.goalRun === true;
|
|
5793
7328
|
const skippedBy = opened.task.skipVerification === true ? "skipped" : "disabled";
|
|
5794
7329
|
const contract = resolveContract({
|
|
5795
7330
|
...settings,
|
|
5796
7331
|
enabled: settings.enabled && opened.task.skipVerification !== true
|
|
5797
|
-
},
|
|
7332
|
+
}, catalog);
|
|
7333
|
+
const notArmed = globalGoalEnabled ? "goal-unavailable" : "goal-disabled";
|
|
5798
7334
|
const initial = {
|
|
5799
7335
|
contract,
|
|
5800
7336
|
attempts: [],
|
|
5801
|
-
applicability: team ? "team-member" : !contract.enabled ? skippedBy :
|
|
7337
|
+
applicability: team ? "team-member" : !contract.enabled ? skippedBy : notArmed
|
|
5802
7338
|
};
|
|
5803
7339
|
this.ledger.setVerification(opened.task.id, opened.execution.id, initial);
|
|
5804
|
-
let attached;
|
|
5805
7340
|
const sessionId = await this.runner.launch(opened.task, {
|
|
5806
7341
|
...reuseSessionId === void 0 ? {} : { reuseSessionId },
|
|
5807
7342
|
...promptContext === void 0 ? {} : { promptContext },
|
|
7343
|
+
goalEnabled,
|
|
7344
|
+
catalog,
|
|
5808
7345
|
onSession: (id) => {
|
|
5809
7346
|
attached = id;
|
|
5810
7347
|
this.ledger.attachSession(opened.task.id, opened.execution.id, id);
|
|
5811
7348
|
},
|
|
5812
7349
|
onGoalArmed: (armed) => {
|
|
5813
|
-
this.setApplicability(opened.task.id, opened.execution.id, team ? "team-member" : !contract.enabled ? skippedBy : armed ? "enforced" :
|
|
7350
|
+
this.setApplicability(opened.task.id, opened.execution.id, team ? "team-member" : !contract.enabled ? skippedBy : armed ? "enforced" : notArmed);
|
|
5814
7351
|
}
|
|
5815
7352
|
});
|
|
5816
7353
|
if (attached === void 0) this.ledger.attachSession(opened.task.id, opened.execution.id, sessionId);
|
|
5817
7354
|
if (team) for (const teammate of others) this.scheduleTeammate(teammate, sessionId);
|
|
5818
7355
|
} catch (error) {
|
|
5819
|
-
|
|
7356
|
+
const launchedSession = error instanceof SessionLaunchError ? error.sessionId : void 0;
|
|
7357
|
+
if (launchedSession !== void 0) this.ledger.attachSession(opened.task.id, opened.execution.id, launchedSession);
|
|
7358
|
+
if (schedule !== void 0 && attached === void 0 && launchedSession === void 0) this.refundUnstartedSchedule(opened.task.id);
|
|
5820
7359
|
this.settleAndNotify(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
5821
7360
|
}
|
|
5822
7361
|
}
|
|
7362
|
+
/**
|
|
7363
|
+
* Read the acceptance contract one run group freezes, in the order the board
|
|
7364
|
+
* has always read it: the acceptance settings, then the global native-/goal
|
|
7365
|
+
* switch, then the model catalog that both resolves the contract's route and
|
|
7366
|
+
* decides the reasoning effort the session may be given.
|
|
7367
|
+
*/
|
|
7368
|
+
async freezeRunContract() {
|
|
7369
|
+
return {
|
|
7370
|
+
settings: this.verificationSettings(),
|
|
7371
|
+
globalGoalEnabled: this.goalRunEnabled() === true,
|
|
7372
|
+
catalog: await this.verificationCatalog()
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7375
|
+
/**
|
|
7376
|
+
* A {@link RunContractFreezer} that reads once and hands the SAME value to
|
|
7377
|
+
* every later caller of one run group. A read that fails is not cached: the
|
|
7378
|
+
* next member reads again, exactly as a per-execution read would have.
|
|
7379
|
+
*/
|
|
7380
|
+
contractFreezerForRunGroup() {
|
|
7381
|
+
let pending;
|
|
7382
|
+
return () => {
|
|
7383
|
+
pending ??= this.freezeRunContract().catch((error) => {
|
|
7384
|
+
pending = void 0;
|
|
7385
|
+
throw error;
|
|
7386
|
+
});
|
|
7387
|
+
return pending;
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
/**
|
|
7391
|
+
* Give back the occurrence a launch consumed, for a launch that never reached
|
|
7392
|
+
* a session: a failed launch, or one the Host abandoned while it was still
|
|
7393
|
+
* waiting for an admission slot. A manual run, and a launch that DID reach a
|
|
7394
|
+
* session, are never refunded.
|
|
7395
|
+
*/
|
|
7396
|
+
refundUnstartedSchedule(taskId) {
|
|
7397
|
+
this.ledger.refundScheduledOccurrence(taskId, this.now());
|
|
7398
|
+
this.refreshSchedule();
|
|
7399
|
+
}
|
|
5823
7400
|
scheduleTeammate(opened, leadSessionId) {
|
|
5824
|
-
this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
7401
|
+
this.enqueueLaunch(opened, () => this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
5825
7402
|
safeConsoleError("[dsh-task-board] teammate spawn settlement failed", error);
|
|
5826
|
-
});
|
|
7403
|
+
}));
|
|
5827
7404
|
}
|
|
5828
7405
|
/**
|
|
5829
7406
|
* Spawn one teammate inside the Lead session and attach the teammate's
|
|
@@ -5854,31 +7431,70 @@ var TaskBoardHostService = class {
|
|
|
5854
7431
|
this.settleAndNotify(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
5855
7432
|
}
|
|
5856
7433
|
}
|
|
7434
|
+
/**
|
|
7435
|
+
* One roster pass, and whether it could read the session tree.
|
|
7436
|
+
*
|
|
7437
|
+
* The board reads the DSH session roster only while it has something the
|
|
7438
|
+
* roster can decide: an execution to inspect, or a card parked in the running
|
|
7439
|
+
* column whose verdict may arrive from a settle this process never saw. That
|
|
7440
|
+
* gate is what removes the cost the reporter measured — `session/list`
|
|
7441
|
+
* rebuilds every persisted session row (string conversion, object allocation,
|
|
7442
|
+
* a stat per record), and an idle board with no tasks was paying it every few
|
|
7443
|
+
* seconds forever.
|
|
7444
|
+
* @returns whether the roster was readable (an idle pass counts as readable).
|
|
7445
|
+
*/
|
|
5857
7446
|
async pollSessions() {
|
|
5858
|
-
if (this.disposed) return;
|
|
5859
|
-
|
|
7447
|
+
if (this.disposed) return true;
|
|
7448
|
+
const runtime = this.ledger.runtimeView();
|
|
7449
|
+
if (!this.active) {
|
|
7450
|
+
if (runtime.openExecutions.length === 0 && runtime.armedSchedules === 0) return true;
|
|
7451
|
+
} else if (!runtime.needsSessionState) return true;
|
|
5860
7452
|
const running = await this.runner.listRunning();
|
|
5861
7453
|
const previous = this.power.snapshot();
|
|
5862
7454
|
if (!running.known) {
|
|
5863
|
-
this.idleSessionIds = void 0;
|
|
5864
7455
|
this.power.updateReasons({
|
|
5865
7456
|
runningSessions: previous.runningSessions,
|
|
5866
|
-
armedSchedules:
|
|
7457
|
+
armedSchedules: runtime.armedSchedules,
|
|
5867
7458
|
sessionStateKnown: false
|
|
5868
7459
|
});
|
|
5869
|
-
return;
|
|
7460
|
+
return false;
|
|
5870
7461
|
}
|
|
5871
|
-
this.idleSessionIds = new Set(running.items.filter((item) => !item.running).map((item) => item.sessionId));
|
|
5872
7462
|
const foldedBefore = this.statusMap();
|
|
5873
7463
|
this.ledger.finalizeReadyRuns();
|
|
5874
7464
|
this.emitStatusChanges(foldedBefore);
|
|
5875
|
-
const
|
|
7465
|
+
const current = this.ledger.runtimeView();
|
|
5876
7466
|
this.power.updateReasons({
|
|
5877
7467
|
runningSessions: running.count,
|
|
5878
|
-
armedSchedules:
|
|
7468
|
+
armedSchedules: current.armedSchedules,
|
|
5879
7469
|
sessionStateKnown: true
|
|
5880
7470
|
});
|
|
5881
|
-
await this.reconcileExecutions(running.items,
|
|
7471
|
+
await this.reconcileExecutions(running.items, current.openExecutions);
|
|
7472
|
+
return true;
|
|
7473
|
+
}
|
|
7474
|
+
/**
|
|
7475
|
+
* The session a run may continue in, for a card that opted into reuse
|
|
7476
|
+
* (issue #1419). Reuse requires positive evidence that the previous session
|
|
7477
|
+
* is present and idle, and that evidence must be read for THIS launch: an
|
|
7478
|
+
* idle board no longer polls, so a roster cached from an earlier pass could
|
|
7479
|
+
* be hours old — either refusing a session that has been idle all along, or
|
|
7480
|
+
* prompting into one that started running since. A card that did not opt in
|
|
7481
|
+
* reads no roster at all, and a read that fails mints a fresh conversation
|
|
7482
|
+
* exactly as an unknown roster always did.
|
|
7483
|
+
* @param task - the task about to run.
|
|
7484
|
+
* @returns the session id to continue in, or undefined for a fresh session.
|
|
7485
|
+
*/
|
|
7486
|
+
async reuseSessionFor(task) {
|
|
7487
|
+
if (task.reuseSession !== true) return void 0;
|
|
7488
|
+
let running;
|
|
7489
|
+
try {
|
|
7490
|
+
running = await this.runner.listRunning();
|
|
7491
|
+
} catch (error) {
|
|
7492
|
+
this.notePollFailure();
|
|
7493
|
+
safeConsoleError("[dsh-task-board] session roster read for session reuse failed; starting a fresh session", error);
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7496
|
+
if (!running.known) return void 0;
|
|
7497
|
+
return reusableSessionId(task, new Set(running.items.filter((item) => !item.running).map((item) => item.sessionId)));
|
|
5882
7498
|
}
|
|
5883
7499
|
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
5884
7500
|
async reconcileExecutions(sessions, executions) {
|
|
@@ -5898,7 +7514,7 @@ var TaskBoardHostService = class {
|
|
|
5898
7514
|
}
|
|
5899
7515
|
this.unreadablePolls.delete(execution.executionId);
|
|
5900
7516
|
if (result.outcome === "succeeded" && verificationRequired(verification) && passedAttempt(verification) === void 0) {
|
|
5901
|
-
this.settleAndNotify(execution.taskId, execution.executionId, "failed",
|
|
7517
|
+
this.settleAndNotify(execution.taskId, execution.executionId, "failed", verificationNeverInvoked(verification) ? NEVER_INVOKED_VERIFICATION_REASON : verificationOnlyInvalid(verification) ? INVALID_ONLY_VERIFICATION_REASON : NO_MATCHING_PASS_VERIFICATION_REASON);
|
|
5902
7518
|
continue;
|
|
5903
7519
|
}
|
|
5904
7520
|
this.settleAndNotify(execution.taskId, execution.executionId, result.outcome, "error" in result ? result.error : void 0);
|
|
@@ -5936,6 +7552,59 @@ var TaskBoardHostService = class {
|
|
|
5936
7552
|
outcome,
|
|
5937
7553
|
...error === void 0 ? {} : { error }
|
|
5938
7554
|
});
|
|
7555
|
+
if (outcome === "succeeded") this.cleanupAcceptanceDetail(taskId, executionId);
|
|
7556
|
+
}
|
|
7557
|
+
/**
|
|
7558
|
+
* Remove one execution's acceptance-only detail after it PASSED and settled.
|
|
7559
|
+
*
|
|
7560
|
+
* Deliberately narrow: only the acceptance mechanism's own per-attempt
|
|
7561
|
+
* material (findings, invalid diagnostics, the bulky judge side) is removed,
|
|
7562
|
+
* and only for THIS execution, keyed by its own id in the ledger it belongs
|
|
7563
|
+
* to. No user file, task artifact, raw session history or other execution's
|
|
7564
|
+
* record is read or written. Idempotent by the record's own state, and it
|
|
7565
|
+
* clearly never touches a run that did not pass (the caller's guard) — a
|
|
7566
|
+
* failed or invalid execution keeps everything a repair or a review needs.
|
|
7567
|
+
* @param taskId - the task whose execution settled.
|
|
7568
|
+
* @param executionId - the settled execution.
|
|
7569
|
+
*/
|
|
7570
|
+
cleanupAcceptanceDetail(taskId, executionId) {
|
|
7571
|
+
const verification = (this.ledger.getTask(taskId)?.executions.find((entry) => entry.id === executionId))?.verification;
|
|
7572
|
+
if (verification === void 0 || passedAttempt(verification) === void 0) return;
|
|
7573
|
+
if (verification.cleanup?.state === "cleaned") return;
|
|
7574
|
+
try {
|
|
7575
|
+
const cleaned = withAcceptanceDetailCleared(verification, this.now());
|
|
7576
|
+
if (cleaned === void 0) return;
|
|
7577
|
+
this.ledger.setVerification(taskId, executionId, cleaned);
|
|
7578
|
+
} catch (error) {
|
|
7579
|
+
const attempts = (verification.cleanup?.attempts ?? 0) + 1;
|
|
7580
|
+
this.ledger.setVerification(taskId, executionId, {
|
|
7581
|
+
...verification,
|
|
7582
|
+
cleanup: {
|
|
7583
|
+
state: "failed",
|
|
7584
|
+
attempts,
|
|
7585
|
+
startedAt: verification.cleanup?.startedAt ?? this.now(),
|
|
7586
|
+
lastError: error instanceof Error ? error.message : String(error),
|
|
7587
|
+
audit: verification.cleanup?.audit ?? []
|
|
7588
|
+
}
|
|
7589
|
+
});
|
|
7590
|
+
safeConsoleError("[dsh-task-board] acceptance-detail cleanup failed; it is retried on the next Host start", error);
|
|
7591
|
+
}
|
|
7592
|
+
}
|
|
7593
|
+
/**
|
|
7594
|
+
* Catch-up pass: clean the acceptance detail of every execution that PASSED
|
|
7595
|
+
* and settled but whose cleanup did not finish (a crash, a failed write, a
|
|
7596
|
+
* Host killed mid-cleanup).
|
|
7597
|
+
*
|
|
7598
|
+
* The same bounds as any other cleanup apply — only a real pass record on a
|
|
7599
|
+
* settled execution is touched — so this can never delete material belonging
|
|
7600
|
+
* to an execution that did not pass, and never a user file.
|
|
7601
|
+
*/
|
|
7602
|
+
cleanupPendingAcceptanceDetail() {
|
|
7603
|
+
for (const task of this.ledger.allTasks()) for (const execution of task.executions) {
|
|
7604
|
+
if (execution.endedAt === void 0) continue;
|
|
7605
|
+
if (execution.result !== "succeeded") continue;
|
|
7606
|
+
this.cleanupAcceptanceDetail(task.id, execution.id);
|
|
7607
|
+
}
|
|
5939
7608
|
}
|
|
5940
7609
|
noteUnreadableInspection(execution, reason) {
|
|
5941
7610
|
const polls = (this.unreadablePolls.get(execution.executionId) ?? 0) + 1;
|
|
@@ -6003,14 +7672,12 @@ var TaskBoardHostService = class {
|
|
|
6003
7672
|
return;
|
|
6004
7673
|
}
|
|
6005
7674
|
const before = this.statusMap();
|
|
6006
|
-
for (const schedule of this.ledger.dueSchedules(now)) {
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
});
|
|
6013
|
-
}
|
|
7675
|
+
for (const schedule of this.ledger.dueSchedules(now)) this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, now), {
|
|
7676
|
+
triggeredAt: now,
|
|
7677
|
+
timeZone: schedule.timeZone,
|
|
7678
|
+
mode: schedule.mode,
|
|
7679
|
+
...schedule.cron === void 0 ? {} : { cron: schedule.cron }
|
|
7680
|
+
});
|
|
6014
7681
|
this.emitStatusChanges(before);
|
|
6015
7682
|
this.armSchedule();
|
|
6016
7683
|
}
|
|
@@ -6023,13 +7690,21 @@ var TaskBoardHostService = class {
|
|
|
6023
7690
|
* member then launches on its own so one refused participant cannot hold the
|
|
6024
7691
|
* others back. A team run's members are spawned inside the root's Lead
|
|
6025
7692
|
* session instead, once that session exists.
|
|
7693
|
+
*
|
|
7694
|
+
* Every launch of one run set goes through the admission queue, which admits
|
|
7695
|
+
* at most `runLaunchWidth` of them at a time — the whole subtree still opens,
|
|
7696
|
+
* members just enter the running column in batches. Order inside the group is
|
|
7697
|
+
* the order they are enqueued here, so the root's session always exists before
|
|
7698
|
+
* any member's, and one member's launch settling (or failing) is what admits
|
|
7699
|
+
* the next. All members share the one contract this group freezes.
|
|
6026
7700
|
*/
|
|
6027
7701
|
dispatchRuns(runs, schedule) {
|
|
6028
7702
|
if (runs.length === 0) return;
|
|
6029
7703
|
const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
|
|
6030
7704
|
const others = runs.filter((run) => run !== root);
|
|
6031
|
-
this.
|
|
6032
|
-
|
|
7705
|
+
const freezeContract = this.contractFreezerForRunGroup();
|
|
7706
|
+
this.scheduleLaunch(root, others, schedule, freezeContract);
|
|
7707
|
+
for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run, [], schedule, freezeContract);
|
|
6033
7708
|
}
|
|
6034
7709
|
/**
|
|
6035
7710
|
* Move one execution's applicability without touching its frozen contract or
|
|
@@ -6043,21 +7718,92 @@ var TaskBoardHostService = class {
|
|
|
6043
7718
|
applicability
|
|
6044
7719
|
});
|
|
6045
7720
|
}
|
|
6046
|
-
scheduleLaunch(opened, others = [], schedule) {
|
|
6047
|
-
this.launch(opened, others, schedule).catch((error) => {
|
|
7721
|
+
scheduleLaunch(opened, others = [], schedule, freezeContract) {
|
|
7722
|
+
this.enqueueLaunch(opened, () => this.launch(opened, others, schedule, freezeContract).catch((error) => {
|
|
6048
7723
|
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
6049
|
-
});
|
|
7724
|
+
}), schedule);
|
|
7725
|
+
}
|
|
7726
|
+
/**
|
|
7727
|
+
* Hand one launch to the admission queue, or abandon it on the spot when the
|
|
7728
|
+
* Host is already stopping: a queue entry created after {@link dispose} would
|
|
7729
|
+
* otherwise never be drained, and its card would sit in the running column
|
|
7730
|
+
* until the next Host start recovered it.
|
|
7731
|
+
*/
|
|
7732
|
+
enqueueLaunch(opened, start, schedule) {
|
|
7733
|
+
const launch = {
|
|
7734
|
+
groupId: opened.execution.runGroupId ?? opened.task.id,
|
|
7735
|
+
start: async () => {
|
|
7736
|
+
if (!this.isExecutionOpen(opened)) return;
|
|
7737
|
+
await start();
|
|
7738
|
+
},
|
|
7739
|
+
abandon: () => this.abandonQueuedRun(opened, schedule)
|
|
7740
|
+
};
|
|
7741
|
+
if (this.disposed) {
|
|
7742
|
+
launch.abandon();
|
|
7743
|
+
return;
|
|
7744
|
+
}
|
|
7745
|
+
this.launches.enqueue(launch);
|
|
7746
|
+
}
|
|
7747
|
+
/** Whether one opened execution is still open in the ledger. */
|
|
7748
|
+
isExecutionOpen(opened) {
|
|
7749
|
+
const execution = this.ledger.getTask(opened.task.id)?.executions.find((entry) => entry.id === opened.execution.id);
|
|
7750
|
+
return execution !== void 0 && execution.endedAt === void 0;
|
|
7751
|
+
}
|
|
7752
|
+
/**
|
|
7753
|
+
* Take one launch that never started out of the running column.
|
|
7754
|
+
*
|
|
7755
|
+
* Settled as CANCELLED, not failed: nothing about the run's work went wrong —
|
|
7756
|
+
* the Host stopped before the launch got a slot, which is exactly what the
|
|
7757
|
+
* next start's interrupted-start recovery records for an execution that never
|
|
7758
|
+
* reached a session. A scheduled occurrence that never reached a session is
|
|
7759
|
+
* refunded on the same terms as any other pre-session failure, so a run the
|
|
7760
|
+
* Host never started cannot silently spend the user's run budget.
|
|
7761
|
+
*/
|
|
7762
|
+
abandonQueuedRun(opened, schedule) {
|
|
7763
|
+
if (schedule !== void 0) this.refundUnstartedSchedule(opened.task.id);
|
|
7764
|
+
this.settleAndNotify(opened.task.id, opened.execution.id, "cancelled", ABANDONED_LAUNCH_REASON);
|
|
6050
7765
|
}
|
|
6051
7766
|
schedulePoll() {
|
|
6052
7767
|
if (this.pollInFlight || this.disposed) return;
|
|
6053
7768
|
this.pollInFlight = true;
|
|
6054
|
-
this.pollSessions().
|
|
7769
|
+
this.pollSessions().then((rosterReadable) => {
|
|
7770
|
+
if (rosterReadable) this.consecutivePollFailures = 0;
|
|
7771
|
+
else this.notePollFailure();
|
|
7772
|
+
}).catch((error) => {
|
|
7773
|
+
this.notePollFailure();
|
|
6055
7774
|
safeConsoleError("[dsh-task-board] session polling failed", error);
|
|
6056
7775
|
}).finally(() => {
|
|
6057
7776
|
this.pollInFlight = false;
|
|
6058
7777
|
});
|
|
6059
7778
|
}
|
|
6060
7779
|
/**
|
|
7780
|
+
* Count one failed pass and arm the retry that follows it.
|
|
7781
|
+
*
|
|
7782
|
+
* A session tree that is down is retried on a doubling delay instead of
|
|
7783
|
+
* hammering it on the fixed cadence: the heartbeat would otherwise repeat
|
|
7784
|
+
* every failure — and every retry inside {@link HostExecutionRunner.listRunning}
|
|
7785
|
+
* — at exactly the interval the reporter measured.
|
|
7786
|
+
*/
|
|
7787
|
+
notePollFailure() {
|
|
7788
|
+
this.consecutivePollFailures += 1;
|
|
7789
|
+
this.armPollBackoff();
|
|
7790
|
+
}
|
|
7791
|
+
/** Arm the retry that follows a failed pass, doubling up to the ceiling. */
|
|
7792
|
+
armPollBackoff() {
|
|
7793
|
+
if (this.disposed) return;
|
|
7794
|
+
this.pollBackoffTimer?.();
|
|
7795
|
+
const delay = Math.min(sessionPollMs(this.sessionPollSeconds) * 2 ** Math.min(this.consecutivePollFailures, 8), POLL_FAILURE_MAX_BACKOFF_MS);
|
|
7796
|
+
this.pollBackoffTimer = this.timers.timeout(() => {
|
|
7797
|
+
this.pollBackoffTimer = void 0;
|
|
7798
|
+
this.schedulePoll();
|
|
7799
|
+
}, delay);
|
|
7800
|
+
}
|
|
7801
|
+
/** Drop a pending retry; the next regular tick takes over. */
|
|
7802
|
+
clearPollBackoff() {
|
|
7803
|
+
this.pollBackoffTimer?.();
|
|
7804
|
+
this.pollBackoffTimer = void 0;
|
|
7805
|
+
}
|
|
7806
|
+
/**
|
|
6061
7807
|
* Re-arm from the ledger's current targets. Callers that just changed a
|
|
6062
7808
|
* schedule (the host routes, the agent tools) invoke this after the write
|
|
6063
7809
|
* commits, so a new or edited trigger arms without waiting for the next fire.
|
|
@@ -6314,6 +8060,64 @@ async function parseTaskDraft(llm, request, signal) {
|
|
|
6314
8060
|
signal?.removeEventListener("abort", abortFromCaller);
|
|
6315
8061
|
}
|
|
6316
8062
|
}
|
|
8063
|
+
/**
|
|
8064
|
+
* Total ceiling of one acceptance attempt, covering evidence rendering and
|
|
8065
|
+
* every judge call including retries. Derived from the per-call default rather
|
|
8066
|
+
* than pinned, because a total budget smaller than
|
|
8067
|
+
* {@link ACCEPTANCE_JUDGE_CALLS} times the per-call ceiling guarantees that no
|
|
8068
|
+
* acceptance can ever finish.
|
|
8069
|
+
*/
|
|
8070
|
+
const DEFAULT_VERIFICATION_BUDGET_SECONDS = 150 * (CODING_CRITERIA.length * 2 + 2);
|
|
8071
|
+
/** Longest accepted total budget, thirty minutes. */
|
|
8072
|
+
const VERIFICATION_BUDGET_MAX_SECONDS = 1800;
|
|
8073
|
+
/** The default total budget in milliseconds, the face the abort timer takes. */
|
|
8074
|
+
const DEFAULT_VERIFICATION_BUDGET_MS = DEFAULT_VERIFICATION_BUDGET_SECONDS * 1e3;
|
|
8075
|
+
/**
|
|
8076
|
+
* Clamp a configured ceiling into the supported range. A value the schema
|
|
8077
|
+
* would refuse (non-finite, below the floor, above the ceiling) still leaves
|
|
8078
|
+
* the board judging: an out-of-range setting never disables acceptance.
|
|
8079
|
+
* @param value - configured seconds, or undefined for the default.
|
|
8080
|
+
* @param fallback - the default to use when nothing usable was configured.
|
|
8081
|
+
* @param min - shortest accepted value.
|
|
8082
|
+
* @param max - longest accepted value.
|
|
8083
|
+
* @returns the effective value in seconds.
|
|
8084
|
+
*/
|
|
8085
|
+
function clampSeconds(value, fallback, min, max) {
|
|
8086
|
+
if (value === void 0 || !Number.isFinite(value)) return fallback;
|
|
8087
|
+
return Math.min(Math.max(Math.trunc(value), min), max);
|
|
8088
|
+
}
|
|
8089
|
+
/**
|
|
8090
|
+
* The effective per-judge-call ceiling.
|
|
8091
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
8092
|
+
* @returns the effective ceiling in seconds.
|
|
8093
|
+
*/
|
|
8094
|
+
function normalizeVerificationCallTimeoutSeconds(seconds) {
|
|
8095
|
+
return clampSeconds(seconds, 150, 30, 600);
|
|
8096
|
+
}
|
|
8097
|
+
/**
|
|
8098
|
+
* The effective per-judge-call ceiling the timer face takes.
|
|
8099
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
8100
|
+
* @returns the effective ceiling in milliseconds.
|
|
8101
|
+
*/
|
|
8102
|
+
function verificationCallTimeoutMs(seconds) {
|
|
8103
|
+
return normalizeVerificationCallTimeoutSeconds(seconds) * 1e3;
|
|
8104
|
+
}
|
|
8105
|
+
/**
|
|
8106
|
+
* The effective total budget of one acceptance attempt.
|
|
8107
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
8108
|
+
* @returns the effective budget in seconds.
|
|
8109
|
+
*/
|
|
8110
|
+
function normalizeVerificationBudgetSeconds(seconds) {
|
|
8111
|
+
return clampSeconds(seconds, DEFAULT_VERIFICATION_BUDGET_SECONDS, 120, VERIFICATION_BUDGET_MAX_SECONDS);
|
|
8112
|
+
}
|
|
8113
|
+
/**
|
|
8114
|
+
* The effective total budget the abort face takes.
|
|
8115
|
+
* @param seconds - configured seconds, or undefined for the default.
|
|
8116
|
+
* @returns the effective budget in milliseconds.
|
|
8117
|
+
*/
|
|
8118
|
+
function verificationBudgetMs(seconds) {
|
|
8119
|
+
return normalizeVerificationBudgetSeconds(seconds) * 1e3;
|
|
8120
|
+
}
|
|
6317
8121
|
//#endregion
|
|
6318
8122
|
//#region src/host/agent-tools.ts
|
|
6319
8123
|
/** Registered tool names, in registration order. */
|
|
@@ -6364,10 +8168,18 @@ function callingSessionId(exec) {
|
|
|
6364
8168
|
function scheduleView(schedule) {
|
|
6365
8169
|
return {
|
|
6366
8170
|
enabled: schedule.enabled,
|
|
6367
|
-
|
|
8171
|
+
mode: schedule.mode,
|
|
8172
|
+
...schedule.cron === void 0 ? {} : { cron: schedule.cron },
|
|
8173
|
+
...schedule.at === void 0 ? {} : { at: schedule.at },
|
|
6368
8174
|
...schedule.timeZone === void 0 ? {} : { timeZone: schedule.timeZone },
|
|
6369
8175
|
...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
|
|
6370
|
-
...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }
|
|
8176
|
+
...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt },
|
|
8177
|
+
runCount: schedule.runCount,
|
|
8178
|
+
...schedule.maxRuns === void 0 ? {} : { maxRuns: schedule.maxRuns },
|
|
8179
|
+
...schedule.endedAt === void 0 ? {} : { endedAt: schedule.endedAt },
|
|
8180
|
+
...schedule.endedReason === void 0 ? {} : { endedReason: schedule.endedReason },
|
|
8181
|
+
...schedule.skippedAt === void 0 ? {} : { skippedAt: schedule.skippedAt },
|
|
8182
|
+
...schedule.skippedReason === void 0 ? {} : { skippedReason: schedule.skippedReason }
|
|
6371
8183
|
};
|
|
6372
8184
|
}
|
|
6373
8185
|
function tagView(tag) {
|
|
@@ -6385,6 +8197,7 @@ function executionView(execution) {
|
|
|
6385
8197
|
...execution.result === void 0 ? {} : { result: execution.result },
|
|
6386
8198
|
...execution.error === void 0 ? {} : { error: execution.error },
|
|
6387
8199
|
...execution.initiatedBy === void 0 ? {} : { initiatedBy: execution.initiatedBy },
|
|
8200
|
+
...execution.external === true ? { external: true } : {},
|
|
6388
8201
|
...execution.runGroupId === void 0 ? {} : { cascade: true },
|
|
6389
8202
|
...execution.ownResult === void 0 ? {} : {
|
|
6390
8203
|
ownResult: execution.ownResult,
|
|
@@ -6413,9 +8226,10 @@ function taskSummary(task, tasks, sessionDefault) {
|
|
|
6413
8226
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
6414
8227
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
6415
8228
|
...task.model === void 0 ? {} : { model: task.model },
|
|
8229
|
+
...task.reasoningEffort === void 0 ? {} : { reasoningEffort: task.reasoningEffort },
|
|
6416
8230
|
...task.reuseSession === true ? { reuseSession: true } : {},
|
|
6417
8231
|
...task.teamRun === true ? { teamRun: true } : {},
|
|
6418
|
-
...task.goalRun ===
|
|
8232
|
+
...task.goalRun === true ? { goalRun: true } : {},
|
|
6419
8233
|
...task.skipVerification === true ? { skipVerification: true } : {},
|
|
6420
8234
|
...task.schedule === void 0 ? {} : { schedule: scheduleView(task.schedule) },
|
|
6421
8235
|
...task.freeze === void 0 ? {} : { continuationCard: true },
|
|
@@ -6510,7 +8324,7 @@ function buildSetParentTool(host) {
|
|
|
6510
8324
|
"Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.",
|
|
6511
8325
|
"Pass an empty parentId to detach.",
|
|
6512
8326
|
"The Host refuses the link when the parent is unknown or archived, when the moved task is one of its own descendants, when the resulting depth exceeds the deployment subtask limit (default one level, maximum three), or while the task has an unsettled execution (its link may still be a running cascade parent pointer).",
|
|
6513
|
-
"Running a task
|
|
8327
|
+
"Running a task opens one run group over its subtree concurrently and leaves a subtask that already sits in the done column alone; a parent card settles only after its own turn and every direct subtask it opened have settled."
|
|
6514
8328
|
].join(" "),
|
|
6515
8329
|
parameters: {
|
|
6516
8330
|
taskId: {
|
|
@@ -6562,7 +8376,7 @@ function buildRunTool(host) {
|
|
|
6562
8376
|
taskId: {
|
|
6563
8377
|
type: "string",
|
|
6564
8378
|
required: true,
|
|
6565
|
-
description: "The task to run; running a root task runs its whole subtree."
|
|
8379
|
+
description: "The task to run; running a root task runs its whole subtree, minus the subtasks that are already in the done column."
|
|
6566
8380
|
},
|
|
6567
8381
|
rerun: {
|
|
6568
8382
|
type: "boolean",
|
|
@@ -6610,7 +8424,9 @@ function buildManageTool(host) {
|
|
|
6610
8424
|
"Move, archive, restore, delete, or settle one task board card.",
|
|
6611
8425
|
"move-backlog, move-todo, move-running, move-done and move-failed are the manual column moves and cover every column; a card the runner is executing (one with an open execution) cannot be moved.",
|
|
6612
8426
|
"A manual move writes the card column only and never fabricates an execution record, so the card reports a declaration rather than evidence of a run: done/failed declare work finished (or failed) outside a Host-run execution — human work, an external system, a decision made elsewhere — and running says the work is under way without a tracked session. Use task_board_run when the work should actually run in a session here.",
|
|
8427
|
+
"record-external-outcome is the record-backed version of move-done and move-failed: use it when YOU, or another agent outside this Host, actually finished or failed the card with your own model, so the terminal column and the run history both derive from one execution record. It takes result (succeeded or failed) and initiatedBy (who completed it, for example your own agent name) plus an optional summary, and it is refused while the card still has an open execution: the Host stays the only authority over a run it is executing. It cannot record running or cancelled — those remain Host-only.",
|
|
6613
8428
|
"settle force-closes the open execution of a card the board can no longer observe (a stuck running card) and records it cancelled with the caller as the reason, so the card returns to the todo column and can be run again.",
|
|
8429
|
+
"reset-verification clears the acceptance anomalies (timeouts, authentication failures, an unresolvable judge route, or an attempt the time budget ended) recorded on a card open execution, so the next update_goal(action: complete) runs a fresh acceptance. It is refused unless that execution really carries such anomalies, it never clears a quality verdict, and a spent anomaly budget otherwise HOLDS the card open instead of failing it: tell the user the acceptance environment is unusable and that this action is how they clear it once they have fixed it.",
|
|
6614
8430
|
"archive takes the whole subtask tree off the board and is refused while any member has an unsettled execution; restore brings the task, its ancestors and its subtree back; delete removes one card and is refused while it still has subtasks (detach or delete them first) or while it runs.",
|
|
6615
8431
|
"It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI."
|
|
6616
8432
|
].join(" "),
|
|
@@ -6632,9 +8448,24 @@ function buildManageTool(host) {
|
|
|
6632
8448
|
"archive",
|
|
6633
8449
|
"restore",
|
|
6634
8450
|
"delete",
|
|
6635
|
-
"settle"
|
|
8451
|
+
"settle",
|
|
8452
|
+
"reset-verification",
|
|
8453
|
+
"record-external-outcome"
|
|
6636
8454
|
],
|
|
6637
8455
|
description: "The lifecycle operation to perform."
|
|
8456
|
+
},
|
|
8457
|
+
result: {
|
|
8458
|
+
type: "string",
|
|
8459
|
+
enum: ["succeeded", "failed"],
|
|
8460
|
+
description: "record-external-outcome only: the verdict the outside agent actually reached."
|
|
8461
|
+
},
|
|
8462
|
+
initiatedBy: {
|
|
8463
|
+
type: "string",
|
|
8464
|
+
description: "record-external-outcome only: who finished the work outside this Host (for example your own agent name)."
|
|
8465
|
+
},
|
|
8466
|
+
summary: {
|
|
8467
|
+
type: "string",
|
|
8468
|
+
description: "record-external-outcome only: optional free-text note stored with the recorded outcome."
|
|
6638
8469
|
}
|
|
6639
8470
|
},
|
|
6640
8471
|
output: {
|
|
@@ -6643,6 +8474,31 @@ function buildManageTool(host) {
|
|
|
6643
8474
|
},
|
|
6644
8475
|
async execute(args, exec) {
|
|
6645
8476
|
const initiator = callingSessionId(exec);
|
|
8477
|
+
if (args.action === "record-external-outcome") {
|
|
8478
|
+
const result = args.result;
|
|
8479
|
+
const by = typeof args.initiatedBy === "string" ? args.initiatedBy.trim() : "";
|
|
8480
|
+
if (result !== "succeeded" && result !== "failed") return refused("missing-result", "record-external-outcome needs result=succeeded or result=failed");
|
|
8481
|
+
if (by === "") return refused("missing-initiator", "record-external-outcome needs initiatedBy naming who completed the work outside this Host");
|
|
8482
|
+
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
8483
|
+
try {
|
|
8484
|
+
const snapshot = await host.apply(crypto.randomUUID(), {
|
|
8485
|
+
kind: "record-external-outcome",
|
|
8486
|
+
taskId: args.taskId,
|
|
8487
|
+
result,
|
|
8488
|
+
initiatedBy: by,
|
|
8489
|
+
...typeof args.summary === "string" && args.summary.trim() !== "" ? { summary: args.summary } : {}
|
|
8490
|
+
}, initiator);
|
|
8491
|
+
const task = snapshot.tasks.find((item) => item.id === args.taskId);
|
|
8492
|
+
return json({
|
|
8493
|
+
ok: true,
|
|
8494
|
+
action: args.action,
|
|
8495
|
+
...task === void 0 ? {} : { task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission) },
|
|
8496
|
+
note: "The outcome is recorded as its own execution, so the terminal column and the run history agree. Read it back with task_board_get."
|
|
8497
|
+
});
|
|
8498
|
+
} catch (error) {
|
|
8499
|
+
return refused("refused", messageOf(error));
|
|
8500
|
+
}
|
|
8501
|
+
}
|
|
6646
8502
|
const action = {
|
|
6647
8503
|
"move-todo": {
|
|
6648
8504
|
kind: "move",
|
|
@@ -6677,6 +8533,10 @@ function buildManageTool(host) {
|
|
|
6677
8533
|
kind: "settle",
|
|
6678
8534
|
taskId: args.taskId
|
|
6679
8535
|
},
|
|
8536
|
+
"reset-verification": {
|
|
8537
|
+
kind: "reset-verification",
|
|
8538
|
+
taskId: args.taskId
|
|
8539
|
+
},
|
|
6680
8540
|
restore: {
|
|
6681
8541
|
kind: "restore",
|
|
6682
8542
|
taskId: args.taskId
|
|
@@ -6712,11 +8572,12 @@ function buildScheduleTool(host) {
|
|
|
6712
8572
|
return defineTool({
|
|
6713
8573
|
name: "task_board_schedule",
|
|
6714
8574
|
description: [
|
|
6715
|
-
"Arm, change, or disarm a task scheduled runs
|
|
6716
|
-
"
|
|
6717
|
-
"
|
|
6718
|
-
"A
|
|
6719
|
-
"
|
|
8575
|
+
"Arm, change, or disarm a task scheduled runs. Two kinds of plan exist: a recurring 5-field cron rule (day-of-month and day-of-week follow Vixie semantics: both restricted means OR, otherwise AND), and a one-shot that runs once at a future instant.",
|
|
8576
|
+
"For a one-shot pass mode \"once\" with at (the planned instant, ms since epoch UTC). For a recurring rule pass mode \"cron\" (or omit mode) with a cron expression, optionally capped by maxRuns (a positive whole number of scheduled executions; omit for unlimited, pass 0 to clear a cap).",
|
|
8577
|
+
"The wall clock is read in the rule stored IANA time zone; omit timeZone to use the Host zone reported by task_board_list/task_board_get. DST gaps are skipped and an ambiguous fall-back time fires once, at the earlier instant.",
|
|
8578
|
+
"A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree minus the subtasks already in the done column.",
|
|
8579
|
+
"The scheduler counts executions it actually starts; once a rule reaches its run budget it stops itself in the ledger, and a manual run never consumes the budget. A schedule whose tree contains an unconfirmed above-default permission is refused and does not consume the budget; the reason is reported in the list board summary as schedulerError.",
|
|
8580
|
+
"Missed occurrences during Host downtime are skipped, never queued (a one-shot whose instant passed while the Host was down is stopped as missed); a task that is already running skips its occurrence without consuming the budget."
|
|
6720
8581
|
].join(" "),
|
|
6721
8582
|
parameters: {
|
|
6722
8583
|
taskId: {
|
|
@@ -6728,13 +8589,26 @@ function buildScheduleTool(host) {
|
|
|
6728
8589
|
type: "boolean",
|
|
6729
8590
|
description: "Arm (true) or disarm (false) the schedule."
|
|
6730
8591
|
},
|
|
8592
|
+
mode: {
|
|
8593
|
+
type: "string",
|
|
8594
|
+
enum: ["cron", "once"],
|
|
8595
|
+
description: "The kind of plan: \"cron\" for a recurring rule, \"once\" for a single future instant. Omit to keep the stored kind."
|
|
8596
|
+
},
|
|
6731
8597
|
cron: {
|
|
6732
8598
|
type: "string",
|
|
6733
|
-
description: "5-field cron expression: minute hour day-of-month month day-of-week."
|
|
8599
|
+
description: "5-field cron expression of a recurring rule: minute hour day-of-month month day-of-week."
|
|
8600
|
+
},
|
|
8601
|
+
at: {
|
|
8602
|
+
type: "integer",
|
|
8603
|
+
description: "Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future."
|
|
6734
8604
|
},
|
|
6735
8605
|
timeZone: {
|
|
6736
8606
|
type: "string",
|
|
6737
|
-
description: "IANA zone the
|
|
8607
|
+
description: "IANA zone the wall clock is read in (for example Asia/Shanghai). Omit to keep the stored zone, or pass an empty string to clear it back to the Host zone."
|
|
8608
|
+
},
|
|
8609
|
+
maxRuns: {
|
|
8610
|
+
type: "integer",
|
|
8611
|
+
description: "Maximum scheduled executions of a recurring rule (positive whole number). Omit to keep the stored cap; pass 0 to clear it back to unlimited."
|
|
6738
8612
|
}
|
|
6739
8613
|
},
|
|
6740
8614
|
output: {
|
|
@@ -6742,12 +8616,15 @@ function buildScheduleTool(host) {
|
|
|
6742
8616
|
render: renderJson
|
|
6743
8617
|
},
|
|
6744
8618
|
async execute(args, exec) {
|
|
6745
|
-
if (args.enabled === void 0 && args.cron === void 0 && args.timeZone === void 0) return refused("nothing-to-change", "pass enabled
|
|
8619
|
+
if (args.enabled === void 0 && args.mode === void 0 && args.cron === void 0 && args.at === void 0 && args.timeZone === void 0 && args.maxRuns === void 0) return refused("nothing-to-change", "pass enabled, mode, cron, at, timeZone and/or maxRuns");
|
|
6746
8620
|
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
6747
8621
|
const patch = {
|
|
6748
8622
|
...args.enabled === void 0 ? {} : { enabled: args.enabled },
|
|
8623
|
+
...args.mode === void 0 ? {} : { mode: args.mode },
|
|
6749
8624
|
...args.cron === void 0 ? {} : { cron: args.cron },
|
|
6750
|
-
...args.
|
|
8625
|
+
...args.at === void 0 ? {} : { at: args.at },
|
|
8626
|
+
...args.timeZone === void 0 ? {} : { timeZone: args.timeZone === "" ? null : args.timeZone },
|
|
8627
|
+
...args.maxRuns === void 0 ? {} : { maxRuns: args.maxRuns <= 0 ? null : args.maxRuns }
|
|
6751
8628
|
};
|
|
6752
8629
|
try {
|
|
6753
8630
|
const snapshot = await host.apply(crypto.randomUUID(), {
|
|
@@ -6879,7 +8756,7 @@ function buildCreateTool(host) {
|
|
|
6879
8756
|
name: "task_board_create",
|
|
6880
8757
|
description: [
|
|
6881
8758
|
"Create a task on the DSH task board, or a subtask by passing parentId.",
|
|
6882
|
-
"A subtask inherits every execution target it does not override (workspace, agent preset, permission, model) from its parent, and inherits the parent human permission confirmation together with an inherited binding.",
|
|
8759
|
+
"A subtask inherits every execution target it does not override (workspace, agent preset, permission, model, reasoning effort) from its parent, and inherits the parent human permission confirmation together with an inherited binding.",
|
|
6883
8760
|
"The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.",
|
|
6884
8761
|
"The task lands in the todo column and does NOT run; call task_board_run to execute it."
|
|
6885
8762
|
].join(" "),
|
|
@@ -6907,16 +8784,20 @@ function buildCreateTool(host) {
|
|
|
6907
8784
|
},
|
|
6908
8785
|
mode: {
|
|
6909
8786
|
type: "string",
|
|
6910
|
-
description: "Agent preset id the execution session is composed from; omit
|
|
8787
|
+
description: "Agent preset id the execution session is composed from; omit to adopt the board new-card default, and the deployment default when none is configured."
|
|
6911
8788
|
},
|
|
6912
8789
|
permission: {
|
|
6913
8790
|
type: "string",
|
|
6914
8791
|
enum: [...TASK_PERMISSIONS],
|
|
6915
|
-
description: "Permission preset for the execution session
|
|
8792
|
+
description: "Permission preset for the execution session; omit to adopt the board new-card default. An omitted permission is never taken from a parent: the launch resolves the parent binding instead. A value above the session default needs a human confirmation in the board UI before the card can run."
|
|
6916
8793
|
},
|
|
6917
8794
|
model: {
|
|
6918
8795
|
type: "string",
|
|
6919
|
-
description: "Pinned model as provider/model (or a model id); omit
|
|
8796
|
+
description: "Pinned model as provider/model (or a model id); omit to adopt the board new-card default, and the host default when none is configured."
|
|
8797
|
+
},
|
|
8798
|
+
reasoningEffort: {
|
|
8799
|
+
type: "string",
|
|
8800
|
+
description: "Reasoning effort id for that model (an adapter-owned level such as \"high\"); omit to adopt the board new-card default for that model, and the model own default when none applies. Only an effort the target model declares is sent; an empty string clears it back to the default."
|
|
6920
8801
|
},
|
|
6921
8802
|
reuseSession: {
|
|
6922
8803
|
type: "boolean",
|
|
@@ -6928,7 +8809,7 @@ function buildCreateTool(host) {
|
|
|
6928
8809
|
},
|
|
6929
8810
|
goalRun: {
|
|
6930
8811
|
type: "boolean",
|
|
6931
|
-
description: "Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes.
|
|
8812
|
+
description: "Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes. Off by default: omit (or pass false) for a single plain turn, and pass true to opt this card in. It only takes effect while the deployment global native /goal switch is on."
|
|
6932
8813
|
},
|
|
6933
8814
|
skipVerification: {
|
|
6934
8815
|
type: "boolean",
|
|
@@ -6956,17 +8837,29 @@ function buildCreateTool(host) {
|
|
|
6956
8837
|
schedule: {
|
|
6957
8838
|
type: "object",
|
|
6958
8839
|
additionalProperties: false,
|
|
6959
|
-
description: "Arm a
|
|
8840
|
+
description: "Arm a schedule at creation time. Use mode \"once\" with at for a single future run, or mode \"cron\" (or omit mode) with a 5-field cron expression, optionally capped by maxRuns.",
|
|
6960
8841
|
properties: {
|
|
6961
8842
|
enabled: {
|
|
6962
8843
|
type: "boolean",
|
|
6963
8844
|
required: true,
|
|
6964
8845
|
description: "Whether the schedule is armed."
|
|
6965
8846
|
},
|
|
8847
|
+
mode: {
|
|
8848
|
+
type: "string",
|
|
8849
|
+
enum: ["cron", "once"],
|
|
8850
|
+
description: "The kind of plan: \"cron\" for a recurring rule, \"once\" for a single future instant."
|
|
8851
|
+
},
|
|
6966
8852
|
cron: {
|
|
6967
8853
|
type: "string",
|
|
6968
|
-
|
|
6969
|
-
|
|
8854
|
+
description: "5-field cron of a recurring rule: minute hour day-of-month month day-of-week."
|
|
8855
|
+
},
|
|
8856
|
+
at: {
|
|
8857
|
+
type: "integer",
|
|
8858
|
+
description: "Planned instant of a one-shot, in milliseconds since the Unix epoch (UTC). Must be in the future."
|
|
8859
|
+
},
|
|
8860
|
+
maxRuns: {
|
|
8861
|
+
type: "integer",
|
|
8862
|
+
description: "Maximum scheduled executions of a recurring rule (positive whole number); omit for unlimited."
|
|
6970
8863
|
}
|
|
6971
8864
|
}
|
|
6972
8865
|
}
|
|
@@ -6991,14 +8884,19 @@ function buildCreateTool(host) {
|
|
|
6991
8884
|
...args.mode === void 0 || args.mode === "" ? {} : { mode: args.mode },
|
|
6992
8885
|
...args.permission === void 0 ? {} : { permission: args.permission },
|
|
6993
8886
|
...args.model === void 0 || args.model === "" ? {} : { model: args.model },
|
|
8887
|
+
...args.reasoningEffort === void 0 || args.reasoningEffort === "" ? {} : { reasoningEffort: args.reasoningEffort },
|
|
8888
|
+
applyCardDefaults: true,
|
|
6994
8889
|
...args.reuseSession === true ? { reuseSession: true } : {},
|
|
6995
8890
|
...args.teamRun === true ? { teamRun: true } : {},
|
|
6996
|
-
...args.goalRun ===
|
|
8891
|
+
...args.goalRun === true ? { goalRun: true } : {},
|
|
6997
8892
|
...args.skipVerification === true ? { skipVerification: true } : {},
|
|
6998
8893
|
...tags.length === 0 ? {} : { tags },
|
|
6999
8894
|
...args.schedule === void 0 ? {} : { schedule: {
|
|
7000
8895
|
enabled: args.schedule.enabled,
|
|
7001
|
-
|
|
8896
|
+
...args.schedule.mode === void 0 ? {} : { mode: args.schedule.mode },
|
|
8897
|
+
...args.schedule.cron === void 0 ? {} : { cron: args.schedule.cron },
|
|
8898
|
+
...args.schedule.at === void 0 ? {} : { at: args.schedule.at },
|
|
8899
|
+
...args.schedule.maxRuns === void 0 ? {} : { maxRuns: args.schedule.maxRuns }
|
|
7002
8900
|
} }
|
|
7003
8901
|
};
|
|
7004
8902
|
try {
|
|
@@ -7023,7 +8921,7 @@ function buildUpdateTool(host) {
|
|
|
7023
8921
|
return defineTool({
|
|
7024
8922
|
name: "task_board_update",
|
|
7025
8923
|
description: [
|
|
7026
|
-
"Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, session reuse).",
|
|
8924
|
+
"Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, reasoning effort, session reuse).",
|
|
7027
8925
|
"Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.",
|
|
7028
8926
|
"Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.",
|
|
7029
8927
|
"Changing the pinned permission re-arms the permission confirmation gate."
|
|
@@ -7068,6 +8966,10 @@ function buildUpdateTool(host) {
|
|
|
7068
8966
|
type: "string",
|
|
7069
8967
|
description: "New pinned model; an empty string clears it."
|
|
7070
8968
|
},
|
|
8969
|
+
reasoningEffort: {
|
|
8970
|
+
type: "string",
|
|
8971
|
+
description: "New reasoning effort id for the pinned model; an empty string clears it back to the model own default. Only an effort the target model declares is sent at run time."
|
|
8972
|
+
},
|
|
7071
8973
|
reuseSession: {
|
|
7072
8974
|
type: "boolean",
|
|
7073
8975
|
description: "Continue later runs in the previous execution session."
|
|
@@ -7078,7 +8980,7 @@ function buildUpdateTool(host) {
|
|
|
7078
8980
|
},
|
|
7079
8981
|
goalRun: {
|
|
7080
8982
|
type: "boolean",
|
|
7081
|
-
description: "Whether each run starts with dsh built-in /goal (default
|
|
8983
|
+
description: "Whether each run starts with dsh built-in /goal (default false, one plain turn). Pass true to opt this card in, or false to return it to the default."
|
|
7082
8984
|
},
|
|
7083
8985
|
skipVerification: {
|
|
7084
8986
|
type: "boolean",
|
|
@@ -7116,10 +9018,11 @@ function buildUpdateTool(host) {
|
|
|
7116
9018
|
if (has(args, "workspaceId") && typeof args.workspaceId === "string") patch.workspaceId = args.workspaceId;
|
|
7117
9019
|
if (has(args, "mode") && typeof args.mode === "string") patch.mode = args.mode;
|
|
7118
9020
|
if (has(args, "model") && typeof args.model === "string") patch.model = args.model;
|
|
9021
|
+
if (has(args, "reasoningEffort") && typeof args.reasoningEffort === "string") patch.reasoningEffort = args.reasoningEffort;
|
|
7119
9022
|
if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
|
|
7120
9023
|
if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
|
|
7121
9024
|
if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
|
|
7122
|
-
if (has(args, "goalRun")) patch.goalRun = args.goalRun ===
|
|
9025
|
+
if (has(args, "goalRun")) patch.goalRun = args.goalRun === true ? true : false;
|
|
7123
9026
|
if (has(args, "skipVerification")) patch.skipVerification = args.skipVerification === true ? true : false;
|
|
7124
9027
|
if (has(args, "tags")) {
|
|
7125
9028
|
const tags = args.tags ?? [];
|
|
@@ -7702,8 +9605,11 @@ function mountOnce(packageName, fn) {
|
|
|
7702
9605
|
const VERIFICATION_MAX_TRACE_CHARS = 8e4;
|
|
7703
9606
|
/** Largest single trace entry; one huge tool result must not eat the budget. */
|
|
7704
9607
|
const VERIFICATION_MAX_ENTRY_CHARS = 4e3;
|
|
7705
|
-
/**
|
|
7706
|
-
const
|
|
9608
|
+
/** Largest verbatim citation kept from one finding. */
|
|
9609
|
+
const VERIFICATION_MAX_QUOTE_CHARS = 600;
|
|
9610
|
+
/** Largest requirement/observation/gap statement kept from one finding. */
|
|
9611
|
+
const VERIFICATION_MAX_PROBLEM_CHARS = 400;
|
|
9612
|
+
const VERIFICATION_MAX_FINDINGS = 12;
|
|
7707
9613
|
const VERIFICATION_MAX_TOKENS = 16384;
|
|
7708
9614
|
const VERIFICATION_TEMPERATURE = .2;
|
|
7709
9615
|
/** Redaction patterns, mirrored from the verifier's defaults. */
|
|
@@ -7858,8 +9764,29 @@ function collectEvidence(session, problem, startedAt) {
|
|
|
7858
9764
|
hash
|
|
7859
9765
|
};
|
|
7860
9766
|
}
|
|
7861
|
-
/**
|
|
7862
|
-
function
|
|
9767
|
+
/** How one cited slot maps onto the caller's own evidence under a swapped round. */
|
|
9768
|
+
function locationOfSlot(slot, round) {
|
|
9769
|
+
return slot === workSlotOfRound(round) ? "trajectory" : "baseline";
|
|
9770
|
+
}
|
|
9771
|
+
/** Resolve one `location` (or legacy `evidence`) attribute into an evidence location. */
|
|
9772
|
+
function readLocation(raw, round) {
|
|
9773
|
+
const value = raw.trim().toUpperCase();
|
|
9774
|
+
if (value === "TASK") return "task";
|
|
9775
|
+
if (value === "CONTEXT" || value === "WORKSPACE") return "workspace";
|
|
9776
|
+
if (value === "TRAJECTORY_A" || value === "A") return locationOfSlot("A", round);
|
|
9777
|
+
if (value === "TRAJECTORY_B" || value === "B") return locationOfSlot("B", round);
|
|
9778
|
+
}
|
|
9779
|
+
/**
|
|
9780
|
+
* Parse the `<finding ...>` elements out of one judge answer.
|
|
9781
|
+
*
|
|
9782
|
+
* A finding must name the criterion this prompt offered and cite an evidence
|
|
9783
|
+
* location the prompt actually rendered; a finding for another criterion, a
|
|
9784
|
+
* citation of a location that was never offered, and an empty body are dropped
|
|
9785
|
+
* here rather than echoed back as if the judge had located something. The
|
|
9786
|
+
* quote is carried verbatim: whether it really occurs in the evidence it names
|
|
9787
|
+
* is decided by the caller, which is the only side holding the evidence text.
|
|
9788
|
+
*/
|
|
9789
|
+
function parseFindings(text, criterion, round) {
|
|
7863
9790
|
const pattern = /<finding\s+([^>]*)>([\s\S]*?)<\/finding>/giu;
|
|
7864
9791
|
const attributes = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([^"]*)"/gu;
|
|
7865
9792
|
const found = [];
|
|
@@ -7869,25 +9796,35 @@ function parseFindings(text, criterion) {
|
|
|
7869
9796
|
for (let attribute = attributes.exec(match[1] ?? ""); attribute !== null; attribute = attributes.exec(match[1] ?? "")) if ([
|
|
7870
9797
|
"criterion",
|
|
7871
9798
|
"evidence",
|
|
9799
|
+
"location",
|
|
9800
|
+
"requirement",
|
|
9801
|
+
"observation",
|
|
9802
|
+
"gap",
|
|
9803
|
+
"quote",
|
|
7872
9804
|
"action"
|
|
7873
9805
|
].includes(attribute[1] ?? "")) attrs.set(attribute[1] ?? "", attribute[2] ?? "");
|
|
7874
9806
|
const named = attrs.get("criterion") ?? "";
|
|
7875
9807
|
if (named !== "" && named.toLowerCase() !== criterion.name.toLowerCase() && named.toLowerCase() !== criterion.id.toLowerCase()) continue;
|
|
7876
|
-
const
|
|
7877
|
-
if (
|
|
7878
|
-
|
|
7879
|
-
|
|
7880
|
-
|
|
7881
|
-
|
|
9808
|
+
const location = readLocation(attrs.get("location") ?? attrs.get("evidence") ?? "", round);
|
|
9809
|
+
if (location === void 0) continue;
|
|
9810
|
+
const quote = (attrs.get("quote") ?? "").trim().slice(0, VERIFICATION_MAX_QUOTE_CHARS);
|
|
9811
|
+
const requirement = (attrs.get("requirement") ?? "").trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS);
|
|
9812
|
+
const observation = (attrs.get("observation") ?? "").trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS);
|
|
9813
|
+
const gap = (attrs.get("gap") ?? "").trim().slice(0, VERIFICATION_MAX_PROBLEM_CHARS);
|
|
7882
9814
|
const body = (match[2] ?? "").trim().slice(0, 400);
|
|
7883
|
-
if (body === "") continue;
|
|
9815
|
+
if (body === "" && quote === "") continue;
|
|
7884
9816
|
const action = (attrs.get("action") ?? "").trim().slice(0, 300);
|
|
7885
|
-
const key =
|
|
9817
|
+
const key = criterion.id + "|" + location + "|" + quote + "|" + body;
|
|
7886
9818
|
if (seen.has(key)) continue;
|
|
7887
9819
|
seen.add(key);
|
|
7888
9820
|
found.push({
|
|
7889
|
-
|
|
7890
|
-
|
|
9821
|
+
criterionId: criterion.id,
|
|
9822
|
+
criterionName: criterion.name,
|
|
9823
|
+
location,
|
|
9824
|
+
quote,
|
|
9825
|
+
requirement,
|
|
9826
|
+
observation,
|
|
9827
|
+
gap,
|
|
7891
9828
|
body,
|
|
7892
9829
|
...action === "" ? {} : { action }
|
|
7893
9830
|
});
|
|
@@ -7896,20 +9833,31 @@ function parseFindings(text, criterion) {
|
|
|
7896
9833
|
}
|
|
7897
9834
|
/** Render one finding as the single feedback line the agent reads. */
|
|
7898
9835
|
function renderFinding(finding) {
|
|
7899
|
-
const locator = "[" +
|
|
9836
|
+
const locator = "[" + finding.criterionName + " " + finding.location + "]";
|
|
7900
9837
|
const action = finding.action === void 0 ? "" : " → 建议核实:" + finding.action;
|
|
7901
|
-
|
|
9838
|
+
const detail = finding.body !== "" ? finding.body : finding.quote;
|
|
9839
|
+
return locator + " " + detail + action;
|
|
7902
9840
|
}
|
|
7903
|
-
/**
|
|
7904
|
-
|
|
9841
|
+
/**
|
|
9842
|
+
* One judge request's outcome.
|
|
9843
|
+
*
|
|
9844
|
+
* Every attempt is admitted against the acceptance budget first: a call whose
|
|
9845
|
+
* own ceiling no longer fits in the time left is refused before it is opened,
|
|
9846
|
+
* so a slow route cannot spend the whole acceptance budget on calls the outer
|
|
9847
|
+
* abort is going to kill anyway.
|
|
9848
|
+
*/
|
|
9849
|
+
async function judgeOnce(llm, route, prompt, signal, budget) {
|
|
7905
9850
|
const usage = emptyUsage();
|
|
7906
9851
|
let lastError;
|
|
9852
|
+
/** The outer abort is the acceptance budget running out, not a cancellation. */
|
|
9853
|
+
const stopped = () => budget.now() >= budget.deadline ? new VerificationError("budget", "the acceptance budget ran out before the judge answered", usage) : new VerificationError("aborted", "the acceptance was cancelled", usage);
|
|
7907
9854
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
7908
|
-
if (signal.aborted) throw
|
|
9855
|
+
if (signal.aborted) throw stopped();
|
|
9856
|
+
if (budget.deadline - budget.now() <= budget.callTimeoutMs) throw new VerificationError("budget", "the acceptance budget cannot fit another judge call", usage);
|
|
7909
9857
|
const timeout = new AbortController();
|
|
7910
9858
|
const timer = setTimeout(() => {
|
|
7911
9859
|
timeout.abort(/* @__PURE__ */ new Error("task-board verification: the judge request timed out"));
|
|
7912
|
-
},
|
|
9860
|
+
}, budget.callTimeoutMs);
|
|
7913
9861
|
const forward = () => {
|
|
7914
9862
|
timeout.abort(signal.reason);
|
|
7915
9863
|
};
|
|
@@ -7944,7 +9892,7 @@ async function judgeOnce(llm, route, prompt, signal) {
|
|
|
7944
9892
|
usage.reasoningTokens += typeof reported.reasoningTokens === "number" ? reported.reasoningTokens : 0;
|
|
7945
9893
|
} else if (row.type === "finish") finishReason = chunk.type === "finish" ? chunk.reason : void 0;
|
|
7946
9894
|
}
|
|
7947
|
-
if (signal.aborted) throw
|
|
9895
|
+
if (signal.aborted) throw stopped();
|
|
7948
9896
|
if (timeout.signal.aborted) throw new VerificationError("timeout", "the judge request timed out", usage);
|
|
7949
9897
|
if (finishReason?.kind === "error" || finishReason?.kind === "aborted") {
|
|
7950
9898
|
const failure = finishReason.failure;
|
|
@@ -7970,8 +9918,8 @@ async function judgeOnce(llm, route, prompt, signal) {
|
|
|
7970
9918
|
} catch (error) {
|
|
7971
9919
|
lastError = error;
|
|
7972
9920
|
if (error instanceof VerificationError && (error.kind === "aborted" || error.kind === "auth")) throw error;
|
|
7973
|
-
if (signal.aborted) throw
|
|
7974
|
-
if (timeout.signal.aborted) lastError = new VerificationError("timeout", "the judge request timed out after " + Math.round(
|
|
9921
|
+
if (signal.aborted) throw stopped();
|
|
9922
|
+
if (timeout.signal.aborted) lastError = new VerificationError("timeout", "the judge request timed out after " + Math.round(budget.callTimeoutMs / 1e3) + "s", usage);
|
|
7975
9923
|
const parseFailure = lastError instanceof VerificationError && lastError.kind === "parse";
|
|
7976
9924
|
if (attempt + 1 >= 3 || !parseFailure && !retryable(lastError)) break;
|
|
7977
9925
|
if (!parseFailure) usage.usageIncomplete = true;
|
|
@@ -7992,11 +9940,17 @@ async function judgeOnce(llm, route, prompt, signal) {
|
|
|
7992
9940
|
* Run one acceptance: every criterion is judged twice, the odd round with the
|
|
7993
9941
|
* A/B slots swapped, and the per-round scores are mapped back and averaged per
|
|
7994
9942
|
* criterion.
|
|
7995
|
-
* @param input - the judge route, evidence, threshold and cancellation.
|
|
7996
|
-
* @returns the recorded attempt (quality verdict or
|
|
9943
|
+
* @param input - the judge route, evidence, threshold, time budget and cancellation.
|
|
9944
|
+
* @returns the recorded attempt (quality verdict, anomaly, or budget stop).
|
|
7997
9945
|
*/
|
|
7998
9946
|
async function runAcceptance(input) {
|
|
7999
|
-
const
|
|
9947
|
+
const now = input.now ?? Date.now;
|
|
9948
|
+
const startedAt = now();
|
|
9949
|
+
const budget = {
|
|
9950
|
+
deadline: input.deadline ?? startedAt + DEFAULT_VERIFICATION_BUDGET_MS,
|
|
9951
|
+
callTimeoutMs: input.callTimeoutMs ?? 15e4,
|
|
9952
|
+
now
|
|
9953
|
+
};
|
|
8000
9954
|
const usage = emptyUsage();
|
|
8001
9955
|
const evidenceSummary = {
|
|
8002
9956
|
chars: input.evidence.trace.length,
|
|
@@ -8009,6 +9963,13 @@ async function runAcceptance(input) {
|
|
|
8009
9963
|
};
|
|
8010
9964
|
const findings = [];
|
|
8011
9965
|
const seenFindings = /* @__PURE__ */ new Set();
|
|
9966
|
+
const structured = [];
|
|
9967
|
+
const seenStructured = /* @__PURE__ */ new Set();
|
|
9968
|
+
const evidenceText = {
|
|
9969
|
+
task: input.evidence.problem,
|
|
9970
|
+
trajectory: input.evidence.trace,
|
|
9971
|
+
workspace: input.context ?? ""
|
|
9972
|
+
};
|
|
8012
9973
|
try {
|
|
8013
9974
|
const perCriterion = /* @__PURE__ */ new Map();
|
|
8014
9975
|
for (const criterion of CODING_CRITERIA) perCriterion.set(criterion.id, {
|
|
@@ -8020,8 +9981,8 @@ async function runAcceptance(input) {
|
|
|
8020
9981
|
const swapped = repeat % 2 === 1;
|
|
8021
9982
|
const candidateA = swapped ? EMPTY_WORK_BASELINE : input.evidence.trace;
|
|
8022
9983
|
const candidateB = swapped ? input.evidence.trace : EMPTY_WORK_BASELINE;
|
|
8023
|
-
const prompt = buildAcceptancePrompt(input.evidence.problem, candidateA, candidateB, criterion, void 0, input.context);
|
|
8024
|
-
const answer = await judgeOnce(input.llm, input.route, prompt, input.signal);
|
|
9984
|
+
const prompt = buildAcceptancePrompt(input.evidence.problem, candidateA, candidateB, criterion, void 0, input.context, workSlotOfRound(repeat));
|
|
9985
|
+
const answer = await judgeOnce(input.llm, input.route, prompt, input.signal, budget);
|
|
8025
9986
|
const scoreA = extractScore(answer.text, "score_A");
|
|
8026
9987
|
const scoreB = extractScore(answer.text, "score_B");
|
|
8027
9988
|
if (answer.usage.usageIncomplete === true) usage.usageIncomplete = true;
|
|
@@ -8032,14 +9993,26 @@ async function runAcceptance(input) {
|
|
|
8032
9993
|
const bucket = perCriterion.get(criterion.id);
|
|
8033
9994
|
bucket.work.push(swapped ? scoreB : scoreA);
|
|
8034
9995
|
bucket.baseline.push(swapped ? scoreA : scoreB);
|
|
8035
|
-
for (const finding of parseFindings(answer.text, criterion)) {
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
|
|
8039
|
-
|
|
9996
|
+
for (const finding of parseFindings(answer.text, criterion, repeat)) {
|
|
9997
|
+
if (structured.length < VERIFICATION_MAX_FINDINGS) {
|
|
9998
|
+
const key = finding.criterionId + "|" + finding.location + "|" + finding.quote + "|" + finding.body;
|
|
9999
|
+
if (!seenStructured.has(key)) {
|
|
10000
|
+
seenStructured.add(key);
|
|
10001
|
+
structured.push({
|
|
10002
|
+
criterionId: finding.criterionId,
|
|
10003
|
+
requirement: finding.requirement !== "" ? finding.requirement : finding.criterionName,
|
|
10004
|
+
observation: finding.observation !== "" ? finding.observation : finding.body,
|
|
10005
|
+
gap: finding.gap !== "" ? finding.gap : finding.body,
|
|
10006
|
+
quote: finding.quote,
|
|
10007
|
+
location: finding.location,
|
|
10008
|
+
...finding.action === void 0 ? {} : { action: finding.action }
|
|
10009
|
+
});
|
|
10010
|
+
}
|
|
10011
|
+
}
|
|
10012
|
+
const line = renderFinding(finding);
|
|
8040
10013
|
if (seenFindings.has(line)) continue;
|
|
8041
10014
|
seenFindings.add(line);
|
|
8042
|
-
if (findings.length <
|
|
10015
|
+
if (findings.length < VERIFICATION_MAX_FINDINGS) findings.push(line);
|
|
8043
10016
|
}
|
|
8044
10017
|
}
|
|
8045
10018
|
const criteria = CODING_CRITERIA.map((criterion) => {
|
|
@@ -8058,11 +10031,17 @@ async function runAcceptance(input) {
|
|
|
8058
10031
|
const score = average(criteria.map((criterion) => criterion.score));
|
|
8059
10032
|
const baseline = average(criteria.map((criterion) => criterion.baseline));
|
|
8060
10033
|
const passed = acceptancePassed(score, baseline, criteria, input.threshold);
|
|
10034
|
+
const validity = passed ? { valid: true } : assessAcceptanceValidity({
|
|
10035
|
+
criteria,
|
|
10036
|
+
findings: structured,
|
|
10037
|
+
evidence: evidenceText,
|
|
10038
|
+
omittedCharacters: input.evidence.omittedCharacters
|
|
10039
|
+
});
|
|
8061
10040
|
return { attempt: {
|
|
8062
10041
|
index: input.index,
|
|
8063
10042
|
at: startedAt,
|
|
8064
|
-
stage: "quality",
|
|
8065
|
-
passed,
|
|
10043
|
+
stage: validity.valid ? "quality" : "invalid",
|
|
10044
|
+
passed: validity.valid ? passed : false,
|
|
8066
10045
|
score,
|
|
8067
10046
|
baseline,
|
|
8068
10047
|
criteria,
|
|
@@ -8071,7 +10050,12 @@ async function runAcceptance(input) {
|
|
|
8071
10050
|
evidence: evidenceSummary,
|
|
8072
10051
|
route: input.route,
|
|
8073
10052
|
channel: "explicit-tag",
|
|
8074
|
-
rounds: 2
|
|
10053
|
+
rounds: 2,
|
|
10054
|
+
...structured.length === 0 ? {} : { criterionFindings: structured },
|
|
10055
|
+
...validity.valid ? {} : {
|
|
10056
|
+
invalidReason: validity.reason,
|
|
10057
|
+
error: classifyInvalid(validity)
|
|
10058
|
+
}
|
|
8075
10059
|
} };
|
|
8076
10060
|
} catch (error) {
|
|
8077
10061
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
@@ -8080,7 +10064,7 @@ async function runAcceptance(input) {
|
|
|
8080
10064
|
return { attempt: {
|
|
8081
10065
|
index: input.index,
|
|
8082
10066
|
at: startedAt,
|
|
8083
|
-
stage: "exception",
|
|
10067
|
+
stage: kind === "budget" ? "budget" : "exception",
|
|
8084
10068
|
passed: false,
|
|
8085
10069
|
score: 0,
|
|
8086
10070
|
baseline: 0,
|
|
@@ -8110,6 +10094,16 @@ function mergeUsage(target, source) {
|
|
|
8110
10094
|
...target.usageIncomplete === true || source.usageIncomplete === true ? { usageIncomplete: true } : {}
|
|
8111
10095
|
};
|
|
8112
10096
|
}
|
|
10097
|
+
/** Prefix one invalid-acceptance message with its class so the report stays actionable. */
|
|
10098
|
+
function classifyInvalid(validity) {
|
|
10099
|
+
return {
|
|
10100
|
+
"missing-finding": "验收无效(未达标判据没有对应的结构化问题反馈)",
|
|
10101
|
+
"unlocatable-quote": "验收无效(问题反馈引用不存在于本次裁判所见的证据中)",
|
|
10102
|
+
"baseline-finding": "验收无效(问题反馈指向空工作基线而非本次工作)",
|
|
10103
|
+
"vacuous-finding": "验收无效(问题反馈空泛,未说明实际观测与差异)",
|
|
10104
|
+
"insufficient-evidence": "验收无效(裁判所见证据不足,无法支撑否决)"
|
|
10105
|
+
}[validity.reason] + ": " + validity.detail;
|
|
10106
|
+
}
|
|
8113
10107
|
/** Prefix one anomaly message with its class so the report stays actionable. */
|
|
8114
10108
|
function classifyMessage(kind, message) {
|
|
8115
10109
|
return {
|
|
@@ -8118,7 +10112,8 @@ function classifyMessage(kind, message) {
|
|
|
8118
10112
|
auth: "验收异常(鉴权失败)",
|
|
8119
10113
|
parse: "验收异常(裁判回答无法解析)",
|
|
8120
10114
|
request: "验收异常(裁判请求失败)",
|
|
8121
|
-
aborted: "验收异常(验收被取消)"
|
|
10115
|
+
aborted: "验收异常(验收被取消)",
|
|
10116
|
+
budget: "验收未完成(时间预算耗尽,未产生任何判定,不计入异常额度)"
|
|
8122
10117
|
}[kind] + ": " + message;
|
|
8123
10118
|
}
|
|
8124
10119
|
/** Character ceiling for one file's rendered comparison. */
|
|
@@ -8329,6 +10324,45 @@ function createGoalVerificationGate(deps) {
|
|
|
8329
10324
|
blockGoal(agent, reason);
|
|
8330
10325
|
return deny(bounded("[任务看板 · goal 验收判定失败] " + reason + "\n本次执行不会完成;请如实向用户报告验收未通过、已记录的分数与问题,不要再尝试标记完成。"), "TASK_BOARD_VERIFICATION_FAILED", reason);
|
|
8331
10326
|
};
|
|
10327
|
+
/**
|
|
10328
|
+
* Hold the cycle open after its non-quality budget ran out.
|
|
10329
|
+
*
|
|
10330
|
+
* A judge route that times out, cannot authenticate or cannot be resolved is
|
|
10331
|
+
* the ENVIRONMENT failing, not the work, so this records the attempt and
|
|
10332
|
+
* refuses the completion claim without writing a `failedReason` and without
|
|
10333
|
+
* blocking the goal (issue #1828). The execution therefore survives for a
|
|
10334
|
+
* user who fixes the environment and clears the recorded anomalies, instead
|
|
10335
|
+
* of carrying a verdict that only the clock produced.
|
|
10336
|
+
*/
|
|
10337
|
+
const hold = (taskId, executionId, verification, message, detail, code) => {
|
|
10338
|
+
write(taskId, executionId, {
|
|
10339
|
+
...verification,
|
|
10340
|
+
inFlight: void 0
|
|
10341
|
+
});
|
|
10342
|
+
return deny(bounded(message), code, detail);
|
|
10343
|
+
};
|
|
10344
|
+
/** The refusal one recorded anomaly returns to the fixing agent. */
|
|
10345
|
+
const anomalyRefusal = (spend, error) => deny(bounded("[任务看板 · 验收异常] " + (error ?? "unknown") + "\n这是验收异常而非质量判定,本次未消耗质量验收额度(已用 " + exceptionAttempts(spend).length + "/2)。请修复环境后再次调用 update_goal(action: complete)。"), "TASK_BOARD_VERIFICATION_ANOMALY", error);
|
|
10346
|
+
/**
|
|
10347
|
+
* The refusal one recorded INVALID acceptance returns.
|
|
10348
|
+
*
|
|
10349
|
+
* The claim is still refused (no pass record exists), but the agent is told
|
|
10350
|
+
* the veto itself was unusable and is explicitly NOT asked to repair anything
|
|
10351
|
+
* on that basis. The reason and scores stay recorded for review; the quality
|
|
10352
|
+
* budget is untouched.
|
|
10353
|
+
*/
|
|
10354
|
+
const invalidRefusal = (spend, attempt) => deny(bounded(invalidAcceptanceFeedback(attempt, 2 - invalidAttempts(spend).length)), "TASK_BOARD_VERIFICATION_INVALID", attempt.error);
|
|
10355
|
+
/**
|
|
10356
|
+
* Hold a cycle whose invalid-acceptance budget is spent.
|
|
10357
|
+
*
|
|
10358
|
+
* No usable veto was ever produced, so the board neither passes nor fails the
|
|
10359
|
+
* card: it stops calling the judge and leaves the execution for a human. Only
|
|
10360
|
+
* an explicit user action (or a rerun) resumes it, which is what bounds the
|
|
10361
|
+
* retries.
|
|
10362
|
+
*/
|
|
10363
|
+
const invalidHeld = (taskId, executionId, spend) => hold(taskId, executionId, spend, "[任务看板 · 验收无效] " + invalidHoldReason(invalidAttempts(spend).length), "acceptance produced no usable veto; a user action must clear the recorded invalid acceptances", "TASK_BOARD_VERIFICATION_INVALID");
|
|
10364
|
+
/** The refusal a spent anomaly budget returns: no verdict, and no card failure. */
|
|
10365
|
+
const anomalyHeld = (taskId, executionId, spend) => hold(taskId, executionId, spend, "[任务看板 · 验收异常] 验收异常额度已用尽(2/2):本执行周期不再调用裁判模型,也不把这次执行判为失败。\n这是环境异常而非质量判定:请如实告诉用户验收环境当前不可用,由用户修复环境后用 task_board_manage(action=reset-verification) 清除已记录的异常,再重新完成目标。", "acceptance anomaly budget exhausted; a user action must clear the recorded anomalies", "TASK_BOARD_VERIFICATION_ANOMALY");
|
|
8332
10366
|
/** Record one attempt and decide whether the completion claim may proceed. */
|
|
8333
10367
|
const settle = async (input) => {
|
|
8334
10368
|
const { task, execution, verification, agent, objective } = input;
|
|
@@ -8373,9 +10407,9 @@ function createGoalVerificationGate(deps) {
|
|
|
8373
10407
|
attempts,
|
|
8374
10408
|
inFlight: void 0
|
|
8375
10409
|
};
|
|
8376
|
-
if (exceptionAttempts(spend).length >= 2) return
|
|
10410
|
+
if (exceptionAttempts(spend).length >= 2) return anomalyHeld(task.id, execution.id, spend);
|
|
8377
10411
|
write(task.id, execution.id, spend);
|
|
8378
|
-
return
|
|
10412
|
+
return anomalyRefusal(spend, attempt.error);
|
|
8379
10413
|
}
|
|
8380
10414
|
write(task.id, execution.id, baselines);
|
|
8381
10415
|
const llm = deps.llm?.();
|
|
@@ -8411,13 +10445,16 @@ function createGoalVerificationGate(deps) {
|
|
|
8411
10445
|
attempts: [...verification.attempts, attempt],
|
|
8412
10446
|
inFlight: void 0
|
|
8413
10447
|
};
|
|
8414
|
-
if (exceptionAttempts(spend).length >= 2) return
|
|
10448
|
+
if (exceptionAttempts(spend).length >= 2) return anomalyHeld(task.id, execution.id, spend);
|
|
8415
10449
|
write(task.id, execution.id, spend);
|
|
8416
10450
|
return deny(bounded("[任务看板 · 验收异常] " + attempt.error), "TASK_BOARD_VERIFICATION_ANOMALY", attempt.error);
|
|
8417
10451
|
}
|
|
8418
10452
|
const session = agent.session;
|
|
8419
10453
|
const evidence = collectEvidence(session, objective, execution.startedAt);
|
|
8420
|
-
const
|
|
10454
|
+
const callTimeout = deps.callTimeoutMs?.() ?? verificationCallTimeoutMs(void 0);
|
|
10455
|
+
const budget = deps.budgetMs?.() ?? DEFAULT_VERIFICATION_BUDGET_MS;
|
|
10456
|
+
const deadline = now() + budget;
|
|
10457
|
+
const acceptance = AbortSignal.timeout(budget);
|
|
8421
10458
|
const source = deps.workspaceChanges?.();
|
|
8422
10459
|
const workspace = source === void 0 ? {
|
|
8423
10460
|
text: "",
|
|
@@ -8437,6 +10474,8 @@ function createGoalVerificationGate(deps) {
|
|
|
8437
10474
|
context: workspace.text === "" ? void 0 : workspace.text,
|
|
8438
10475
|
workspaceFiles: workspace.files,
|
|
8439
10476
|
signal: acceptance,
|
|
10477
|
+
deadline,
|
|
10478
|
+
callTimeoutMs: callTimeout,
|
|
8440
10479
|
now
|
|
8441
10480
|
})).attempt;
|
|
8442
10481
|
const attempts = [...verification.attempts, attempt];
|
|
@@ -8445,10 +10484,16 @@ function createGoalVerificationGate(deps) {
|
|
|
8445
10484
|
attempts,
|
|
8446
10485
|
inFlight: void 0
|
|
8447
10486
|
};
|
|
10487
|
+
if (attempt.stage === "budget") return hold(task.id, execution.id, spend, "[任务看板 · 验收未完成] " + (attempt.error ?? "时间预算耗尽") + "\n本次没有产生任何质量判定,也不消耗质量或异常额度。请稍后重试;若裁判路由确实很慢,可调大验收的单次调用上限或总时间预算。", attempt.error ?? "the acceptance time budget ran out before a verdict", "TASK_BOARD_VERIFICATION_BUDGET");
|
|
8448
10488
|
if (attempt.stage === "exception") {
|
|
8449
|
-
if (exceptionAttempts(spend).length >= 2) return
|
|
10489
|
+
if (exceptionAttempts(spend).length >= 2) return anomalyHeld(task.id, execution.id, spend);
|
|
10490
|
+
write(task.id, execution.id, spend);
|
|
10491
|
+
return anomalyRefusal(spend, attempt.error);
|
|
10492
|
+
}
|
|
10493
|
+
if (attempt.stage === "invalid") {
|
|
10494
|
+
if (invalidAttempts(spend).length >= 2) return invalidHeld(task.id, execution.id, spend);
|
|
8450
10495
|
write(task.id, execution.id, spend);
|
|
8451
|
-
return
|
|
10496
|
+
return invalidRefusal(spend, attempt);
|
|
8452
10497
|
}
|
|
8453
10498
|
if (attempt.passed) {
|
|
8454
10499
|
write(task.id, execution.id, spend);
|
|
@@ -8457,7 +10502,7 @@ function createGoalVerificationGate(deps) {
|
|
|
8457
10502
|
const used = qualityAttempts(spend).length;
|
|
8458
10503
|
if (used >= 2) return finalize(task.id, execution.id, spend, finalFailureReason(attempt, used, verification.contract.threshold), agent);
|
|
8459
10504
|
write(task.id, execution.id, spend);
|
|
8460
|
-
return deny(bounded(qualityFeedback(attempt, 2 - used)), "TASK_BOARD_VERIFICATION_FAILED", attempt
|
|
10505
|
+
return deny(bounded(qualityFeedback(attempt, 2 - used)), "TASK_BOARD_VERIFICATION_FAILED", locatedFindingDetail(attempt));
|
|
8461
10506
|
};
|
|
8462
10507
|
/** Evaluate one tool call; undefined means "not this gate's business". */
|
|
8463
10508
|
const evaluate = async (exec) => {
|
|
@@ -8472,7 +10517,9 @@ function createGoalVerificationGate(deps) {
|
|
|
8472
10517
|
if (verification.applicability !== "enforced") return void 0;
|
|
8473
10518
|
if (passedAttempt(verification) !== void 0) return { kind: "allow" };
|
|
8474
10519
|
if (verification.failedReason !== void 0) return deny(bounded("[任务看板 · goal 验收判定失败] " + verification.failedReason + "\n本次执行已被判失败,不会再运行新的验收。请如实报告结果。"), "TASK_BOARD_VERIFICATION_FAILED", verification.failedReason);
|
|
8475
|
-
if (!
|
|
10520
|
+
if (!hasInvalidBudget(verification)) return invalidHeld(binding.task.id, binding.execution.id, verification);
|
|
10521
|
+
if (!hasQualityBudget(verification)) return finalize(binding.task.id, binding.execution.id, verification, "goal 验收额度已耗尽,本次 execution 判失败。", exec.agent);
|
|
10522
|
+
if (!hasExceptionBudget(verification)) return anomalyHeld(binding.task.id, binding.execution.id, verification);
|
|
8476
10523
|
const key = binding.execution.id;
|
|
8477
10524
|
const running = inFlight.get(key);
|
|
8478
10525
|
if (running !== void 0) return await running;
|
|
@@ -8514,6 +10561,17 @@ function createGoalVerificationGate(deps) {
|
|
|
8514
10561
|
}
|
|
8515
10562
|
};
|
|
8516
10563
|
}
|
|
10564
|
+
/**
|
|
10565
|
+
* The structured detail of one quality refusal: the judge's own findings, in
|
|
10566
|
+
* the order the acceptance recorded them, so the denial carries exactly the
|
|
10567
|
+
* located evidence the report shows.
|
|
10568
|
+
*/
|
|
10569
|
+
function locatedFindingDetail(attempt) {
|
|
10570
|
+
const lines = [];
|
|
10571
|
+
for (const finding of attempt.criterionFindings ?? []) lines.push("[" + finding.criterionId + " " + finding.location + "] \"" + finding.quote + "\"");
|
|
10572
|
+
if (lines.length === 0) lines.push(...attempt.findings);
|
|
10573
|
+
return lines.join("\n");
|
|
10574
|
+
}
|
|
8517
10575
|
/** The bare execution prompt, used when the goal service carries no objective. */
|
|
8518
10576
|
function taskPrompt(task) {
|
|
8519
10577
|
return task.prompt.trim() !== "" ? task.prompt : task.title;
|
|
@@ -8547,14 +10605,29 @@ const Config = z.object({
|
|
|
8547
10605
|
proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
|
|
8548
10606
|
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
8549
10607
|
maxSubtaskDepth: z.number().min(1).max(3).default(1).volatile(),
|
|
10608
|
+
sessionPollSeconds: z.number().min(1).max(300).default(5).volatile(),
|
|
10609
|
+
runLaunchWidth: z.number().min(1).max(16).default(3).volatile(),
|
|
8550
10610
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
10611
|
+
goalRunEnabled: z.boolean().default(false).volatile(),
|
|
10612
|
+
cardDefaultMode: z.string().default("").volatile(),
|
|
10613
|
+
cardDefaultPermission: z.union([...TASK_PERMISSIONS, ""]).default("").volatile(),
|
|
10614
|
+
cardDefaultModel: z.string().default("").volatile(),
|
|
10615
|
+
cardDefaultReasoningEffort: z.string().default("").volatile(),
|
|
8551
10616
|
goalVerification: z.boolean().default(true).volatile(),
|
|
8552
10617
|
goalVerificationModel: z.string().default("").volatile(),
|
|
8553
|
-
goalVerificationReasoningEffort: z.string().default("").volatile()
|
|
10618
|
+
goalVerificationReasoningEffort: z.string().default("").volatile(),
|
|
10619
|
+
goalVerificationCallTimeoutSeconds: z.number().min(30).max(600).default(150).volatile(),
|
|
10620
|
+
goalVerificationBudgetSeconds: z.number().min(120).max(VERIFICATION_BUDGET_MAX_SECONDS).default(DEFAULT_VERIFICATION_BUDGET_SECONDS).volatile()
|
|
8554
10621
|
});
|
|
8555
10622
|
/** Schema default for the goal-acceptance switch, re-read for hand-built contexts. */
|
|
8556
10623
|
const DEFAULT_GOAL_VERIFICATION = true;
|
|
8557
10624
|
/**
|
|
10625
|
+
* Schema default for the GLOBAL native-/goal switch, re-read for hand-built
|
|
10626
|
+
* contexts. Deliberately false: a deployment that never configured it (and
|
|
10627
|
+
* every card written before the option existed) runs plain single turns.
|
|
10628
|
+
*/
|
|
10629
|
+
const DEFAULT_GOAL_RUN = false;
|
|
10630
|
+
/**
|
|
8558
10631
|
* Read one config field's current value.
|
|
8559
10632
|
*
|
|
8560
10633
|
* The Loader hands schema-volatile fields as stable references it commits in
|
|
@@ -8722,6 +10795,50 @@ function resolveLlmRuntime(ctx) {
|
|
|
8722
10795
|
}
|
|
8723
10796
|
}
|
|
8724
10797
|
/**
|
|
10798
|
+
* Resolve the optional `sessionController` service. Like the tool registry and
|
|
10799
|
+
* the llm runtime, it is read dynamically so capture-only test contexts that do
|
|
10800
|
+
* not mount the full session controller still activate the board.
|
|
10801
|
+
* @param ctx - the plugin context.
|
|
10802
|
+
* @returns the session controller face, or undefined when unavailable.
|
|
10803
|
+
*/
|
|
10804
|
+
function resolveSessionController(ctx) {
|
|
10805
|
+
try {
|
|
10806
|
+
const controller = ctx.get?.("sessionController");
|
|
10807
|
+
return controller !== void 0 && typeof controller.resolveAgent === "function" ? controller : void 0;
|
|
10808
|
+
} catch {
|
|
10809
|
+
return;
|
|
10810
|
+
}
|
|
10811
|
+
}
|
|
10812
|
+
/**
|
|
10813
|
+
* Resolve the live Agent for an execution session, resuming a cold persisted
|
|
10814
|
+
* session through `sessionController.resolveAgent` when it is not yet in
|
|
10815
|
+
* `ctx.agents` (issue #1861).
|
|
10816
|
+
*
|
|
10817
|
+
* A fresh run (`session/create`) or a session the browser is currently
|
|
10818
|
+
* following is already in `ctx.agents`, so the fast path returns immediately.
|
|
10819
|
+
* A scheduled or manual reuse of a cold session — one that `session/list`
|
|
10820
|
+
* reports as idle from disk without activating its Agent — is not in
|
|
10821
|
+
* `ctx.agents` when `pinAndPrompt` runs `/permission` ahead of
|
|
10822
|
+
* `session/selectModel` and `session/prompt`; delegating to
|
|
10823
|
+
* `sessionController.resolveAgent` resumes it with its recorded preset and
|
|
10824
|
+
* model-selection state, matching what those gateway endpoints do next.
|
|
10825
|
+
* @param ctx - the plugin context.
|
|
10826
|
+
* @param sessionId - the target execution session id.
|
|
10827
|
+
* @returns the live Agent for `sessionId`.
|
|
10828
|
+
*/
|
|
10829
|
+
async function resolveExecutionAgent(ctx, sessionId) {
|
|
10830
|
+
const live = ctx.agents.get(sessionId);
|
|
10831
|
+
if (live !== void 0) return live;
|
|
10832
|
+
const controller = resolveSessionController(ctx);
|
|
10833
|
+
if (controller !== void 0) {
|
|
10834
|
+
const resolved = await controller.resolveAgent(sessionId);
|
|
10835
|
+
if ("agent" in resolved && resolved.agent !== void 0) return resolved.agent;
|
|
10836
|
+
const detail = "error" in resolved && typeof resolved.error?.message === "string" && resolved.error.message !== "" ? `: ${resolved.error.message}` : "";
|
|
10837
|
+
throw new Error(`execution session ${sessionId} is not available${detail}`);
|
|
10838
|
+
}
|
|
10839
|
+
throw new Error(`execution session ${sessionId} is not available`);
|
|
10840
|
+
}
|
|
10841
|
+
/**
|
|
8725
10842
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
8726
10843
|
* inhibitor, and the model-facing announcement.
|
|
8727
10844
|
*
|
|
@@ -8744,6 +10861,14 @@ function applyImpl(ctx, config) {
|
|
|
8744
10861
|
const preventIdleSleep = () => readConfigField(config?.preventIdleSleep, false);
|
|
8745
10862
|
/** Current subtask depth limit (read live: the settings card edits it in place). */
|
|
8746
10863
|
const maxSubtaskDepth = () => readConfigField(config?.maxSubtaskDepth, 1);
|
|
10864
|
+
/** Current roster-poll cadence in seconds (read live: the settings card edits it in place). */
|
|
10865
|
+
const sessionPollSeconds = () => readConfigField(config?.sessionPollSeconds, 5);
|
|
10866
|
+
/**
|
|
10867
|
+
* Current run-launch admission width. Read live on every admission, so the
|
|
10868
|
+
* settings card bounds the next launch of a run that is still queueing without
|
|
10869
|
+
* remounting the row; the Host clamps a hand-edited value into the schema range.
|
|
10870
|
+
*/
|
|
10871
|
+
const runLaunchWidth = () => readConfigField(config?.runLaunchWidth, 3);
|
|
8747
10872
|
/**
|
|
8748
10873
|
* The baseline the permission confirmation gate judges against: the row's own
|
|
8749
10874
|
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
@@ -8758,27 +10883,59 @@ function applyImpl(ctx, config) {
|
|
|
8758
10883
|
model: readConfigField(config?.goalVerificationModel, ""),
|
|
8759
10884
|
reasoningEffort: readConfigField(config?.goalVerificationReasoningEffort, "")
|
|
8760
10885
|
});
|
|
10886
|
+
/**
|
|
10887
|
+
* The acceptance time ceilings, read live: raising one must unblock a card
|
|
10888
|
+
* whose acceptance is stuck on a slow judge route without a remount, so
|
|
10889
|
+
* these deliberately are NOT frozen into the per-execution contract.
|
|
10890
|
+
*/
|
|
10891
|
+
const verificationCallTimeoutMsLive = () => verificationCallTimeoutMs(readConfigField(config?.goalVerificationCallTimeoutSeconds, 150));
|
|
10892
|
+
const verificationBudgetMsLive = () => verificationBudgetMs(readConfigField(config?.goalVerificationBudgetSeconds, DEFAULT_VERIFICATION_BUDGET_SECONDS));
|
|
8761
10893
|
let hostForCatalog;
|
|
8762
10894
|
const verificationCatalog = async () => {
|
|
8763
10895
|
const runner = hostForCatalog?.runner;
|
|
8764
10896
|
return runner === void 0 ? void 0 : normalizeCatalog(await runner.modelCatalog());
|
|
8765
10897
|
};
|
|
10898
|
+
/**
|
|
10899
|
+
* Live reader of the GLOBAL native-/goal switch. The service calls it exactly
|
|
10900
|
+
* once per execution and carries the value, so a settings edit only reaches
|
|
10901
|
+
* executions that start afterwards.
|
|
10902
|
+
*/
|
|
10903
|
+
const goalRunEnabled = () => readConfigField(config?.goalRunEnabled, false);
|
|
10904
|
+
/**
|
|
10905
|
+
* Live reader of the new-card defaults, read at every use like the switch
|
|
10906
|
+
* above: the create dialog prefills from the published snapshot, and each
|
|
10907
|
+
* creating session re-reads them when it submits the create. A blank value
|
|
10908
|
+
* means "not set" and reaches the resolver as an absent target.
|
|
10909
|
+
*/
|
|
10910
|
+
const cardDefaults = () => {
|
|
10911
|
+
const permission = readConfigField(config?.cardDefaultPermission, "");
|
|
10912
|
+
return {
|
|
10913
|
+
mode: readConfigField(config?.cardDefaultMode, "").trim() || void 0,
|
|
10914
|
+
permission: isTaskPermission(permission) ? permission : void 0,
|
|
10915
|
+
model: readConfigField(config?.cardDefaultModel, "").trim() || void 0,
|
|
10916
|
+
reasoningEffort: readConfigField(config?.cardDefaultReasoningEffort, "").trim() || void 0
|
|
10917
|
+
};
|
|
10918
|
+
};
|
|
10919
|
+
const ownerPeer = createOwnerPeer(ctx);
|
|
8766
10920
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
8767
10921
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
10922
|
+
ownerPeer,
|
|
8768
10923
|
sessionDefaultPermission,
|
|
8769
10924
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
10925
|
+
goalRunEnabled,
|
|
10926
|
+
runLaunchWidth,
|
|
10927
|
+
cardDefaults,
|
|
8770
10928
|
verificationSettings,
|
|
8771
10929
|
verificationCatalog,
|
|
8772
10930
|
timers: resolveHostTimers(ctx),
|
|
8773
10931
|
team: buildTeamDispatcher(ctx, config?.teamProvider ?? "spawn"),
|
|
8774
10932
|
commandDispatcher: { async execute(sessionId, line, signal) {
|
|
8775
|
-
const agent = ctx
|
|
8776
|
-
if (agent === void 0) throw new Error(`execution session ${sessionId} is not available`);
|
|
10933
|
+
const agent = await resolveExecutionAgent(ctx, sessionId);
|
|
8777
10934
|
return (await ctx.commands.execute(agent, line, [], signal))?.result;
|
|
8778
10935
|
} }
|
|
8779
10936
|
});
|
|
8780
10937
|
hostForCatalog = host;
|
|
8781
|
-
host.setConfiguration(enabled(), preventIdleSleep());
|
|
10938
|
+
host.setConfiguration(enabled(), preventIdleSleep(), sessionPollSeconds());
|
|
8782
10939
|
host.start();
|
|
8783
10940
|
if (typeof ctx.provide === "function") ctx.provide(TASK_BOARD_SERVICE_NAME, {
|
|
8784
10941
|
registerExtension: (extension) => host.registerExtension(extension),
|
|
@@ -8824,6 +10981,8 @@ function applyImpl(ctx, config) {
|
|
|
8824
10981
|
}
|
|
8825
10982
|
},
|
|
8826
10983
|
workspaceChanges: () => probeWorkspaceChanges(ctx),
|
|
10984
|
+
callTimeoutMs: verificationCallTimeoutMsLive,
|
|
10985
|
+
budgetMs: verificationBudgetMsLive,
|
|
8827
10986
|
logger: { warn: (message, ...rest) => {
|
|
8828
10987
|
const logger = ctx.logger;
|
|
8829
10988
|
if (typeof logger?.warn === "function") logger.warn(message, ...rest);
|
|
@@ -8870,12 +11029,13 @@ function applyImpl(ctx, config) {
|
|
|
8870
11029
|
enabled: enabled(),
|
|
8871
11030
|
announceToAgent: announceToAgent(),
|
|
8872
11031
|
preventIdleSleep: preventIdleSleep(),
|
|
8873
|
-
maxSubtaskDepth: maxSubtaskDepth()
|
|
11032
|
+
maxSubtaskDepth: maxSubtaskDepth(),
|
|
11033
|
+
sessionPollSeconds: sessionPollSeconds()
|
|
8874
11034
|
};
|
|
8875
|
-
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) return;
|
|
11035
|
+
if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth && applied.sessionPollSeconds === next.sessionPollSeconds) return;
|
|
8876
11036
|
applied = next;
|
|
8877
11037
|
host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth);
|
|
8878
|
-
host.setConfiguration(next.enabled, next.preventIdleSleep);
|
|
11038
|
+
host.setConfiguration(next.enabled, next.preventIdleSleep, next.sessionPollSeconds);
|
|
8879
11039
|
setToolsEnabled(next.enabled);
|
|
8880
11040
|
if (disposeSection !== void 0) {
|
|
8881
11041
|
disposeSection();
|
|
@@ -8894,4 +11054,4 @@ function applyImpl(ctx, config) {
|
|
|
8894
11054
|
sync();
|
|
8895
11055
|
}
|
|
8896
11056
|
//#endregion
|
|
8897
|
-
export { Config, DEFAULT_GOAL_VERIFICATION, DEFAULT_PROXY_TOKEN_ENV, DEFAULT_TEAM_PROVIDER, TASK_BOARD_GUIDANCE, apply, buildTeamDispatcher, inject, readConfigField, resolveAgentTeams, resolveHostDefaultPermission, resolveHostTimers, resolveLlmRuntime, resolveProxyAccess, resolveToolRegistry };
|
|
11057
|
+
export { Config, DEFAULT_GOAL_RUN, DEFAULT_GOAL_VERIFICATION, DEFAULT_PROXY_TOKEN_ENV, DEFAULT_TEAM_PROVIDER, TASK_BOARD_GUIDANCE, apply, buildTeamDispatcher, inject, readConfigField, resolveAgentTeams, resolveExecutionAgent, resolveHostDefaultPermission, resolveHostTimers, resolveLlmRuntime, resolveProxyAccess, resolveSessionController, resolveToolRegistry };
|