@linxin666/dsh-client-ui-task-board 0.4.6 → 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 +14 -6
- package/README.zh.md +14 -6
- package/lib/client.js +1502 -820
- package/lib/client.js.map +1 -1
- package/lib/index.js +674 -75
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +49 -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/locales.d.ts +16 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +32 -7
- package/lib/types/core/launch-width.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 +19 -0
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host-ledger.d.ts +9 -4
- package/lib/types/host-runner.d.ts +79 -4
- package/lib/types/host-service.d.ts +85 -2
- package/lib/types/index.d.ts +87 -0
- package/lib/types/protocol.d.ts +13 -0
- package/package.json +28 -28
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +214 -16
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +86 -40
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskDetail.tsx +42 -6
- 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/team-run-promotion.ts +39 -0
- package/src/client/index.ts +98 -94
- package/src/client/locales.ts +34 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +44 -9
- package/src/core/launch-width.ts +38 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +5 -0
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +20 -0
- package/src/core/use-cases/task-create.ts +27 -16
- package/src/core/use-cases/task-update.ts +6 -1
- package/src/host/agent-tools.ts +21 -8
- package/src/host/launch-admission.ts +140 -0
- package/src/host-ledger.ts +18 -6
- package/src/host-runner.ts +184 -18
- package/src/host-service.ts +247 -23
- package/src/index.ts +149 -2
- package/src/protocol.ts +28 -4
package/lib/index.js
CHANGED
|
@@ -5,9 +5,21 @@ 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";
|
|
10
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
|
+
/**
|
|
11
23
|
* Clamp a configured cadence into the supported range. A value the schema
|
|
12
24
|
* would refuse (non-finite, below the floor, above the ceiling) still leaves
|
|
13
25
|
* the board polling: an out-of-range setting never disables the timer.
|
|
@@ -59,6 +71,92 @@ function reusableSessionId(task, idleSessionIds) {
|
|
|
59
71
|
return idleSessionIds.has(last.sessionId) ? last.sessionId : void 0;
|
|
60
72
|
}
|
|
61
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
|
|
62
160
|
//#region src/dsh-home.ts
|
|
63
161
|
/**
|
|
64
162
|
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
@@ -659,6 +757,7 @@ function createTask(input, now, id) {
|
|
|
659
757
|
mode: normalizeTargetId(input.mode),
|
|
660
758
|
permission: isTaskPermission(input.permission) ? input.permission : void 0,
|
|
661
759
|
model: normalizeTargetId(input.model),
|
|
760
|
+
reasoningEffort: normalizeTargetId(input.reasoningEffort),
|
|
662
761
|
reuseSession: input.reuseSession === true ? true : void 0,
|
|
663
762
|
teamRun: input.teamRun === true ? true : void 0,
|
|
664
763
|
goalRun: input.goalRun === true ? true : void 0,
|
|
@@ -921,15 +1020,38 @@ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth, lineage) {
|
|
|
921
1020
|
}
|
|
922
1021
|
/**
|
|
923
1022
|
* The tasks one run request opens executions for: the requested task first,
|
|
924
|
-
* then its on-board descendants within the configured depth.
|
|
925
|
-
*
|
|
926
|
-
* 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.
|
|
927
1029
|
*/
|
|
928
1030
|
function cascadeTargets(tasks, rootId, maxSubtaskDepth) {
|
|
929
1031
|
const root = tasks.find((task) => task.id === rootId);
|
|
930
1032
|
if (root === void 0) return [];
|
|
931
1033
|
return [root, ...descendantTasks(tasks, rootId, maxSubtaskDepth)];
|
|
932
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
|
+
}
|
|
933
1055
|
/** The open execution of a task inside a run group, if it still exists. */
|
|
934
1056
|
function openGroupExecution(task, runGroupId) {
|
|
935
1057
|
return task.executions.find((execution) => execution.runGroupId === runGroupId && execution.endedAt === void 0);
|
|
@@ -989,11 +1111,12 @@ function effectiveTaskPermission(task) {
|
|
|
989
1111
|
/**
|
|
990
1112
|
* Resolve the execution targets a (sub)task actually runs with: its own value
|
|
991
1113
|
* wins, and an unset target inherits the nearest ancestor up to the root.
|
|
992
|
-
* Workspace, agent preset and
|
|
993
|
-
* handover bundle is a per-card continuation artefact and is never
|
|
994
|
-
* The permission inherits the ancestor's EFFECTIVE binding, and its
|
|
995
|
-
* confirmation travels with it, so a subtask of an already-confirmed
|
|
996
|
-
* 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.
|
|
997
1120
|
*/
|
|
998
1121
|
/**
|
|
999
1122
|
* The immutable teammate name for one team-mode member. Teammate names are
|
|
@@ -1039,19 +1162,22 @@ function stableTag(value) {
|
|
|
1039
1162
|
function resolveExecutionTargets(task, tasks, maxSubtaskDepth = 3) {
|
|
1040
1163
|
if (task.parentId === void 0) return task;
|
|
1041
1164
|
const chain = [task, ...ancestorChain(tasks, task).slice(0, maxSubtaskDepth)];
|
|
1042
|
-
const first = (key) => {
|
|
1043
|
-
for (const candidate of
|
|
1165
|
+
const first = (key, candidates) => {
|
|
1166
|
+
for (const candidate of candidates) {
|
|
1044
1167
|
const value = candidate[key];
|
|
1045
1168
|
if (value !== void 0) return value;
|
|
1046
1169
|
}
|
|
1047
1170
|
};
|
|
1171
|
+
const boundary = chain.findIndex((candidate) => candidate.model !== void 0);
|
|
1172
|
+
const effortChain = boundary === -1 ? chain : chain.slice(0, boundary + 1);
|
|
1048
1173
|
const own = effectiveTaskPermission(task);
|
|
1049
1174
|
const owner = own === void 0 ? chain.slice(1).find((candidate) => effectiveTaskPermission(candidate) !== void 0) : void 0;
|
|
1050
1175
|
return {
|
|
1051
1176
|
...task,
|
|
1052
|
-
workspaceId: first("workspaceId"),
|
|
1053
|
-
mode: first("mode"),
|
|
1054
|
-
model: first("model"),
|
|
1177
|
+
workspaceId: first("workspaceId", chain),
|
|
1178
|
+
mode: first("mode", chain),
|
|
1179
|
+
model: first("model", chain),
|
|
1180
|
+
reasoningEffort: first("reasoningEffort", effortChain),
|
|
1055
1181
|
permission: own ?? (owner === void 0 ? void 0 : effectiveTaskPermission(owner)),
|
|
1056
1182
|
permissionConfirmedAt: own === void 0 ? owner?.permissionConfirmedAt : task.permissionConfirmedAt
|
|
1057
1183
|
};
|
|
@@ -2277,6 +2403,7 @@ function isTaskRecordShape(value) {
|
|
|
2277
2403
|
if (record.parentId !== void 0 && typeof record.parentId !== "string") return false;
|
|
2278
2404
|
if (record.workspaceId !== void 0 && typeof record.workspaceId !== "string") return false;
|
|
2279
2405
|
if (record.mode !== void 0 && typeof record.mode !== "string") return false;
|
|
2406
|
+
if (record.reasoningEffort !== void 0 && typeof record.reasoningEffort !== "string") return false;
|
|
2280
2407
|
if (record.permission !== void 0 && typeof record.permission !== "string") return false;
|
|
2281
2408
|
if (record.integrations !== void 0 && (typeof record.integrations !== "object" || record.integrations === null || Array.isArray(record.integrations))) return false;
|
|
2282
2409
|
if (record.hidden !== void 0 && typeof record.hidden !== "boolean") return false;
|
|
@@ -2446,6 +2573,7 @@ function parseLedger(raw) {
|
|
|
2446
2573
|
}));
|
|
2447
2574
|
task.workspaceId = normalizeTargetId(row.workspaceId);
|
|
2448
2575
|
task.mode = normalizeTargetId(row.mode);
|
|
2576
|
+
task.reasoningEffort = normalizeTargetId(row.reasoningEffort);
|
|
2449
2577
|
task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
|
|
2450
2578
|
task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
|
|
2451
2579
|
task.reuseSession = row.reuseSession === true ? true : void 0;
|
|
@@ -2546,6 +2674,57 @@ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
|
2546
2674
|
};
|
|
2547
2675
|
}
|
|
2548
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
|
|
2549
2728
|
//#region src/core/use-cases/task-create.ts
|
|
2550
2729
|
/**
|
|
2551
2730
|
* Create-task use case: mint a new task from user input, rejecting a blank
|
|
@@ -2558,18 +2737,22 @@ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
|
|
|
2558
2737
|
* appended ledger, or the unchanged ledger when the title is blank or the
|
|
2559
2738
|
* requested parent link is refused.
|
|
2560
2739
|
*
|
|
2561
|
-
*
|
|
2562
|
-
*
|
|
2563
|
-
*
|
|
2564
|
-
*
|
|
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.
|
|
2565
2747
|
* @param tasks - current ledger.
|
|
2566
2748
|
* @param input - raw user input (title/description/prompt + optional schedule/parent).
|
|
2567
2749
|
* @param now - clock instant (ms epoch).
|
|
2568
2750
|
* @param id - minted task id.
|
|
2569
2751
|
* @param maxSubtaskDepth - deployment subtask depth limit.
|
|
2570
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.
|
|
2571
2754
|
*/
|
|
2572
|
-
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone()) {
|
|
2755
|
+
function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZone = resolveHostTimeZone(), cardDefaults) {
|
|
2573
2756
|
if (input.title.trim() === "") return {
|
|
2574
2757
|
task: void 0,
|
|
2575
2758
|
tasks,
|
|
@@ -2595,11 +2778,22 @@ function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1, hostTimeZon
|
|
|
2595
2778
|
error: `subtask depth limit (${limit}) exceeded`
|
|
2596
2779
|
};
|
|
2597
2780
|
}
|
|
2598
|
-
|
|
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({
|
|
2599
2791
|
...input,
|
|
2600
|
-
workspaceId: input.workspaceId ?? parent
|
|
2601
|
-
mode:
|
|
2602
|
-
|
|
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
|
|
2603
2797
|
}, now, id);
|
|
2604
2798
|
const requested = input.schedule;
|
|
2605
2799
|
if (requested?.enabled === true) {
|
|
@@ -3119,6 +3313,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
3119
3313
|
const mode = "mode" in patch ? normalizeTargetId(patch.mode) : void 0;
|
|
3120
3314
|
const permission = "permission" in patch ? normalizePermission(task.permission, patch.permission) : void 0;
|
|
3121
3315
|
const model = "model" in patch ? normalizeTargetId(patch.model) : void 0;
|
|
3316
|
+
const reasoningEffort = "reasoningEffort" in patch ? normalizeTargetId(patch.reasoningEffort) : void 0;
|
|
3122
3317
|
const next = {
|
|
3123
3318
|
...task,
|
|
3124
3319
|
...rest,
|
|
@@ -3144,6 +3339,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
3144
3339
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
3145
3340
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
3146
3341
|
if (model !== void 0 || "model" in patch) next.model = model;
|
|
3342
|
+
if (reasoningEffort !== void 0 || "reasoningEffort" in patch) next.reasoningEffort = reasoningEffort;
|
|
3147
3343
|
return next;
|
|
3148
3344
|
});
|
|
3149
3345
|
}
|
|
@@ -3229,6 +3425,7 @@ function validImportedSchedule(value) {
|
|
|
3229
3425
|
}
|
|
3230
3426
|
function validImportedKnownFields(value) {
|
|
3231
3427
|
if (value.tags !== void 0 && !isTaskTagList(value.tags)) return false;
|
|
3428
|
+
if (value.model !== void 0 && typeof value.model !== "string") return false;
|
|
3232
3429
|
if (value.integrations !== void 0 && normalizeTaskIntegrations(value.integrations) === void 0) return false;
|
|
3233
3430
|
if (value.schedule !== void 0 && !validImportedSchedule(value.schedule)) return false;
|
|
3234
3431
|
if (value.executions !== void 0) {
|
|
@@ -3281,7 +3478,9 @@ function importedTask(value) {
|
|
|
3281
3478
|
...task.parentId === void 0 ? {} : { parentId: task.parentId },
|
|
3282
3479
|
...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
|
|
3283
3480
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
3481
|
+
...typeof task.model !== "string" || task.model.trim() === "" ? {} : { model: task.model },
|
|
3284
3482
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
3483
|
+
...task.reasoningEffort === void 0 ? {} : { reasoningEffort: task.reasoningEffort },
|
|
3285
3484
|
...task.reuseSession === void 0 ? {} : { reuseSession: task.reuseSession },
|
|
3286
3485
|
...task.goalRun === void 0 ? {} : { goalRun: task.goalRun },
|
|
3287
3486
|
...task.skipVerification === void 0 ? {} : { skipVerification: task.skipVerification },
|
|
@@ -3330,6 +3529,8 @@ function createInput(value) {
|
|
|
3330
3529
|
"freeze",
|
|
3331
3530
|
"handover",
|
|
3332
3531
|
"model",
|
|
3532
|
+
"reasoningEffort",
|
|
3533
|
+
"applyCardDefaults",
|
|
3333
3534
|
"reuseSession",
|
|
3334
3535
|
"teamRun",
|
|
3335
3536
|
"goalRun",
|
|
@@ -3339,7 +3540,8 @@ function createInput(value) {
|
|
|
3339
3540
|
])) return false;
|
|
3340
3541
|
if (input.parentId !== void 0 && (typeof input.parentId !== "string" || input.parentId.trim() === "")) return false;
|
|
3341
3542
|
if (typeof input.title !== "string" || typeof input.description !== "string" || typeof input.prompt !== "string") return false;
|
|
3342
|
-
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;
|
|
3343
3545
|
if (input.reuseSession !== void 0 && typeof input.reuseSession !== "boolean") return false;
|
|
3344
3546
|
if (input.teamRun !== void 0 && typeof input.teamRun !== "boolean") return false;
|
|
3345
3547
|
if (input.goalRun !== void 0 && typeof input.goalRun !== "boolean") return false;
|
|
@@ -3380,6 +3582,7 @@ function updatePatch(value) {
|
|
|
3380
3582
|
"freeze",
|
|
3381
3583
|
"handover",
|
|
3382
3584
|
"model",
|
|
3585
|
+
"reasoningEffort",
|
|
3383
3586
|
"reuseSession",
|
|
3384
3587
|
"teamRun",
|
|
3385
3588
|
"goalRun",
|
|
@@ -3396,7 +3599,8 @@ function updatePatch(value) {
|
|
|
3396
3599
|
"prompt",
|
|
3397
3600
|
"workspaceId",
|
|
3398
3601
|
"mode",
|
|
3399
|
-
"model"
|
|
3602
|
+
"model",
|
|
3603
|
+
"reasoningEffort"
|
|
3400
3604
|
]) if (!optionalString(patch[key])) return false;
|
|
3401
3605
|
if (patch.permission !== void 0 && !isTaskPermission(patch.permission)) return false;
|
|
3402
3606
|
if (patch.tags !== void 0 && patch.tags !== null && !isTaskTagList(patch.tags)) return false;
|
|
@@ -3985,11 +4189,14 @@ var HostTaskLedger = class {
|
|
|
3985
4189
|
* instead of remounting the row.
|
|
3986
4190
|
*/
|
|
3987
4191
|
depthLimit;
|
|
4192
|
+
/** Live reader of the deployment's new-card defaults (a volatile config read). */
|
|
4193
|
+
cardDefaults;
|
|
3988
4194
|
constructor(dir = join(dshHome(), "task-board"), now = Date.now, options = {}) {
|
|
3989
4195
|
this.now = now;
|
|
3990
4196
|
const baseline = options.sessionDefaultPermission;
|
|
3991
4197
|
this.sessionDefault = typeof baseline === "function" ? baseline : () => baseline ?? "read-only";
|
|
3992
4198
|
this.depthLimit = normalizeSubtaskDepth(options.maxSubtaskDepth ?? 1);
|
|
4199
|
+
this.cardDefaults = options.cardDefaults ?? (() => ({}));
|
|
3993
4200
|
mkdirSync(dir, { recursive: true });
|
|
3994
4201
|
this.file = join(dir, "ledger-v2.json");
|
|
3995
4202
|
this.lockFile = join(dir, "ledger-v2.lock");
|
|
@@ -4409,7 +4616,7 @@ var HostTaskLedger = class {
|
|
|
4409
4616
|
frozenBy: initiator
|
|
4410
4617
|
}
|
|
4411
4618
|
};
|
|
4412
|
-
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());
|
|
4413
4620
|
if (result.task === void 0) throw new Error(result.error ?? "invalid task");
|
|
4414
4621
|
this.document.tasks = [...result.tasks];
|
|
4415
4622
|
break;
|
|
@@ -4639,10 +4846,11 @@ var HostTaskLedger = class {
|
|
|
4639
4846
|
return cascadeTargets(this.document.tasks, id, this.maxSubtaskDepth).some((task) => hasOpenExecution(task));
|
|
4640
4847
|
}
|
|
4641
4848
|
/**
|
|
4642
|
-
* Open one execution per cascade participant
|
|
4643
|
-
* on-board descendants, within the depth limit) under
|
|
4644
|
-
* commit the ledger once.
|
|
4645
|
-
*
|
|
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.
|
|
4646
4854
|
* @param root - the task the user ran.
|
|
4647
4855
|
* @param now - clock instant (ms epoch).
|
|
4648
4856
|
* @param initiator - the DSH session that asked for the run (audit only).
|
|
@@ -4659,6 +4867,7 @@ var HostTaskLedger = class {
|
|
|
4659
4867
|
const runs = [];
|
|
4660
4868
|
for (const task of cascadeTargets(before, root.id, this.maxSubtaskDepth)) {
|
|
4661
4869
|
if (task.archivedAt !== void 0) continue;
|
|
4870
|
+
if (!joinsCascadeRun(task, root.id)) continue;
|
|
4662
4871
|
if (hasOpenExecution(task)) continue;
|
|
4663
4872
|
const opened = startExecution(rerun && task.id === root.id ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator, groupId);
|
|
4664
4873
|
started.set(task.id, opened.task);
|
|
@@ -5212,7 +5421,96 @@ function mostRecentWorkspaceId(items) {
|
|
|
5212
5421
|
return best?.id ?? items[0]?.id;
|
|
5213
5422
|
}
|
|
5214
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
|
|
5215
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
|
+
}
|
|
5216
5514
|
function sessionAddress(sessionId) {
|
|
5217
5515
|
return {
|
|
5218
5516
|
kind: "session",
|
|
@@ -5413,27 +5711,39 @@ function invokeWireArgs(namespace, method, request) {
|
|
|
5413
5711
|
if (namespace === "session" && method === "list") return { _request: request };
|
|
5414
5712
|
return { request };
|
|
5415
5713
|
}
|
|
5416
|
-
var HostExecutionRunner = class {
|
|
5714
|
+
var HostExecutionRunner = class HostExecutionRunner {
|
|
5417
5715
|
gateway;
|
|
5418
5716
|
commands;
|
|
5419
5717
|
workspaceRegistry;
|
|
5420
|
-
/**
|
|
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
|
+
*/
|
|
5421
5723
|
scanMemos = /* @__PURE__ */ new Map();
|
|
5422
5724
|
unavailableAttempts;
|
|
5423
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;
|
|
5424
5732
|
unsupportedSessionListWarned = false;
|
|
5425
|
-
constructor(gateway, commands, workspaceRegistry, unavailableRetry) {
|
|
5733
|
+
constructor(gateway, commands, workspaceRegistry, unavailableRetry, ownerPeer) {
|
|
5426
5734
|
this.gateway = gateway;
|
|
5427
5735
|
this.commands = commands;
|
|
5428
5736
|
this.workspaceRegistry = workspaceRegistry;
|
|
5429
5737
|
this.unavailableAttempts = unavailableRetry?.attempts ?? SERVICE_UNAVAILABLE_ATTEMPTS;
|
|
5430
5738
|
this.unavailableBackoffMs = unavailableRetry?.backoffMs ?? SERVICE_UNAVAILABLE_BACKOFF_MS;
|
|
5739
|
+
this.ownerPeer = ownerPeer ?? createOwnerPeer(new Context());
|
|
5431
5740
|
}
|
|
5432
5741
|
invoke(namespace, method, request, signal) {
|
|
5433
5742
|
return this.gateway.invoke({
|
|
5434
5743
|
namespace,
|
|
5435
5744
|
method,
|
|
5436
5745
|
args: invokeWireArgs(namespace, method, request),
|
|
5746
|
+
peer: this.ownerPeer,
|
|
5437
5747
|
...signal === void 0 ? {} : { signal }
|
|
5438
5748
|
});
|
|
5439
5749
|
}
|
|
@@ -5443,6 +5753,7 @@ var HostExecutionRunner = class {
|
|
|
5443
5753
|
namespace,
|
|
5444
5754
|
method,
|
|
5445
5755
|
args: { request },
|
|
5756
|
+
peer: this.ownerPeer,
|
|
5446
5757
|
...signal === void 0 ? {} : { signal }
|
|
5447
5758
|
});
|
|
5448
5759
|
}
|
|
@@ -5484,7 +5795,7 @@ var HostExecutionRunner = class {
|
|
|
5484
5795
|
try {
|
|
5485
5796
|
options.onSession?.(reused);
|
|
5486
5797
|
await this.assertReusedPreset(reused, mode);
|
|
5487
|
-
await this.pinAndPrompt(reused, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true);
|
|
5798
|
+
await this.pinAndPrompt(reused, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true, options.catalog);
|
|
5488
5799
|
} catch (error) {
|
|
5489
5800
|
throw new SessionLaunchError(reused, error);
|
|
5490
5801
|
}
|
|
@@ -5500,7 +5811,7 @@ var HostExecutionRunner = class {
|
|
|
5500
5811
|
sessionId,
|
|
5501
5812
|
title: task.title
|
|
5502
5813
|
});
|
|
5503
|
-
await this.pinAndPrompt(sessionId, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true);
|
|
5814
|
+
await this.pinAndPrompt(sessionId, task, permission, options.promptContext, options.onGoalArmed, options.goalEnabled === true, options.catalog);
|
|
5504
5815
|
} catch (error) {
|
|
5505
5816
|
throw new SessionLaunchError(sessionId, error);
|
|
5506
5817
|
}
|
|
@@ -5527,30 +5838,85 @@ var HostExecutionRunner = class {
|
|
|
5527
5838
|
throw new Error("reused session " + sessionId + " was composed from agent preset " + (typeof recorded === "string" ? "\"" + recorded + "\"" : "an unreadable value") + ", but the task pins \"" + pinned + "\"");
|
|
5528
5839
|
}
|
|
5529
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
|
+
/**
|
|
5530
5888
|
* Re-assert the pinned execution contract on a session and queue the task
|
|
5531
5889
|
* prompt. Shared by the fresh-session and reuse paths so both apply exactly
|
|
5532
|
-
* 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.
|
|
5533
5893
|
*/
|
|
5534
|
-
async pinAndPrompt(sessionId, task, permission, context = {}, onGoalArmed, goalEnabled = false) {
|
|
5894
|
+
async pinAndPrompt(sessionId, task, permission, context = {}, onGoalArmed, goalEnabled = false, catalog) {
|
|
5535
5895
|
if (permission !== void 0) {
|
|
5536
5896
|
if (this.commands === void 0) throw new Error("permission command dispatcher is unavailable");
|
|
5537
5897
|
const command = await this.commands.execute(sessionId, "/permission " + permission, AbortSignal.timeout(3e4));
|
|
5538
5898
|
if (command === void 0) throw new Error("permission command was not acknowledged");
|
|
5539
5899
|
if (command.kind !== "success") throw new Error(command.text ?? "permission command failed");
|
|
5540
5900
|
}
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
|
|
5544
|
-
const
|
|
5545
|
-
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;
|
|
5546
5910
|
try {
|
|
5547
5911
|
await this.invoke("session", "selectModel", {
|
|
5548
5912
|
sessionId,
|
|
5549
|
-
...provider ? {
|
|
5550
|
-
model: modelId
|
|
5913
|
+
...provider === void 0 ? {} : { provider },
|
|
5914
|
+
model: modelId,
|
|
5915
|
+
...decision.effort === void 0 ? {} : { reasoningEffort: decision.effort }
|
|
5551
5916
|
});
|
|
5552
5917
|
} catch (modelError) {
|
|
5553
|
-
|
|
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);
|
|
5554
5920
|
}
|
|
5555
5921
|
}
|
|
5556
5922
|
await this.invoke("session", "prompt", {
|
|
@@ -5765,10 +6131,13 @@ var HostExecutionRunner = class {
|
|
|
5765
6131
|
}
|
|
5766
6132
|
const openingEvents = opening.records.map((record) => ({ event: recordEvent(record) }));
|
|
5767
6133
|
const newestSeq = openingEvents.reduce((newest, entry) => newest === void 0 ? entry.event.seq : Math.max(newest, entry.event.seq), void 0);
|
|
5768
|
-
|
|
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;
|
|
5769
6138
|
const events = [...openingEvents];
|
|
5770
6139
|
let beforeSeq;
|
|
5771
|
-
let reachedExecutionBoundary = !opening.hasMore;
|
|
6140
|
+
let reachedExecutionBoundary = !opening.hasMore || coveredThrough;
|
|
5772
6141
|
for (let page = 0; page < HISTORY_PAGE_LIMIT && !reachedExecutionBoundary; page += 1) {
|
|
5773
6142
|
let history;
|
|
5774
6143
|
try {
|
|
@@ -5789,11 +6158,11 @@ var HostExecutionRunner = class {
|
|
|
5789
6158
|
const pageEntries = pageEvents(history);
|
|
5790
6159
|
events.push(...pageEntries);
|
|
5791
6160
|
const oldestTime = pageEntries.reduce((oldest, entry) => oldest === void 0 ? entry.event.time : Math.min(oldest, entry.event.time), void 0);
|
|
5792
|
-
|
|
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) {
|
|
5793
6163
|
reachedExecutionBoundary = true;
|
|
5794
6164
|
break;
|
|
5795
6165
|
}
|
|
5796
|
-
const oldestSeq = pageEntries.reduce((oldest, entry) => oldest === void 0 ? entry.event.seq : Math.min(oldest, entry.event.seq), void 0);
|
|
5797
6166
|
if (oldestSeq === void 0 || oldestSeq === beforeSeq) return { outcome: "pending" };
|
|
5798
6167
|
beforeSeq = oldestSeq;
|
|
5799
6168
|
}
|
|
@@ -6550,6 +6919,13 @@ const MAX_TIMER_DELAY_MS = 2147483647;
|
|
|
6550
6919
|
*/
|
|
6551
6920
|
const UNREADABLE_SETTLE_POLLS = 24;
|
|
6552
6921
|
/**
|
|
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
|
+
/**
|
|
6553
6929
|
* Why an enforced goal execution settled failed while its acceptance DID run:
|
|
6554
6930
|
* attempts were recorded and none of them passed. The judge answered about the
|
|
6555
6931
|
* work and the work did not pass.
|
|
@@ -6574,10 +6950,20 @@ const NEVER_INVOKED_VERIFICATION_REASON = "goal 验收未触发:本次执行
|
|
|
6574
6950
|
*/
|
|
6575
6951
|
const INVALID_ONLY_VERIFICATION_REASON = "goal 验收无效:本次执行只记录了无效验收(裁判未能给出与任务要求对应、且可定位到本次证据的否决依据),看板已停止调用裁判。这**不是**有证据的质量判负:验收本身不可用,本次运行因没有匹配的通过记录而结束。请检查验收环境与证据可见性,或用 task_board_manage(action=reset-verification) 清除记录的无效验收后重新完成目标。";
|
|
6576
6952
|
/**
|
|
6577
|
-
* Actions that can
|
|
6578
|
-
* an unrelated card edit leaves the pending fire
|
|
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.
|
|
6579
6963
|
*/
|
|
6580
6964
|
const SCHEDULE_WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
6965
|
+
"create",
|
|
6966
|
+
"import",
|
|
6581
6967
|
"set-schedule",
|
|
6582
6968
|
"delete",
|
|
6583
6969
|
"archive"
|
|
@@ -6597,6 +6983,26 @@ const PROCESS_TIMERS = {
|
|
|
6597
6983
|
};
|
|
6598
6984
|
}
|
|
6599
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
|
+
}
|
|
6600
7006
|
var TaskBoardHostService = class {
|
|
6601
7007
|
ledger;
|
|
6602
7008
|
runner;
|
|
@@ -6618,6 +7024,12 @@ var TaskBoardHostService = class {
|
|
|
6618
7024
|
pollInFlight = false;
|
|
6619
7025
|
active = true;
|
|
6620
7026
|
/**
|
|
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}).
|
|
7030
|
+
*/
|
|
7031
|
+
launches;
|
|
7032
|
+
/**
|
|
6621
7033
|
* Consecutive unreadable-history polls per open execution (see
|
|
6622
7034
|
* {@link noteUnreadableInspection}). Cleared as soon as an inspection
|
|
6623
7035
|
* resolves, so a transient reader failure never fails a card.
|
|
@@ -6630,6 +7042,14 @@ var TaskBoardHostService = class {
|
|
|
6630
7042
|
timers;
|
|
6631
7043
|
/** Live reader of the global native-/goal switch; frozen per execution. */
|
|
6632
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;
|
|
6633
7053
|
lastPowerJson = "";
|
|
6634
7054
|
now;
|
|
6635
7055
|
/** The deployment's workspaces, consulted to resolve a card that pins none. */
|
|
@@ -6643,15 +7063,19 @@ var TaskBoardHostService = class {
|
|
|
6643
7063
|
constructor(gateway, options = {}) {
|
|
6644
7064
|
this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
|
|
6645
7065
|
sessionDefaultPermission: options.sessionDefaultPermission,
|
|
6646
|
-
maxSubtaskDepth: options.maxSubtaskDepth
|
|
7066
|
+
maxSubtaskDepth: options.maxSubtaskDepth,
|
|
7067
|
+
cardDefaults: options.cardDefaults
|
|
6647
7068
|
});
|
|
6648
|
-
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
|
|
7069
|
+
this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry, void 0, options.ownerPeer);
|
|
6649
7070
|
this.workspaceRegistry = options.workspaceRegistry;
|
|
6650
7071
|
this.team = options.team;
|
|
6651
7072
|
this.timers = options.timers ?? PROCESS_TIMERS;
|
|
6652
7073
|
this.power = options.power ?? new PowerInhibitor();
|
|
6653
7074
|
this.now = options.now ?? Date.now;
|
|
6654
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()));
|
|
6655
7079
|
this.verificationSettings = options.verificationSettings ?? (() => ({
|
|
6656
7080
|
enabled: false,
|
|
6657
7081
|
model: "",
|
|
@@ -6728,6 +7152,7 @@ var TaskBoardHostService = class {
|
|
|
6728
7152
|
snapshot() {
|
|
6729
7153
|
const state = this.ledger.state();
|
|
6730
7154
|
const published = this.extensions.published();
|
|
7155
|
+
const cardDefaults = cardDefaultsOf(this.cardDefaults());
|
|
6731
7156
|
return {
|
|
6732
7157
|
schemaVersion: 6,
|
|
6733
7158
|
revision: state.revision,
|
|
@@ -6738,6 +7163,7 @@ var TaskBoardHostService = class {
|
|
|
6738
7163
|
maxSubtaskDepth: this.ledger.maxSubtaskDepth,
|
|
6739
7164
|
teamRunAvailable: this.team !== void 0,
|
|
6740
7165
|
goalRunEnabled: this.goalRunEnabled(),
|
|
7166
|
+
...cardDefaults === void 0 ? {} : { cardDefaults },
|
|
6741
7167
|
...Object.keys(published).length === 0 ? {} : { extensions: published }
|
|
6742
7168
|
};
|
|
6743
7169
|
}
|
|
@@ -6870,6 +7296,8 @@ var TaskBoardHostService = class {
|
|
|
6870
7296
|
}
|
|
6871
7297
|
dispose() {
|
|
6872
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`);
|
|
6873
7301
|
this.extensions.dispose();
|
|
6874
7302
|
this.clearScheduleTimer();
|
|
6875
7303
|
this.clearPollBackoff();
|
|
@@ -6879,7 +7307,7 @@ var TaskBoardHostService = class {
|
|
|
6879
7307
|
this.ledger.dispose();
|
|
6880
7308
|
this.listeners.clear();
|
|
6881
7309
|
}
|
|
6882
|
-
async launch(opened, others = [], schedule) {
|
|
7310
|
+
async launch(opened, others = [], schedule, freezeContract) {
|
|
6883
7311
|
/** The session this launch attached to the execution, once it exists. */
|
|
6884
7312
|
let attached;
|
|
6885
7313
|
try {
|
|
@@ -6895,14 +7323,13 @@ var TaskBoardHostService = class {
|
|
|
6895
7323
|
...team ? { team: true } : {},
|
|
6896
7324
|
...schedule === void 0 ? {} : { schedule }
|
|
6897
7325
|
};
|
|
6898
|
-
const settings = this.
|
|
6899
|
-
const globalGoalEnabled = this.goalRunEnabled() === true;
|
|
7326
|
+
const { settings, globalGoalEnabled, catalog } = await (freezeContract ?? (() => this.freezeRunContract()))();
|
|
6900
7327
|
const goalEnabled = globalGoalEnabled && opened.task.goalRun === true;
|
|
6901
7328
|
const skippedBy = opened.task.skipVerification === true ? "skipped" : "disabled";
|
|
6902
7329
|
const contract = resolveContract({
|
|
6903
7330
|
...settings,
|
|
6904
7331
|
enabled: settings.enabled && opened.task.skipVerification !== true
|
|
6905
|
-
},
|
|
7332
|
+
}, catalog);
|
|
6906
7333
|
const notArmed = globalGoalEnabled ? "goal-unavailable" : "goal-disabled";
|
|
6907
7334
|
const initial = {
|
|
6908
7335
|
contract,
|
|
@@ -6914,6 +7341,7 @@ var TaskBoardHostService = class {
|
|
|
6914
7341
|
...reuseSessionId === void 0 ? {} : { reuseSessionId },
|
|
6915
7342
|
...promptContext === void 0 ? {} : { promptContext },
|
|
6916
7343
|
goalEnabled,
|
|
7344
|
+
catalog,
|
|
6917
7345
|
onSession: (id) => {
|
|
6918
7346
|
attached = id;
|
|
6919
7347
|
this.ledger.attachSession(opened.task.id, opened.execution.id, id);
|
|
@@ -6927,14 +7355,52 @@ var TaskBoardHostService = class {
|
|
|
6927
7355
|
} catch (error) {
|
|
6928
7356
|
const launchedSession = error instanceof SessionLaunchError ? error.sessionId : void 0;
|
|
6929
7357
|
if (launchedSession !== void 0) this.ledger.attachSession(opened.task.id, opened.execution.id, launchedSession);
|
|
6930
|
-
if (schedule !== void 0 && attached === void 0 && launchedSession === void 0) this.
|
|
7358
|
+
if (schedule !== void 0 && attached === void 0 && launchedSession === void 0) this.refundUnstartedSchedule(opened.task.id);
|
|
6931
7359
|
this.settleAndNotify(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
|
|
6932
7360
|
}
|
|
6933
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
|
+
}
|
|
6934
7400
|
scheduleTeammate(opened, leadSessionId) {
|
|
6935
|
-
this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
7401
|
+
this.enqueueLaunch(opened, () => this.spawnTeammate(opened, leadSessionId).catch((error) => {
|
|
6936
7402
|
safeConsoleError("[dsh-task-board] teammate spawn settlement failed", error);
|
|
6937
|
-
});
|
|
7403
|
+
}));
|
|
6938
7404
|
}
|
|
6939
7405
|
/**
|
|
6940
7406
|
* Spawn one teammate inside the Lead session and attach the teammate's
|
|
@@ -7224,13 +7690,21 @@ var TaskBoardHostService = class {
|
|
|
7224
7690
|
* member then launches on its own so one refused participant cannot hold the
|
|
7225
7691
|
* others back. A team run's members are spawned inside the root's Lead
|
|
7226
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.
|
|
7227
7700
|
*/
|
|
7228
7701
|
dispatchRuns(runs, schedule) {
|
|
7229
7702
|
if (runs.length === 0) return;
|
|
7230
7703
|
const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
|
|
7231
7704
|
const others = runs.filter((run) => run !== root);
|
|
7232
|
-
this.
|
|
7233
|
-
|
|
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);
|
|
7234
7708
|
}
|
|
7235
7709
|
/**
|
|
7236
7710
|
* Move one execution's applicability without touching its frozen contract or
|
|
@@ -7244,10 +7718,50 @@ var TaskBoardHostService = class {
|
|
|
7244
7718
|
applicability
|
|
7245
7719
|
});
|
|
7246
7720
|
}
|
|
7247
|
-
scheduleLaunch(opened, others = [], schedule) {
|
|
7248
|
-
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) => {
|
|
7249
7723
|
safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
|
|
7250
|
-
});
|
|
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);
|
|
7251
7765
|
}
|
|
7252
7766
|
schedulePoll() {
|
|
7253
7767
|
if (this.pollInFlight || this.disposed) return;
|
|
@@ -7712,6 +8226,7 @@ function taskSummary(task, tasks, sessionDefault) {
|
|
|
7712
8226
|
...task.mode === void 0 ? {} : { mode: task.mode },
|
|
7713
8227
|
...task.permission === void 0 ? {} : { permission: task.permission },
|
|
7714
8228
|
...task.model === void 0 ? {} : { model: task.model },
|
|
8229
|
+
...task.reasoningEffort === void 0 ? {} : { reasoningEffort: task.reasoningEffort },
|
|
7715
8230
|
...task.reuseSession === true ? { reuseSession: true } : {},
|
|
7716
8231
|
...task.teamRun === true ? { teamRun: true } : {},
|
|
7717
8232
|
...task.goalRun === true ? { goalRun: true } : {},
|
|
@@ -7809,7 +8324,7 @@ function buildSetParentTool(host) {
|
|
|
7809
8324
|
"Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.",
|
|
7810
8325
|
"Pass an empty parentId to detach.",
|
|
7811
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).",
|
|
7812
|
-
"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."
|
|
7813
8328
|
].join(" "),
|
|
7814
8329
|
parameters: {
|
|
7815
8330
|
taskId: {
|
|
@@ -7861,7 +8376,7 @@ function buildRunTool(host) {
|
|
|
7861
8376
|
taskId: {
|
|
7862
8377
|
type: "string",
|
|
7863
8378
|
required: true,
|
|
7864
|
-
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."
|
|
7865
8380
|
},
|
|
7866
8381
|
rerun: {
|
|
7867
8382
|
type: "boolean",
|
|
@@ -8060,7 +8575,7 @@ function buildScheduleTool(host) {
|
|
|
8060
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.",
|
|
8061
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).",
|
|
8062
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.",
|
|
8063
|
-
"A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.",
|
|
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.",
|
|
8064
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.",
|
|
8065
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."
|
|
8066
8581
|
].join(" "),
|
|
@@ -8241,7 +8756,7 @@ function buildCreateTool(host) {
|
|
|
8241
8756
|
name: "task_board_create",
|
|
8242
8757
|
description: [
|
|
8243
8758
|
"Create a task on the DSH task board, or a subtask by passing parentId.",
|
|
8244
|
-
"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.",
|
|
8245
8760
|
"The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.",
|
|
8246
8761
|
"The task lands in the todo column and does NOT run; call task_board_run to execute it."
|
|
8247
8762
|
].join(" "),
|
|
@@ -8269,16 +8784,20 @@ function buildCreateTool(host) {
|
|
|
8269
8784
|
},
|
|
8270
8785
|
mode: {
|
|
8271
8786
|
type: "string",
|
|
8272
|
-
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."
|
|
8273
8788
|
},
|
|
8274
8789
|
permission: {
|
|
8275
8790
|
type: "string",
|
|
8276
8791
|
enum: [...TASK_PERMISSIONS],
|
|
8277
|
-
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."
|
|
8278
8793
|
},
|
|
8279
8794
|
model: {
|
|
8280
8795
|
type: "string",
|
|
8281
|
-
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."
|
|
8282
8801
|
},
|
|
8283
8802
|
reuseSession: {
|
|
8284
8803
|
type: "boolean",
|
|
@@ -8365,6 +8884,8 @@ function buildCreateTool(host) {
|
|
|
8365
8884
|
...args.mode === void 0 || args.mode === "" ? {} : { mode: args.mode },
|
|
8366
8885
|
...args.permission === void 0 ? {} : { permission: args.permission },
|
|
8367
8886
|
...args.model === void 0 || args.model === "" ? {} : { model: args.model },
|
|
8887
|
+
...args.reasoningEffort === void 0 || args.reasoningEffort === "" ? {} : { reasoningEffort: args.reasoningEffort },
|
|
8888
|
+
applyCardDefaults: true,
|
|
8368
8889
|
...args.reuseSession === true ? { reuseSession: true } : {},
|
|
8369
8890
|
...args.teamRun === true ? { teamRun: true } : {},
|
|
8370
8891
|
...args.goalRun === true ? { goalRun: true } : {},
|
|
@@ -8400,7 +8921,7 @@ function buildUpdateTool(host) {
|
|
|
8400
8921
|
return defineTool({
|
|
8401
8922
|
name: "task_board_update",
|
|
8402
8923
|
description: [
|
|
8403
|
-
"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).",
|
|
8404
8925
|
"Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.",
|
|
8405
8926
|
"Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.",
|
|
8406
8927
|
"Changing the pinned permission re-arms the permission confirmation gate."
|
|
@@ -8445,6 +8966,10 @@ function buildUpdateTool(host) {
|
|
|
8445
8966
|
type: "string",
|
|
8446
8967
|
description: "New pinned model; an empty string clears it."
|
|
8447
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
|
+
},
|
|
8448
8973
|
reuseSession: {
|
|
8449
8974
|
type: "boolean",
|
|
8450
8975
|
description: "Continue later runs in the previous execution session."
|
|
@@ -8493,6 +9018,7 @@ function buildUpdateTool(host) {
|
|
|
8493
9018
|
if (has(args, "workspaceId") && typeof args.workspaceId === "string") patch.workspaceId = args.workspaceId;
|
|
8494
9019
|
if (has(args, "mode") && typeof args.mode === "string") patch.mode = args.mode;
|
|
8495
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;
|
|
8496
9022
|
if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
|
|
8497
9023
|
if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
|
|
8498
9024
|
if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
|
|
@@ -10080,8 +10606,13 @@ const Config = z.object({
|
|
|
10080
10606
|
sessionDefaultPermission: z.union(TASK_PERMISSIONS),
|
|
10081
10607
|
maxSubtaskDepth: z.number().min(1).max(3).default(1).volatile(),
|
|
10082
10608
|
sessionPollSeconds: z.number().min(1).max(300).default(5).volatile(),
|
|
10609
|
+
runLaunchWidth: z.number().min(1).max(16).default(3).volatile(),
|
|
10083
10610
|
teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER),
|
|
10084
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(),
|
|
10085
10616
|
goalVerification: z.boolean().default(true).volatile(),
|
|
10086
10617
|
goalVerificationModel: z.string().default("").volatile(),
|
|
10087
10618
|
goalVerificationReasoningEffort: z.string().default("").volatile(),
|
|
@@ -10264,6 +10795,50 @@ function resolveLlmRuntime(ctx) {
|
|
|
10264
10795
|
}
|
|
10265
10796
|
}
|
|
10266
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
|
+
/**
|
|
10267
10842
|
* Activate the board's host half: ledger, routes, cron scheduler, power
|
|
10268
10843
|
* inhibitor, and the model-facing announcement.
|
|
10269
10844
|
*
|
|
@@ -10289,6 +10864,12 @@ function applyImpl(ctx, config) {
|
|
|
10289
10864
|
/** Current roster-poll cadence in seconds (read live: the settings card edits it in place). */
|
|
10290
10865
|
const sessionPollSeconds = () => readConfigField(config?.sessionPollSeconds, 5);
|
|
10291
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);
|
|
10872
|
+
/**
|
|
10292
10873
|
* The baseline the permission confirmation gate judges against: the row's own
|
|
10293
10874
|
* `sessionDefaultPermission` when the deployment pins one, otherwise the Host's
|
|
10294
10875
|
* default preset. Read per call through the ledger, so a Host Settings change
|
|
@@ -10320,18 +10901,36 @@ function applyImpl(ctx, config) {
|
|
|
10320
10901
|
* executions that start afterwards.
|
|
10321
10902
|
*/
|
|
10322
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);
|
|
10323
10920
|
const host = new TaskBoardHostService(ctx.typertGateway, {
|
|
10324
10921
|
workspaceRegistry: ctx.workspaceRegistry,
|
|
10922
|
+
ownerPeer,
|
|
10325
10923
|
sessionDefaultPermission,
|
|
10326
10924
|
maxSubtaskDepth: maxSubtaskDepth(),
|
|
10327
10925
|
goalRunEnabled,
|
|
10926
|
+
runLaunchWidth,
|
|
10927
|
+
cardDefaults,
|
|
10328
10928
|
verificationSettings,
|
|
10329
10929
|
verificationCatalog,
|
|
10330
10930
|
timers: resolveHostTimers(ctx),
|
|
10331
10931
|
team: buildTeamDispatcher(ctx, config?.teamProvider ?? "spawn"),
|
|
10332
10932
|
commandDispatcher: { async execute(sessionId, line, signal) {
|
|
10333
|
-
const agent = ctx
|
|
10334
|
-
if (agent === void 0) throw new Error(`execution session ${sessionId} is not available`);
|
|
10933
|
+
const agent = await resolveExecutionAgent(ctx, sessionId);
|
|
10335
10934
|
return (await ctx.commands.execute(agent, line, [], signal))?.result;
|
|
10336
10935
|
} }
|
|
10337
10936
|
});
|
|
@@ -10455,4 +11054,4 @@ function applyImpl(ctx, config) {
|
|
|
10455
11054
|
sync();
|
|
10456
11055
|
}
|
|
10457
11056
|
//#endregion
|
|
10458
|
-
export { Config, DEFAULT_GOAL_RUN, 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 };
|