@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.
Files changed (58) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +14 -6
  3. package/README.zh.md +14 -6
  4. package/lib/client.js +1502 -820
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +674 -75
  7. package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +49 -0
  9. package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
  10. package/lib/types/client/board/model-options.d.ts +67 -0
  11. package/lib/types/client/board/preset-label.d.ts +21 -0
  12. package/lib/types/client/board/preset-roster.d.ts +45 -0
  13. package/lib/types/client/board/team-run-promotion.d.ts +26 -0
  14. package/lib/types/client/locales.d.ts +16 -0
  15. package/lib/types/core/card-defaults.d.ts +78 -0
  16. package/lib/types/core/controller.d.ts +32 -7
  17. package/lib/types/core/launch-width.d.ts +31 -0
  18. package/lib/types/core/reasoning-effort.d.ts +57 -0
  19. package/lib/types/core/subtask.d.ts +30 -8
  20. package/lib/types/core/tasks.d.ts +19 -0
  21. package/lib/types/core/use-cases/task-create.d.ts +10 -5
  22. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  23. package/lib/types/host/launch-admission.d.ts +72 -0
  24. package/lib/types/host-ledger.d.ts +9 -4
  25. package/lib/types/host-runner.d.ts +79 -4
  26. package/lib/types/host-service.d.ts +85 -2
  27. package/lib/types/index.d.ts +87 -0
  28. package/lib/types/protocol.d.ts +13 -0
  29. package/package.json +28 -28
  30. package/src/client/CardDefaultPresetField.tsx +101 -0
  31. package/src/client/TaskBoardSettingsCard.tsx +214 -16
  32. package/src/client/board/LinkSubtaskModal.tsx +8 -0
  33. package/src/client/board/NewTaskModal.tsx +86 -40
  34. package/src/client/board/ReasoningEffortField.tsx +50 -0
  35. package/src/client/board/TaskDetail.tsx +42 -6
  36. package/src/client/board/model-options.ts +178 -0
  37. package/src/client/board/preset-label.ts +32 -0
  38. package/src/client/board/preset-roster.ts +58 -0
  39. package/src/client/board/team-run-promotion.ts +39 -0
  40. package/src/client/index.ts +98 -94
  41. package/src/client/locales.ts +34 -2
  42. package/src/client/settings-entry-form.ts +0 -1
  43. package/src/core/card-defaults.ts +132 -0
  44. package/src/core/controller.ts +44 -9
  45. package/src/core/launch-width.ts +38 -0
  46. package/src/core/reasoning-effort.ts +92 -0
  47. package/src/core/store.ts +5 -0
  48. package/src/core/subtask.ts +48 -13
  49. package/src/core/tasks.ts +20 -0
  50. package/src/core/use-cases/task-create.ts +27 -16
  51. package/src/core/use-cases/task-update.ts +6 -1
  52. package/src/host/agent-tools.ts +21 -8
  53. package/src/host/launch-admission.ts +140 -0
  54. package/src/host-ledger.ts +18 -6
  55. package/src/host-runner.ts +184 -18
  56. package/src/host-service.ts +247 -23
  57. package/src/index.ts +149 -2
  58. 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. Archived tasks
925
- * never run; a participant that already has an open execution is skipped by
926
- * the caller, which cannot start it twice.
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 model inherit the ancestor's PIN only — a
993
- * handover bundle is a per-card continuation artefact and is never inherited.
994
- * The permission inherits the ancestor's EFFECTIVE binding, and its human
995
- * confirmation travels with it, so a subtask of an already-confirmed elevated
996
- * card is runnable while a subtask's own unconfirmed binding stays gated.
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 chain) {
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
- * A subtask inherits the execution targets its parent already pins (workspace,
2562
- * agent preset, permission, model) unless the form supplied its own; the
2563
- * parent human permission confirmation travels with an inherited permission,
2564
- * so a subtask of an already-confirmed elevated card is runnable at once.
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
- let task = createTask(parent === void 0 ? input : {
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.workspaceId,
2601
- mode: input.mode ?? parent.mode,
2602
- model: input.model ?? parent.model
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 (the requested task and its
4643
- * on-board descendants, within the depth limit) under a single run group, and
4644
- * commit the ledger once. The returned runs are ordered root-first, so the
4645
- * Host launches the parent before its subtasks.
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
- /** Newest scanned event sequence per session with no matching execution end. */
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
- if (task.model !== void 0 && task.model.trim() !== "") {
5542
- const rawModel = task.model.trim();
5543
- const slashIdx = rawModel.indexOf("/");
5544
- const provider = slashIdx >= 0 ? rawModel.slice(0, slashIdx).trim() : void 0;
5545
- const modelId = slashIdx >= 0 ? rawModel.slice(slashIdx + 1).trim() : rawModel;
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 ? { 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
- console.warn(`[dsh-task-board] failed to select model "${task.model}" for session ${sessionId}, falling back to default:`, modelError);
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
- if (newestSeq !== void 0 && this.scanMemos.get(sessionId) === newestSeq) return { outcome: "pending" };
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
- if (!history.hasMore || oldestTime !== void 0 && oldestTime <= startedAt) {
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 move an armed trigger. Only these re-arm the native timer;
6578
- * an unrelated card edit leaves the pending fire untouched.
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.verificationSettings();
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
- }, await this.verificationCatalog());
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.ledger.refundScheduledOccurrence(opened.task.id, this.now());
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.scheduleLaunch(root, others, schedule);
7233
- for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run, [], schedule);
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 runs its whole subtree concurrently; a parent card settles only after its own turn and every direct subtask settle."
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 for the deployment default or the parent value."
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. Omit to inherit the parent binding; a value above the session default needs a human confirmation in the board UI before the card can run."
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 for the host default or the parent value."
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.agents.get(sessionId);
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 };