@linxin666/dsh-client-ui-task-board 0.4.3 → 0.4.4
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 +17 -10
- package/README.zh.md +17 -10
- package/lib/client.js +1350 -552
- package/lib/client.js.map +1 -1
- package/lib/index.js +655 -134
- package/lib/types/client/board/TaskForm.d.ts +19 -1
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/locales.d.ts +38 -2
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/core/controller.d.ts +1 -0
- package/lib/types/core/degraded-reason.d.ts +46 -0
- package/lib/types/core/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +26 -2
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host-ledger.d.ts +56 -4
- package/lib/types/host-runner.d.ts +76 -3
- package/lib/types/host-service.d.ts +26 -0
- package/lib/types/protocol.d.ts +9 -3
- package/package.json +25 -25
- package/src/client/board/LinkSubtaskModal.tsx +21 -11
- package/src/client/board/NewTaskModal.tsx +271 -150
- package/src/client/board/TaskCard.tsx +24 -5
- package/src/client/board/TaskDetail.tsx +83 -33
- package/src/client/board/TaskForm.tsx +48 -3
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board.module.css +114 -5
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +5 -1
- package/src/client/locales.ts +82 -8
- package/src/client/schedule-zone.ts +135 -0
- package/src/core/controller.ts +7 -2
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +10 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +38 -4
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-update.ts +4 -1
- package/src/host/agent-tools.ts +40 -8
- package/src/host-ledger.ts +203 -23
- package/src/host-runner.ts +164 -14
- package/src/host-service.ts +94 -17
- package/src/protocol.ts +27 -8
- package/lib/types/host/run-guarded.d.ts +0 -39
- package/src/host/run-guarded.ts +0 -76
package/lib/index.js
CHANGED
|
@@ -16,6 +16,112 @@ const FIELD_RANGES = [
|
|
|
16
16
|
[1, 12],
|
|
17
17
|
[0, 7]
|
|
18
18
|
];
|
|
19
|
+
/** IANA `Area/Location`, plus the bare `UTC` the platform reports. */
|
|
20
|
+
const IANA_ZONE = /^[A-Za-z][A-Za-z0-9_+.-]*(?:\/[A-Za-z0-9_+.-]+)+$/;
|
|
21
|
+
/** Cached formatters: constructing an Intl formatter dominates the cost of a resolve. */
|
|
22
|
+
const FORMATTERS = /* @__PURE__ */ new Map();
|
|
23
|
+
function zoneFormatter(timeZone) {
|
|
24
|
+
let formatter = FORMATTERS.get(timeZone);
|
|
25
|
+
if (formatter === void 0) {
|
|
26
|
+
formatter = new Intl.DateTimeFormat("en-US", {
|
|
27
|
+
timeZone,
|
|
28
|
+
hourCycle: "h23",
|
|
29
|
+
year: "numeric",
|
|
30
|
+
month: "2-digit",
|
|
31
|
+
day: "2-digit",
|
|
32
|
+
hour: "2-digit",
|
|
33
|
+
minute: "2-digit"
|
|
34
|
+
});
|
|
35
|
+
FORMATTERS.set(timeZone, formatter);
|
|
36
|
+
}
|
|
37
|
+
return formatter;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The wall-clock reading of one instant in an explicit zone. Reading through
|
|
41
|
+
* Intl (rather than the process `Date` accessors) is what makes the result
|
|
42
|
+
* independent of the Host's own TZ.
|
|
43
|
+
* @param timeZone - explicit IANA zone.
|
|
44
|
+
* @param epochMs - instant to read.
|
|
45
|
+
* @returns the zone-local calendar and clock fields.
|
|
46
|
+
*/
|
|
47
|
+
function partsInZone(timeZone, epochMs) {
|
|
48
|
+
const fields = {};
|
|
49
|
+
for (const part of zoneFormatter(timeZone).formatToParts(new Date(epochMs))) if (part.type !== "literal") fields[part.type] = part.value;
|
|
50
|
+
return {
|
|
51
|
+
year: Number(fields.year),
|
|
52
|
+
month: Number(fields.month),
|
|
53
|
+
day: Number(fields.day),
|
|
54
|
+
hour: Number(fields.hour),
|
|
55
|
+
minute: Number(fields.minute)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** The zone's UTC offset in effect at one instant, in milliseconds. */
|
|
59
|
+
function offsetAt(timeZone, epochMs) {
|
|
60
|
+
const parts = partsInZone(timeZone, epochMs);
|
|
61
|
+
return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute) - epochMs;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a wall clock in an explicit zone to its instant.
|
|
65
|
+
*
|
|
66
|
+
* The offset is probed a day either side of the naive guess so both DST
|
|
67
|
+
* transitions are covered, then every candidate offset whose inverse maps back
|
|
68
|
+
* to the requested wall clock is kept. No survivor means the wall clock does
|
|
69
|
+
* not exist (a spring-forward gap); several means it is ambiguous (a fall-back
|
|
70
|
+
* overlap), and the earliest instant wins so the repeated clock is taken once.
|
|
71
|
+
* @param timeZone - explicit IANA zone.
|
|
72
|
+
* @param year - full calendar year.
|
|
73
|
+
* @param month - 1-12.
|
|
74
|
+
* @param day - day of month.
|
|
75
|
+
* @param hour - 0-23.
|
|
76
|
+
* @param minute - 0-59.
|
|
77
|
+
* @returns the instant in ms epoch, or undefined when the wall clock is skipped.
|
|
78
|
+
*/
|
|
79
|
+
function zonedEpoch(timeZone, year, month, day, hour, minute) {
|
|
80
|
+
const naive = Date.UTC(year, month - 1, day, hour, minute);
|
|
81
|
+
const offsets = /* @__PURE__ */ new Set([
|
|
82
|
+
offsetAt(timeZone, naive - 864e5),
|
|
83
|
+
offsetAt(timeZone, naive),
|
|
84
|
+
offsetAt(timeZone, naive + 864e5)
|
|
85
|
+
]);
|
|
86
|
+
let earliest;
|
|
87
|
+
for (const offset of offsets) {
|
|
88
|
+
const candidate = naive - offset;
|
|
89
|
+
const parts = partsInZone(timeZone, candidate);
|
|
90
|
+
if (parts.year !== year || parts.month !== month || parts.day !== day) continue;
|
|
91
|
+
if (parts.hour !== hour || parts.minute !== minute) continue;
|
|
92
|
+
if (earliest === void 0 || candidate < earliest) earliest = candidate;
|
|
93
|
+
}
|
|
94
|
+
return earliest;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The Host process's IANA zone, used for schedules that store no zone of their
|
|
98
|
+
* own (a ledger written before zones were persisted).
|
|
99
|
+
* @returns a zone name Intl accepts, never empty.
|
|
100
|
+
*/
|
|
101
|
+
function resolveHostTimeZone() {
|
|
102
|
+
try {
|
|
103
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
104
|
+
return typeof zone === "string" && zone !== "" ? zone : "UTC";
|
|
105
|
+
} catch {
|
|
106
|
+
return "UTC";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether a value names a zone this runtime can actually resolve. A stored
|
|
111
|
+
* zone is trusted only after it round-trips through Intl, so a typo or a zone
|
|
112
|
+
* that this ICU build does not know can never arm a schedule.
|
|
113
|
+
* @param value - candidate zone name.
|
|
114
|
+
* @returns true when the name is usable.
|
|
115
|
+
*/
|
|
116
|
+
function isValidTimeZone(value) {
|
|
117
|
+
if (value.trim() !== value || value === "") return false;
|
|
118
|
+
if (value !== "UTC" && !IANA_ZONE.test(value)) return false;
|
|
119
|
+
try {
|
|
120
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: value }).resolvedOptions().timeZone !== void 0;
|
|
121
|
+
} catch {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
19
125
|
/**
|
|
20
126
|
* Parse a 5-field cron expression.
|
|
21
127
|
* @returns the match sets, or null when the expression is invalid.
|
|
@@ -38,8 +144,8 @@ function parseCron(expr) {
|
|
|
38
144
|
days: sets[2],
|
|
39
145
|
months: sets[3],
|
|
40
146
|
weekdays,
|
|
41
|
-
|
|
42
|
-
|
|
147
|
+
dayStarred: fields[2].startsWith("*"),
|
|
148
|
+
weekdayStarred: fields[4].startsWith("*")
|
|
43
149
|
};
|
|
44
150
|
}
|
|
45
151
|
/** Whether the expression parses. */
|
|
@@ -47,52 +153,57 @@ function isValidCron(expr) {
|
|
|
47
153
|
return parseCron(expr) !== null;
|
|
48
154
|
}
|
|
49
155
|
/**
|
|
50
|
-
* Compute the next matching instant after `fromMs`
|
|
51
|
-
*
|
|
52
|
-
* of the matching minute's start, or undefined
|
|
53
|
-
* can never match (for example `0 0 30 2 *`).
|
|
54
|
-
* a full leap cycle, so a valid February 29
|
|
55
|
-
* every non-leap year.
|
|
156
|
+
* Compute the next matching instant after `fromMs` strictly greater than it,
|
|
157
|
+
* interpreting the expression's wall clock in `timeZone` (the Host zone when
|
|
158
|
+
* omitted). Returns the ms epoch of the matching minute's start, or undefined
|
|
159
|
+
* when the calendar constraint can never match (for example `0 0 30 2 *`).
|
|
160
|
+
* The five-year horizon includes a full leap cycle, so a valid February 29
|
|
161
|
+
* schedule remains reachable from every non-leap year.
|
|
56
162
|
*
|
|
57
163
|
* Walks candidate year/month/day/hour/minute values straight from the parsed
|
|
58
164
|
* field sets instead of scanning every minute: a sparse expression such as
|
|
59
165
|
* `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
|
|
60
|
-
* next leap day.
|
|
61
|
-
*
|
|
62
|
-
*
|
|
166
|
+
* next leap day. A candidate wall clock that the zone skips (spring-forward
|
|
167
|
+
* gap) is passed over, which is what makes the gap semantics fall out of the
|
|
168
|
+
* candidate walk rather than needing a separate rule.
|
|
169
|
+
* @param expr - 5-field cron expression.
|
|
170
|
+
* @param fromMs - exclusive lower bound, ms epoch.
|
|
171
|
+
* @param timeZone - explicit IANA zone; defaults to the Host zone.
|
|
172
|
+
* @returns the next trigger instant, or undefined when unreachable.
|
|
63
173
|
*/
|
|
64
|
-
function nextRunAtMs(expr, fromMs) {
|
|
174
|
+
function nextRunAtMs(expr, fromMs, timeZone) {
|
|
65
175
|
const schedule = parseCron(expr);
|
|
66
176
|
if (schedule === null) return void 0;
|
|
67
177
|
if (!hasPossibleCalendarDay(schedule)) return void 0;
|
|
68
|
-
const
|
|
178
|
+
const zone = timeZone ?? resolveHostTimeZone();
|
|
179
|
+
const start = partsInZone(zone, fromMs);
|
|
69
180
|
const limitMs = fromMs + 5 * 366 * 24 * 60 * 60 * 1e3;
|
|
70
181
|
const sortedMinutes = [...schedule.minutes].sort((a, b) => a - b);
|
|
71
182
|
const sortedHours = [...schedule.hours].sort((a, b) => a - b);
|
|
72
183
|
const sortedMonths = [...schedule.months].sort((a, b) => a - b);
|
|
73
|
-
let year =
|
|
74
|
-
let month =
|
|
75
|
-
let day =
|
|
76
|
-
let hour =
|
|
77
|
-
let minute =
|
|
78
|
-
while (
|
|
184
|
+
let year = start.year;
|
|
185
|
+
let month = start.month;
|
|
186
|
+
let day = start.day;
|
|
187
|
+
let hour = start.hour;
|
|
188
|
+
let minute = start.minute + 1;
|
|
189
|
+
while (Date.UTC(year, month - 1, 1) <= limitMs + 864e5) {
|
|
79
190
|
for (const candidateMonth of sortedMonths) {
|
|
80
191
|
if (candidateMonth < month) continue;
|
|
81
|
-
const daysInMonth = new Date(year, candidateMonth, 0).
|
|
192
|
+
const daysInMonth = new Date(Date.UTC(year, candidateMonth, 0)).getUTCDate();
|
|
82
193
|
const dayStart = candidateMonth === month ? day : 1;
|
|
83
194
|
for (let candidateDay = dayStart; candidateDay <= daysInMonth; candidateDay += 1) {
|
|
84
|
-
if (!dayCandidate(schedule,
|
|
195
|
+
if (!dayCandidate(schedule, year, candidateMonth, candidateDay)) continue;
|
|
85
196
|
const hourStart = candidateMonth === month && candidateDay === day ? hour : 0;
|
|
86
197
|
for (const candidateHour of sortedHours) {
|
|
87
198
|
if (candidateHour < hourStart) continue;
|
|
88
199
|
const minuteStart = candidateMonth === month && candidateDay === day && candidateHour === hour ? minute : 0;
|
|
89
200
|
for (const candidateMinute of sortedMinutes) {
|
|
90
201
|
if (candidateMinute < minuteStart) continue;
|
|
91
|
-
const
|
|
92
|
-
|
|
202
|
+
const time = zonedEpoch(zone, year, candidateMonth, candidateDay, candidateHour, candidateMinute);
|
|
203
|
+
if (time === void 0) continue;
|
|
93
204
|
if (time <= fromMs) continue;
|
|
94
205
|
if (time > limitMs) return void 0;
|
|
95
|
-
|
|
206
|
+
return time;
|
|
96
207
|
}
|
|
97
208
|
}
|
|
98
209
|
}
|
|
@@ -104,17 +215,20 @@ function nextRunAtMs(expr, fromMs) {
|
|
|
104
215
|
minute = 0;
|
|
105
216
|
}
|
|
106
217
|
}
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The Vixie day gate shared by the candidate walk. Both fields restricted is
|
|
220
|
+
* OR; any other combination is AND, because a starred field matches every
|
|
221
|
+
* value and so cannot widen the result.
|
|
222
|
+
*/
|
|
223
|
+
function dayCandidate(schedule, year, month, day) {
|
|
224
|
+
const dayMatches = schedule.days.has(day);
|
|
225
|
+
const weekdayMatches = schedule.weekdays.has(new Date(Date.UTC(year, month - 1, day)).getUTCDay());
|
|
226
|
+
if (!schedule.dayStarred && !schedule.weekdayStarred) return dayMatches || weekdayMatches;
|
|
227
|
+
return dayMatches && weekdayMatches;
|
|
114
228
|
}
|
|
115
229
|
/** Reject impossible month/day pairs without spending the multi-year scan. */
|
|
116
230
|
function hasPossibleCalendarDay(schedule) {
|
|
117
|
-
if (schedule.
|
|
231
|
+
if (schedule.dayStarred || !schedule.weekdayStarred) return true;
|
|
118
232
|
const maximumDay = /* @__PURE__ */ new Map([
|
|
119
233
|
[1, 31],
|
|
120
234
|
[2, 29],
|
|
@@ -165,13 +279,6 @@ function parseField(field, min, max, out) {
|
|
|
165
279
|
}
|
|
166
280
|
return true;
|
|
167
281
|
}
|
|
168
|
-
/** Day/weekday OR semantics: a restricted day field alone gates, and vice versa. */
|
|
169
|
-
function matches(schedule, date) {
|
|
170
|
-
if (!schedule.minutes.has(date.getMinutes())) return false;
|
|
171
|
-
if (!schedule.hours.has(date.getHours())) return false;
|
|
172
|
-
if (!schedule.months.has(date.getMonth() + 1)) return false;
|
|
173
|
-
return dayCandidate(schedule, date);
|
|
174
|
-
}
|
|
175
282
|
function isDigits(value) {
|
|
176
283
|
return /^\d+$/.test(value);
|
|
177
284
|
}
|
|
@@ -379,6 +486,7 @@ function createTask(input, now, id) {
|
|
|
379
486
|
model: normalizeTargetId(input.model),
|
|
380
487
|
reuseSession: input.reuseSession === true ? true : void 0,
|
|
381
488
|
teamRun: input.teamRun === true ? true : void 0,
|
|
489
|
+
goalRun: input.goalRun === false ? false : void 0,
|
|
382
490
|
...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
|
|
383
491
|
...input.handover === void 0 ? {} : { handover: {
|
|
384
492
|
...input.handover,
|
|
@@ -406,11 +514,14 @@ function withSchedule(task, patch, now) {
|
|
|
406
514
|
const schedule = {
|
|
407
515
|
enabled: current?.enabled ?? false,
|
|
408
516
|
cron: current?.cron ?? "",
|
|
517
|
+
...current?.timeZone === void 0 ? {} : { timeZone: current.timeZone },
|
|
409
518
|
nextRunAt: current?.nextRunAt,
|
|
410
519
|
lastTriggeredAt: current?.lastTriggeredAt
|
|
411
520
|
};
|
|
412
521
|
if ("enabled" in patch) schedule.enabled = patch.enabled ?? false;
|
|
413
522
|
if ("cron" in patch) schedule.cron = patch.cron ?? "";
|
|
523
|
+
if ("timeZone" in patch) if (patch.timeZone === void 0) delete schedule.timeZone;
|
|
524
|
+
else schedule.timeZone = patch.timeZone;
|
|
414
525
|
if ("nextRunAt" in patch) schedule.nextRunAt = patch.nextRunAt;
|
|
415
526
|
if ("lastTriggeredAt" in patch) schedule.lastTriggeredAt = patch.lastTriggeredAt;
|
|
416
527
|
return {
|
|
@@ -501,8 +612,8 @@ function indexOf(tasks) {
|
|
|
501
612
|
* under a larger limit survives that limit being lowered, and restore and
|
|
502
613
|
* inheritance must still see all of it.
|
|
503
614
|
*/
|
|
504
|
-
function ancestorChain(tasks, task) {
|
|
505
|
-
const index = indexOf(tasks);
|
|
615
|
+
function ancestorChain(tasks, task, lineage) {
|
|
616
|
+
const index = lineage?.byId ?? indexOf(tasks);
|
|
506
617
|
const chain = [];
|
|
507
618
|
const seen = /* @__PURE__ */ new Set([task.id]);
|
|
508
619
|
let current = task.parentId === void 0 ? void 0 : index.get(task.parentId);
|
|
@@ -515,29 +626,33 @@ function ancestorChain(tasks, task) {
|
|
|
515
626
|
return chain;
|
|
516
627
|
}
|
|
517
628
|
/** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
|
|
518
|
-
function taskDepth(tasks, id) {
|
|
519
|
-
const task = indexOf(tasks).get(id);
|
|
520
|
-
return task === void 0 ? 0 : ancestorChain(tasks, task).length;
|
|
629
|
+
function taskDepth(tasks, id, lineage) {
|
|
630
|
+
const task = (lineage?.byId ?? indexOf(tasks)).get(id);
|
|
631
|
+
return task === void 0 ? 0 : ancestorChain(tasks, task, lineage).length;
|
|
521
632
|
}
|
|
522
633
|
/** Direct subtasks of a task, in ledger order. */
|
|
523
|
-
function directSubtasks(tasks, id) {
|
|
634
|
+
function directSubtasks(tasks, id, lineage) {
|
|
635
|
+
if (lineage !== void 0) return [...lineage.childrenByParent.get(id) ?? []];
|
|
524
636
|
return tasks.filter((task) => task.parentId === id);
|
|
525
637
|
}
|
|
526
638
|
/**
|
|
527
639
|
* Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
|
|
528
640
|
* total depth. The visited set keeps a malformed ledger from looping.
|
|
529
641
|
*/
|
|
530
|
-
function descendantTasks(tasks, id, maxSubtaskDepth) {
|
|
642
|
+
function descendantTasks(tasks, id, maxSubtaskDepth, lineage) {
|
|
531
643
|
const visited = /* @__PURE__ */ new Set([id]);
|
|
532
644
|
const found = [];
|
|
533
645
|
let frontier = [id];
|
|
534
646
|
for (let depth = 1; depth <= maxSubtaskDepth && frontier.length > 0; depth += 1) {
|
|
535
647
|
const next = [];
|
|
536
|
-
for (const parentId of frontier)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
648
|
+
for (const parentId of frontier) {
|
|
649
|
+
const children = lineage === void 0 ? tasks.filter((task) => task.parentId === parentId) : lineage.childrenByParent.get(parentId) ?? [];
|
|
650
|
+
for (const task of children) {
|
|
651
|
+
if (visited.has(task.id)) continue;
|
|
652
|
+
visited.add(task.id);
|
|
653
|
+
found.push(task);
|
|
654
|
+
next.push(task.id);
|
|
655
|
+
}
|
|
541
656
|
}
|
|
542
657
|
frontier = next;
|
|
543
658
|
}
|
|
@@ -548,13 +663,13 @@ function descendantTasks(tasks, id, maxSubtaskDepth) {
|
|
|
548
663
|
* The visited set makes the walk terminate on a cyclic hand-edited ledger
|
|
549
664
|
* instead of recursing until the stack overflows.
|
|
550
665
|
*/
|
|
551
|
-
function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set()) {
|
|
666
|
+
function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set(), lineage) {
|
|
552
667
|
if (visited.has(id)) return 0;
|
|
553
668
|
const seen = /* @__PURE__ */ new Set([...visited, id]);
|
|
554
|
-
const children = directSubtasks(tasks, id);
|
|
669
|
+
const children = directSubtasks(tasks, id, lineage);
|
|
555
670
|
if (children.length === 0) return 0;
|
|
556
671
|
let height = 0;
|
|
557
|
-
for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen));
|
|
672
|
+
for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen, lineage));
|
|
558
673
|
return Math.min(height, 3);
|
|
559
674
|
}
|
|
560
675
|
/**
|
|
@@ -564,8 +679,8 @@ function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set()) {
|
|
|
564
679
|
* and the resulting depth (parent depth + 1 + the child own subtree height)
|
|
565
680
|
* must stay within maxSubtaskDepth.
|
|
566
681
|
*/
|
|
567
|
-
function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
|
|
568
|
-
if (tasks.find((task) => task.id === childId) === void 0) return {
|
|
682
|
+
function checkParentLink(tasks, childId, parentId, maxSubtaskDepth, lineage) {
|
|
683
|
+
if ((lineage === void 0 ? tasks.find((task) => task.id === childId) : lineage.byId.get(childId)) === void 0) return {
|
|
569
684
|
ok: false,
|
|
570
685
|
reason: "unknown-task"
|
|
571
686
|
};
|
|
@@ -574,7 +689,7 @@ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
|
|
|
574
689
|
ok: false,
|
|
575
690
|
reason: "self-parent"
|
|
576
691
|
};
|
|
577
|
-
const parent = tasks.find((task) => task.id === parentId);
|
|
692
|
+
const parent = lineage === void 0 ? tasks.find((task) => task.id === parentId) : lineage.byId.get(parentId);
|
|
578
693
|
if (parent === void 0) return {
|
|
579
694
|
ok: false,
|
|
580
695
|
reason: "unknown-parent"
|
|
@@ -583,11 +698,11 @@ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
|
|
|
583
698
|
ok: false,
|
|
584
699
|
reason: "archived-parent"
|
|
585
700
|
};
|
|
586
|
-
if (descendantTasks(tasks, childId, 3).some((task) => task.id === parentId)) return {
|
|
701
|
+
if (descendantTasks(tasks, childId, 3, lineage).some((task) => task.id === parentId)) return {
|
|
587
702
|
ok: false,
|
|
588
703
|
reason: "cycle"
|
|
589
704
|
};
|
|
590
|
-
if (taskDepth(tasks, parentId) + 1 + subtreeHeight(tasks, childId) > maxSubtaskDepth) return {
|
|
705
|
+
if (taskDepth(tasks, parentId, lineage) + 1 + subtreeHeight(tasks, childId, /* @__PURE__ */ new Set(), lineage) > maxSubtaskDepth) return {
|
|
591
706
|
ok: false,
|
|
592
707
|
reason: "depth-exceeded"
|
|
593
708
|
};
|
|
@@ -671,18 +786,44 @@ function effectiveTaskPermission(task) {
|
|
|
671
786
|
*/
|
|
672
787
|
/**
|
|
673
788
|
* The immutable teammate name for one team-mode member. Teammate names are
|
|
674
|
-
* lower-kebab-case and must stay unique for the lifetime of the Team
|
|
675
|
-
* run-group token
|
|
676
|
-
*
|
|
677
|
-
*
|
|
789
|
+
* lower-kebab-case and must stay unique for the lifetime of the Team: the
|
|
790
|
+
* run-group token separates two runs of the same tree, and the member
|
|
791
|
+
* discriminator separates two members of one run that reduce to the same slug.
|
|
792
|
+
*
|
|
793
|
+
* The discriminator is not cosmetic. A title with no ASCII word characters (a
|
|
794
|
+
* CJK title, for example) carries no slug at all, so every such title of one
|
|
795
|
+
* run group collides on the generic prefix; Agent Teams answers every member
|
|
796
|
+
* after the first with `teammate name "..." was already used in this Team`,
|
|
797
|
+
* and those subtasks never start. Two ASCII titles that share a slug
|
|
798
|
+
* (`AI 强度预测` next to `AI 配比优化`) collide the same way.
|
|
678
799
|
* @param title - the member task's title.
|
|
679
800
|
* @param token - the run group id (the task id when no group is recorded).
|
|
801
|
+
* @param memberId - the member task's id, which makes the name unique in the run.
|
|
680
802
|
* @returns the teammate name handed to the Agent Teams service.
|
|
681
803
|
*/
|
|
682
|
-
function teammateName(title, token) {
|
|
804
|
+
function teammateName(title, token, memberId) {
|
|
683
805
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32).replace(/-+$/g, "");
|
|
684
806
|
const suffix = token.replace(/[^a-z0-9]/gi, "").slice(0, 8).toLowerCase();
|
|
685
|
-
|
|
807
|
+
const member = memberId === void 0 || memberId === "" ? void 0 : stableTag(memberId);
|
|
808
|
+
return [
|
|
809
|
+
slug === "" ? "subtask" : slug,
|
|
810
|
+
member,
|
|
811
|
+
suffix === "" ? "run" : suffix
|
|
812
|
+
].filter((part) => part !== void 0).join("-");
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* A stable 4-hex tag for one member id. The name is derived twice for the same
|
|
816
|
+
* member — once for the Lead's prompt and once for the spawn request — so the
|
|
817
|
+
* tag must be a pure function of the id, never of a clock or a counter. FNV-1a
|
|
818
|
+
* keeps it dependency-free in `core/`, which both halves compile.
|
|
819
|
+
*/
|
|
820
|
+
function stableTag(value) {
|
|
821
|
+
let hash = 2166136261;
|
|
822
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
823
|
+
hash ^= value.charCodeAt(index);
|
|
824
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
825
|
+
}
|
|
826
|
+
return hash.toString(16).padStart(8, "0").slice(0, 4);
|
|
686
827
|
}
|
|
687
828
|
function resolveExecutionTargets(task, tasks, maxSubtaskDepth = 3) {
|
|
688
829
|
if (task.parentId === void 0) return task;
|
|
@@ -910,6 +1051,7 @@ function isTaskRecordShape(value) {
|
|
|
910
1051
|
if (record.mode !== void 0 && typeof record.mode !== "string") return false;
|
|
911
1052
|
if (record.permission !== void 0 && typeof record.permission !== "string") return false;
|
|
912
1053
|
if (record.reuseSession !== void 0 && typeof record.reuseSession !== "boolean") return false;
|
|
1054
|
+
if (record.goalRun !== void 0 && typeof record.goalRun !== "boolean") return false;
|
|
913
1055
|
if (!Array.isArray(record.executions)) return false;
|
|
914
1056
|
for (const execution of record.executions) {
|
|
915
1057
|
if (typeof execution !== "object" || execution === null) return false;
|
|
@@ -944,9 +1086,11 @@ function normalizeSchedule(schedule) {
|
|
|
944
1086
|
const rule = schedule;
|
|
945
1087
|
if (typeof rule.cron !== "string") return void 0;
|
|
946
1088
|
if (rule.cron.trim() === "" || !isValidCron(rule.cron)) return void 0;
|
|
1089
|
+
const timeZone = typeof rule.timeZone === "string" && isValidTimeZone(rule.timeZone) ? rule.timeZone : void 0;
|
|
947
1090
|
return {
|
|
948
1091
|
enabled: rule.enabled === true,
|
|
949
1092
|
cron: rule.cron,
|
|
1093
|
+
...timeZone === void 0 ? {} : { timeZone },
|
|
950
1094
|
nextRunAt: typeof rule.nextRunAt === "number" ? rule.nextRunAt : void 0,
|
|
951
1095
|
lastTriggeredAt: typeof rule.lastTriggeredAt === "number" ? rule.lastTriggeredAt : void 0
|
|
952
1096
|
};
|
|
@@ -1031,6 +1175,7 @@ function parseLedger(raw) {
|
|
|
1031
1175
|
task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
|
|
1032
1176
|
task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
|
|
1033
1177
|
task.reuseSession = row.reuseSession === true ? true : void 0;
|
|
1178
|
+
task.goalRun = row.goalRun === false ? false : void 0;
|
|
1034
1179
|
task.freeze = normalizeFreeze(row.freeze);
|
|
1035
1180
|
task.handover = normalizeHandover(row.handover);
|
|
1036
1181
|
task.tags = normalizeTags(row.tags);
|
|
@@ -1145,8 +1290,9 @@ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
|
1145
1290
|
* @param now - clock instant (ms epoch).
|
|
1146
1291
|
* @param id - minted task id.
|
|
1147
1292
|
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
1293
|
+
* @param hostTimeZone - zone a rule that stores no zone of its own follows.
|
|
1148
1294
|
*/
|
|
1149
|
-
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1) {
|
|
1295
|
+
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone()) {
|
|
1150
1296
|
if (input.title.trim() === "") return {
|
|
1151
1297
|
task: void 0,
|
|
1152
1298
|
tasks,
|
|
@@ -1181,10 +1327,18 @@ function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1) {
|
|
|
1181
1327
|
const requested = input.schedule;
|
|
1182
1328
|
if (requested?.enabled === true && requested.cron.trim() !== "" && isValidCron(requested.cron)) {
|
|
1183
1329
|
const cron = requested.cron.trim();
|
|
1184
|
-
|
|
1330
|
+
const requestedZone = requested.timeZone;
|
|
1331
|
+
if (requestedZone !== void 0 && !isValidTimeZone(requestedZone)) return {
|
|
1332
|
+
task: void 0,
|
|
1333
|
+
tasks,
|
|
1334
|
+
error: "invalid schedule time zone"
|
|
1335
|
+
};
|
|
1336
|
+
const nextRunAt = nextRunAtMs(cron, now, requestedZone ?? hostTimeZone);
|
|
1337
|
+
if (nextRunAt !== void 0) task = withSchedule(task, {
|
|
1185
1338
|
enabled: true,
|
|
1186
1339
|
cron,
|
|
1187
|
-
|
|
1340
|
+
timeZone: requestedZone,
|
|
1341
|
+
nextRunAt
|
|
1188
1342
|
}, now);
|
|
1189
1343
|
}
|
|
1190
1344
|
return {
|
|
@@ -1222,15 +1376,17 @@ function applyDeleteTask(tasks, selectedTaskId, id) {
|
|
|
1222
1376
|
* the core cron parser (schedule.ts) and the withSchedule transition.
|
|
1223
1377
|
*/
|
|
1224
1378
|
/**
|
|
1225
|
-
* Set an on-board task's schedule rule. A blank or invalid cron, or
|
|
1226
|
-
* archived task, is rejected (state untouched); an enabled
|
|
1227
|
-
* next run instant immediately
|
|
1379
|
+
* Set an on-board task's schedule rule. A blank or invalid cron, an unknown or
|
|
1380
|
+
* archived task, or an unusable zone is rejected (state untouched); an enabled
|
|
1381
|
+
* rule computes the next run instant immediately in the rule's own zone, a
|
|
1382
|
+
* disabled one carries no next-run instant.
|
|
1228
1383
|
* @param tasks - current ledger.
|
|
1229
1384
|
* @param id - the task to schedule.
|
|
1230
1385
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
1231
1386
|
* @param now - clock instant (ms epoch).
|
|
1387
|
+
* @param hostTimeZone - zone a rule with no stored zone follows.
|
|
1232
1388
|
*/
|
|
1233
|
-
function applySetSchedule(tasks, id, patch, now) {
|
|
1389
|
+
function applySetSchedule(tasks, id, patch, now, hostTimeZone) {
|
|
1234
1390
|
const task = tasks.find((candidate) => candidate.id === id);
|
|
1235
1391
|
if (task === void 0 || task.archivedAt !== void 0) return {
|
|
1236
1392
|
tasks,
|
|
@@ -1242,8 +1398,13 @@ function applySetSchedule(tasks, id, patch, now) {
|
|
|
1242
1398
|
tasks,
|
|
1243
1399
|
applied: false
|
|
1244
1400
|
};
|
|
1401
|
+
const requestedZone = patch.timeZone === void 0 ? current?.timeZone : patch.timeZone ?? void 0;
|
|
1402
|
+
if (requestedZone !== void 0 && !isValidTimeZone(requestedZone)) return {
|
|
1403
|
+
tasks,
|
|
1404
|
+
applied: false
|
|
1405
|
+
};
|
|
1245
1406
|
const enabled = patch.enabled ?? current?.enabled ?? false;
|
|
1246
|
-
const nextRunAt = enabled ? nextRunAtMs(cron, now) : void 0;
|
|
1407
|
+
const nextRunAt = enabled ? nextRunAtMs(cron, now, requestedZone ?? hostTimeZone) : void 0;
|
|
1247
1408
|
if (enabled && nextRunAt === void 0) return {
|
|
1248
1409
|
tasks,
|
|
1249
1410
|
applied: false
|
|
@@ -1252,6 +1413,7 @@ function applySetSchedule(tasks, id, patch, now) {
|
|
|
1252
1413
|
tasks: tasks.map((candidate) => candidate.id === id ? withSchedule(candidate, {
|
|
1253
1414
|
enabled,
|
|
1254
1415
|
cron,
|
|
1416
|
+
timeZone: requestedZone ?? void 0,
|
|
1255
1417
|
nextRunAt
|
|
1256
1418
|
}, now) : candidate),
|
|
1257
1419
|
applied: true
|
|
@@ -1411,6 +1573,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
1411
1573
|
if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
|
|
1412
1574
|
if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
|
|
1413
1575
|
if ("teamRun" in patch) next.teamRun = patch.teamRun === true ? true : void 0;
|
|
1576
|
+
if ("goalRun" in patch) next.goalRun = patch.goalRun === false ? false : void 0;
|
|
1414
1577
|
if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
|
|
1415
1578
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
1416
1579
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
@@ -1464,6 +1627,7 @@ function validImportedKnownFields(value) {
|
|
|
1464
1627
|
const schedule = record(value.schedule);
|
|
1465
1628
|
if (schedule === void 0 || typeof schedule.enabled !== "boolean" || typeof schedule.cron !== "string") return false;
|
|
1466
1629
|
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false;
|
|
1630
|
+
if (schedule.timeZone !== void 0 && (typeof schedule.timeZone !== "string" || !isValidTimeZone(schedule.timeZone))) return false;
|
|
1467
1631
|
}
|
|
1468
1632
|
if (value.executions !== void 0) {
|
|
1469
1633
|
if (!Array.isArray(value.executions)) return false;
|
|
@@ -1511,6 +1675,7 @@ function importedTask(value) {
|
|
|
1511
1675
|
...task.schedule === void 0 ? {} : { schedule: {
|
|
1512
1676
|
enabled: task.schedule.enabled,
|
|
1513
1677
|
cron: task.schedule.cron,
|
|
1678
|
+
...task.schedule.timeZone === void 0 ? {} : { timeZone: task.schedule.timeZone },
|
|
1514
1679
|
nextRunAt: task.schedule.nextRunAt,
|
|
1515
1680
|
lastTriggeredAt: task.schedule.lastTriggeredAt
|
|
1516
1681
|
} },
|
|
@@ -1519,6 +1684,7 @@ function importedTask(value) {
|
|
|
1519
1684
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
1520
1685
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
1521
1686
|
...task.reuseSession === void 0 ? {} : { reuseSession: task.reuseSession },
|
|
1687
|
+
...task.goalRun === void 0 ? {} : { goalRun: task.goalRun },
|
|
1522
1688
|
...task.archivedAt === void 0 ? {} : { archivedAt: task.archivedAt },
|
|
1523
1689
|
...task.freeze === void 0 ? {} : { freeze: task.freeze },
|
|
1524
1690
|
...task.handover === void 0 ? {} : { handover: task.handover },
|
|
@@ -1565,6 +1731,7 @@ function createInput(value) {
|
|
|
1565
1731
|
"model",
|
|
1566
1732
|
"reuseSession",
|
|
1567
1733
|
"teamRun",
|
|
1734
|
+
"goalRun",
|
|
1568
1735
|
"tags"
|
|
1569
1736
|
])) return false;
|
|
1570
1737
|
if (input.parentId !== void 0 && (typeof input.parentId !== "string" || input.parentId.trim() === "")) return false;
|
|
@@ -1572,14 +1739,20 @@ function createInput(value) {
|
|
|
1572
1739
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false;
|
|
1573
1740
|
if (input.reuseSession !== void 0 && typeof input.reuseSession !== "boolean") return false;
|
|
1574
1741
|
if (input.teamRun !== void 0 && typeof input.teamRun !== "boolean") return false;
|
|
1742
|
+
if (input.goalRun !== void 0 && typeof input.goalRun !== "boolean") return false;
|
|
1575
1743
|
if (input.permission !== void 0 && !isTaskPermission(input.permission)) return false;
|
|
1576
1744
|
if (input.tags !== void 0 && !isTaskTagList(input.tags)) return false;
|
|
1577
1745
|
if (input.freeze !== void 0 && freezePayload(input.freeze) === void 0) return false;
|
|
1578
1746
|
if (input.handover !== void 0 && handoverPayload(input.handover) === void 0) return false;
|
|
1579
1747
|
if (input.schedule !== void 0) {
|
|
1580
1748
|
const schedule = record(input.schedule);
|
|
1581
|
-
if (schedule === void 0 || !exactKeys(schedule, [
|
|
1749
|
+
if (schedule === void 0 || !exactKeys(schedule, [
|
|
1750
|
+
"enabled",
|
|
1751
|
+
"cron",
|
|
1752
|
+
"timeZone"
|
|
1753
|
+
])) return false;
|
|
1582
1754
|
if (typeof schedule.enabled !== "boolean" || typeof schedule.cron !== "string") return false;
|
|
1755
|
+
if (schedule.timeZone !== void 0 && (typeof schedule.timeZone !== "string" || !isValidTimeZone(schedule.timeZone))) return false;
|
|
1583
1756
|
}
|
|
1584
1757
|
return true;
|
|
1585
1758
|
}
|
|
@@ -1597,10 +1770,12 @@ function updatePatch(value) {
|
|
|
1597
1770
|
"model",
|
|
1598
1771
|
"reuseSession",
|
|
1599
1772
|
"teamRun",
|
|
1773
|
+
"goalRun",
|
|
1600
1774
|
"tags"
|
|
1601
1775
|
])) return false;
|
|
1602
1776
|
if (patch.reuseSession !== void 0 && patch.reuseSession !== null && typeof patch.reuseSession !== "boolean") return false;
|
|
1603
1777
|
if (patch.teamRun !== void 0 && patch.teamRun !== null && typeof patch.teamRun !== "boolean") return false;
|
|
1778
|
+
if (patch.goalRun !== void 0 && patch.goalRun !== null && typeof patch.goalRun !== "boolean") return false;
|
|
1604
1779
|
for (const key of [
|
|
1605
1780
|
"title",
|
|
1606
1781
|
"description",
|
|
@@ -1616,7 +1791,11 @@ function updatePatch(value) {
|
|
|
1616
1791
|
}
|
|
1617
1792
|
function schedulePatch(value) {
|
|
1618
1793
|
const patch = record(value);
|
|
1619
|
-
return patch !== void 0 && exactKeys(patch, [
|
|
1794
|
+
return patch !== void 0 && exactKeys(patch, [
|
|
1795
|
+
"enabled",
|
|
1796
|
+
"cron",
|
|
1797
|
+
"timeZone"
|
|
1798
|
+
]) && (patch.enabled === void 0 || typeof patch.enabled === "boolean") && (patch.cron === void 0 || typeof patch.cron === "string") && (patch.timeZone === void 0 || patch.timeZone === null || typeof patch.timeZone === "string" && isValidTimeZone(patch.timeZone));
|
|
1620
1799
|
}
|
|
1621
1800
|
function parseActionEnvelope(value) {
|
|
1622
1801
|
const parsed = parseEnvelopeAction(value);
|
|
@@ -1752,6 +1931,7 @@ function parseEnvelopeAction(value) {
|
|
|
1752
1931
|
case "delete":
|
|
1753
1932
|
case "archive":
|
|
1754
1933
|
case "restore":
|
|
1934
|
+
case "settle":
|
|
1755
1935
|
case "run":
|
|
1756
1936
|
case "rerun":
|
|
1757
1937
|
if (!exactKeys(action, ["kind", "taskId"])) return void 0;
|
|
@@ -1773,7 +1953,11 @@ function bindingRefusalMessage(refusal, sessionDefault, surface) {
|
|
|
1773
1953
|
}
|
|
1774
1954
|
const MAX_REQUEST_CACHE = 256;
|
|
1775
1955
|
function timeZone() {
|
|
1776
|
-
return
|
|
1956
|
+
return resolveHostTimeZone();
|
|
1957
|
+
}
|
|
1958
|
+
/** The zone a schedule is evaluated in: its own, or the Host zone when it stores none. */
|
|
1959
|
+
function scheduleZone(schedule) {
|
|
1960
|
+
return schedule.timeZone ?? timeZone();
|
|
1777
1961
|
}
|
|
1778
1962
|
function cloneTasks(tasks) {
|
|
1779
1963
|
return JSON.parse(JSON.stringify(tasks));
|
|
@@ -2044,6 +2228,7 @@ function parseHostTasks(values) {
|
|
|
2044
2228
|
schedule: {
|
|
2045
2229
|
enabled: false,
|
|
2046
2230
|
cron: schedule.cron,
|
|
2231
|
+
...typeof schedule.timeZone === "string" && isValidTimeZone(schedule.timeZone) ? { timeZone: schedule.timeZone } : {},
|
|
2047
2232
|
nextRunAt: void 0,
|
|
2048
2233
|
lastTriggeredAt: typeof schedule.lastTriggeredAt === "number" && Number.isFinite(schedule.lastTriggeredAt) ? schedule.lastTriggeredAt : void 0
|
|
2049
2234
|
}
|
|
@@ -2084,6 +2269,7 @@ var HostTaskLedger = class {
|
|
|
2084
2269
|
for (const request of this.document.recentRequests) this.requestCache.set(request.requestId, { fingerprint: request.fingerprint });
|
|
2085
2270
|
this.repairSchedules(true);
|
|
2086
2271
|
this.reconcileInterruptedStarts();
|
|
2272
|
+
this.finalizeReadyRuns(false);
|
|
2087
2273
|
this.commit(false);
|
|
2088
2274
|
} catch (error) {
|
|
2089
2275
|
this.dispose();
|
|
@@ -2137,6 +2323,11 @@ var HostTaskLedger = class {
|
|
|
2137
2323
|
*/
|
|
2138
2324
|
runtimeView() {
|
|
2139
2325
|
let armedSchedules = 0;
|
|
2326
|
+
const teamGroups = /* @__PURE__ */ new Set();
|
|
2327
|
+
for (const task of this.document.tasks) {
|
|
2328
|
+
if (task.teamRun !== true) continue;
|
|
2329
|
+
for (const execution of task.executions) if (execution.runGroupId !== void 0) teamGroups.add(execution.runGroupId);
|
|
2330
|
+
}
|
|
2140
2331
|
const openExecutions = [];
|
|
2141
2332
|
for (const task of this.document.tasks) {
|
|
2142
2333
|
if (task.archivedAt === void 0 && task.schedule?.enabled === true) armedSchedules += 1;
|
|
@@ -2147,7 +2338,8 @@ var HostTaskLedger = class {
|
|
|
2147
2338
|
taskId: task.id,
|
|
2148
2339
|
executionId: execution.id,
|
|
2149
2340
|
sessionId: execution.sessionId,
|
|
2150
|
-
startedAt: execution.startedAt
|
|
2341
|
+
startedAt: execution.startedAt,
|
|
2342
|
+
teamMember: task.teamRun !== true && execution.runGroupId !== void 0 && teamGroups.has(execution.runGroupId)
|
|
2151
2343
|
});
|
|
2152
2344
|
}
|
|
2153
2345
|
}
|
|
@@ -2191,6 +2383,7 @@ var HostTaskLedger = class {
|
|
|
2191
2383
|
due.push({
|
|
2192
2384
|
taskId: task.id,
|
|
2193
2385
|
cron: schedule.cron,
|
|
2386
|
+
timeZone: scheduleZone(schedule),
|
|
2194
2387
|
nextRunAt: schedule.nextRunAt
|
|
2195
2388
|
});
|
|
2196
2389
|
}
|
|
@@ -2263,7 +2456,7 @@ var HostTaskLedger = class {
|
|
|
2263
2456
|
...task,
|
|
2264
2457
|
schedule: {
|
|
2265
2458
|
...schedule,
|
|
2266
|
-
nextRunAt: nextRunAtMs(schedule.cron, now)
|
|
2459
|
+
nextRunAt: nextRunAtMs(schedule.cron, now, scheduleZone(schedule))
|
|
2267
2460
|
},
|
|
2268
2461
|
updatedAt: now
|
|
2269
2462
|
};
|
|
@@ -2316,15 +2509,17 @@ var HostTaskLedger = class {
|
|
|
2316
2509
|
});
|
|
2317
2510
|
changed = true;
|
|
2318
2511
|
}
|
|
2512
|
+
if (task.teamRun === true && this.closeTeamMembers(taskId, groupId, execution.ownResult ?? outcome, execution.ownError ?? error, now)) changed = true;
|
|
2319
2513
|
if (this.settleCascade(taskId, groupId, now)) changed = true;
|
|
2320
2514
|
if (changed) this.commit();
|
|
2321
2515
|
}
|
|
2322
2516
|
attachSession(taskId, executionId, sessionId) {
|
|
2323
2517
|
const now = this.now();
|
|
2324
|
-
this.document.tasks
|
|
2325
|
-
|
|
2518
|
+
if (this.document.tasks.find((item) => item.id === taskId)?.executions.find((entry) => entry.id === executionId)?.endedAt !== void 0) return;
|
|
2519
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== taskId ? item : {
|
|
2520
|
+
...item,
|
|
2326
2521
|
updatedAt: now,
|
|
2327
|
-
executions:
|
|
2522
|
+
executions: item.executions.map((entry) => entry.id === executionId ? {
|
|
2328
2523
|
...entry,
|
|
2329
2524
|
sessionId
|
|
2330
2525
|
} : entry)
|
|
@@ -2347,11 +2542,12 @@ var HostTaskLedger = class {
|
|
|
2347
2542
|
this.document.scheduler.error = invalidScheduleIds.length === 0 ? void 0 : `invalid cron disabled for task(s): ${invalidScheduleIds.join(", ")}`;
|
|
2348
2543
|
this.repairSchedules(true, false);
|
|
2349
2544
|
this.reconcileInterruptedStarts(false);
|
|
2545
|
+
this.finalizeReadyRuns(false);
|
|
2350
2546
|
break;
|
|
2351
2547
|
}
|
|
2352
2548
|
case "create": {
|
|
2353
2549
|
if (this.document.tasks.some((task) => task.id === action.id)) throw new Error("task id already exists");
|
|
2354
|
-
if (action.input.schedule?.enabled === true && (!isValidCron(action.input.schedule.cron) || nextRunAtMs(action.input.schedule.cron, now) === void 0)) throw new Error("invalid schedule");
|
|
2550
|
+
if (action.input.schedule?.enabled === true && (!isValidCron(action.input.schedule.cron) || !isValidTimeZone(action.input.schedule.timeZone ?? timeZone()) || nextRunAtMs(action.input.schedule.cron, now, action.input.schedule.timeZone ?? timeZone()) === void 0)) throw new Error("invalid schedule");
|
|
2355
2551
|
const input = action.input.freeze === void 0 || initiator === void 0 || initiator === "" ? action.input : {
|
|
2356
2552
|
...action.input,
|
|
2357
2553
|
freeze: {
|
|
@@ -2359,7 +2555,7 @@ var HostTaskLedger = class {
|
|
|
2359
2555
|
frozenBy: initiator
|
|
2360
2556
|
}
|
|
2361
2557
|
};
|
|
2362
|
-
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth);
|
|
2558
|
+
const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth, timeZone());
|
|
2363
2559
|
if (result.task === void 0) throw new Error(result.error ?? "invalid task");
|
|
2364
2560
|
this.document.tasks = [...result.tasks];
|
|
2365
2561
|
break;
|
|
@@ -2410,6 +2606,19 @@ var HostTaskLedger = class {
|
|
|
2410
2606
|
this.document.tasks = [...result.tasks];
|
|
2411
2607
|
break;
|
|
2412
2608
|
}
|
|
2609
|
+
case "settle": {
|
|
2610
|
+
const task = this.document.tasks.find((item) => item.id === action.taskId);
|
|
2611
|
+
if (task === void 0) throw new Error("task not found");
|
|
2612
|
+
if (task.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
2613
|
+
if (!task.executions.some((entry) => entry.endedAt === void 0)) throw new Error("task has no open execution");
|
|
2614
|
+
const reason = "execution closed manually by " + (initiator === void 0 || initiator === "" ? "the operator" : initiator) + ": no verdict was recorded";
|
|
2615
|
+
for (const member of cascadeTargets(this.document.tasks, action.taskId, this.maxSubtaskDepth)) for (const execution of member.executions) {
|
|
2616
|
+
if (execution.endedAt !== void 0) continue;
|
|
2617
|
+
this.document.tasks = this.document.tasks.map((item) => item.id !== member.id ? item : settleExecution(item, execution.id, "cancelled", now, reason));
|
|
2618
|
+
}
|
|
2619
|
+
this.finalizeReadyRuns(false);
|
|
2620
|
+
break;
|
|
2621
|
+
}
|
|
2413
2622
|
case "restore": {
|
|
2414
2623
|
if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be restored");
|
|
2415
2624
|
const result = applyRestoreTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
|
|
@@ -2430,7 +2639,7 @@ var HostTaskLedger = class {
|
|
|
2430
2639
|
}
|
|
2431
2640
|
case "set-schedule": {
|
|
2432
2641
|
if (this.document.tasks.find((task) => task.id === action.taskId)?.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
2433
|
-
const result = applySetSchedule(this.document.tasks, action.taskId, action.patch, now);
|
|
2642
|
+
const result = applySetSchedule(this.document.tasks, action.taskId, action.patch, now, timeZone());
|
|
2434
2643
|
if (!result.applied) throw new Error("invalid schedule");
|
|
2435
2644
|
this.document.tasks = [...result.tasks];
|
|
2436
2645
|
break;
|
|
@@ -2574,6 +2783,86 @@ var HostTaskLedger = class {
|
|
|
2574
2783
|
}
|
|
2575
2784
|
return changed;
|
|
2576
2785
|
}
|
|
2786
|
+
/**
|
|
2787
|
+
* Close every run that is already decided, and fold every lineage whose
|
|
2788
|
+
* members are all settled. The Host poll calls this on every tick, so a run
|
|
2789
|
+
* can no longer be left in the running column by a settle nobody observed:
|
|
2790
|
+
*
|
|
2791
|
+
* - A team run is governed by its Lead. Once the Lead's own outcome is
|
|
2792
|
+
* recorded, its still-open members are settled with that verdict. A
|
|
2793
|
+
* teammate is a durable member of the Team and may never report a turn of
|
|
2794
|
+
* its own, and without this the whole lineage waits on it forever.
|
|
2795
|
+
* - A cascade parent whose members are all settled is finalized here even
|
|
2796
|
+
* when the settle that completed the tree happened in another process, or
|
|
2797
|
+
* was interrupted between its child write and its parent write.
|
|
2798
|
+
*
|
|
2799
|
+
* Idempotent: a run that is already folded is left untouched, so a caller may
|
|
2800
|
+
* invoke it on every tick.
|
|
2801
|
+
* @param persist - false to leave the document for the caller to commit.
|
|
2802
|
+
* @returns whether the document changed.
|
|
2803
|
+
*/
|
|
2804
|
+
finalizeReadyRuns(persist = true) {
|
|
2805
|
+
const now = this.now();
|
|
2806
|
+
let changed = false;
|
|
2807
|
+
const groups = /* @__PURE__ */ new Set();
|
|
2808
|
+
for (const task of this.document.tasks) for (const execution of task.executions) if (execution.endedAt === void 0 && execution.runGroupId !== void 0) groups.add(execution.runGroupId);
|
|
2809
|
+
for (const groupId of groups) {
|
|
2810
|
+
const lead = this.document.tasks.find((task) => task.teamRun === true && task.executions.some((entry) => entry.runGroupId === groupId));
|
|
2811
|
+
const verdict = lead === void 0 ? void 0 : this.leadVerdict(lead, groupId);
|
|
2812
|
+
if (lead !== void 0 && verdict !== void 0 && this.closeTeamMembers(lead.id, groupId, verdict.result, verdict.error, now)) changed = true;
|
|
2813
|
+
for (const task of this.document.tasks) if (this.settleCascade(task.id, groupId, now)) changed = true;
|
|
2814
|
+
}
|
|
2815
|
+
if (changed && persist) this.commit();
|
|
2816
|
+
return changed;
|
|
2817
|
+
}
|
|
2818
|
+
/**
|
|
2819
|
+
* The verdict a team run's Lead has already recorded: its own turn outcome
|
|
2820
|
+
* once that turn ended, or the folded outcome once its card settled.
|
|
2821
|
+
*/
|
|
2822
|
+
leadVerdict(lead, groupId) {
|
|
2823
|
+
const execution = lead.executions.find((entry) => entry.runGroupId === groupId);
|
|
2824
|
+
const result = execution?.ownResult ?? execution?.result;
|
|
2825
|
+
if (result === void 0) return void 0;
|
|
2826
|
+
return {
|
|
2827
|
+
result,
|
|
2828
|
+
error: execution?.ownError ?? execution?.error
|
|
2829
|
+
};
|
|
2830
|
+
}
|
|
2831
|
+
/**
|
|
2832
|
+
* Settle every still-open member of a team run with the Lead's verdict.
|
|
2833
|
+
* A teammate that already reported its own outcome keeps it: the ordinary
|
|
2834
|
+
* fold still lets a failure dominate.
|
|
2835
|
+
* @param leadId - the Lead card's task id (never settled here).
|
|
2836
|
+
* @param groupId - the team run's group.
|
|
2837
|
+
* @param verdict - the Lead's outcome, inherited by members that never reported.
|
|
2838
|
+
* @param error - the Lead's failure text, inherited with a failed verdict.
|
|
2839
|
+
* @param now - clock instant (ms epoch).
|
|
2840
|
+
* @returns whether the document changed.
|
|
2841
|
+
*/
|
|
2842
|
+
closeTeamMembers(leadId, groupId, verdict, error, now) {
|
|
2843
|
+
const members = [];
|
|
2844
|
+
let recorded = false;
|
|
2845
|
+
this.document.tasks = this.document.tasks.map((task) => {
|
|
2846
|
+
if (task.id === leadId) return task;
|
|
2847
|
+
const execution = openGroupExecution(task, groupId);
|
|
2848
|
+
if (execution === void 0) return task;
|
|
2849
|
+
members.push(task.id);
|
|
2850
|
+
if (execution.ownResult !== void 0) return task;
|
|
2851
|
+
recorded = true;
|
|
2852
|
+
return {
|
|
2853
|
+
...task,
|
|
2854
|
+
updatedAt: now,
|
|
2855
|
+
executions: task.executions.map((entry) => entry.id === execution.id ? {
|
|
2856
|
+
...entry,
|
|
2857
|
+
ownResult: verdict,
|
|
2858
|
+
ownError: error
|
|
2859
|
+
} : entry)
|
|
2860
|
+
};
|
|
2861
|
+
});
|
|
2862
|
+
let changed = recorded;
|
|
2863
|
+
for (const memberId of members) if (this.settleCascade(memberId, groupId, now)) changed = true;
|
|
2864
|
+
return changed;
|
|
2865
|
+
}
|
|
2577
2866
|
repairSchedules(skipPast, persist = true) {
|
|
2578
2867
|
const now = this.now();
|
|
2579
2868
|
let changed = false;
|
|
@@ -2581,7 +2870,7 @@ var HostTaskLedger = class {
|
|
|
2581
2870
|
const schedule = task.schedule;
|
|
2582
2871
|
if (schedule === void 0 || !schedule.enabled) return task;
|
|
2583
2872
|
if (!skipPast && schedule.nextRunAt !== void 0) return task;
|
|
2584
|
-
const next = nextRunAtMs(schedule.cron, now);
|
|
2873
|
+
const next = nextRunAtMs(schedule.cron, now, scheduleZone(schedule));
|
|
2585
2874
|
if (next === void 0) {
|
|
2586
2875
|
changed = true;
|
|
2587
2876
|
this.document.scheduler.error = `invalid cron disabled for task: ${task.id}`;
|
|
@@ -2631,14 +2920,36 @@ var HostTaskLedger = class {
|
|
|
2631
2920
|
if (changed && persist) this.commit();
|
|
2632
2921
|
}
|
|
2633
2922
|
/**
|
|
2634
|
-
* Field-preserving
|
|
2635
|
-
*
|
|
2636
|
-
*
|
|
2637
|
-
*
|
|
2923
|
+
* Field-preserving migration of a pre-v4 document. It first proves every task
|
|
2924
|
+
* row is structurally valid, so a document that would silently drop or coerce
|
|
2925
|
+
* rows fails loudly instead (no quarantined-empty restart), and then reuses
|
|
2926
|
+
* the current normalization.
|
|
2927
|
+
*
|
|
2928
|
+
* v4 adds `ScheduleRule.timeZone`. A rule written before v4 was evaluated in
|
|
2929
|
+
* whatever zone the Host process happened to report, so the migration stamps
|
|
2930
|
+
* that zone onto every enabled rule: the trigger instant is preserved exactly
|
|
2931
|
+
* (the stored `nextRunAt` already encodes the old zone), but the rule stops
|
|
2932
|
+
* following a later `TZ` change, which is what made an existing schedule
|
|
2933
|
+
* silently move when the Host's zone changed.
|
|
2638
2934
|
*/
|
|
2639
2935
|
migrateLegacyDocument(parsed) {
|
|
2640
|
-
if (!Array.isArray(parsed.tasks) || !parsed.tasks.every((row) => isTaskRecord(row))) throw new Error(
|
|
2641
|
-
|
|
2936
|
+
if (!Array.isArray(parsed.tasks) || !parsed.tasks.every((row) => isTaskRecord(row))) throw new Error(`v${String(parsed.schemaVersion)} document contains structurally invalid task rows`);
|
|
2937
|
+
const rows = parsed.tasks.map((value) => {
|
|
2938
|
+
const schedule = value.schedule;
|
|
2939
|
+
if (typeof schedule !== "object" || schedule === null) return value;
|
|
2940
|
+
if (typeof schedule.timeZone === "string") return value;
|
|
2941
|
+
return {
|
|
2942
|
+
...value,
|
|
2943
|
+
schedule: {
|
|
2944
|
+
...schedule,
|
|
2945
|
+
timeZone: timeZone()
|
|
2946
|
+
}
|
|
2947
|
+
};
|
|
2948
|
+
});
|
|
2949
|
+
return this.normalizeDocument({
|
|
2950
|
+
...parsed,
|
|
2951
|
+
tasks: rows
|
|
2952
|
+
});
|
|
2642
2953
|
}
|
|
2643
2954
|
load(dir) {
|
|
2644
2955
|
const existed = existsSync(this.file);
|
|
@@ -2648,13 +2959,13 @@ var HostTaskLedger = class {
|
|
|
2648
2959
|
} catch (error) {
|
|
2649
2960
|
return this.recoverCorrupt(dir, existed, error);
|
|
2650
2961
|
}
|
|
2651
|
-
if (parsed.schemaVersion === 2) try {
|
|
2962
|
+
if (parsed.schemaVersion === 3 || parsed.schemaVersion === 2) try {
|
|
2652
2963
|
return this.migrateLegacyDocument(parsed);
|
|
2653
2964
|
} catch (error) {
|
|
2654
|
-
throw new Error(`ledger
|
|
2965
|
+
throw new Error(`ledger v${String(parsed.schemaVersion)} to v4 migration failed; original file kept at ${this.file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2655
2966
|
}
|
|
2656
2967
|
try {
|
|
2657
|
-
if (parsed.schemaVersion !==
|
|
2968
|
+
if (parsed.schemaVersion !== 4 || !Array.isArray(parsed.tasks)) throw new Error("unsupported ledger schema");
|
|
2658
2969
|
return this.normalizeDocument(parsed);
|
|
2659
2970
|
} catch (error) {
|
|
2660
2971
|
return this.recoverCorrupt(dir, existed, error);
|
|
@@ -2676,7 +2987,7 @@ var HostTaskLedger = class {
|
|
|
2676
2987
|
const sidecarLastTickAt = this.readSchedulerSidecar();
|
|
2677
2988
|
const lastTickAt = sidecarLastTickAt === void 0 || documentLastTickAt !== void 0 && documentLastTickAt >= sidecarLastTickAt ? documentLastTickAt : sidecarLastTickAt;
|
|
2678
2989
|
return {
|
|
2679
|
-
schemaVersion:
|
|
2990
|
+
schemaVersion: 4,
|
|
2680
2991
|
revision: Number.isSafeInteger(parsed.revision) && parsed.revision >= 0 ? parsed.revision : 0,
|
|
2681
2992
|
tasks,
|
|
2682
2993
|
scheduler: {
|
|
@@ -2702,7 +3013,7 @@ var HostTaskLedger = class {
|
|
|
2702
3013
|
if (existed) renameSync(this.file, `${this.file}.corrupt-${this.now()}-${process.pid}-${crypto.randomUUID()}`);
|
|
2703
3014
|
mkdirSync(dir, { recursive: true });
|
|
2704
3015
|
return {
|
|
2705
|
-
schemaVersion:
|
|
3016
|
+
schemaVersion: 4,
|
|
2706
3017
|
revision: 0,
|
|
2707
3018
|
tasks: [],
|
|
2708
3019
|
scheduler: {
|
|
@@ -2894,6 +3205,16 @@ function isDefinitionWithdrawn(error) {
|
|
|
2894
3205
|
}
|
|
2895
3206
|
const SERVICE_UNAVAILABLE_ATTEMPTS = 5;
|
|
2896
3207
|
const SERVICE_UNAVAILABLE_BACKOFF_MS = 2e3;
|
|
3208
|
+
/**
|
|
3209
|
+
* Largest number of history pages one inspection walks back. A session whose
|
|
3210
|
+
* window is wider than this cannot be resolved from its log, which the caller
|
|
3211
|
+
* treats as an unreadable history rather than as pending work.
|
|
3212
|
+
*/
|
|
3213
|
+
const HISTORY_PAGE_LIMIT = 100;
|
|
3214
|
+
/** One-line text of a gateway or stream failure, for a decideable reason. */
|
|
3215
|
+
function failureText(error) {
|
|
3216
|
+
return error instanceof Error ? error.message : String(error);
|
|
3217
|
+
}
|
|
2897
3218
|
function delay(ms) {
|
|
2898
3219
|
return new Promise((resolve) => {
|
|
2899
3220
|
setTimeout(resolve, ms);
|
|
@@ -2932,13 +3253,25 @@ function peerPromptPreamble(context) {
|
|
|
2932
3253
|
const peers = context.peers ?? [];
|
|
2933
3254
|
if (peers.length === 0) return void 0;
|
|
2934
3255
|
if (context.team === true) {
|
|
2935
|
-
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id)})`);
|
|
3256
|
+
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id, peer.id)})`);
|
|
2936
3257
|
return `本任务是 Agent Team 的 Lead:本次运行不额外开启独立会话,以下 ${peers.length} 个子任务成员已在本会话中作为 teammate 启动。用 list_agents / send_message / wait_agent 协调它们,用任务看板工具读写它们在看板上的卡片:\n${lines.join("\n")}`;
|
|
2937
3258
|
}
|
|
2938
3259
|
const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(任务 ${peer.id})`);
|
|
2939
3260
|
return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join("\n")}`;
|
|
2940
3261
|
}
|
|
2941
3262
|
/**
|
|
3263
|
+
* The scheduled-run section: this execution was triggered by the card's cron
|
|
3264
|
+
* rule rather than by a person. It states the firing instant, the rule's zone,
|
|
3265
|
+
* and the rule text, so the run's own clock is unambiguous — the board's
|
|
3266
|
+
* scheduler fires on a wall clock in a specific IANA zone, which is not
|
|
3267
|
+
* necessarily the zone of whatever session the run lands in.
|
|
3268
|
+
*/
|
|
3269
|
+
function schedulePromptPreamble(context) {
|
|
3270
|
+
const schedule = context.schedule;
|
|
3271
|
+
if (schedule === void 0) return void 0;
|
|
3272
|
+
return `本次执行由任务看板的定时规则自动触发:触发时间 ${new Date(schedule.triggeredAt).toISOString()}(UTC),规则时区 ${schedule.timeZone},cron 表达式 ${schedule.cron}。需要判断「今天」「现在」或计算时间窗口时,以该时区的触发时间为准。`;
|
|
3273
|
+
}
|
|
3274
|
+
/**
|
|
2942
3275
|
* Build the execution prompt for one task.
|
|
2943
3276
|
* @param task - the task being launched.
|
|
2944
3277
|
* @param context - the run's other members, for the execution-shape section.
|
|
@@ -2951,6 +3284,7 @@ function promptText(task, context = {}) {
|
|
|
2951
3284
|
const preambles = [
|
|
2952
3285
|
tagPromptPreamble(task),
|
|
2953
3286
|
handoverPreamble,
|
|
3287
|
+
schedulePromptPreamble(context),
|
|
2954
3288
|
peerPromptPreamble(context)
|
|
2955
3289
|
].filter((part) => part !== void 0);
|
|
2956
3290
|
const preamble = preambles.length === 0 ? void 0 : preambles.join("\n\n");
|
|
@@ -2961,6 +3295,25 @@ function promptText(task, context = {}) {
|
|
|
2961
3295
|
return preamble === void 0 ? declaration : `${preamble}\n\n${declaration}`;
|
|
2962
3296
|
}
|
|
2963
3297
|
/**
|
|
3298
|
+
* The parts of an objective the built-in `/goal` command reads as an operation
|
|
3299
|
+
* rather than as an objective: an exact `clear`/`pause`/`resume`/`edit`
|
|
3300
|
+
* (case-insensitive) and a leading `edit <objective>`.
|
|
3301
|
+
*/
|
|
3302
|
+
const GOAL_COMMAND_KEYWORD = /^(?:clear|pause|resume|edit)$|^edit\s/i;
|
|
3303
|
+
/**
|
|
3304
|
+
* Build the objective the built-in `/goal` command receives. A prompt whose
|
|
3305
|
+
* text would be read as a goal operation is prefixed with a neutral label: the
|
|
3306
|
+
* instruction stays intact, and the command arms an objective instead of
|
|
3307
|
+
* clearing, pausing, or editing an unrelated goal (or refusing for a missing
|
|
3308
|
+
* one).
|
|
3309
|
+
* @param prompt - the composed execution prompt.
|
|
3310
|
+
* @returns the objective text for `/goal <objective>`.
|
|
3311
|
+
*/
|
|
3312
|
+
function goalObjective(prompt) {
|
|
3313
|
+
const text = prompt.trim();
|
|
3314
|
+
return GOAL_COMMAND_KEYWORD.test(text) ? `Goal: ${text}` : text;
|
|
3315
|
+
}
|
|
3316
|
+
/**
|
|
2964
3317
|
* Build the tag section of the execution prompt (issue #1521). Only tags with
|
|
2965
3318
|
* a non-blank `promptPrefix` contribute; a task whose tags are all bare names
|
|
2966
3319
|
* (or which has no tags at all) yields undefined and the prompt is byte-for-byte
|
|
@@ -3150,6 +3503,73 @@ var HostExecutionRunner = class {
|
|
|
3150
3503
|
text: promptText(task, context)
|
|
3151
3504
|
}]
|
|
3152
3505
|
});
|
|
3506
|
+
await this.armGoal(sessionId, task, context);
|
|
3507
|
+
}
|
|
3508
|
+
/**
|
|
3509
|
+
* Arm dsh's built-in goal on the session. The task prompt is queued FIRST, so
|
|
3510
|
+
* the session's first turn carries the instruction verbatim; `/goal
|
|
3511
|
+
* <objective>` then turns the same text into a persistent objective, and
|
|
3512
|
+
* dsh's goal-round driver keeps starting continuation rounds until the agent
|
|
3513
|
+
* marks it complete (see {@link goalVerdict} for how the run settles).
|
|
3514
|
+
*
|
|
3515
|
+
* Opt-out: a task whose `goalRun` is an explicit false runs one plain turn.
|
|
3516
|
+
* A refusal (no command dispatcher, no `/goal` command in this cohort, an
|
|
3517
|
+
* objective the command rejects) is reported and the run continues as that
|
|
3518
|
+
* plain turn: the task was asked to run, and a missing goal mode must not
|
|
3519
|
+
* lose the work.
|
|
3520
|
+
*/
|
|
3521
|
+
async armGoal(sessionId, task, context) {
|
|
3522
|
+
if (task.goalRun === false) return;
|
|
3523
|
+
if (this.commands === void 0) {
|
|
3524
|
+
console.warn("[dsh-task-board] no command dispatcher is available; task " + task.id + " runs without /goal");
|
|
3525
|
+
return;
|
|
3526
|
+
}
|
|
3527
|
+
try {
|
|
3528
|
+
const command = await this.commands.execute(sessionId, "/goal " + goalObjective(promptText(task, context)), AbortSignal.timeout(3e4));
|
|
3529
|
+
if (command === void 0) {
|
|
3530
|
+
console.warn("[dsh-task-board] /goal was not acknowledged for session " + sessionId + "; the run continues as a plain turn");
|
|
3531
|
+
return;
|
|
3532
|
+
}
|
|
3533
|
+
if (command.kind !== "success") console.warn("[dsh-task-board] /goal was refused for session " + sessionId + ": " + (command.text ?? "no reason reported") + "; the run continues as a plain turn");
|
|
3534
|
+
} catch (error) {
|
|
3535
|
+
console.warn("[dsh-task-board] could not arm /goal on session " + sessionId + "; the run continues as a plain turn", error);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
/**
|
|
3539
|
+
* The verdict an armed dsh goal imposes on an otherwise completed turn.
|
|
3540
|
+
*
|
|
3541
|
+
* A goal run is one session working many automatic continuation rounds:
|
|
3542
|
+
* dsh's goal-round driver queues the next round whenever the agent goes
|
|
3543
|
+
* idle, so the first completed `turn/end` is not the execution's result.
|
|
3544
|
+
* Settling there would leave the card in `done` while the session keeps
|
|
3545
|
+
* working, and a scheduled task would fire a second, concurrent session.
|
|
3546
|
+
* The session projection is the authority: an `active` goal keeps the
|
|
3547
|
+
* execution pending, a `blocked` goal fails it with the recorded reason, and
|
|
3548
|
+
* any other phase (complete, paused, or no goal at all) leaves the turn
|
|
3549
|
+
* verdict in place.
|
|
3550
|
+
*
|
|
3551
|
+
* An unreadable projection falls back to that turn verdict: a cohort whose
|
|
3552
|
+
* gateway withdrew `session/projections` must not hang every execution.
|
|
3553
|
+
*/
|
|
3554
|
+
async goalVerdict(sessionId) {
|
|
3555
|
+
let projections;
|
|
3556
|
+
try {
|
|
3557
|
+
projections = await this.invoke("session", "projections", { sessionId });
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
console.warn("[dsh-task-board] session/projections failed while reading the goal phase; settling from the turn result", error);
|
|
3560
|
+
return;
|
|
3561
|
+
}
|
|
3562
|
+
const goal = projections?.values?.goal;
|
|
3563
|
+
if (goal === void 0 || goal === null) return void 0;
|
|
3564
|
+
const view = goal.goal;
|
|
3565
|
+
if (view?.phase === "active") return { outcome: "pending" };
|
|
3566
|
+
if (view?.phase === "blocked") {
|
|
3567
|
+
const message = view.blockedReason?.message;
|
|
3568
|
+
return {
|
|
3569
|
+
outcome: "failed",
|
|
3570
|
+
error: "goal is blocked: " + (typeof message === "string" && message !== "" ? message : "no reason recorded")
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3153
3573
|
}
|
|
3154
3574
|
async listRunning() {
|
|
3155
3575
|
for (let attempt = 1;; attempt++) try {
|
|
@@ -3181,8 +3601,17 @@ var HostExecutionRunner = class {
|
|
|
3181
3601
|
await delay(this.unavailableBackoffMs);
|
|
3182
3602
|
}
|
|
3183
3603
|
}
|
|
3184
|
-
/**
|
|
3185
|
-
|
|
3604
|
+
/**
|
|
3605
|
+
* Resolve an execution outcome from the session list and bounded history pages.
|
|
3606
|
+
* @param sessionId - the session the execution runs in.
|
|
3607
|
+
* @param startedAt - instant the execution opened; earlier events are not its own.
|
|
3608
|
+
* @param sessions - roster from the caller's own poll, when it already has one.
|
|
3609
|
+
* @param options.whileRunning - read the first completed turn even while the
|
|
3610
|
+
* roster reports the session as running. A durable Agent Teams teammate
|
|
3611
|
+
* stays alive after its turn ends, so `running` never clears for it and the
|
|
3612
|
+
* turn it completed is the only verdict it will ever expose.
|
|
3613
|
+
*/
|
|
3614
|
+
async inspect(sessionId, startedAt = 0, sessions, options = {}) {
|
|
3186
3615
|
let items;
|
|
3187
3616
|
if (sessions !== void 0) items = sessions;
|
|
3188
3617
|
else {
|
|
@@ -3214,7 +3643,7 @@ var HostExecutionRunner = class {
|
|
|
3214
3643
|
error: "execution session no longer exists"
|
|
3215
3644
|
};
|
|
3216
3645
|
}
|
|
3217
|
-
if (summary.running) return { outcome: "pending" };
|
|
3646
|
+
if (summary.running && options.whileRunning !== true) return { outcome: "pending" };
|
|
3218
3647
|
let opening;
|
|
3219
3648
|
try {
|
|
3220
3649
|
const iterator = (await this.stream("session", "follow", {
|
|
@@ -3224,7 +3653,11 @@ var HostExecutionRunner = class {
|
|
|
3224
3653
|
const next = await iterator.next();
|
|
3225
3654
|
if (typeof iterator.return === "function") await iterator.return();
|
|
3226
3655
|
const follow = next.done === true ? void 0 : next.value;
|
|
3227
|
-
if (follow === void 0 || follow.type !== "snapshot" || typeof follow.cursor !== "number" || follow.records === void 0 || typeof follow.hasMore !== "boolean") return {
|
|
3656
|
+
if (follow === void 0 || follow.type !== "snapshot" || typeof follow.cursor !== "number" || follow.records === void 0 || typeof follow.hasMore !== "boolean") return {
|
|
3657
|
+
outcome: "pending",
|
|
3658
|
+
unreadable: true,
|
|
3659
|
+
reason: "session/follow returned no opening frame"
|
|
3660
|
+
};
|
|
3228
3661
|
opening = {
|
|
3229
3662
|
cursor: follow.cursor,
|
|
3230
3663
|
records: follow.records,
|
|
@@ -3232,7 +3665,11 @@ var HostExecutionRunner = class {
|
|
|
3232
3665
|
};
|
|
3233
3666
|
} catch (error) {
|
|
3234
3667
|
console.warn("[dsh-task-board] session/follow failed during execution inspection; keeping the outcome pending", error);
|
|
3235
|
-
return {
|
|
3668
|
+
return {
|
|
3669
|
+
outcome: "pending",
|
|
3670
|
+
unreadable: true,
|
|
3671
|
+
reason: "session/follow failed: " + failureText(error)
|
|
3672
|
+
};
|
|
3236
3673
|
}
|
|
3237
3674
|
const openingEvents = opening.records.map((record) => ({ event: recordEvent(record) }));
|
|
3238
3675
|
const newestSeq = openingEvents.reduce((newest, entry) => newest === void 0 ? entry.event.seq : Math.max(newest, entry.event.seq), void 0);
|
|
@@ -3240,7 +3677,7 @@ var HostExecutionRunner = class {
|
|
|
3240
3677
|
const events = [...openingEvents];
|
|
3241
3678
|
let beforeSeq;
|
|
3242
3679
|
let reachedExecutionBoundary = !opening.hasMore;
|
|
3243
|
-
for (let page = 0; page <
|
|
3680
|
+
for (let page = 0; page < HISTORY_PAGE_LIMIT && !reachedExecutionBoundary; page += 1) {
|
|
3244
3681
|
let history;
|
|
3245
3682
|
try {
|
|
3246
3683
|
history = await this.invoke("session", "page", {
|
|
@@ -3251,7 +3688,11 @@ var HostExecutionRunner = class {
|
|
|
3251
3688
|
});
|
|
3252
3689
|
} catch (error) {
|
|
3253
3690
|
console.warn("[dsh-task-board] session/page failed during execution inspection; keeping the outcome pending", error);
|
|
3254
|
-
return {
|
|
3691
|
+
return {
|
|
3692
|
+
outcome: "pending",
|
|
3693
|
+
unreadable: true,
|
|
3694
|
+
reason: "session/page failed: " + failureText(error)
|
|
3695
|
+
};
|
|
3255
3696
|
}
|
|
3256
3697
|
const pageEntries = pageEvents(history);
|
|
3257
3698
|
events.push(...pageEntries);
|
|
@@ -3264,7 +3705,11 @@ var HostExecutionRunner = class {
|
|
|
3264
3705
|
if (oldestSeq === void 0 || oldestSeq === beforeSeq) return { outcome: "pending" };
|
|
3265
3706
|
beforeSeq = oldestSeq;
|
|
3266
3707
|
}
|
|
3267
|
-
if (!reachedExecutionBoundary) return {
|
|
3708
|
+
if (!reachedExecutionBoundary) return {
|
|
3709
|
+
outcome: "pending",
|
|
3710
|
+
unreadable: true,
|
|
3711
|
+
reason: "history scan did not reach the execution start within 100 pages"
|
|
3712
|
+
};
|
|
3268
3713
|
const turnEnd = events.filter((entry) => entry.event.type === "turn/end" && (startedAt <= 0 || entry.event.time >= startedAt)).sort((a, b) => a.event.seq - b.event.seq)[0];
|
|
3269
3714
|
if (turnEnd === void 0) {
|
|
3270
3715
|
if (newestSeq !== void 0) this.scanMemos.set(sessionId, newestSeq);
|
|
@@ -3272,7 +3717,7 @@ var HostExecutionRunner = class {
|
|
|
3272
3717
|
}
|
|
3273
3718
|
this.scanMemos.delete(sessionId);
|
|
3274
3719
|
const reason = nonCompletedTurnEnd(turnEnd.event.data);
|
|
3275
|
-
if (reason === void 0) return { outcome: "succeeded" };
|
|
3720
|
+
if (reason === void 0) return await this.goalVerdict(sessionId) ?? { outcome: "succeeded" };
|
|
3276
3721
|
return reason === "error" ? {
|
|
3277
3722
|
outcome: "failed",
|
|
3278
3723
|
error: "agent turn ended with an error"
|
|
@@ -3599,6 +4044,15 @@ const RECOVERY_TOLERANCE_MS = 6e4;
|
|
|
3599
4044
|
/** Largest delay a Node timer represents without clamping; longer targets re-arm in segments. */
|
|
3600
4045
|
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
3601
4046
|
/**
|
|
4047
|
+
* Consecutive polls that may report one execution's session history as
|
|
4048
|
+
* unreadable before it is reported failed. The roster poll runs every 5 s, so
|
|
4049
|
+
* this is two minutes of a session that is NOT running: no turn is executing
|
|
4050
|
+
* there and its history cannot be read, which means no verdict will ever
|
|
4051
|
+
* arrive. Reporting that as a failure keeps a card — and every ancestor of it —
|
|
4052
|
+
* out of the running column, which nothing else can rescue.
|
|
4053
|
+
*/
|
|
4054
|
+
const UNREADABLE_SETTLE_POLLS = 24;
|
|
4055
|
+
/**
|
|
3602
4056
|
* Actions that can move an armed trigger. Only these re-arm the native timer;
|
|
3603
4057
|
* an unrelated card edit leaves the pending fire untouched.
|
|
3604
4058
|
*/
|
|
@@ -3643,6 +4097,12 @@ var TaskBoardHostService = class {
|
|
|
3643
4097
|
* conversation instead of prompting into a session it cannot see.
|
|
3644
4098
|
*/
|
|
3645
4099
|
idleSessionIds;
|
|
4100
|
+
/**
|
|
4101
|
+
* Consecutive unreadable-history polls per open execution (see
|
|
4102
|
+
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
4103
|
+
* resolves, so a transient reader failure never fails a card.
|
|
4104
|
+
*/
|
|
4105
|
+
unreadablePolls = /* @__PURE__ */ new Map();
|
|
3646
4106
|
preventIdleSleep = false;
|
|
3647
4107
|
team;
|
|
3648
4108
|
timers;
|
|
@@ -3701,7 +4161,7 @@ var TaskBoardHostService = class {
|
|
|
3701
4161
|
snapshot() {
|
|
3702
4162
|
const state = this.ledger.state();
|
|
3703
4163
|
return {
|
|
3704
|
-
schemaVersion:
|
|
4164
|
+
schemaVersion: 4,
|
|
3705
4165
|
revision: state.revision,
|
|
3706
4166
|
tasks: state.tasks,
|
|
3707
4167
|
scheduler: state.scheduler,
|
|
@@ -3735,7 +4195,7 @@ var TaskBoardHostService = class {
|
|
|
3735
4195
|
if (result.runs !== void 0) this.dispatchRuns(result.runs);
|
|
3736
4196
|
if (SCHEDULE_WRITE_ACTIONS.has(action.kind)) this.refreshSchedule();
|
|
3737
4197
|
return {
|
|
3738
|
-
schemaVersion:
|
|
4198
|
+
schemaVersion: 4,
|
|
3739
4199
|
revision: result.state.revision,
|
|
3740
4200
|
tasks: result.state.tasks,
|
|
3741
4201
|
scheduler: result.state.scheduler,
|
|
@@ -3751,17 +4211,19 @@ var TaskBoardHostService = class {
|
|
|
3751
4211
|
this.ledger.dispose();
|
|
3752
4212
|
this.listeners.clear();
|
|
3753
4213
|
}
|
|
3754
|
-
async launch(opened, others = []) {
|
|
4214
|
+
async launch(opened, others = [], schedule) {
|
|
3755
4215
|
try {
|
|
3756
4216
|
const team = opened.task.teamRun === true;
|
|
3757
4217
|
const reuseSessionId = team ? void 0 : reusableSessionId(opened.task, this.idleSessionIds);
|
|
3758
|
-
const
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
...
|
|
4218
|
+
const peers = others.length === 0 ? void 0 : others.map((other) => ({
|
|
4219
|
+
id: other.task.id,
|
|
4220
|
+
title: other.task.title,
|
|
4221
|
+
...team ? { name: teammateName(other.task.title, other.execution.runGroupId ?? other.task.id, other.task.id) } : {}
|
|
4222
|
+
}));
|
|
4223
|
+
const promptContext = peers === void 0 && schedule === void 0 ? void 0 : {
|
|
4224
|
+
...peers === void 0 ? {} : { peers },
|
|
4225
|
+
...team ? { team: true } : {},
|
|
4226
|
+
...schedule === void 0 ? {} : { schedule }
|
|
3765
4227
|
};
|
|
3766
4228
|
const sessionId = await this.runner.launch(opened.task, {
|
|
3767
4229
|
...reuseSessionId === void 0 ? {} : { reuseSessionId },
|
|
@@ -3795,7 +4257,7 @@ var TaskBoardHostService = class {
|
|
|
3795
4257
|
try {
|
|
3796
4258
|
const member = await team.spawn({
|
|
3797
4259
|
leadSessionId,
|
|
3798
|
-
name: teammateName(opened.task.title, opened.execution.runGroupId ?? opened.task.id),
|
|
4260
|
+
name: teammateName(opened.task.title, opened.execution.runGroupId ?? opened.task.id, opened.task.id),
|
|
3799
4261
|
description: opened.task.title,
|
|
3800
4262
|
prompt: promptText(opened.task)
|
|
3801
4263
|
});
|
|
@@ -3823,6 +4285,7 @@ var TaskBoardHostService = class {
|
|
|
3823
4285
|
return;
|
|
3824
4286
|
}
|
|
3825
4287
|
this.idleSessionIds = new Set(running.items.filter((item) => !item.running).map((item) => item.sessionId));
|
|
4288
|
+
this.ledger.finalizeReadyRuns();
|
|
3826
4289
|
const runtime = this.ledger.runtimeView();
|
|
3827
4290
|
this.power.updateReasons({
|
|
3828
4291
|
runningSessions: running.count,
|
|
@@ -3836,11 +4299,36 @@ var TaskBoardHostService = class {
|
|
|
3836
4299
|
for (const execution of executions) {
|
|
3837
4300
|
if (execution.sessionId === void 0) continue;
|
|
3838
4301
|
try {
|
|
3839
|
-
const result = await this.runner.inspect(execution.sessionId, execution.startedAt, sessions);
|
|
3840
|
-
if (result.outcome === "pending")
|
|
4302
|
+
const result = await this.runner.inspect(execution.sessionId, execution.startedAt, sessions, { whileRunning: execution.teamMember });
|
|
4303
|
+
if (result.outcome === "pending") {
|
|
4304
|
+
if (result.unreadable === true) this.noteUnreadableInspection(execution, result.reason);
|
|
4305
|
+
else this.unreadablePolls.delete(execution.executionId);
|
|
4306
|
+
continue;
|
|
4307
|
+
}
|
|
4308
|
+
this.unreadablePolls.delete(execution.executionId);
|
|
3841
4309
|
this.ledger.settle(execution.taskId, execution.executionId, result.outcome, "error" in result ? result.error : void 0);
|
|
3842
4310
|
} catch {}
|
|
3843
4311
|
}
|
|
4312
|
+
const open = new Set(executions.map((execution) => execution.executionId));
|
|
4313
|
+
for (const executionId of [...this.unreadablePolls.keys()]) if (!open.has(executionId)) this.unreadablePolls.delete(executionId);
|
|
4314
|
+
}
|
|
4315
|
+
/**
|
|
4316
|
+
* Count one poll whose session history could not be read. A reader failure is
|
|
4317
|
+
* not progress: the session is not running (the runner only reads history for
|
|
4318
|
+
* one that is idle), so nothing will ever change that verdict. After
|
|
4319
|
+
* {@link UNREADABLE_SETTLE_POLLS} consecutive polls the execution is reported
|
|
4320
|
+
* failed with the recorded reason, instead of holding its card — and every
|
|
4321
|
+
* ancestor of it — in the running column with no way out. The first poll of
|
|
4322
|
+
* each streak is logged, so the Host log names the session.
|
|
4323
|
+
*/
|
|
4324
|
+
noteUnreadableInspection(execution, reason) {
|
|
4325
|
+
const polls = (this.unreadablePolls.get(execution.executionId) ?? 0) + 1;
|
|
4326
|
+
this.unreadablePolls.set(execution.executionId, polls);
|
|
4327
|
+
const detail = reason ?? "no reason reported";
|
|
4328
|
+
if (polls === 1) safeConsoleError("[dsh-task-board] execution session " + (execution.sessionId ?? "unknown") + " history is unreadable; it stays pending for up to 24 polls: " + detail);
|
|
4329
|
+
if (polls < UNREADABLE_SETTLE_POLLS) return;
|
|
4330
|
+
this.unreadablePolls.delete(execution.executionId);
|
|
4331
|
+
this.ledger.settle(execution.taskId, execution.executionId, "failed", "execution session history is unreadable (24 consecutive polls); the outcome cannot be determined: " + detail);
|
|
3844
4332
|
}
|
|
3845
4333
|
/** Drop the armed schedule timer and forget its target. */
|
|
3846
4334
|
clearScheduleTimer() {
|
|
@@ -3899,8 +4387,12 @@ var TaskBoardHostService = class {
|
|
|
3899
4387
|
return;
|
|
3900
4388
|
}
|
|
3901
4389
|
for (const schedule of this.ledger.dueSchedules(now)) {
|
|
3902
|
-
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
|
|
3903
|
-
this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, next, now)
|
|
4390
|
+
const next = nextRunAtMs(schedule.cron, schedule.nextRunAt, schedule.timeZone);
|
|
4391
|
+
this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, next, now), {
|
|
4392
|
+
triggeredAt: now,
|
|
4393
|
+
timeZone: schedule.timeZone,
|
|
4394
|
+
cron: schedule.cron
|
|
4395
|
+
});
|
|
3904
4396
|
}
|
|
3905
4397
|
this.armSchedule();
|
|
3906
4398
|
}
|
|
@@ -3914,15 +4406,15 @@ var TaskBoardHostService = class {
|
|
|
3914
4406
|
* others back. A team run's members are spawned inside the root's Lead
|
|
3915
4407
|
* session instead, once that session exists.
|
|
3916
4408
|
*/
|
|
3917
|
-
dispatchRuns(runs) {
|
|
4409
|
+
dispatchRuns(runs, schedule) {
|
|
3918
4410
|
if (runs.length === 0) return;
|
|
3919
4411
|
const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
|
|
3920
4412
|
const others = runs.filter((run) => run !== root);
|
|
3921
|
-
this.scheduleLaunch(root, others);
|
|
3922
|
-
for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run);
|
|
4413
|
+
this.scheduleLaunch(root, others, schedule);
|
|
4414
|
+
for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run, [], schedule);
|
|
3923
4415
|
}
|
|
3924
|
-
scheduleLaunch(opened, others = []) {
|
|
3925
|
-
this.launch(opened, others).catch((error) => {
|
|
4416
|
+
scheduleLaunch(opened, others = [], schedule) {
|
|
4417
|
+
this.launch(opened, others, schedule).catch((error) => {
|
|
3926
4418
|
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
3927
4419
|
});
|
|
3928
4420
|
}
|
|
@@ -4166,6 +4658,7 @@ function scheduleView(schedule) {
|
|
|
4166
4658
|
return {
|
|
4167
4659
|
enabled: schedule.enabled,
|
|
4168
4660
|
cron: schedule.cron,
|
|
4661
|
+
...schedule.timeZone === void 0 ? {} : { timeZone: schedule.timeZone },
|
|
4169
4662
|
...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
|
|
4170
4663
|
...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }
|
|
4171
4664
|
};
|
|
@@ -4215,6 +4708,7 @@ function taskSummary(task, tasks, sessionDefault) {
|
|
|
4215
4708
|
...task.model === void 0 ? {} : { model: task.model },
|
|
4216
4709
|
...task.reuseSession === true ? { reuseSession: true } : {},
|
|
4217
4710
|
...task.teamRun === true ? { teamRun: true } : {},
|
|
4711
|
+
...task.goalRun === false ? { goalRun: false } : {},
|
|
4218
4712
|
...task.schedule === void 0 ? {} : { schedule: scheduleView(task.schedule) },
|
|
4219
4713
|
...task.freeze === void 0 ? {} : { continuationCard: true },
|
|
4220
4714
|
...task.permissionConfirmedAt === void 0 ? {} : { permissionConfirmedAt: task.permissionConfirmedAt },
|
|
@@ -4407,11 +4901,12 @@ function buildManageTool(host) {
|
|
|
4407
4901
|
return defineTool({
|
|
4408
4902
|
name: "task_board_manage",
|
|
4409
4903
|
description: [
|
|
4410
|
-
"Move, archive, restore, or
|
|
4904
|
+
"Move, archive, restore, delete, or settle one task board card.",
|
|
4411
4905
|
"move-todo and move-backlog are the manual column moves; a running card cannot be moved.",
|
|
4906
|
+
"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.",
|
|
4412
4907
|
"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.",
|
|
4413
4908
|
"It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.",
|
|
4414
|
-
"Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task."
|
|
4909
|
+
"Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task, 卡住, stuck, 强制结算, settle."
|
|
4415
4910
|
].join(" "),
|
|
4416
4911
|
parameters: {
|
|
4417
4912
|
taskId: {
|
|
@@ -4427,7 +4922,8 @@ function buildManageTool(host) {
|
|
|
4427
4922
|
"move-backlog",
|
|
4428
4923
|
"archive",
|
|
4429
4924
|
"restore",
|
|
4430
|
-
"delete"
|
|
4925
|
+
"delete",
|
|
4926
|
+
"settle"
|
|
4431
4927
|
],
|
|
4432
4928
|
description: "The lifecycle operation to perform."
|
|
4433
4929
|
}
|
|
@@ -4453,6 +4949,10 @@ function buildManageTool(host) {
|
|
|
4453
4949
|
kind: "archive",
|
|
4454
4950
|
taskId: args.taskId
|
|
4455
4951
|
},
|
|
4952
|
+
settle: {
|
|
4953
|
+
kind: "settle",
|
|
4954
|
+
taskId: args.taskId
|
|
4955
|
+
},
|
|
4456
4956
|
restore: {
|
|
4457
4957
|
kind: "restore",
|
|
4458
4958
|
taskId: args.taskId
|
|
@@ -4488,7 +4988,8 @@ function buildScheduleTool(host) {
|
|
|
4488
4988
|
return defineTool({
|
|
4489
4989
|
name: "task_board_schedule",
|
|
4490
4990
|
description: [
|
|
4491
|
-
"Arm, change, or disarm a task scheduled runs (5-field cron
|
|
4991
|
+
"Arm, change, or disarm a task scheduled runs (5-field cron; day-of-month and day-of-week follow Vixie semantics: both restricted means OR, otherwise AND).",
|
|
4992
|
+
"The cron 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.",
|
|
4492
4993
|
"A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.",
|
|
4493
4994
|
"A schedule whose tree contains an unconfirmed above-default permission is refused and rolls to its next occurrence; the reason is reported in the list board summary as schedulerError.",
|
|
4494
4995
|
"Missed occurrences during Host downtime are skipped, never queued; a task that is already running skips its occurrence.",
|
|
@@ -4507,6 +5008,10 @@ function buildScheduleTool(host) {
|
|
|
4507
5008
|
cron: {
|
|
4508
5009
|
type: "string",
|
|
4509
5010
|
description: "5-field cron expression: minute hour day-of-month month day-of-week."
|
|
5011
|
+
},
|
|
5012
|
+
timeZone: {
|
|
5013
|
+
type: "string",
|
|
5014
|
+
description: "IANA zone the cron 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."
|
|
4510
5015
|
}
|
|
4511
5016
|
},
|
|
4512
5017
|
output: {
|
|
@@ -4514,11 +5019,12 @@ function buildScheduleTool(host) {
|
|
|
4514
5019
|
render: renderJson
|
|
4515
5020
|
},
|
|
4516
5021
|
async execute(args, exec) {
|
|
4517
|
-
if (args.enabled === void 0 && args.cron === void 0) return refused("nothing-to-change", "pass enabled and/or cron");
|
|
5022
|
+
if (args.enabled === void 0 && args.cron === void 0 && args.timeZone === void 0) return refused("nothing-to-change", "pass enabled and/or cron and/or timeZone");
|
|
4518
5023
|
if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
|
|
4519
5024
|
const patch = {
|
|
4520
5025
|
...args.enabled === void 0 ? {} : { enabled: args.enabled },
|
|
4521
|
-
...args.cron === void 0 ? {} : { cron: args.cron }
|
|
5026
|
+
...args.cron === void 0 ? {} : { cron: args.cron },
|
|
5027
|
+
...args.timeZone === void 0 ? {} : { timeZone: args.timeZone === "" ? null : args.timeZone }
|
|
4522
5028
|
};
|
|
4523
5029
|
try {
|
|
4524
5030
|
const snapshot = host.apply(crypto.randomUUID(), {
|
|
@@ -4703,6 +5209,10 @@ function buildCreateTool(host) {
|
|
|
4703
5209
|
type: "boolean",
|
|
4704
5210
|
description: "Run this task as an Agent Team: running it starts one Team Lead session and the Host spawns a teammate per subtask inside it. Omit or false for one independent session per member. Refused when the deployment serves no Agent Teams service."
|
|
4705
5211
|
},
|
|
5212
|
+
goalRun: {
|
|
5213
|
+
type: "boolean",
|
|
5214
|
+
description: "Start each run with dsh built-in /goal so the session keeps working continuation rounds until the goal completes. Default true (omit to keep it); pass false for a single plain turn."
|
|
5215
|
+
},
|
|
4706
5216
|
tags: {
|
|
4707
5217
|
type: "array",
|
|
4708
5218
|
description: "Labels: the name renders as a badge and drives the board filter; promptPrefix is injected ahead of the execution prompt on every run.",
|
|
@@ -4762,6 +5272,7 @@ function buildCreateTool(host) {
|
|
|
4762
5272
|
...args.model === void 0 || args.model === "" ? {} : { model: args.model },
|
|
4763
5273
|
...args.reuseSession === true ? { reuseSession: true } : {},
|
|
4764
5274
|
...args.teamRun === true ? { teamRun: true } : {},
|
|
5275
|
+
...args.goalRun === false ? { goalRun: false } : {},
|
|
4765
5276
|
...tags.length === 0 ? {} : { tags },
|
|
4766
5277
|
...args.schedule === void 0 ? {} : { schedule: {
|
|
4767
5278
|
enabled: args.schedule.enabled,
|
|
@@ -4823,8 +5334,13 @@ function buildUpdateTool(host) {
|
|
|
4823
5334
|
description: "New agent preset id; an empty string clears it."
|
|
4824
5335
|
},
|
|
4825
5336
|
permission: {
|
|
4826
|
-
|
|
4827
|
-
|
|
5337
|
+
oneOf: [{
|
|
5338
|
+
type: "string",
|
|
5339
|
+
enum: [...TASK_PERMISSIONS]
|
|
5340
|
+
}, {
|
|
5341
|
+
type: "string",
|
|
5342
|
+
const: ""
|
|
5343
|
+
}],
|
|
4828
5344
|
description: "New permission preset; an empty string clears it back to the session default."
|
|
4829
5345
|
},
|
|
4830
5346
|
model: {
|
|
@@ -4839,6 +5355,10 @@ function buildUpdateTool(host) {
|
|
|
4839
5355
|
type: "boolean",
|
|
4840
5356
|
description: "Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask)."
|
|
4841
5357
|
},
|
|
5358
|
+
goalRun: {
|
|
5359
|
+
type: "boolean",
|
|
5360
|
+
description: "Whether each run starts with dsh built-in /goal (default true). Pass false to run one plain turn instead."
|
|
5361
|
+
},
|
|
4842
5362
|
tags: {
|
|
4843
5363
|
type: "array",
|
|
4844
5364
|
description: "Replacement label set; an empty array clears all labels.",
|
|
@@ -4874,6 +5394,7 @@ function buildUpdateTool(host) {
|
|
|
4874
5394
|
if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
|
|
4875
5395
|
if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
|
|
4876
5396
|
if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
|
|
5397
|
+
if (has(args, "goalRun")) patch.goalRun = args.goalRun === false ? false : true;
|
|
4877
5398
|
if (has(args, "tags")) {
|
|
4878
5399
|
const tags = args.tags ?? [];
|
|
4879
5400
|
patch.tags = tags.length === 0 ? null : tags.map((tag) => ({
|