@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3

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 (69) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +28 -3
  3. package/README.zh.md +28 -3
  4. package/lib/client.js +1142 -467
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2066 -1475
  7. package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
  8. package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
  9. package/lib/types/client/board/NewTaskModal.d.ts +7 -1
  10. package/lib/types/client/board/TaskCard.d.ts +11 -1
  11. package/lib/types/client/index.d.ts +15 -1
  12. package/lib/types/client/locales.d.ts +33 -0
  13. package/lib/types/client/native-panel.d.ts +62 -0
  14. package/lib/types/client/settings-entry-form.d.ts +49 -0
  15. package/lib/types/core/controller.d.ts +71 -3
  16. package/lib/types/core/subtask.d.ts +122 -0
  17. package/lib/types/core/tasks.d.ts +46 -3
  18. package/lib/types/core/use-cases/task-archive.d.ts +10 -13
  19. package/lib/types/core/use-cases/task-create.d.ts +13 -4
  20. package/lib/types/core/use-cases/task-delete.d.ts +7 -1
  21. package/lib/types/core/use-cases/task-parent.d.ts +21 -0
  22. package/lib/types/core/use-cases/task-update.d.ts +1 -1
  23. package/lib/types/host/agent-tools.d.ts +27 -0
  24. package/lib/types/host-ledger.d.ts +97 -4
  25. package/lib/types/host-runner.d.ts +39 -1
  26. package/lib/types/host-service.d.ts +98 -5
  27. package/lib/types/index.d.ts +93 -5
  28. package/lib/types/mount-once.d.ts +38 -7
  29. package/lib/types/protocol.d.ts +14 -0
  30. package/package.json +26 -21
  31. package/src/client/TaskBoardSettingsCard.tsx +48 -3
  32. package/src/client/board/LinkSubtaskModal.tsx +82 -0
  33. package/src/client/board/NewTaskModal.tsx +40 -6
  34. package/src/client/board/TaskBoard.tsx +88 -8
  35. package/src/client/board/TaskCard.tsx +52 -2
  36. package/src/client/board/TaskDetail.tsx +124 -1
  37. package/src/client/board.module.css +112 -131
  38. package/src/client/host-api.ts +14 -4
  39. package/src/client/index.ts +56 -18
  40. package/src/client/locales.ts +70 -2
  41. package/src/client/native-panel.tsx +124 -0
  42. package/src/client/settings-entry-form.ts +142 -0
  43. package/src/core/controller.ts +128 -10
  44. package/src/core/store.ts +15 -0
  45. package/src/core/subtask.ts +286 -0
  46. package/src/core/tasks.ts +50 -2
  47. package/src/core/use-cases/task-archive.ts +46 -12
  48. package/src/core/use-cases/task-create.ts +42 -7
  49. package/src/core/use-cases/task-delete.ts +11 -1
  50. package/src/core/use-cases/task-parent.ts +64 -0
  51. package/src/core/use-cases/task-update.ts +3 -1
  52. package/src/host/agent-tools.ts +643 -0
  53. package/src/host-ledger.ts +343 -45
  54. package/src/host-routes.ts +37 -1
  55. package/src/host-runner.ts +141 -12
  56. package/src/host-service.ts +265 -32
  57. package/src/index.ts +203 -9
  58. package/src/mount-once.ts +107 -13
  59. package/src/protocol.ts +26 -2
  60. package/lib/types/client/board-mount.d.ts +0 -23
  61. package/lib/types/client/body-mutations.d.ts +0 -45
  62. package/lib/types/client/panel-mount-core.d.ts +0 -56
  63. package/lib/types/client/sidebar-entry-core.d.ts +0 -55
  64. package/lib/types/client/sidebar-entry.d.ts +0 -29
  65. package/src/client/board-mount.tsx +0 -43
  66. package/src/client/body-mutations.ts +0 -133
  67. package/src/client/panel-mount-core.ts +0 -155
  68. package/src/client/sidebar-entry-core.ts +0 -237
  69. package/src/client/sidebar-entry.ts +0 -53
package/lib/index.js CHANGED
@@ -1,4 +1,3 @@
1
- import { createRequire } from "node:module";
2
1
  import z from "@deepseek-ai/schemastery";
3
2
  import { spawn, spawnSync } from "node:child_process";
4
3
  import { createHash, timingSafeEqual } from "node:crypto";
@@ -6,7 +5,8 @@ import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readF
6
5
  import { dirname, isAbsolute, join, win32 } from "node:path";
7
6
  import { homedir } from "node:os";
8
7
  import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
9
- import { Context, Service } from "@deepseek-ai/cordis";
8
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
9
+ import { defineTool } from "@deepseek-ai/dsh-tools";
10
10
  //#region src/core/schedule.ts
11
11
  /** Inclusive ranges per field, in cron order. */
12
12
  const FIELD_RANGES = [
@@ -368,6 +368,7 @@ function createTask(input, now, id) {
368
368
  title: input.title.trim(),
369
369
  description: input.description.trim(),
370
370
  prompt: input.prompt.trim(),
371
+ parentId: normalizeTargetId(input.parentId),
371
372
  status: "todo",
372
373
  createdAt: now,
373
374
  updatedAt: now,
@@ -377,6 +378,7 @@ function createTask(input, now, id) {
377
378
  permission: isTaskPermission(input.permission) ? input.permission : void 0,
378
379
  model: normalizeTargetId(input.model),
379
380
  reuseSession: input.reuseSession === true ? true : void 0,
381
+ teamRun: input.teamRun === true ? true : void 0,
380
382
  ...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
381
383
  ...input.handover === void 0 ? {} : { handover: {
382
384
  ...input.handover,
@@ -420,8 +422,10 @@ function withSchedule(task, patch, now) {
420
422
  /**
421
423
  * Open a fresh execution on a task: move it to 'running' and append a
422
424
  * running execution record. Returns the new task and the new execution.
425
+ * @param runGroupId - cascade run group, when this run also opened executions
426
+ * for the task's subtasks (see `core/subtask.ts`).
423
427
  */
424
- function startExecution(task, now, executionId, initiatedBy) {
428
+ function startExecution(task, now, executionId, initiatedBy, runGroupId) {
425
429
  const execution = {
426
430
  id: executionId,
427
431
  sessionId: void 0,
@@ -429,6 +433,7 @@ function startExecution(task, now, executionId, initiatedBy) {
429
433
  endedAt: void 0,
430
434
  result: void 0,
431
435
  error: void 0,
436
+ ...runGroupId === void 0 || runGroupId === "" ? {} : { runGroupId },
432
437
  ...initiatedBy === void 0 || initiatedBy === "" ? {} : { initiatedBy },
433
438
  ...task.freeze === void 0 ? {} : {
434
439
  frozenAt: task.freeze.frozenAt,
@@ -471,6 +476,234 @@ function settleExecution(task, executionId, outcome, now, error) {
471
476
  executions
472
477
  };
473
478
  }
479
+ /**
480
+ * Clamp a configured depth into the supported range. Anything unusable
481
+ * (absent, non-numeric, not finite) falls back to the default, so a bad
482
+ * configuration narrows the tree instead of lifting the guard.
483
+ */
484
+ function normalizeSubtaskDepth(value) {
485
+ if (typeof value !== "number" || !Number.isFinite(value)) return 1;
486
+ const depth = Math.trunc(value);
487
+ if (depth < 1) return 1;
488
+ if (depth > 3) return 3;
489
+ return depth;
490
+ }
491
+ /** Whether an unknown value is a settled execution outcome. */
492
+ function isExecutionOutcome(value) {
493
+ return value === "succeeded" || value === "failed" || value === "cancelled";
494
+ }
495
+ function indexOf(tasks) {
496
+ return new Map(tasks.map((task) => [task.id, task]));
497
+ }
498
+ /**
499
+ * Nearest-first ancestor chain of a task. The walk is cycle-safe and bounded
500
+ * by the ledger size rather than by the deployment maximum: a chain stored
501
+ * under a larger limit survives that limit being lowered, and restore and
502
+ * inheritance must still see all of it.
503
+ */
504
+ function ancestorChain(tasks, task) {
505
+ const index = indexOf(tasks);
506
+ const chain = [];
507
+ const seen = /* @__PURE__ */ new Set([task.id]);
508
+ let current = task.parentId === void 0 ? void 0 : index.get(task.parentId);
509
+ while (current !== void 0 && chain.length < tasks.length) {
510
+ if (seen.has(current.id)) break;
511
+ seen.add(current.id);
512
+ chain.push(current);
513
+ current = current.parentId === void 0 ? void 0 : index.get(current.parentId);
514
+ }
515
+ return chain;
516
+ }
517
+ /** Root-to-task depth: a root task is 0, its subtask 1, and so on. */
518
+ function taskDepth(tasks, id) {
519
+ const task = indexOf(tasks).get(id);
520
+ return task === void 0 ? 0 : ancestorChain(tasks, task).length;
521
+ }
522
+ /** Direct subtasks of a task, in ledger order. */
523
+ function directSubtasks(tasks, id) {
524
+ return tasks.filter((task) => task.parentId === id);
525
+ }
526
+ /**
527
+ * Descendants of a task in breadth-first order, bounded by maxSubtaskDepth
528
+ * total depth. The visited set keeps a malformed ledger from looping.
529
+ */
530
+ function descendantTasks(tasks, id, maxSubtaskDepth) {
531
+ const visited = /* @__PURE__ */ new Set([id]);
532
+ const found = [];
533
+ let frontier = [id];
534
+ for (let depth = 1; depth <= maxSubtaskDepth && frontier.length > 0; depth += 1) {
535
+ const next = [];
536
+ for (const parentId of frontier) for (const task of tasks) {
537
+ if (task.parentId !== parentId || visited.has(task.id)) continue;
538
+ visited.add(task.id);
539
+ found.push(task);
540
+ next.push(task.id);
541
+ }
542
+ frontier = next;
543
+ }
544
+ return found;
545
+ }
546
+ /**
547
+ * Height of a task's own subtree (0 for a leaf), bounded by the hard maximum.
548
+ * The visited set makes the walk terminate on a cyclic hand-edited ledger
549
+ * instead of recursing until the stack overflows.
550
+ */
551
+ function subtreeHeight(tasks, id, visited = /* @__PURE__ */ new Set()) {
552
+ if (visited.has(id)) return 0;
553
+ const seen = /* @__PURE__ */ new Set([...visited, id]);
554
+ const children = directSubtasks(tasks, id);
555
+ if (children.length === 0) return 0;
556
+ let height = 0;
557
+ for (const child of children) height = Math.max(height, 1 + subtreeHeight(tasks, child.id, seen));
558
+ return Math.min(height, 3);
559
+ }
560
+ /**
561
+ * Whether childId may be attached under parentId. A null parent detaches and
562
+ * is always allowed for a known task. Otherwise the link must name an
563
+ * on-board task that is neither the child itself nor one of its descendants,
564
+ * and the resulting depth (parent depth + 1 + the child own subtree height)
565
+ * must stay within maxSubtaskDepth.
566
+ */
567
+ function checkParentLink(tasks, childId, parentId, maxSubtaskDepth) {
568
+ if (tasks.find((task) => task.id === childId) === void 0) return {
569
+ ok: false,
570
+ reason: "unknown-task"
571
+ };
572
+ if (parentId === null || parentId === "") return { ok: true };
573
+ if (parentId === childId) return {
574
+ ok: false,
575
+ reason: "self-parent"
576
+ };
577
+ const parent = tasks.find((task) => task.id === parentId);
578
+ if (parent === void 0) return {
579
+ ok: false,
580
+ reason: "unknown-parent"
581
+ };
582
+ if (parent.archivedAt !== void 0) return {
583
+ ok: false,
584
+ reason: "archived-parent"
585
+ };
586
+ if (descendantTasks(tasks, childId, 3).some((task) => task.id === parentId)) return {
587
+ ok: false,
588
+ reason: "cycle"
589
+ };
590
+ if (taskDepth(tasks, parentId) + 1 + subtreeHeight(tasks, childId) > maxSubtaskDepth) return {
591
+ ok: false,
592
+ reason: "depth-exceeded"
593
+ };
594
+ return { ok: true };
595
+ }
596
+ /**
597
+ * The tasks one run request opens executions for: the requested task first,
598
+ * then its on-board descendants within the configured depth. Archived tasks
599
+ * never run; a participant that already has an open execution is skipped by
600
+ * the caller, which cannot start it twice.
601
+ */
602
+ function cascadeTargets(tasks, rootId, maxSubtaskDepth) {
603
+ const root = tasks.find((task) => task.id === rootId);
604
+ if (root === void 0) return [];
605
+ return [root, ...descendantTasks(tasks, rootId, maxSubtaskDepth)];
606
+ }
607
+ /** The open execution of a task inside a run group, if it still exists. */
608
+ function openGroupExecution(task, runGroupId) {
609
+ return task.executions.find((execution) => execution.runGroupId === runGroupId && execution.endedAt === void 0);
610
+ }
611
+ /**
612
+ * The direct subtasks of parentTaskId whose execution in this run group is
613
+ * still open: the parent remaining waiters. The parent may settle once this
614
+ * list is empty and its own outcome is known.
615
+ */
616
+ function pendingCascadeChildren(tasks, parentTaskId, runGroupId) {
617
+ const pending = [];
618
+ for (const task of tasks) {
619
+ if (task.parentId !== parentTaskId) continue;
620
+ const execution = openGroupExecution(task, runGroupId);
621
+ if (execution !== void 0) pending.push({
622
+ taskId: task.id,
623
+ execution
624
+ });
625
+ }
626
+ return pending;
627
+ }
628
+ /**
629
+ * Fold a cascade parent own outcome and its children outcomes into one
630
+ * verdict: failure dominates, then cancellation, then success. The error text
631
+ * is the first failure, falling back to the first cancellation.
632
+ */
633
+ function combineCascadeOutcome(entries) {
634
+ let result = "succeeded";
635
+ let error;
636
+ for (const entry of entries) {
637
+ if (entry.result === "failed") {
638
+ if (result !== "failed") error = void 0;
639
+ result = "failed";
640
+ if (error === void 0 && entry.error !== void 0 && entry.error !== "") error = entry.error;
641
+ continue;
642
+ }
643
+ if (entry.result === "cancelled") {
644
+ if (result === "succeeded") result = "cancelled";
645
+ if (error === void 0 && entry.error !== void 0 && entry.error !== "") error = entry.error;
646
+ }
647
+ }
648
+ return error === void 0 ? {
649
+ result,
650
+ error: void 0
651
+ } : {
652
+ result,
653
+ error
654
+ };
655
+ }
656
+ /**
657
+ * The permission a task would actually run under: its handover bundle's pin
658
+ * wins over the plain pin field, exactly as the runner resolves it.
659
+ */
660
+ function effectiveTaskPermission(task) {
661
+ return task.handover?.permission ?? task.permission;
662
+ }
663
+ /**
664
+ * Resolve the execution targets a (sub)task actually runs with: its own value
665
+ * wins, and an unset target inherits the nearest ancestor up to the root.
666
+ * Workspace, agent preset and model inherit the ancestor's PIN only — a
667
+ * handover bundle is a per-card continuation artefact and is never inherited.
668
+ * The permission inherits the ancestor's EFFECTIVE binding, and its human
669
+ * confirmation travels with it, so a subtask of an already-confirmed elevated
670
+ * card is runnable while a subtask's own unconfirmed binding stays gated.
671
+ */
672
+ /**
673
+ * The immutable teammate name for one team-mode member. Teammate names are
674
+ * lower-kebab-case and must stay unique for the lifetime of the Team, so the
675
+ * run-group token is appended: two runs of the same tree never collide. A title
676
+ * with no ASCII word characters (a CJK title, for example) keeps a stable
677
+ * generic prefix and is still distinguished by the token.
678
+ * @param title - the member task's title.
679
+ * @param token - the run group id (the task id when no group is recorded).
680
+ * @returns the teammate name handed to the Agent Teams service.
681
+ */
682
+ function teammateName(title, token) {
683
+ const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32).replace(/-+$/g, "");
684
+ const suffix = token.replace(/[^a-z0-9]/gi, "").slice(0, 8).toLowerCase();
685
+ return `${slug === "" ? "subtask" : slug}-${suffix === "" ? "run" : suffix}`;
686
+ }
687
+ function resolveExecutionTargets(task, tasks, maxSubtaskDepth = 3) {
688
+ if (task.parentId === void 0) return task;
689
+ const chain = [task, ...ancestorChain(tasks, task).slice(0, maxSubtaskDepth)];
690
+ const first = (key) => {
691
+ for (const candidate of chain) {
692
+ const value = candidate[key];
693
+ if (value !== void 0) return value;
694
+ }
695
+ };
696
+ const own = effectiveTaskPermission(task);
697
+ const owner = own === void 0 ? chain.slice(1).find((candidate) => effectiveTaskPermission(candidate) !== void 0) : void 0;
698
+ return {
699
+ ...task,
700
+ workspaceId: first("workspaceId"),
701
+ mode: first("mode"),
702
+ model: first("model"),
703
+ permission: own ?? (owner === void 0 ? void 0 : effectiveTaskPermission(owner)),
704
+ permissionConfirmedAt: own === void 0 ? owner?.permissionConfirmedAt : task.permissionConfirmedAt
705
+ };
706
+ }
474
707
  /** Marker replacing every sensitive match. */
475
708
  const REDACTED_MARKER = "[REDACTED]";
476
709
  const SECTION_ORDER$1 = [
@@ -672,6 +905,7 @@ function isTaskRecordShape(value) {
672
905
  if (typeof record.prompt !== "string") return false;
673
906
  if (typeof record.createdAt !== "number") return false;
674
907
  if (typeof record.updatedAt !== "number") return false;
908
+ if (record.parentId !== void 0 && typeof record.parentId !== "string") return false;
675
909
  if (record.workspaceId !== void 0 && typeof record.workspaceId !== "string") return false;
676
910
  if (record.mode !== void 0 && typeof record.mode !== "string") return false;
677
911
  if (record.permission !== void 0 && typeof record.permission !== "string") return false;
@@ -785,6 +1019,13 @@ function parseLedger(raw) {
785
1019
  status: normalizeStatus(row.status)
786
1020
  };
787
1021
  task.schedule = normalizeSchedule(row.schedule);
1022
+ task.parentId = normalizeTargetId(row.parentId);
1023
+ task.executions = task.executions.map((execution) => ({
1024
+ ...execution,
1025
+ runGroupId: normalizeTargetId(execution.runGroupId),
1026
+ ownResult: isExecutionOutcome(execution.ownResult) ? execution.ownResult : void 0,
1027
+ ownError: typeof execution.ownError === "string" ? execution.ownError : void 0
1028
+ }));
788
1029
  task.workspaceId = normalizeTargetId(row.workspaceId);
789
1030
  task.mode = normalizeTargetId(row.mode);
790
1031
  task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
@@ -801,17 +1042,46 @@ function parseLedger(raw) {
801
1042
  //#endregion
802
1043
  //#region src/core/use-cases/task-archive.ts
803
1044
  /**
804
- * Archive one task: only a `running` task stays on the board (its runner
805
- * still owns its lifecycle until the execution settles); every other status
806
- * can be archived. Archiving disarms a schedule; already-archived tasks are
807
- * a no-op.
1045
+ * Archive/restore task use case: move a task off the main board and back,
1046
+ * whatever its status but running. The task keeps its status, execution
1047
+ * history, and transcript references, while archiving disarms any schedule so
1048
+ * it cannot create more execution records until the user restores and
1049
+ * re-enables it.
1050
+ *
1051
+ * Subtask trees move as a unit: archiving takes the whole subtree (an on-board
1052
+ * subtask under an archived parent would be unreachable from its parent) and
1053
+ * refuses the whole group while any member is running. Restoring brings the
1054
+ * task, its ancestors and its subtree back together.
1055
+ */
1056
+ /** The archive/restore id set: the task, its subtree, and (for restore) its ancestors. */
1057
+ function groupIds(tasks, id, maxSubtaskDepth, withAncestors) {
1058
+ const target = tasks.find((task) => task.id === id);
1059
+ const ids = /* @__PURE__ */ new Set([id]);
1060
+ for (const task of descendantTasks(tasks, id, normalizeSubtaskDepth(maxSubtaskDepth))) ids.add(task.id);
1061
+ if (withAncestors && target !== void 0) for (const ancestor of ancestorChain(tasks, target)) ids.add(ancestor.id);
1062
+ return ids;
1063
+ }
1064
+ /**
1065
+ * Archive one task and its subtasks: only a running member keeps the group on
1066
+ * the board (its runner still owns its lifecycle until the execution settles);
1067
+ * any other status can be archived. Archiving disarms schedules; an
1068
+ * already-archived task is a no-op.
808
1069
  */
809
- function applyArchiveTask(tasks, id, now) {
1070
+ function applyArchiveTask(tasks, id, now, maxSubtaskDepth = 1) {
1071
+ const target = tasks.find((task) => task.id === id);
1072
+ if (target === void 0 || target.archivedAt !== void 0) return {
1073
+ tasks,
1074
+ archived: false
1075
+ };
1076
+ const ids = groupIds(tasks, id, maxSubtaskDepth, false);
1077
+ if (tasks.some((task) => ids.has(task.id) && !ARCHIVABLE_STATUSES.includes(task.status))) return {
1078
+ tasks,
1079
+ archived: false
1080
+ };
810
1081
  let applied = false;
811
1082
  return {
812
1083
  tasks: tasks.map((task) => {
813
- if (task.id !== id || task.archivedAt !== void 0) return task;
814
- if (!ARCHIVABLE_STATUSES.includes(task.status)) return task;
1084
+ if (!ids.has(task.id) || task.archivedAt !== void 0) return task;
815
1085
  applied = true;
816
1086
  const schedule = task.schedule === void 0 ? void 0 : {
817
1087
  ...task.schedule,
@@ -828,12 +1098,21 @@ function applyArchiveTask(tasks, id, now) {
828
1098
  archived: applied
829
1099
  };
830
1100
  }
831
- /** Restore one task back onto the main board (clears the archive marker). */
832
- function applyRestoreTask(tasks, id, now) {
1101
+ /**
1102
+ * Restore one task back onto the main board, together with its ancestors (an
1103
+ * on-board subtask must not point at an archived parent) and its subtasks.
1104
+ */
1105
+ function applyRestoreTask(tasks, id, now, maxSubtaskDepth = 1) {
1106
+ const target = tasks.find((task) => task.id === id);
1107
+ if (target === void 0 || target.archivedAt === void 0) return {
1108
+ tasks,
1109
+ archived: false
1110
+ };
1111
+ const ids = groupIds(tasks, id, maxSubtaskDepth, true);
833
1112
  let applied = false;
834
1113
  return {
835
1114
  tasks: tasks.map((task) => {
836
- if (task.id !== id || task.archivedAt === void 0) return task;
1115
+ if (!ids.has(task.id) || task.archivedAt === void 0) return task;
837
1116
  applied = true;
838
1117
  const { archivedAt: _archived, ...rest } = task;
839
1118
  return {
@@ -848,23 +1127,57 @@ function applyRestoreTask(tasks, id, now) {
848
1127
  //#region src/core/use-cases/task-create.ts
849
1128
  /**
850
1129
  * Create-task use case: mint a new task from user input, rejecting a blank
851
- * title. Pure ledger transition (no persistence or notify — the controller
852
- * orchestrates those), so it is unit-testable without any runtime face.
1130
+ * title and any parent link the lineage gate refuses. Pure ledger transition
1131
+ * (no persistence or notify - the controller orchestrates those), so it is
1132
+ * unit-testable without any runtime face.
853
1133
  */
854
1134
  /**
855
1135
  * Apply a create against the current ledger. Returns the new task and the
856
- * appended ledger, or the unchanged ledger when the title is blank.
1136
+ * appended ledger, or the unchanged ledger when the title is blank or the
1137
+ * requested parent link is refused.
1138
+ *
1139
+ * A subtask inherits the execution targets its parent already pins (workspace,
1140
+ * agent preset, permission, model) unless the form supplied its own; the
1141
+ * parent human permission confirmation travels with an inherited permission,
1142
+ * so a subtask of an already-confirmed elevated card is runnable at once.
857
1143
  * @param tasks - current ledger.
858
- * @param input - raw user input (title/description/prompt + optional schedule).
1144
+ * @param input - raw user input (title/description/prompt + optional schedule/parent).
859
1145
  * @param now - clock instant (ms epoch).
860
1146
  * @param id - minted task id.
1147
+ * @param maxSubtaskDepth - deployment subtask depth limit.
861
1148
  */
862
- function applyCreateTask(tasks, input, now, id) {
1149
+ function applyCreateTask(tasks, input, now, id, maxSubtaskDepth = 1) {
863
1150
  if (input.title.trim() === "") return {
864
1151
  task: void 0,
865
- tasks
1152
+ tasks,
1153
+ error: "title is required"
866
1154
  };
867
- let task = createTask(input, now, id);
1155
+ const limit = normalizeSubtaskDepth(maxSubtaskDepth);
1156
+ let parent;
1157
+ if (input.parentId !== void 0) {
1158
+ parent = tasks.find((task) => task.id === input.parentId);
1159
+ if (parent === void 0) return {
1160
+ task: void 0,
1161
+ tasks,
1162
+ error: "parent task not found"
1163
+ };
1164
+ if (parent.archivedAt !== void 0) return {
1165
+ task: void 0,
1166
+ tasks,
1167
+ error: "parent task is archived"
1168
+ };
1169
+ if (taskDepth(tasks, parent.id) + 1 > limit) return {
1170
+ task: void 0,
1171
+ tasks,
1172
+ error: `subtask depth limit (${limit}) exceeded`
1173
+ };
1174
+ }
1175
+ let task = createTask(parent === void 0 ? input : {
1176
+ ...input,
1177
+ workspaceId: input.workspaceId ?? parent.workspaceId,
1178
+ mode: input.mode ?? parent.mode,
1179
+ model: input.model ?? parent.model
1180
+ }, now, id);
868
1181
  const requested = input.schedule;
869
1182
  if (requested?.enabled === true && requested.cron.trim() !== "" && isValidCron(requested.cron)) {
870
1183
  const cron = requested.cron.trim();
@@ -889,9 +1202,15 @@ function applyCreateTask(tasks, input, now, id) {
889
1202
  * @param id - the task to remove.
890
1203
  */
891
1204
  function applyDeleteTask(tasks, selectedTaskId, id) {
1205
+ if (tasks.some((task) => task.parentId === id)) return {
1206
+ tasks,
1207
+ selectionCleared: false,
1208
+ blocked: true
1209
+ };
892
1210
  return {
893
1211
  tasks: tasks.filter((task) => task.id !== id),
894
- selectionCleared: selectedTaskId === id
1212
+ selectionCleared: selectedTaskId === id,
1213
+ blocked: false
895
1214
  };
896
1215
  }
897
1216
  //#endregion
@@ -955,6 +1274,70 @@ function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
955
1274
  }, now) : task);
956
1275
  }
957
1276
  //#endregion
1277
+ //#region src/core/use-cases/task-parent.ts
1278
+ /**
1279
+ * Parent-link use case: attach an existing on-board task under another as a
1280
+ * subtask, or detach it back to the root. The lineage gate is the same one
1281
+ * creation uses (unknown or archived parent, self, cycle, depth), so the two
1282
+ * ways into the tree cannot disagree. Pure ledger transition.
1283
+ */
1284
+ /** Host error text per refusal reason (the wire carries the same strings). */
1285
+ const REJECTION_TEXT = {
1286
+ "unknown-task": "task not found",
1287
+ "unknown-parent": "parent task not found",
1288
+ "archived-parent": "parent task is archived",
1289
+ "self-parent": "a task cannot be its own parent",
1290
+ "cycle": "a task cannot be attached under its own subtask",
1291
+ "depth-exceeded": "subtask depth limit exceeded"
1292
+ };
1293
+ /**
1294
+ * Attach or detach one task. A null (or blank) parent detaches, which is always
1295
+ * allowed for a known on-board task; attaching validates the whole chain so a
1296
+ * cyclic or too-deep ledger can never be stored.
1297
+ * @param tasks - current ledger.
1298
+ * @param id - the task being moved.
1299
+ * @param parentId - the new parent, or null to detach.
1300
+ * @param now - clock instant (ms epoch).
1301
+ * @param maxSubtaskDepth - deployment subtask depth limit.
1302
+ */
1303
+ function applySetParent(tasks, id, parentId, now, maxSubtaskDepth = 1) {
1304
+ const task = tasks.find((candidate) => candidate.id === id);
1305
+ if (task === void 0) return {
1306
+ tasks,
1307
+ applied: false,
1308
+ error: REJECTION_TEXT["unknown-task"]
1309
+ };
1310
+ if (task.archivedAt !== void 0) return {
1311
+ tasks,
1312
+ applied: false,
1313
+ error: "archived task is read-only"
1314
+ };
1315
+ if (task.status === "running" || task.executions.some((execution) => execution.endedAt === void 0)) return {
1316
+ tasks,
1317
+ applied: false,
1318
+ error: "running task cannot be re-parented"
1319
+ };
1320
+ const check = checkParentLink(tasks, id, parentId, normalizeSubtaskDepth(maxSubtaskDepth));
1321
+ if (!check.ok) return {
1322
+ tasks,
1323
+ applied: false,
1324
+ error: REJECTION_TEXT[check.reason ?? "unknown-task"]
1325
+ };
1326
+ const nextParentId = parentId === null || parentId === "" ? void 0 : parentId;
1327
+ if (nextParentId === task.parentId) return {
1328
+ tasks: [...tasks],
1329
+ applied: true
1330
+ };
1331
+ return {
1332
+ tasks: tasks.map((candidate) => candidate.id === id ? {
1333
+ ...candidate,
1334
+ parentId: nextParentId,
1335
+ updatedAt: now
1336
+ } : candidate),
1337
+ applied: true
1338
+ };
1339
+ }
1340
+ //#endregion
958
1341
  //#region src/core/use-cases/task-update.ts
959
1342
  /**
960
1343
  * Update-task use case: apply an editable-field patch (title/description/
@@ -1027,6 +1410,7 @@ function applyUpdateTask(tasks, id, patch, now) {
1027
1410
  if ("tags" in patch) next.tags = tagsPatch == null ? void 0 : normalizeTags(tagsPatch);
1028
1411
  if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
1029
1412
  if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
1413
+ if ("teamRun" in patch) next.teamRun = patch.teamRun === true ? true : void 0;
1030
1414
  if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
1031
1415
  if (mode !== void 0 || "mode" in patch) next.mode = mode;
1032
1416
  if (permission !== void 0 || "permission" in patch) next.permission = permission;
@@ -1130,6 +1514,7 @@ function importedTask(value) {
1130
1514
  nextRunAt: task.schedule.nextRunAt,
1131
1515
  lastTriggeredAt: task.schedule.lastTriggeredAt
1132
1516
  } },
1517
+ ...task.parentId === void 0 ? {} : { parentId: task.parentId },
1133
1518
  ...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
1134
1519
  ...task.mode === void 0 ? {} : { mode: task.mode },
1135
1520
  ...task.permission === void 0 ? {} : { permission: task.permission },
@@ -1170,6 +1555,7 @@ function createInput(value) {
1170
1555
  "title",
1171
1556
  "description",
1172
1557
  "prompt",
1558
+ "parentId",
1173
1559
  "workspaceId",
1174
1560
  "mode",
1175
1561
  "permission",
@@ -1178,11 +1564,14 @@ function createInput(value) {
1178
1564
  "handover",
1179
1565
  "model",
1180
1566
  "reuseSession",
1567
+ "teamRun",
1181
1568
  "tags"
1182
1569
  ])) return false;
1570
+ if (input.parentId !== void 0 && (typeof input.parentId !== "string" || input.parentId.trim() === "")) return false;
1183
1571
  if (typeof input.title !== "string" || typeof input.description !== "string" || typeof input.prompt !== "string") return false;
1184
1572
  if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false;
1185
1573
  if (input.reuseSession !== void 0 && typeof input.reuseSession !== "boolean") return false;
1574
+ if (input.teamRun !== void 0 && typeof input.teamRun !== "boolean") return false;
1186
1575
  if (input.permission !== void 0 && !isTaskPermission(input.permission)) return false;
1187
1576
  if (input.tags !== void 0 && !isTaskTagList(input.tags)) return false;
1188
1577
  if (input.freeze !== void 0 && freezePayload(input.freeze) === void 0) return false;
@@ -1207,9 +1596,11 @@ function updatePatch(value) {
1207
1596
  "handover",
1208
1597
  "model",
1209
1598
  "reuseSession",
1599
+ "teamRun",
1210
1600
  "tags"
1211
1601
  ])) return false;
1212
1602
  if (patch.reuseSession !== void 0 && patch.reuseSession !== null && typeof patch.reuseSession !== "boolean") return false;
1603
+ if (patch.teamRun !== void 0 && patch.teamRun !== null && typeof patch.teamRun !== "boolean") return false;
1213
1604
  for (const key of [
1214
1605
  "title",
1215
1606
  "description",
@@ -1319,6 +1710,24 @@ function parseEnvelopeAction(value) {
1319
1710
  }
1320
1711
  };
1321
1712
  }
1713
+ case "set-parent": {
1714
+ if (!exactKeys(action, [
1715
+ "kind",
1716
+ "taskId",
1717
+ "parentId"
1718
+ ])) return void 0;
1719
+ if (taskId === void 0) return void 0;
1720
+ const parentId = action.parentId;
1721
+ if (parentId !== null && (typeof parentId !== "string" || parentId.trim() === "")) return void 0;
1722
+ return {
1723
+ requestId: envelope.requestId,
1724
+ action: {
1725
+ kind: "set-parent",
1726
+ taskId,
1727
+ parentId
1728
+ }
1729
+ };
1730
+ }
1322
1731
  case "set-schedule":
1323
1732
  if (!exactKeys(action, [
1324
1733
  "kind",
@@ -1355,6 +1764,13 @@ function parseEnvelopeAction(value) {
1355
1764
  }
1356
1765
  //#endregion
1357
1766
  //#region src/host-ledger.ts
1767
+ /** Human-facing text for a refused binding, per surface. */
1768
+ function bindingRefusalMessage(refusal, sessionDefault, surface) {
1769
+ if (refusal.kind === "subtask-pin") return `team run cannot honor the permission binding of subtask "${refusal.title}": a teammate runs inside the Lead session; clear that card's permission or run the tree without Agent Team`;
1770
+ if (surface === "schedule") return `task "${refusal.title}" has an unconfirmed above-default permission`;
1771
+ if (refusal.kind === "root") return `confirmation-required: the effective permission is above the session default (${sessionDefault}); confirm the card's permission binding first`;
1772
+ return `confirmation-required: subtask "${refusal.title}" inherits an above-default permission; confirm that card's permission binding first`;
1773
+ }
1358
1774
  const MAX_REQUEST_CACHE = 256;
1359
1775
  function timeZone() {
1360
1776
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "local";
@@ -1580,6 +1996,37 @@ function mergeTask(a, b) {
1580
1996
  executions: retainRecentExecutions(executions)
1581
1997
  };
1582
1998
  }
1999
+ /**
2000
+ * Repair parent links after a load or an import: a link whose parent row is
2001
+ * missing, cyclic, or names an archived parent while the child is on board is
2002
+ * dropped (the task becomes a root) instead of dropping the task. A chain
2003
+ * deeper than the current `maxSubtaskDepth` is deliberately KEPT: the limit
2004
+ * gates new links, it never deletes a stored one, and every cascade walk is
2005
+ * bounded by the limit anyway. The Host is the only writer of these links, so
2006
+ * this is a last-resort guard against a hand-edited or imported document.
2007
+ */
2008
+ function repairParentLinks(tasks) {
2009
+ const byId = new Map(tasks.map((task) => [task.id, task]));
2010
+ return tasks.map((task) => {
2011
+ if (task.parentId === void 0) return task;
2012
+ const seen = /* @__PURE__ */ new Set([task.id]);
2013
+ let current = byId.get(task.parentId);
2014
+ let depth = 0;
2015
+ while (current !== void 0) {
2016
+ if (seen.has(current.id) || depth === 0 && current.archivedAt !== void 0 && task.archivedAt === void 0) return {
2017
+ ...task,
2018
+ parentId: void 0
2019
+ };
2020
+ seen.add(current.id);
2021
+ depth += 1;
2022
+ current = current.parentId === void 0 ? void 0 : byId.get(current.parentId);
2023
+ }
2024
+ return depth === 0 ? {
2025
+ ...task,
2026
+ parentId: void 0
2027
+ } : task;
2028
+ });
2029
+ }
1583
2030
  function parseHostTasks(values) {
1584
2031
  const rawById = /* @__PURE__ */ new Map();
1585
2032
  for (const value of values) {
@@ -1587,7 +2034,7 @@ function parseHostTasks(values) {
1587
2034
  const raw = value;
1588
2035
  if (typeof raw.id === "string") rawById.set(raw.id, raw);
1589
2036
  }
1590
- return parseLedger(JSON.stringify(values)).map((task) => {
2037
+ return repairParentLinks(parseLedger(JSON.stringify(values))).map((task) => {
1591
2038
  const rawSchedule = rawById.get(task.id)?.schedule;
1592
2039
  if (typeof rawSchedule !== "object" || rawSchedule === null) return task;
1593
2040
  const schedule = rawSchedule;
@@ -1616,9 +2063,16 @@ var HostTaskLedger = class {
1616
2063
  schedulerFile;
1617
2064
  /** Session-default permission the confirmation gate compares against. */
1618
2065
  sessionDefaultPermission;
2066
+ /**
2067
+ * Deployment subtask depth limit (1..3): the lineage gate every write obeys.
2068
+ * The settings card edits it live, so {@link setMaxSubtaskDepth} mutates it
2069
+ * instead of remounting the row.
2070
+ */
2071
+ depthLimit;
1619
2072
  constructor(dir = join(dshHome(), "task-board"), now = Date.now, options = {}) {
1620
2073
  this.now = now;
1621
2074
  this.sessionDefaultPermission = options.sessionDefaultPermission ?? "read-only";
2075
+ this.depthLimit = normalizeSubtaskDepth(options.maxSubtaskDepth ?? 1);
1622
2076
  mkdirSync(dir, { recursive: true });
1623
2077
  this.file = join(dir, "ledger-v2.json");
1624
2078
  this.lockFile = join(dir, "ledger-v2.lock");
@@ -1636,6 +2090,21 @@ var HostTaskLedger = class {
1636
2090
  throw error;
1637
2091
  }
1638
2092
  }
2093
+ /** Current subtask depth limit (1..3). */
2094
+ get maxSubtaskDepth() {
2095
+ return this.depthLimit;
2096
+ }
2097
+ /**
2098
+ * Apply a live settings edit of the subtask depth limit. A no-op when the
2099
+ * normalized limit is unchanged, so a coarse volatile invalidation that
2100
+ * changed nothing emits nothing.
2101
+ */
2102
+ setMaxSubtaskDepth(depth) {
2103
+ const next = normalizeSubtaskDepth(depth);
2104
+ if (next === this.depthLimit) return;
2105
+ this.depthLimit = next;
2106
+ this.notify();
2107
+ }
1639
2108
  /** Remove leftover *.tmp-* files from previous crashes or interrupted writes. */
1640
2109
  cleanStaleTemporaryFiles(dir) {
1641
2110
  try {
@@ -1673,6 +2142,7 @@ var HostTaskLedger = class {
1673
2142
  if (task.archivedAt === void 0 && task.schedule?.enabled === true) armedSchedules += 1;
1674
2143
  for (const execution of task.executions) {
1675
2144
  if (execution.endedAt !== void 0) continue;
2145
+ if (execution.ownResult !== void 0) continue;
1676
2146
  openExecutions.push({
1677
2147
  taskId: task.id,
1678
2148
  executionId: execution.id,
@@ -1692,6 +2162,25 @@ var HostTaskLedger = class {
1692
2162
  for (const task of this.document.tasks) if (task.archivedAt === void 0 && task.schedule?.enabled === true) count += 1;
1693
2163
  return count;
1694
2164
  }
2165
+ /**
2166
+ * The earliest armed `nextRunAt` strictly after the supplied Host time, or
2167
+ * undefined when no schedule is armed in the future. This is the instant the
2168
+ * Host arms its native timer at, so the board wakes exactly when work is due
2169
+ * instead of polling a fixed heartbeat.
2170
+ * @param now - current Host time in ms epoch.
2171
+ * @returns the nearest future trigger, or undefined when none is armed.
2172
+ */
2173
+ nextArmedRunAt(now) {
2174
+ let nearest;
2175
+ for (const task of this.document.tasks) {
2176
+ if (task.archivedAt !== void 0) continue;
2177
+ const schedule = task.schedule;
2178
+ if (schedule === void 0 || !schedule.enabled || schedule.nextRunAt === void 0) continue;
2179
+ if (schedule.nextRunAt <= now) continue;
2180
+ if (nearest === void 0 || schedule.nextRunAt < nearest) nearest = schedule.nextRunAt;
2181
+ }
2182
+ return nearest;
2183
+ }
1695
2184
  /** Return value-only references for schedules due at the supplied Host time. */
1696
2185
  dueSchedules(now) {
1697
2186
  const due = [];
@@ -1740,24 +2229,29 @@ var HostTaskLedger = class {
1740
2229
  throw error;
1741
2230
  }
1742
2231
  }
2232
+ /**
2233
+ * Open the cascade one due schedule triggers: an empty array means nothing
2234
+ * ran (already running, or a participant whose elevated permission is still
2235
+ * unconfirmed), and the rule rolls to its next occurrence either way.
2236
+ */
1743
2237
  openScheduled(taskId, nextRunAt, triggeredAt) {
1744
2238
  const task = this.document.tasks.find((item) => item.id === taskId);
1745
- if (task === void 0 || task.archivedAt !== void 0) return void 0;
1746
- if (requiresPermissionConfirmation(task, this.sessionDefaultPermission)) {
2239
+ if (task === void 0 || task.archivedAt !== void 0) return [];
2240
+ const rollForward = () => {
1747
2241
  this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, task.schedule?.lastTriggeredAt, triggeredAt)];
1748
2242
  this.commit();
1749
- return;
2243
+ };
2244
+ const refusal = this.bindingRefusal(task);
2245
+ if (refusal !== void 0) {
2246
+ this.document.scheduler.error = `scheduled run refused for task ${taskId}: ${bindingRefusalMessage(refusal, this.sessionDefaultPermission, "schedule")}`;
2247
+ rollForward();
2248
+ return [];
1750
2249
  }
1751
2250
  if (task.status === "running" || hasOpenExecution(task)) {
1752
- this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, task.schedule?.lastTriggeredAt, triggeredAt)];
1753
- this.commit();
1754
- return;
2251
+ rollForward();
2252
+ return [];
1755
2253
  }
1756
- const opened = startExecution(task, triggeredAt, crypto.randomUUID());
1757
- this.document.tasks = this.document.tasks.map((item) => item.id === taskId ? opened.task : item);
1758
- this.document.tasks = [...applyScheduleNextRun(this.document.tasks, taskId, nextRunAt, triggeredAt, triggeredAt)];
1759
- this.commit();
1760
- return opened;
2254
+ return this.startCascade(task, triggeredAt, void 0, false, nextRunAt);
1761
2255
  }
1762
2256
  skipMissed(now) {
1763
2257
  let changed = false;
@@ -1792,6 +2286,39 @@ var HostTaskLedger = class {
1792
2286
  }
1793
2287
  this.commit(false);
1794
2288
  }
2289
+ /**
2290
+ * Settle one execution. A cascade participant first records its OWN outcome
2291
+ * and only finalizes once every child execution in its run group has
2292
+ * settled, so a parent card leaves 'running' with the whole tree's verdict
2293
+ * rather than its own turn alone.
2294
+ */
2295
+ settle(taskId, executionId, outcome, error) {
2296
+ const now = this.now();
2297
+ const task = this.document.tasks.find((item) => item.id === taskId);
2298
+ const execution = task?.executions.find((entry) => entry.id === executionId);
2299
+ if (task === void 0 || execution === void 0 || execution.endedAt !== void 0) return;
2300
+ const groupId = execution.runGroupId;
2301
+ if (groupId === void 0) {
2302
+ this.document.tasks = this.document.tasks.map((item) => item.id === taskId ? settleExecution(item, executionId, outcome, now, error) : item);
2303
+ this.commit();
2304
+ return;
2305
+ }
2306
+ let changed = false;
2307
+ if (execution.ownResult === void 0) {
2308
+ this.document.tasks = this.document.tasks.map((item) => item.id !== taskId ? item : {
2309
+ ...item,
2310
+ updatedAt: now,
2311
+ executions: item.executions.map((entry) => entry.id === executionId ? {
2312
+ ...entry,
2313
+ ownResult: outcome,
2314
+ ownError: error
2315
+ } : entry)
2316
+ });
2317
+ changed = true;
2318
+ }
2319
+ if (this.settleCascade(taskId, groupId, now)) changed = true;
2320
+ if (changed) this.commit();
2321
+ }
1795
2322
  attachSession(taskId, executionId, sessionId) {
1796
2323
  const now = this.now();
1797
2324
  this.document.tasks = this.document.tasks.map((task) => task.id !== taskId ? task : {
@@ -1804,13 +2331,9 @@ var HostTaskLedger = class {
1804
2331
  });
1805
2332
  this.commit();
1806
2333
  }
1807
- settle(taskId, executionId, outcome, error) {
1808
- this.document.tasks = this.document.tasks.map((task) => task.id === taskId ? settleExecution(task, executionId, outcome, this.now(), error) : task);
1809
- this.commit();
1810
- }
1811
2334
  apply(action, initiator) {
1812
2335
  const now = this.now();
1813
- let run;
2336
+ let runs;
1814
2337
  switch (action.kind) {
1815
2338
  case "import": {
1816
2339
  const sources = new Set(this.document.scheduler.importedSources ?? []);
@@ -1819,7 +2342,7 @@ var HostTaskLedger = class {
1819
2342
  const incoming = parseHostTasks(action.tasks);
1820
2343
  const merged = new Map(this.document.tasks.map((task) => [task.id, task]));
1821
2344
  for (const task of incoming) merged.set(task.id, merged.has(task.id) ? mergeTask(merged.get(task.id), task) : task);
1822
- this.document.tasks = [...merged.values()];
2345
+ this.document.tasks = repairParentLinks([...merged.values()]);
1823
2346
  this.document.scheduler.importedSources = [...sources, action.sourceId];
1824
2347
  this.document.scheduler.error = invalidScheduleIds.length === 0 ? void 0 : `invalid cron disabled for task(s): ${invalidScheduleIds.join(", ")}`;
1825
2348
  this.repairSchedules(true, false);
@@ -1836,8 +2359,8 @@ var HostTaskLedger = class {
1836
2359
  frozenBy: initiator
1837
2360
  }
1838
2361
  };
1839
- const result = applyCreateTask(this.document.tasks, input, now, action.id);
1840
- if (result.task === void 0) throw new Error("invalid task");
2362
+ const result = applyCreateTask(this.document.tasks, input, now, action.id, this.maxSubtaskDepth);
2363
+ if (result.task === void 0) throw new Error(result.error ?? "invalid task");
1841
2364
  this.document.tasks = [...result.tasks];
1842
2365
  break;
1843
2366
  }
@@ -1857,14 +2380,20 @@ var HostTaskLedger = class {
1857
2380
  this.document.tasks = [...applyUpdateTask(this.document.tasks, action.taskId, patch, now)];
1858
2381
  break;
1859
2382
  }
1860
- case "delete":
1861
- {
1862
- const task = this.document.tasks.find((task) => task.id === action.taskId);
1863
- if (task === void 0) throw new Error("task not found");
1864
- if (task.status === "running" || hasOpenExecution(task)) throw new Error("running task cannot be deleted");
1865
- }
2383
+ case "delete": {
2384
+ const task = this.document.tasks.find((task) => task.id === action.taskId);
2385
+ if (task === void 0) throw new Error("task not found");
2386
+ if (task.status === "running" || hasOpenExecution(task)) throw new Error("running task cannot be deleted");
2387
+ if (this.document.tasks.some((item) => item.parentId === action.taskId)) throw new Error("task has subtasks; detach or delete them first");
1866
2388
  this.document.tasks = [...applyDeleteTask(this.document.tasks, void 0, action.taskId).tasks];
1867
2389
  break;
2390
+ }
2391
+ case "set-parent": {
2392
+ const result = applySetParent(this.document.tasks, action.taskId, action.parentId, now, this.maxSubtaskDepth);
2393
+ if (!result.applied) throw new Error(result.error ?? "parent link refused");
2394
+ this.document.tasks = [...result.tasks];
2395
+ break;
2396
+ }
1868
2397
  case "move": {
1869
2398
  const task = this.document.tasks.find((item) => item.id === action.taskId);
1870
2399
  if (task === void 0) throw new Error("task not found");
@@ -1875,13 +2404,15 @@ var HostTaskLedger = class {
1875
2404
  break;
1876
2405
  }
1877
2406
  case "archive": {
1878
- const result = applyArchiveTask(this.document.tasks, action.taskId, now);
2407
+ if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be archived");
2408
+ const result = applyArchiveTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
1879
2409
  if (!result.archived) throw new Error("task cannot be archived");
1880
2410
  this.document.tasks = [...result.tasks];
1881
2411
  break;
1882
2412
  }
1883
2413
  case "restore": {
1884
- const result = applyRestoreTask(this.document.tasks, action.taskId, now);
2414
+ if (this.subtreeHasOpenExecution(action.taskId)) throw new Error("running task cannot be restored");
2415
+ const result = applyRestoreTask(this.document.tasks, action.taskId, now, this.maxSubtaskDepth);
1885
2416
  if (!result.archived) throw new Error("task is not archived");
1886
2417
  this.document.tasks = [...result.tasks];
1887
2418
  break;
@@ -1909,18 +2440,140 @@ var HostTaskLedger = class {
1909
2440
  const task = this.document.tasks.find((item) => item.id === action.taskId);
1910
2441
  if (task?.archivedAt !== void 0) throw new Error("archived task is read-only");
1911
2442
  if (task === void 0 || task.status === "running" || hasOpenExecution(task)) throw new Error("task is already running or missing");
1912
- if (requiresPermissionConfirmation(task, this.sessionDefaultPermission)) throw new Error(`confirmation-required: the effective permission is above the session default (${this.sessionDefaultPermission}); confirm the card's permission binding first`);
1913
- run = startExecution(action.kind === "rerun" ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator);
1914
- this.document.tasks = this.document.tasks.map((item) => item.id === task.id ? run.task : item);
2443
+ const refusal = this.bindingRefusal(task);
2444
+ if (refusal !== void 0) throw new Error(bindingRefusalMessage(refusal, this.sessionDefaultPermission, "run"));
2445
+ runs = this.startCascade(task, now, initiator, action.kind === "rerun");
1915
2446
  break;
1916
2447
  }
1917
2448
  }
1918
- this.commit();
2449
+ if (runs === void 0 || runs.length === 0) this.commit();
1919
2450
  return {
1920
2451
  state: this.state(),
1921
- ...run === void 0 ? {} : { run }
2452
+ ...runs === void 0 ? {} : { runs }
2453
+ };
2454
+ }
2455
+ /**
2456
+ * The binding that makes a run illegal, if any.
2457
+ *
2458
+ * A plain cascade launches one session per participant, so every participant
2459
+ * carries its own resolved binding and each one gates the run. A team run
2460
+ * launches only the Lead session: the Lead's binding gates it, while a
2461
+ * subtask's OWN above-default pin cannot be applied to a teammate and is
2462
+ * refused instead of being silently dropped (an inherited binding is the
2463
+ * Lead's own and stays allowed once the Lead is confirmed).
2464
+ * @param root - the task being run.
2465
+ * @returns the first refusal, or undefined when the run may start.
2466
+ */
2467
+ bindingRefusal(root) {
2468
+ const participants = this.cascadeParticipants(root.id);
2469
+ if (root.teamRun === true) {
2470
+ const lead = participants.find((participant) => participant.id === root.id);
2471
+ if (lead !== void 0 && requiresPermissionConfirmation(lead, this.sessionDefaultPermission)) return {
2472
+ kind: "root",
2473
+ title: lead.title
2474
+ };
2475
+ const pinned = participants.find((participant) => {
2476
+ if (participant.id === root.id) return false;
2477
+ const raw = this.document.tasks.find((task) => task.id === participant.id);
2478
+ return raw !== void 0 && requiresPermissionConfirmation(raw, this.sessionDefaultPermission);
2479
+ });
2480
+ return pinned === void 0 ? void 0 : {
2481
+ kind: "subtask-pin",
2482
+ title: pinned.title
2483
+ };
2484
+ }
2485
+ const unconfirmed = participants.find((participant) => requiresPermissionConfirmation(participant, this.sessionDefaultPermission));
2486
+ if (unconfirmed === void 0) return void 0;
2487
+ return {
2488
+ kind: unconfirmed.id === root.id ? "root" : "inherited",
2489
+ title: unconfirmed.title
1922
2490
  };
1923
2491
  }
2492
+ /**
2493
+ * The tasks a cascade from `rootId` would actually open executions for, with
2494
+ * their effective execution targets resolved. Archived members never run, and
2495
+ * a member that already has an open execution is skipped (a task cannot run
2496
+ * twice), so the result is the participant set the launch will use.
2497
+ */
2498
+ cascadeParticipants(rootId) {
2499
+ return cascadeTargets(this.document.tasks, rootId, this.maxSubtaskDepth).map((participant) => resolveExecutionTargets(participant, this.document.tasks, this.maxSubtaskDepth)).filter((participant) => participant.archivedAt === void 0 && participant.status !== "running" && !hasOpenExecution(participant));
2500
+ }
2501
+ /** Whether a task or any of its subtasks still has an open execution. */
2502
+ subtreeHasOpenExecution(id) {
2503
+ return cascadeTargets(this.document.tasks, id, this.maxSubtaskDepth).some((task) => task.status === "running" || hasOpenExecution(task));
2504
+ }
2505
+ /**
2506
+ * Open one execution per cascade participant (the requested task and its
2507
+ * on-board descendants, within the depth limit) under a single run group, and
2508
+ * commit the ledger once. The returned runs are ordered root-first, so the
2509
+ * Host launches the parent before its subtasks.
2510
+ * @param root - the task the user ran.
2511
+ * @param now - clock instant (ms epoch).
2512
+ * @param initiator - the DSH session that asked for the run (audit only).
2513
+ * @param rerun - true to reset the root to 'todo' before starting it.
2514
+ * @param nextRunAt - when set, the root's schedule also rolls forward.
2515
+ */
2516
+ startCascade(root, now, initiator, rerun = false, nextRunAt) {
2517
+ const before = this.document.tasks;
2518
+ const groupId = crypto.randomUUID();
2519
+ const team = root.teamRun === true;
2520
+ const started = /* @__PURE__ */ new Map();
2521
+ const runs = [];
2522
+ for (const task of cascadeTargets(before, root.id, this.maxSubtaskDepth)) {
2523
+ if (task.archivedAt !== void 0) continue;
2524
+ if (task.status === "running" || hasOpenExecution(task)) continue;
2525
+ const opened = startExecution(rerun && task.id === root.id ? withStatus(task, "todo", now) : task, now, crypto.randomUUID(), initiator, groupId);
2526
+ started.set(task.id, opened.task);
2527
+ runs.push({
2528
+ task: resolveExecutionTargets(opened.task, before, this.maxSubtaskDepth),
2529
+ execution: opened.execution,
2530
+ ...team && task.id !== root.id ? { dispatch: "teammate" } : {}
2531
+ });
2532
+ }
2533
+ if (runs.length === 0) return [];
2534
+ let tasks = before.map((task) => started.get(task.id) ?? task);
2535
+ if (nextRunAt !== void 0) tasks = applyScheduleNextRun(tasks, root.id, nextRunAt, now, now);
2536
+ this.document.tasks = [...tasks];
2537
+ this.commit();
2538
+ return runs;
2539
+ }
2540
+ /**
2541
+ * Finalize every cascade parent that is ready, walking from `taskId` up the
2542
+ * lineage: a member settles once its own outcome is recorded and none of its
2543
+ * group children is still open, and its verdict folds those children in
2544
+ * (failure dominates, then cancellation).
2545
+ */
2546
+ settleCascade(taskId, groupId, now) {
2547
+ let changed = false;
2548
+ const visited = /* @__PURE__ */ new Set();
2549
+ let current = taskId;
2550
+ while (current !== void 0 && !visited.has(current)) {
2551
+ visited.add(current);
2552
+ const task = this.document.tasks.find((item) => item.id === current);
2553
+ if (task === void 0) break;
2554
+ const execution = openGroupExecution(task, groupId);
2555
+ if (execution === void 0 || execution.ownResult === void 0) break;
2556
+ if (pendingCascadeChildren(this.document.tasks, current, groupId).length > 0) break;
2557
+ const entries = [{
2558
+ result: execution.ownResult,
2559
+ ...execution.ownError === void 0 ? {} : { error: execution.ownError }
2560
+ }];
2561
+ for (const child of this.document.tasks) {
2562
+ if (child.parentId !== current) continue;
2563
+ const childExecution = child.executions.find((entry) => entry.runGroupId === groupId && entry.result !== void 0);
2564
+ if (childExecution?.result === void 0) continue;
2565
+ entries.push({
2566
+ result: childExecution.result,
2567
+ ...childExecution.error === void 0 ? {} : { error: childExecution.error }
2568
+ });
2569
+ }
2570
+ const combined = combineCascadeOutcome(entries);
2571
+ this.document.tasks = this.document.tasks.map((item) => item.id === current ? settleExecution(item, execution.id, combined.result, now, combined.error) : item);
2572
+ changed = true;
2573
+ current = task.parentId;
2574
+ }
2575
+ return changed;
2576
+ }
1924
2577
  repairSchedules(skipPast, persist = true) {
1925
2578
  const now = this.now();
1926
2579
  let changed = false;
@@ -1958,13 +2611,23 @@ var HostTaskLedger = class {
1958
2611
  reconcileInterruptedStarts(persist = true) {
1959
2612
  const now = this.now();
1960
2613
  let changed = false;
2614
+ const interrupted = [];
1961
2615
  this.document.tasks = this.document.tasks.map((task) => {
1962
2616
  if (task.status !== "running") return task;
1963
2617
  const execution = task.executions.at(-1);
1964
2618
  if (execution === void 0 || execution.endedAt !== void 0 || execution.sessionId !== void 0) return task;
1965
2619
  changed = true;
2620
+ interrupted.push({
2621
+ taskId: task.id,
2622
+ runGroupId: execution.runGroupId
2623
+ });
1966
2624
  return settleExecution(task, execution.id, "cancelled", now, "host restarted before the execution session was recorded");
1967
2625
  });
2626
+ for (const entry of interrupted) {
2627
+ if (entry.runGroupId === void 0) continue;
2628
+ const parentId = this.document.tasks.find((task) => task.id === entry.taskId)?.parentId;
2629
+ if (parentId !== void 0 && this.settleCascade(parentId, entry.runGroupId, now)) changed = true;
2630
+ }
1968
2631
  if (changed && persist) this.commit();
1969
2632
  }
1970
2633
  /**
@@ -2218,6 +2881,17 @@ function isInvocationUnavailable(error) {
2218
2881
  const code = error.code;
2219
2882
  return code === "invocation-unavailable" || code === "gateway/invocation-unavailable";
2220
2883
  }
2884
+ /**
2885
+ * Whether the endpoint exists but its strict definition was withdrawn by the
2886
+ * running gateway (the cohort changed under this plugin). The plugin can send
2887
+ * the call correctly and still be refused, and nothing in this repository can
2888
+ * restore the definition, so the roster is treated as unknown quietly instead
2889
+ * of printing an error the user has no action for (#1672).
2890
+ */
2891
+ function isDefinitionWithdrawn(error) {
2892
+ const code = error.code;
2893
+ return code === "definition-unavailable" || code === "gateway/definition-unavailable";
2894
+ }
2221
2895
  const SERVICE_UNAVAILABLE_ATTEMPTS = 5;
2222
2896
  const SERVICE_UNAVAILABLE_BACKOFF_MS = 2e3;
2223
2897
  function delay(ms) {
@@ -2250,20 +2924,35 @@ function escapeProvenanceDelimiter(value) {
2250
2924
  return value.replaceAll("来源声明 开始", "来源声明·开始").replaceAll("来源声明 结束", "来源声明·结束");
2251
2925
  }
2252
2926
  /**
2253
- * Compose the execution prompt (issue #6): a continuation card (one carrying
2254
- * a frozen snapshot) has its instruction mandatorily wrapped in a source
2255
- * declaration (freeze instant, source session, unreviewed-content warning)
2256
- * templated by the board, so the picking-up agent stays wary of stored
2257
- * prompt-instruction injection in card text (adversarial scenario c). The
2258
- * wrap composes with the T4 handover preamble: the reference preamble comes
2259
- * first, the provenance wrap then encloses the instruction. Plain tasks (no
2260
- * freeze) keep the bare handover preamble + prompt.
2927
+ * The execution-shape section: what else this run opens. A plain cascade opens
2928
+ * one independent session per member; a team run starts every other member as a
2929
+ * teammate inside THIS session, so the prompt names the Team tools instead.
2930
+ */
2931
+ function peerPromptPreamble(context) {
2932
+ const peers = context.peers ?? [];
2933
+ if (peers.length === 0) return void 0;
2934
+ if (context.team === true) {
2935
+ const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(teammate: ${peer.name ?? teammateName(peer.title, peer.id)})`);
2936
+ return `本任务是 Agent Team 的 Lead:本次运行不额外开启独立会话,以下 ${peers.length} 个子任务成员已在本会话中作为 teammate 启动。用 list_agents / send_message / wait_agent 协调它们,用任务看板工具读写它们在看板上的卡片:\n${lines.join("\n")}`;
2937
+ }
2938
+ const lines = peers.map((peer) => `- ${escapeProvenanceDelimiter(peer.title)}(任务 ${peer.id})`);
2939
+ return `本次运行同时并发开启 ${peers.length} 个独立 DSH 会话执行下列子任务成员(可用 task_board_* 工具查看它们的进度):\n${lines.join("\n")}`;
2940
+ }
2941
+ /**
2942
+ * Build the execution prompt for one task.
2943
+ * @param task - the task being launched.
2944
+ * @param context - the run's other members, for the execution-shape section.
2945
+ * @returns the prompt text.
2261
2946
  */
2262
- function promptText(task) {
2947
+ function promptText(task, context = {}) {
2263
2948
  const body = task.prompt !== "" ? task.prompt : task.title;
2264
2949
  const handover = task.handover;
2265
2950
  const handoverPreamble = handover === void 0 || handover.references.length === 0 ? void 0 : `交接包引用(来自任务看板续接卡片,冻结于 ${new Date(handover.bundledAt).toISOString()}):\n${handover.references.map((reference) => `- ${reference}`).join("\n")}`;
2266
- const preambles = [tagPromptPreamble(task), handoverPreamble].filter((part) => part !== void 0);
2951
+ const preambles = [
2952
+ tagPromptPreamble(task),
2953
+ handoverPreamble,
2954
+ peerPromptPreamble(context)
2955
+ ].filter((part) => part !== void 0);
2267
2956
  const preamble = preambles.length === 0 ? void 0 : preambles.join("\n\n");
2268
2957
  const freeze = task.freeze;
2269
2958
  if (freeze === void 0) return preamble === void 0 ? body : `${preamble}\n\n${body}`;
@@ -2287,10 +2976,27 @@ function tagPromptPreamble(task) {
2287
2976
  if (lines.length === 0) return void 0;
2288
2977
  return `标签提示(任务看板标签,每次执行前注入):\n${lines.join("\n")}`;
2289
2978
  }
2290
- function isErrorTurnEnd(data) {
2291
- if (typeof data !== "object" || data === null) return false;
2979
+ /**
2980
+ * Why a `turn/end` did not complete successfully, or undefined when it did.
2981
+ *
2982
+ * The harness treats exactly `completed` as success (`headless` maps the turn
2983
+ * reason to its exit code, `subagent/projection` records `lastTurnCompleted`,
2984
+ * and `max-tokens` is a ceiling, not a result). A task-board execution is a
2985
+ * SUCCESS only on positive evidence of a completed turn; every other reason —
2986
+ * and an unreadable one — must be reported, or the board stamps a failed run
2987
+ * as `succeeded` and a schedule dies silently (issue #1708). `interrupted` is
2988
+ * the reason a restart produces: `interruptedTurnClosers` synthesizes it for a
2989
+ * log whose tail turn never ended, which is exactly an aborted resume.
2990
+ * @param data - the `turn/end` event payload.
2991
+ * @returns the reason kind when it is not `completed`, else undefined.
2992
+ */
2993
+ function nonCompletedTurnEnd(data) {
2994
+ if (typeof data !== "object" || data === null) return "unknown";
2292
2995
  const reason = data.reason;
2293
- return typeof reason === "object" && reason !== null && reason.kind === "error";
2996
+ if (typeof reason !== "object" || reason === null) return "unknown";
2997
+ const kind = reason.kind;
2998
+ if (typeof kind !== "string") return "unknown";
2999
+ return kind === "completed" ? void 0 : kind;
2294
3000
  }
2295
3001
  /**
2296
3002
  * Wire-argument layout of the 0.1.2-alpha.2 descriptor tables; the gateway's
@@ -2366,7 +3072,8 @@ var HostExecutionRunner = class {
2366
3072
  const reused = options.reuseSessionId;
2367
3073
  if (reused !== void 0) {
2368
3074
  try {
2369
- await this.pinAndPrompt(reused, task, permission);
3075
+ await this.assertReusedPreset(reused, mode);
3076
+ await this.pinAndPrompt(reused, task, permission, options.promptContext);
2370
3077
  } catch (error) {
2371
3078
  throw new SessionLaunchError(reused, error);
2372
3079
  }
@@ -2381,18 +3088,38 @@ var HostExecutionRunner = class {
2381
3088
  sessionId,
2382
3089
  title: task.title
2383
3090
  });
2384
- await this.pinAndPrompt(sessionId, task, permission);
3091
+ await this.pinAndPrompt(sessionId, task, permission, options.promptContext);
2385
3092
  } catch (error) {
2386
3093
  throw new SessionLaunchError(sessionId, error);
2387
3094
  }
2388
3095
  return sessionId;
2389
3096
  }
2390
3097
  /**
3098
+ * Fail closed when a reused session records a different Agent preset than
3099
+ * the one this task pins.
3100
+ *
3101
+ * The fresh branch asserts the pin by passing `agentPreset` to
3102
+ * `session/create`; the reuse branch must not call that method (it would
3103
+ * create or re-adopt the very session the board only meant to continue), so
3104
+ * the recorded value is read through `session/projections` — the documented
3105
+ * read that resolves no Agent — and compared here (issue #1708). A session
3106
+ * whose preset cannot be read is not treated as a match: silently continuing
3107
+ * under an unknown composition is the failure this guards.
3108
+ * @param sessionId - the session this execution continues in.
3109
+ * @param pinned - the preset id the task pins, when it pins one.
3110
+ */
3111
+ async assertReusedPreset(sessionId, pinned) {
3112
+ if (pinned === void 0) return;
3113
+ const recorded = (await this.invoke("session", "projections", { sessionId }))?.values?.agentPreset;
3114
+ if (recorded === pinned) return;
3115
+ throw new Error("reused session " + sessionId + " was composed from agent preset " + (typeof recorded === "string" ? "\"" + recorded + "\"" : "an unreadable value") + ", but the task pins \"" + pinned + "\"");
3116
+ }
3117
+ /**
2391
3118
  * Re-assert the pinned execution contract on a session and queue the task
2392
3119
  * prompt. Shared by the fresh-session and reuse paths so both apply exactly
2393
3120
  * the same permission/model pins before the prompt.
2394
3121
  */
2395
- async pinAndPrompt(sessionId, task, permission) {
3122
+ async pinAndPrompt(sessionId, task, permission, context = {}) {
2396
3123
  if (permission !== void 0) {
2397
3124
  if (this.commands === void 0) throw new Error("permission command dispatcher is unavailable");
2398
3125
  const command = await this.commands.execute(sessionId, "/permission " + permission, AbortSignal.timeout(3e4));
@@ -2420,7 +3147,7 @@ var HostExecutionRunner = class {
2420
3147
  mode: "queue",
2421
3148
  content: [{
2422
3149
  type: "text",
2423
- text: promptText(task)
3150
+ text: promptText(task, context)
2424
3151
  }]
2425
3152
  });
2426
3153
  }
@@ -2440,6 +3167,13 @@ var HostExecutionRunner = class {
2440
3167
  }
2441
3168
  return { known: false };
2442
3169
  }
3170
+ if (isDefinitionWithdrawn(error)) {
3171
+ if (!this.unsupportedSessionListWarned) {
3172
+ this.unsupportedSessionListWarned = true;
3173
+ console.warn("[dsh-task-board] the host gateway withdrew the session/list definition for this DSH cohort; task board roster auto-discovery is disabled", error);
3174
+ }
3175
+ return { known: false };
3176
+ }
2443
3177
  if (!isServiceUnavailable(error) || attempt >= this.unavailableAttempts) {
2444
3178
  console.error("[dsh-task-board] session/list failed; treating the host session roster as unknown", error);
2445
3179
  return { known: false };
@@ -2463,6 +3197,10 @@ var HostExecutionRunner = class {
2463
3197
  }
2464
3198
  return { outcome: "pending" };
2465
3199
  }
3200
+ if (isDefinitionWithdrawn(error)) {
3201
+ console.warn("[dsh-task-board] the host gateway withdrew the session/list definition for this DSH cohort; keeping the outcome pending", error);
3202
+ return { outcome: "pending" };
3203
+ }
2466
3204
  console.warn("[dsh-task-board] session/list failed during execution inspection; keeping the outcome pending", error);
2467
3205
  return { outcome: "pending" };
2468
3206
  }
@@ -2533,10 +3271,15 @@ var HostExecutionRunner = class {
2533
3271
  return { outcome: "pending" };
2534
3272
  }
2535
3273
  this.scanMemos.delete(sessionId);
2536
- return isErrorTurnEnd(turnEnd.event.data) ? {
3274
+ const reason = nonCompletedTurnEnd(turnEnd.event.data);
3275
+ if (reason === void 0) return { outcome: "succeeded" };
3276
+ return reason === "error" ? {
2537
3277
  outcome: "failed",
2538
3278
  error: "agent turn ended with an error"
2539
- } : { outcome: "succeeded" };
3279
+ } : {
3280
+ outcome: "failed",
3281
+ error: "agent turn ended without completing: " + reason
3282
+ };
2540
3283
  }
2541
3284
  };
2542
3285
  //#endregion
@@ -2843,19 +3586,55 @@ var PowerInhibitor = class {
2843
3586
  };
2844
3587
  //#endregion
2845
3588
  //#region src/host-service.ts
3589
+ /** Session-roster poll cadence — the one recurring Host timer this service still holds. */
2846
3590
  const SESSION_POLL_MS = 5e3;
2847
- const SCHEDULE_TICK_MS = 3e4;
2848
- const RESUME_GAP_MS = 45e3;
3591
+ /**
3592
+ * How late an armed schedule fire may be before it counts as a resume rather
3593
+ * than a normal occurrence. The schedule timer is armed AT the next due
3594
+ * instant, so landing this far past its target means the Host was suspended,
3595
+ * the process throttled, or the wall clock jumped forward — the same condition
3596
+ * the old fixed 30 s heartbeat detected through its own gap threshold.
3597
+ */
3598
+ const RECOVERY_TOLERANCE_MS = 6e4;
3599
+ /** Largest delay a Node timer represents without clamping; longer targets re-arm in segments. */
3600
+ const MAX_TIMER_DELAY_MS = 2147483647;
3601
+ /**
3602
+ * Actions that can move an armed trigger. Only these re-arm the native timer;
3603
+ * an unrelated card edit leaves the pending fire untouched.
3604
+ */
3605
+ const SCHEDULE_WRITE_ACTIONS = /* @__PURE__ */ new Set([
3606
+ "set-schedule",
3607
+ "delete",
3608
+ "archive"
3609
+ ]);
3610
+ /** Process-global fallback used when the deployment serves no cordis timer service. */
3611
+ const PROCESS_TIMERS = {
3612
+ timeout(callback, delay) {
3613
+ const handle = setTimeout(callback, delay);
3614
+ return () => {
3615
+ clearTimeout(handle);
3616
+ };
3617
+ },
3618
+ interval(callback, delay) {
3619
+ const handle = setInterval(callback, delay);
3620
+ return () => {
3621
+ clearInterval(handle);
3622
+ };
3623
+ }
3624
+ };
2849
3625
  var TaskBoardHostService = class {
2850
3626
  ledger;
2851
3627
  runner;
2852
3628
  power;
2853
3629
  listeners = /* @__PURE__ */ new Set();
2854
- timers = [];
2855
- lastScheduleTick;
3630
+ /** The one recurring timer: the session-roster poll. */
3631
+ pollTimer;
3632
+ /** The armed schedule timer, if a trigger is pending. */
3633
+ scheduleTimer;
3634
+ /** The instant the armed schedule timer targets (ms epoch), for resume detection. */
3635
+ scheduleTarget;
2856
3636
  disposed = false;
2857
3637
  pollInFlight = false;
2858
- tickInFlight = false;
2859
3638
  active = true;
2860
3639
  /**
2861
3640
  * Ids the last roster poll saw as present and idle; undefined while the
@@ -2865,11 +3644,18 @@ var TaskBoardHostService = class {
2865
3644
  */
2866
3645
  idleSessionIds;
2867
3646
  preventIdleSleep = false;
3647
+ team;
3648
+ timers;
2868
3649
  lastPowerJson = "";
2869
3650
  now;
2870
3651
  constructor(gateway, options = {}) {
2871
- this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, { sessionDefaultPermission: options.sessionDefaultPermission });
3652
+ this.ledger = options.ledger ?? new HostTaskLedger(void 0, void 0, {
3653
+ sessionDefaultPermission: options.sessionDefaultPermission,
3654
+ maxSubtaskDepth: options.maxSubtaskDepth
3655
+ });
2872
3656
  this.runner = new HostExecutionRunner(gateway, options.commandDispatcher, options.workspaceRegistry);
3657
+ this.team = options.team;
3658
+ this.timers = options.timers ?? PROCESS_TIMERS;
2873
3659
  this.power = options.power ?? new PowerInhibitor();
2874
3660
  this.now = options.now ?? Date.now;
2875
3661
  installStreamErrorGuards();
@@ -2885,16 +3671,13 @@ var TaskBoardHostService = class {
2885
3671
  });
2886
3672
  }
2887
3673
  start() {
2888
- if (this.disposed || this.timers.length > 0) return;
3674
+ if (this.disposed || this.pollTimer !== void 0) return;
2889
3675
  this.syncPowerReasons();
2890
- this.timers.push(setInterval(() => {
3676
+ this.pollTimer = this.timers.interval(() => {
2891
3677
  this.schedulePoll();
2892
- }, SESSION_POLL_MS));
2893
- this.timers.push(setInterval(() => {
2894
- this.scheduleTick(false);
2895
- }, SCHEDULE_TICK_MS));
3678
+ }, SESSION_POLL_MS);
2896
3679
  this.schedulePoll();
2897
- this.scheduleTick(true);
3680
+ this.recoverSchedule();
2898
3681
  }
2899
3682
  setConfiguration(active, preventIdleSleep) {
2900
3683
  const resumed = !this.active && active;
@@ -2911,8 +3694,8 @@ var TaskBoardHostService = class {
2911
3694
  this.power.setEnabled(active && preventIdleSleep);
2912
3695
  if (resumed) {
2913
3696
  this.schedulePoll();
2914
- this.scheduleTick(true);
2915
- }
3697
+ this.recoverSchedule();
3698
+ } else if (!active) this.clearScheduleTimer();
2916
3699
  this.emit();
2917
3700
  }
2918
3701
  snapshot() {
@@ -2923,7 +3706,9 @@ var TaskBoardHostService = class {
2923
3706
  tasks: state.tasks,
2924
3707
  scheduler: state.scheduler,
2925
3708
  power: this.power.snapshot(),
2926
- sessionDefaultPermission: this.ledger.sessionDefaultPermission
3709
+ sessionDefaultPermission: this.ledger.sessionDefaultPermission,
3710
+ maxSubtaskDepth: this.ledger.maxSubtaskDepth,
3711
+ teamRunAvailable: this.team !== void 0
2927
3712
  };
2928
3713
  }
2929
3714
  /** SSE frame payload; deliberately skips the tasks deep-clone of {@link snapshot}. */
@@ -2943,8 +3728,12 @@ var TaskBoardHostService = class {
2943
3728
  }
2944
3729
  apply(requestId, action, initiator) {
2945
3730
  if (!this.active) throw new Error("task board is disabled");
3731
+ if (this.team === void 0 && (action.kind === "run" || action.kind === "rerun")) {
3732
+ if (this.ledger.state().tasks.find((item) => item.id === action.taskId)?.teamRun === true) throw new Error("Agent Teams is unavailable in this deployment");
3733
+ }
2946
3734
  const result = this.ledger.applyRequest(requestId, action, initiator);
2947
- if (result.run !== void 0) this.scheduleLaunch(result.run);
3735
+ if (result.runs !== void 0) this.dispatchRuns(result.runs);
3736
+ if (SCHEDULE_WRITE_ACTIONS.has(action.kind)) this.refreshSchedule();
2948
3737
  return {
2949
3738
  schemaVersion: 3,
2950
3739
  revision: result.state.revision,
@@ -2955,25 +3744,74 @@ var TaskBoardHostService = class {
2955
3744
  }
2956
3745
  dispose() {
2957
3746
  this.disposed = true;
2958
- for (const timer of this.timers.splice(0)) clearInterval(timer);
3747
+ this.clearScheduleTimer();
3748
+ this.pollTimer?.();
3749
+ this.pollTimer = void 0;
2959
3750
  this.power.dispose();
2960
3751
  this.ledger.dispose();
2961
3752
  this.listeners.clear();
2962
3753
  }
2963
- async launch(opened) {
3754
+ async launch(opened, others = []) {
2964
3755
  try {
2965
- const reuseSessionId = reusableSessionId(opened.task, this.idleSessionIds);
2966
- const sessionId = await this.runner.launch(opened.task, reuseSessionId === void 0 ? {} : { reuseSessionId });
3756
+ const team = opened.task.teamRun === true;
3757
+ const reuseSessionId = team ? void 0 : reusableSessionId(opened.task, this.idleSessionIds);
3758
+ const promptContext = others.length === 0 ? void 0 : {
3759
+ peers: others.map((other) => ({
3760
+ id: other.task.id,
3761
+ title: other.task.title,
3762
+ ...team ? { name: teammateName(other.task.title, other.execution.runGroupId ?? other.task.id) } : {}
3763
+ })),
3764
+ ...team ? { team: true } : {}
3765
+ };
3766
+ const sessionId = await this.runner.launch(opened.task, {
3767
+ ...reuseSessionId === void 0 ? {} : { reuseSessionId },
3768
+ ...promptContext === void 0 ? {} : { promptContext }
3769
+ });
2967
3770
  this.ledger.attachSession(opened.task.id, opened.execution.id, sessionId);
3771
+ if (team) for (const teammate of others) this.scheduleTeammate(teammate, sessionId);
2968
3772
  } catch (error) {
2969
3773
  if (error instanceof SessionLaunchError) this.ledger.attachSession(opened.task.id, opened.execution.id, error.sessionId);
2970
3774
  this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
2971
3775
  }
2972
3776
  }
2973
- async pollSessions() {
2974
- if (this.disposed) return;
2975
- if (!this.active && this.ledger.runtimeView().openExecutions.length === 0) return;
2976
- const running = await this.runner.listRunning();
3777
+ scheduleTeammate(opened, leadSessionId) {
3778
+ this.spawnTeammate(opened, leadSessionId).catch((error) => {
3779
+ safeConsoleError("[dsh-task-board] teammate spawn settlement failed", error);
3780
+ });
3781
+ }
3782
+ /**
3783
+ * Spawn one teammate inside the Lead session and attach the teammate's
3784
+ * session to the subtask execution, so the existing session monitor settles
3785
+ * it from the teammate's own turn like any other execution. A spawn that
3786
+ * fails settles the subtask as failed immediately, which then folds into the
3787
+ * Lead's cascade verdict.
3788
+ */
3789
+ async spawnTeammate(opened, leadSessionId) {
3790
+ const team = this.team;
3791
+ if (team === void 0) {
3792
+ this.ledger.settle(opened.task.id, opened.execution.id, "failed", "Agent Teams is unavailable in this deployment");
3793
+ return;
3794
+ }
3795
+ try {
3796
+ const member = await team.spawn({
3797
+ leadSessionId,
3798
+ name: teammateName(opened.task.title, opened.execution.runGroupId ?? opened.task.id),
3799
+ description: opened.task.title,
3800
+ prompt: promptText(opened.task)
3801
+ });
3802
+ if (member.sessionId === void 0 || member.sessionId === "") {
3803
+ this.ledger.settle(opened.task.id, opened.execution.id, "failed", member.error ?? "teammate provisioning failed");
3804
+ return;
3805
+ }
3806
+ this.ledger.attachSession(opened.task.id, opened.execution.id, member.sessionId);
3807
+ } catch (error) {
3808
+ this.ledger.settle(opened.task.id, opened.execution.id, "failed", error instanceof Error ? error.message : String(error));
3809
+ }
3810
+ }
3811
+ async pollSessions() {
3812
+ if (this.disposed) return;
3813
+ if (!this.active && this.ledger.runtimeView().openExecutions.length === 0) return;
3814
+ const running = await this.runner.listRunning();
2977
3815
  const previous = this.power.snapshot();
2978
3816
  if (!running.known) {
2979
3817
  this.idleSessionIds = void 0;
@@ -3004,27 +3842,87 @@ var TaskBoardHostService = class {
3004
3842
  } catch {}
3005
3843
  }
3006
3844
  }
3007
- async tickSchedule(first) {
3845
+ /** Drop the armed schedule timer and forget its target. */
3846
+ clearScheduleTimer() {
3847
+ this.scheduleTimer?.();
3848
+ this.scheduleTimer = void 0;
3849
+ this.scheduleTarget = void 0;
3850
+ }
3851
+ /**
3852
+ * Boot / resume recovery: skip every occurrence that came due while the
3853
+ * board was not running and roll each schedule to its next future target,
3854
+ * then arm the timer for the nearest one. Rendering the occurrence is
3855
+ * deliberately not attempted: the ACL of a card that fired hours ago is
3856
+ * stale, and the board's own recovery contract is "missed triggers are
3857
+ * skipped, never replayed".
3858
+ */
3859
+ recoverSchedule() {
3860
+ if (this.disposed) return;
3861
+ this.clearScheduleTimer();
3862
+ const now = this.now();
3863
+ this.ledger.setScheduler({ lastTickAt: now });
3864
+ this.ledger.skipMissed(now);
3865
+ this.armSchedule();
3866
+ }
3867
+ /**
3868
+ * Arm the native timer at the nearest armed future trigger. One timer serves
3869
+ * every schedule: the ledger's next target is the only instant the Host has
3870
+ * to wake for. A target beyond the platform's timer ceiling re-arms in
3871
+ * segments, and a target already past (the wall clock jumped, or the process
3872
+ * was suspended) is handled immediately as a recovery.
3873
+ */
3874
+ armSchedule() {
3875
+ if (this.disposed || !this.active) return;
3876
+ this.clearScheduleTimer();
3877
+ const target = this.ledger.nextArmedRunAt(this.now());
3878
+ if (target === void 0) return;
3879
+ this.scheduleTarget = target;
3880
+ const delay = Math.max(0, Math.min(target - this.now(), MAX_TIMER_DELAY_MS));
3881
+ this.scheduleTimer = this.timers.timeout(() => {
3882
+ this.scheduleTimer = void 0;
3883
+ this.onScheduleFire(target);
3884
+ }, delay);
3885
+ }
3886
+ /**
3887
+ * One armed target became due. A fire landing well past its target is a
3888
+ * resume (suspend, throttle, forward clock jump) rather than a normal
3889
+ * occurrence, so it takes the recovery path instead of launching a run for a
3890
+ * long-stale instant.
3891
+ */
3892
+ onScheduleFire(target) {
3008
3893
  if (this.disposed || !this.active) return;
3009
3894
  const now = this.now();
3010
- const recovered = first || this.lastScheduleTick !== void 0 && now - this.lastScheduleTick > RESUME_GAP_MS;
3011
- this.lastScheduleTick = now;
3895
+ this.scheduleTarget = void 0;
3012
3896
  this.ledger.setScheduler({ lastTickAt: now });
3013
- if (recovered) {
3014
- this.ledger.skipMissed(now);
3897
+ if (now - target > RECOVERY_TOLERANCE_MS) {
3898
+ this.recoverSchedule();
3015
3899
  return;
3016
3900
  }
3017
3901
  for (const schedule of this.ledger.dueSchedules(now)) {
3018
3902
  const next = nextRunAtMs(schedule.cron, schedule.nextRunAt);
3019
- const opened = this.ledger.openScheduled(schedule.taskId, next, now);
3020
- if (opened !== void 0) this.scheduleLaunch(opened);
3903
+ this.dispatchRuns(this.ledger.openScheduled(schedule.taskId, next, now));
3021
3904
  }
3905
+ this.armSchedule();
3022
3906
  }
3023
3907
  armedSchedules() {
3024
3908
  return this.ledger.armedScheduleCount();
3025
3909
  }
3026
- scheduleLaunch(opened) {
3027
- this.launch(opened).catch((error) => {
3910
+ /**
3911
+ * Launch one run set. The root goes first and receives the run shape in its
3912
+ * prompt (which members this run opens, and how they run); every plain-cascade
3913
+ * member then launches on its own so one refused participant cannot hold the
3914
+ * others back. A team run's members are spawned inside the root's Lead
3915
+ * session instead, once that session exists.
3916
+ */
3917
+ dispatchRuns(runs) {
3918
+ if (runs.length === 0) return;
3919
+ const root = runs.find((run) => run.dispatch !== "teammate") ?? runs[0];
3920
+ const others = runs.filter((run) => run !== root);
3921
+ this.scheduleLaunch(root, others);
3922
+ for (const run of others) if (run.dispatch !== "teammate") this.scheduleLaunch(run);
3923
+ }
3924
+ scheduleLaunch(opened, others = []) {
3925
+ this.launch(opened, others).catch((error) => {
3028
3926
  safeConsoleError("[dsh-task-board] execution launch settlement failed", error);
3029
3927
  });
3030
3928
  }
@@ -3037,14 +3935,18 @@ var TaskBoardHostService = class {
3037
3935
  this.pollInFlight = false;
3038
3936
  });
3039
3937
  }
3040
- scheduleTick(first) {
3041
- if (this.tickInFlight || this.disposed) return;
3042
- this.tickInFlight = true;
3043
- this.tickSchedule(first).catch((error) => {
3044
- safeConsoleError("[dsh-task-board] scheduler tick failed", error);
3045
- }).finally(() => {
3046
- this.tickInFlight = false;
3047
- });
3938
+ /**
3939
+ * Re-arm from the ledger's current targets. Callers that just changed a
3940
+ * schedule (the host routes, the agent tools) invoke this after the write
3941
+ * commits, so a new or edited trigger arms without waiting for the next fire.
3942
+ */
3943
+ refreshSchedule() {
3944
+ if (this.disposed) return;
3945
+ if (!this.active) {
3946
+ this.clearScheduleTimer();
3947
+ return;
3948
+ }
3949
+ this.armSchedule();
3048
3950
  }
3049
3951
  syncPowerReasons() {
3050
3952
  const current = this.power.snapshot();
@@ -3066,1346 +3968,17 @@ var TaskBoardHostService = class {
3066
3968
  */
3067
3969
  function installStreamErrorGuards() {
3068
3970
  for (const stream of [process.stderr, process.stdout]) if (stream && typeof stream.on === "function") {
3069
- if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
3070
- }
3071
- }
3072
- /**
3073
- * Defensively log to console.error without letting stderr write failures
3074
- * (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
3075
- */
3076
- function safeConsoleError(message, ...args) {
3077
- try {
3078
- console.error(message, ...args);
3079
- } catch {}
3080
- }
3081
- //#endregion
3082
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-typert-protocol@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-typert-protocol/lib/index.js
3083
- /** The one Remote failure class shared by owners, the Gateway, and consumers. */
3084
- /**
3085
- * One Remote call failure: a real Error carrying its stable code and typed
3086
- * details. Owners throw it at the failure point; the Host Gateway encodes it
3087
- * onto the wire unchanged; the Client face rebuilds an instance for the
3088
- * `RemoteResult` error branch, so `throw result.error` keeps throw semantics.
3089
- * Discrimination is always by `code`, never by instanceof.
3090
- */
3091
- var RemoteError = class extends Error {
3092
- code;
3093
- details;
3094
- /** Structural marker: cross-realm/bundle identification never uses instanceof. */
3095
- isDSHRemoteError = true;
3096
- /**
3097
- * @param code - stable failure code declared in {@link RemoteErrorDetailsMap}.
3098
- * @param message - human diagnostic carried across the wire.
3099
- * @param details - structured payload typed by the code.
3100
- * @param options - standard Error options (`cause` survives in-process only).
3101
- */
3102
- constructor(code, message, details, options) {
3103
- super(message, options);
3104
- this.code = code;
3105
- this.details = details;
3106
- this.name = "RemoteError";
3107
- }
3108
- };
3109
- /**
3110
- * Remote decorators and explicit Gateway bindings backed by versioned
3111
- * descriptors carried on decorated class prototypes. Strict reflection
3112
- * remains a Typert compiler responsibility.
3113
- * @module @deepseek-ai/dsh-typert-protocol
3114
- */
3115
- const TYPERT_REMOTE_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
3116
- /**
3117
- * Test one generated Remote name against the Connection endpoint grammar.
3118
- * @param value - namespace, method, lookup, or Context segment.
3119
- * @returns whether the value can cross the shared RPC carrier unchanged.
3120
- */
3121
- function isTypertRemoteSegment(value) {
3122
- return value !== "." && value !== ".." && TYPERT_REMOTE_SEGMENT_PATTERN.test(value);
3123
- }
3124
- const REMOTE_METHOD_DESCRIPTOR = "@deepseek-ai/dsh-typert-protocol/remote-methods";
3125
- /**
3126
- * Bind one visible Service field to a Cordis key and Remote namespace. A
3127
- * service that owns a Cordis Context also gives its tree `ctx.invocation`,
3128
- * `undefined` outside a Remote call, so no `TypertRemoteService` is needed for
3129
- * a Host composition to read it.
3130
- * @param service - owning Service instance, normally `this`.
3131
- * @param serviceKey - exact Cordis service key.
3132
- * @param options - optional distinct wire namespace.
3133
- * @returns a frozen, inspectable binding with no compiler-injected metadata.
3134
- */
3135
- function bindTypertRemote(service, serviceKey, options = {}) {
3136
- validateName("service key", serviceKey);
3137
- const namespace = options.namespace ?? serviceKey;
3138
- validateName("namespace", namespace);
3139
- const ctx = Reflect.get(service, "ctx");
3140
- if (ctx instanceof Context) provideInvocationAccessor(ctx);
3141
- return Object.freeze({
3142
- service,
3143
- serviceKey,
3144
- namespace
3145
- });
3146
- }
3147
- /** Cordis Service base that exposes its registered name through Typert Gateway. */
3148
- var TypertRemoteService = class extends Service {
3149
- /** Visible binding consumed by the Gateway's source-mode discovery. */
3150
- typertRemote;
3151
- /**
3152
- * Register the Service and bind the same key to Typert Gateway.
3153
- * @param ctx - owning Cordis Context.
3154
- * @param serviceKey - exact Cordis service key and default wire namespace.
3155
- * @param options - optional distinct wire namespace.
3156
- */
3157
- constructor(ctx, serviceKey, options = {}) {
3158
- super(ctx, serviceKey);
3159
- this.typertRemote = bindTypertRemote(this, this.name, options);
3160
- }
3161
- };
3162
- /**
3163
- * Make `ctx.invocation` read as `undefined` outside a Remote call instead of the
3164
- * reflect service's "cannot get property" error; a call-derived Context shadows
3165
- * the accessor with its own property. The first Remote Service constructed in a
3166
- * tree registers it on the root, where it outlives any one Service.
3167
- */
3168
- function provideInvocationAccessor(ctx) {
3169
- if (Object.hasOwn(ctx.root.reflect.props, "invocation")) return;
3170
- ctx.root.accessor("invocation", { get: () => void 0 });
3171
- }
3172
- function Remote(methodExportOrOptions, context) {
3173
- if (typeof methodExportOrOptions === "string") {
3174
- validateName("Remote export name", methodExportOrOptions);
3175
- return remoteDecorator({ kind: "direct" }, void 0, methodExportOrOptions);
3176
- }
3177
- if (typeof methodExportOrOptions === "object") {
3178
- if (remoteOptionMode(methodExportOrOptions) !== "stream" || Reflect.ownKeys(methodExportOrOptions).length !== 1) throw new TypeError("typert-protocol: Remote options must contain exactly mode: \"stream\"");
3179
- return remoteDecorator({ kind: "direct" }, "stream");
3180
- }
3181
- if (context === void 0) throw new TypeError("typert-protocol: Remote decorator context is missing");
3182
- addMarkerInitializer(context, { kind: "direct" });
3183
- }
3184
- function remoteOptionMode(options) {
3185
- return Reflect.get(options, "mode");
3186
- }
3187
- function remoteDecorator(invocation, mode, exportName) {
3188
- return function(_method, context) {
3189
- addMarkerInitializer(context, invocation, mode, exportName);
3190
- };
3191
- }
3192
- function readRemoteMethodDescriptor(prototype) {
3193
- const property = Object.getOwnPropertyDescriptor(prototype, REMOTE_METHOD_DESCRIPTOR);
3194
- if (property === void 0) return void 0;
3195
- const descriptor = property.value;
3196
- if (descriptor === null || typeof descriptor !== "object") throw new TypeError("typert-protocol: Remote method descriptor must be an object");
3197
- const version = Reflect.get(descriptor, "version");
3198
- if (version !== 1) throw new TypeError(`typert-protocol: unsupported Remote method descriptor version ${String(version)}`);
3199
- const methods = Reflect.get(descriptor, "methods");
3200
- if (!Array.isArray(methods)) throw new TypeError("typert-protocol: Remote method descriptor methods must be an array");
3201
- return descriptor;
3202
- }
3203
- function addMarkerInitializer(context, invocation, mode, exportName) {
3204
- if (context.private || context.static || typeof context.name !== "string") throw new TypeError("typert-protocol: Remote decorators require a public instance method with a string name");
3205
- const method = context.name;
3206
- context.addInitializer(function() {
3207
- const prototype = Object.getPrototypeOf(this);
3208
- if (prototype === null) throw new TypeError(`typert-protocol: cannot mark Remote method "${method}" on an object without a prototype`);
3209
- mark(prototype, method, invocation, mode, exportName);
3210
- });
3211
- }
3212
- function mark(prototype, method, invocation, mode, exportName) {
3213
- const descriptor = readRemoteMethodDescriptor(prototype);
3214
- const marker = Object.freeze({
3215
- method,
3216
- ...exportName === void 0 || exportName === method ? {} : { exportName },
3217
- ...mode === void 0 ? {} : { mode },
3218
- invocation: Object.freeze(invocation)
3219
- });
3220
- const current = descriptor?.methods.find((candidate) => candidate.method === method);
3221
- if (current !== void 0) {
3222
- if (current.exportName === marker.exportName && current.mode === marker.mode && sameInvocation(current.invocation, invocation)) return;
3223
- throw new Error(`typert-protocol: Remote method "${method}" has conflicting invocation markers`);
3224
- }
3225
- Object.defineProperty(prototype, REMOTE_METHOD_DESCRIPTOR, {
3226
- configurable: true,
3227
- value: Object.freeze({
3228
- version: 1,
3229
- methods: Object.freeze([...descriptor?.methods ?? [], marker])
3230
- })
3231
- });
3232
- }
3233
- function sameInvocation(left, right) {
3234
- if (left.kind === "direct") return right.kind === "direct";
3235
- if (right.kind === "direct") return false;
3236
- return left.context === right.context;
3237
- }
3238
- function validateName(subject, value) {
3239
- if (!isTypertRemoteSegment(value)) throw new TypeError(`typert-protocol: ${subject} must contain only RPC endpoint segment characters`);
3240
- }
3241
- //#endregion
3242
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-values@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-values/lib/index.js
3243
- /**
3244
- * Deep-freeze an object graph in place while leaving live AbortSignal objects mutable.
3245
- * @param value - value to freeze.
3246
- * @returns the same value after every reachable enumerable child is frozen.
3247
- */
3248
- function deepFreeze(value) {
3249
- const seen = /* @__PURE__ */ new WeakSet();
3250
- const pending = [{
3251
- kind: "visit",
3252
- node: value
3253
- }];
3254
- while (pending.length > 0) {
3255
- const task = pending.pop();
3256
- /* v8 ignore next -- the loop condition guarantees one pending task. */
3257
- if (task === void 0) continue;
3258
- if (task.kind === "property") {
3259
- pending.push({
3260
- kind: "visit",
3261
- node: task.source[task.key]
3262
- });
3263
- continue;
3264
- }
3265
- const node = task.node;
3266
- if (node === null || typeof node !== "object") continue;
3267
- if (node instanceof AbortSignal) continue;
3268
- if (seen.has(node)) continue;
3269
- seen.add(node);
3270
- Object.freeze(node);
3271
- const keys = Object.keys(node);
3272
- for (let index = keys.length - 1; index >= 0; index--) {
3273
- const key = keys[index];
3274
- /* v8 ignore next -- the loop is bounded by the captured key count. */
3275
- if (key === void 0) continue;
3276
- pending.push({
3277
- kind: "property",
3278
- source: node,
3279
- key
3280
- });
3281
- }
3282
- }
3283
- return value;
3284
- }
3285
- //#endregion
3286
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-util-crypto@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-util-crypto/lib/index.js
3287
- /**
3288
- * Random v4 UUID, minted from `crypto.getRandomValues`.
3289
- * @returns the UUID string.
3290
- */
3291
- function randomUUID() {
3292
- const bytes = globalThis.crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16));
3293
- const hex = Array.from(bytes, (byte, index) => {
3294
- return (index === 6 ? byte & 15 | 64 : index === 8 ? byte & 63 | 128 : byte).toString(16).padStart(2, "0");
3295
- }).join("");
3296
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
3297
- }
3298
- //#endregion
3299
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-brand@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-brand/lib/index.js
3300
- /**
3301
- * Duplicate-install-safe nominal primitive helpers.
3302
- *
3303
- * A brand makes structurally identical strings or numbers non-interchangeable
3304
- * at the type level: a `SessionId` cannot be passed where a `ToolCallId` is
3305
- * expected, and an event sequence cannot be passed as a log offset. Comparison,
3306
- * logging, and serialization retain the underlying primitive behavior.
3307
- *
3308
- * This package owns no concrete domain value and keeps no runtime identity or mutable
3309
- * state, so independently installed copies produce interchangeable values.
3310
- *
3311
- * @module @deepseek-ai/dsh-brand
3312
- */
3313
- /**
3314
- * Apply a compile-time string brand without changing the value.
3315
- * @param value - string admitted by the domain that owns the target brand.
3316
- * @returns the same string with the requested compile-time brand.
3317
- */
3318
- function brandString(value) {
3319
- return value;
3320
- }
3321
- //#endregion
3322
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-timeout@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-timeout/lib/index.js
3323
- /** Largest delay Node schedules without clamping it to one millisecond. */
3324
- const MAX_TIMER_DELAY_MS = 2147483647;
3325
- //#endregion
3326
- //#region ../../node_modules/.pnpm/@deepseek-ai+dsh-llm@0.1.7-rc.1_@deepseek-ai+cordis@4.0.4/node_modules/@deepseek-ai/dsh-llm/lib/index.js
3327
- /**
3328
- * Detach and deep-freeze a message whose identity already exists.
3329
- * @param message - complete message, including its stable identity.
3330
- * @returns an immutable snapshot that preserves the identity.
3331
- */
3332
- function freezeMessage(message) {
3333
- return deepFreeze(structuredClone(message));
3334
- }
3335
- /**
3336
- * Create one identified message and freeze it before publication.
3337
- * @param input - complete role, content, and source for a new message.
3338
- * @returns an immutable message with a fresh stable identity.
3339
- */
3340
- function createMessage(input) {
3341
- return deepFreeze(structuredClone({
3342
- ...input,
3343
- id: brandString(randomUUID())
3344
- }));
3345
- }
3346
- /**
3347
- * Create one identified user-role message and freeze it before publication.
3348
- * @param input - complete content and source for a new user message.
3349
- * @returns an immutable user message with a fresh stable identity.
3350
- */
3351
- function createUserMessage(input) {
3352
- return createMessage({
3353
- ...input,
3354
- role: "user"
3355
- });
3356
- }
3357
- /**
3358
- * Harness error base with a stable machine-routable code and chained cause.
3359
- * Package errors extend it so tool results and replay can retain failure class.
3360
- * @module @deepseek-ai/dsh-llm/error
3361
- */
3362
- /**
3363
- * Base class for all harness errors. Carries a `code` (stable, programmatic —
3364
- * e.g. `NO_ADAPTER`, `INVALID_ARGS`, `INVARIANT`) distinct from the
3365
- * human-readable `message`, and supports `cause` chaining via the standard
3366
- * `ErrorOptions`. `name` defaults to the subclass constructor name.
3367
- */
3368
- var HarnessError = class extends Error {
3369
- /** Stable machine-routable failure class (e.g. `RATE_LIMIT`); route on this, never by parsing `message`. */
3370
- code;
3371
- constructor(message, code, options) {
3372
- super(message, options);
3373
- this.code = code;
3374
- this.name = new.target.name;
3375
- }
3376
- };
3377
- /**
3378
- * Canonical provider-neutral code for a response that completed normally but
3379
- * carried no content blocks at all. Providers occasionally emit a degenerate
3380
- * completion (a terminal stop with zero output); adapters classify it as this
3381
- * failure instead of yielding an empty assistant message, because an empty
3382
- * message silently ends the turn with nothing for the user or the loop to act
3383
- * on. The attempt produced nothing durable, so retry policy treats it as safe
3384
- * to repeat.
3385
- */
3386
- const EMPTY_RESPONSE_CODE = "EMPTY_RESPONSE";
3387
- new RegExp(String.raw`(?:^|[^a-z0-9])context[\s_-](?:length|window)[\s_-]` + String.raw`(?:exceed(?:ed|s)?|overflow(?:ed)?|limit[\s_-]exceeded)(?:$|[^a-z0-9])`, "i");
3388
- new RegExp(String.raw`\b(?:request|prompt|input|messages?)\s+(?:is\s+|are\s+)?` + String.raw`too\s+(?:large|long)\s+for\s+(?:(?:this|the)\s+)?` + String.raw`(?:model(?:'s)?\s+)?context(?:\s+window)?\b`, "i");
3389
- new RegExp(String.raw`\b(?:input|prompt|request|messages?)\b.{0,40}` + String.raw`\b(?:exceed(?:s|ed)?|overflows?|is\s+larger\s+than)\b.{0,40}` + String.raw`\b(?:the\s+)?(?:model(?:'s)?\s+)?context(?:\s+(?:length|window))?\b`, "i");
3390
- /**
3391
- * Provider-owned request-retry policy configuration and resolution.
3392
- *
3393
- * Adapters expose one resolved policy per registered provider route; the
3394
- * optional dsh-llm-retry plugin executes it on the agent's failed-step extension point.
3395
- *
3396
- * @module @deepseek-ai/dsh-llm/retry-policy
3397
- */
3398
- const DEFAULT_MAX_RETRIES = 5;
3399
- const DEFAULT_INITIAL_DELAY_MS = 500;
3400
- const DEFAULT_MAX_DELAY_MS = 1e4;
3401
- const DEFAULT_JITTER_RATIO = .1;
3402
- const DEFAULT_RETRYABLE_CODES = Object.freeze([
3403
- EMPTY_RESPONSE_CODE,
3404
- "RATE_LIMIT",
3405
- "SERVER",
3406
- "TIMEOUT",
3407
- "TRANSPORT"
3408
- ]);
3409
- const backoffSchema = z.object({
3410
- initialDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_INITIAL_DELAY_MS),
3411
- maxDelayMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_MAX_DELAY_MS),
3412
- jitterRatio: z.number().min(0).max(1).default(DEFAULT_JITTER_RATIO)
3413
- });
3414
- const normalPolicySchema = z.object({
3415
- mode: z.const("normal").required(),
3416
- maxRetries: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).default(DEFAULT_MAX_RETRIES),
3417
- retryableCodes: z.array(z.string()).default([...DEFAULT_RETRYABLE_CODES]),
3418
- backoff: backoffSchema
3419
- });
3420
- const alwaysPolicySchema = z.object({
3421
- mode: z.const("always").required(),
3422
- backoff: backoffSchema
3423
- });
3424
- z.union([normalPolicySchema, alwaysPolicySchema]);
3425
- const NORMAL_POLICY_KEYS = /* @__PURE__ */ new Set([
3426
- "mode",
3427
- "maxRetries",
3428
- "retryableCodes",
3429
- "backoff"
3430
- ]);
3431
- const ALWAYS_POLICY_KEYS = /* @__PURE__ */ new Set([
3432
- "mode",
3433
- "maxRetries",
3434
- "retryableCodes",
3435
- "backoff"
3436
- ]);
3437
- const BACKOFF_KEYS = /* @__PURE__ */ new Set([
3438
- "initialDelayMs",
3439
- "maxDelayMs",
3440
- "jitterRatio"
3441
- ]);
3442
- function validateKeys(value, allowed, path) {
3443
- for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`${path}: unknown key "${key}"`);
3444
- }
3445
- function resolveBackoff(config, path) {
3446
- if (config !== void 0) validateKeys(config, BACKOFF_KEYS, path);
3447
- const initialDelayMs = config?.initialDelayMs ?? DEFAULT_INITIAL_DELAY_MS;
3448
- const maxDelayMs = config?.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
3449
- const jitterRatio = config?.jitterRatio ?? DEFAULT_JITTER_RATIO;
3450
- if (!Number.isFinite(initialDelayMs) || initialDelayMs <= 0 || initialDelayMs > 2147483647) throw new Error(`${path}.initialDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
3451
- if (!Number.isFinite(maxDelayMs) || maxDelayMs <= 0 || maxDelayMs > 2147483647) throw new Error(`${path}.maxDelayMs must be a positive finite number no greater than ${MAX_TIMER_DELAY_MS}`);
3452
- if (initialDelayMs > maxDelayMs) throw new Error(`${path}.initialDelayMs must be less than or equal to maxDelayMs`);
3453
- if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1) throw new Error(`${path}.jitterRatio must be between 0 and 1`);
3454
- return Object.freeze({
3455
- initialDelayMs,
3456
- maxDelayMs,
3457
- jitterRatio
3458
- });
3459
- }
3460
- /**
3461
- * Validate, default, and detach one provider-owned retry policy.
3462
- * @param config - optional provider configuration; omission selects normal defaults.
3463
- * @param path - diagnostic path naming the provider config that owns the value.
3464
- * @returns an immutable policy safe to capture in provider registration state.
3465
- */
3466
- function resolveRetryPolicy(config, path) {
3467
- if (config === void 0) return Object.freeze({
3468
- mode: "normal",
3469
- maxRetries: DEFAULT_MAX_RETRIES,
3470
- retryableCodes: DEFAULT_RETRYABLE_CODES,
3471
- ...resolveBackoff(void 0, `${path}.backoff`)
3472
- });
3473
- switch (config.mode) {
3474
- case "normal": {
3475
- validateKeys(config, NORMAL_POLICY_KEYS, path);
3476
- const maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
3477
- const retryableCodes = config.retryableCodes ?? [...DEFAULT_RETRYABLE_CODES];
3478
- if (!Number.isSafeInteger(maxRetries) || maxRetries < 0) throw new Error(`${path}.maxRetries must be a non-negative safe integer`);
3479
- if (retryableCodes.length === 0) throw new Error(`${path}.retryableCodes must not be empty`);
3480
- if (retryableCodes.some((code) => typeof code !== "string" || code.length === 0)) throw new Error(`${path}.retryableCodes must contain only non-empty strings`);
3481
- if (new Set(retryableCodes).size !== retryableCodes.length) throw new Error(`${path}.retryableCodes must not contain duplicates`);
3482
- return Object.freeze({
3483
- mode: "normal",
3484
- maxRetries,
3485
- retryableCodes: Object.freeze([...retryableCodes]),
3486
- ...resolveBackoff(config.backoff, `${path}.backoff`)
3487
- });
3488
- }
3489
- case "always":
3490
- validateKeys(config, ALWAYS_POLICY_KEYS, path);
3491
- return Object.freeze({
3492
- mode: "always",
3493
- ...resolveBackoff(config.backoff, `${path}.backoff`)
3494
- });
3495
- default: throw new Error(`${path}.mode must be "normal" or "always"`);
3496
- }
3497
- }
3498
- /**
3499
- * Field-wise equality over {@link LlmCallConfig} — the comparison a caller
3500
- * runs to decide whether a proposed configuration is a real change (worth a
3501
- * logged header snapshot) or the held one restated.
3502
- * @param a - one configuration.
3503
- * @param b - the other.
3504
- * @returns whether every field (including the `stop` list, element-wise) matches.
3505
- */
3506
- function callConfigEquals(a, b) {
3507
- if (a.provider !== b.provider || a.model !== b.model || a.reasoningEffort !== b.reasoningEffort || a.temperature !== b.temperature || a.maxTokens !== b.maxTokens) return false;
3508
- if (a.stop === void 0 || b.stop === void 0) return a.stop === b.stop;
3509
- return a.stop.length === b.stop.length && a.stop.every((s, i) => s === b.stop?.[i]);
3510
- }
3511
- /**
3512
- * Normalization for values thrown by a final LLM adapter boundary.
3513
- *
3514
- * @module @deepseek-ai/dsh-llm/adapter-failure
3515
- */
3516
- /**
3517
- * Detach serializable provider facts from a value thrown by an adapter.
3518
- * @param value - arbitrary value thrown during adapter dispatch or iteration.
3519
- * @returns immutable provider-neutral facts suitable for a terminal finish chunk.
3520
- * @internal
3521
- */
3522
- function normalizeLlmFailure(value) {
3523
- const error = value instanceof Error ? value : new HarnessError(thrownMessage(value), "UNKNOWN", { cause: value });
3524
- const carried = ownFailureSnapshot(error);
3525
- if (carried !== void 0 && carried.code === ownErrorCode(error)) return carried;
3526
- return Object.freeze({
3527
- message: errorMessage(error),
3528
- code: harnessErrorCode(error)
3529
- });
3530
- }
3531
- /** Render a non-Error throw without letting hostile coercion escape normalization. */
3532
- function thrownMessage(value) {
3533
- try {
3534
- const message = String(value);
3535
- return message.length > 0 ? message : "LLM adapter failed";
3536
- } catch (_hostileThrownValue) {
3537
- return "LLM adapter failed";
3538
- }
3539
- }
3540
- /** Read a foreign error's own data-backed `code` without invoking accessors. */
3541
- function ownErrorCode(error) {
3542
- try {
3543
- const descriptor = Object.getOwnPropertyDescriptor(error, "code");
3544
- return descriptor !== void 0 && "value" in descriptor ? descriptor.value : void 0;
3545
- } catch (_sdkPropertyTrap) {
3546
- return;
3547
- }
3548
- }
3549
- /** Snapshot an own data property without invoking an SDK-defined accessor. */
3550
- function ownFailureSnapshot(error) {
3551
- try {
3552
- const descriptor = Object.getOwnPropertyDescriptor(error, "failure");
3553
- return descriptor !== void 0 && "value" in descriptor ? failureSnapshot(descriptor.value) : void 0;
3554
- } catch (_sdkPropertyTrap) {
3555
- return;
3556
- }
3557
- }
3558
- /** Validate and detach an arbitrary serializable failure payload. */
3559
- function failureSnapshot(value) {
3560
- if (typeof value !== "object" || value === null) return void 0;
3561
- try {
3562
- const candidate = value;
3563
- const message = candidate.message;
3564
- const code = candidate.code;
3565
- const status = candidate.status;
3566
- const providerRetryAfterMs = candidate.providerRetryAfterMs;
3567
- const requestId = candidate.requestId;
3568
- const offloadImages = candidate.offloadImages;
3569
- if (typeof message !== "string" || message.length === 0 || typeof code !== "string" || code.length === 0 || status !== void 0 && (!Number.isInteger(status) || status < 100 || status > 599) || providerRetryAfterMs !== void 0 && (!Number.isFinite(providerRetryAfterMs) || providerRetryAfterMs <= 0) || requestId !== void 0 && (typeof requestId !== "string" || requestId.length === 0) || offloadImages !== void 0 && (!Number.isSafeInteger(offloadImages) || offloadImages <= 0)) return void 0;
3570
- return Object.freeze({
3571
- message,
3572
- code,
3573
- ...status === void 0 ? {} : { status },
3574
- ...providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs },
3575
- ...requestId === void 0 ? {} : { requestId },
3576
- ...offloadImages === void 0 ? {} : { offloadImages }
3577
- });
3578
- } catch (_sdkFailureGetter) {
3579
- return;
3580
- }
3581
- }
3582
- /** Read an SDK error message without letting an accessor replace the primary failure. */
3583
- function errorMessage(error) {
3584
- try {
3585
- const message = error.message;
3586
- if (typeof message === "string" && message.length > 0) return message;
3587
- } catch (_sdkMessageGetter) {}
3588
- return "LLM adapter failed";
3589
- }
3590
- /** Trust only Harness-owned codes; third-party SDK codes are not our taxonomy. */
3591
- function harnessErrorCode(error) {
3592
- return error instanceof HarnessError ? error.code : "UNKNOWN";
3593
- }
3594
- function quoted(value) {
3595
- return JSON.stringify(value);
3596
- }
3597
- /**
3598
- * Stable text shown to a model that cannot accept one durable image reference.
3599
- * @param ref - durable normalized attachment omitted from the request.
3600
- * @returns deterministic text-only placeholder.
3601
- */
3602
- function textOnlyImageText(ref) {
3603
- return `[image omitted because this model accepts text only; attachment sha256:${String(ref.attachmentId).slice(7, 15)}]`;
3604
- }
3605
- /**
3606
- * True when typed model content contains an image block. This is the one image
3607
- * walk shared by every image policy (capability gating, text-only
3608
- * serialization, compaction survey), so a consumer cannot silently diverge.
3609
- * @param content - typed model content blocks.
3610
- * @returns whether any block is an image.
3611
- */
3612
- function contentHasImage(content) {
3613
- return content.some((block) => block.type === "image");
3614
- }
3615
- /**
3616
- * True when typed model content contains a file block.
3617
- * Reads current content on every call without retaining scan results.
3618
- * @param content - typed model content blocks.
3619
- * @returns whether any block is a file.
3620
- */
3621
- function contentHasFile(content) {
3622
- for (const block of content) if (block.type === "file") return true;
3623
- return false;
3624
- }
3625
- /**
3626
- * Stable model-facing handle for one durable file reference: the address of
3627
- * the verbatim stored copy and the instruction to read it on demand. This is
3628
- * the only representation a provider ever receives for a file.
3629
- * @param ref - durable verbatim file reference.
3630
- * @param readonlyPath - execution-world path of the stored copy, when resolvable.
3631
- * @returns deterministic handle text naming the file, its size, and its address.
3632
- */
3633
- function fileHandleText(ref, readonlyPath) {
3634
- const digest = String(ref.attachmentId).slice(7, 15);
3635
- const identity = `File ${quoted(ref.name)} (${ref.bytes} bytes, sha256:${digest})`;
3636
- if (readonlyPath === void 0) return `[${identity} was uploaded, but the current execution environment cannot access a readable path. Report that limitation if its contents are needed; do not claim to have read it.]`;
3637
- return `[${identity}: verbatim read-only copy saved at ${quoted(readonlyPath)}. Read that path with your file tools when its contents are needed; copy it to a writable location before modifying it. When delegating file work, include this saved path in the delegation prompt; only subagents sharing this execution environment can read it.]`;
3638
- }
3639
- /** Replace every file occurrence with handle text. */
3640
- function replaceFilesWithHandles(blocks, resolvePath) {
3641
- let next;
3642
- for (const [index, block] of blocks.entries()) {
3643
- if (block.type === "file") {
3644
- next ??= blocks.slice(0, index);
3645
- next.push({
3646
- type: "text",
3647
- text: fileHandleText(block.attachment, resolvePath(block.attachment))
3648
- });
3649
- continue;
3650
- }
3651
- next?.push(block);
3652
- }
3653
- return next ?? blocks;
3654
- }
3655
- function projectFilesToText(messages, resolvePath) {
3656
- if (!messages.some((message) => contentHasFile(message.content))) return messages;
3657
- return messages.map((message) => {
3658
- const content = replaceFilesWithHandles(message.content, resolvePath);
3659
- return content === message.content ? message : {
3660
- ...message,
3661
- content
3662
- };
3663
- });
3664
- }
3665
- /** Replace every image occurrence for a text-only model. */
3666
- function replaceImagesForTextModel(blocks) {
3667
- let next;
3668
- for (const [index, block] of blocks.entries()) {
3669
- if (block.type === "image") {
3670
- next ??= blocks.slice(0, index);
3671
- next.push({
3672
- type: "text",
3673
- text: textOnlyImageText(block.attachment)
3674
- });
3675
- continue;
3676
- }
3677
- next?.push(block);
3678
- }
3679
- return next ?? blocks;
3680
- }
3681
- function projectImagesForTextModel(messages) {
3682
- if (!messages.some((message) => contentHasImage(message.content))) return messages;
3683
- return messages.map((message) => {
3684
- const content = replaceImagesForTextModel(message.content);
3685
- return content === message.content ? message : {
3686
- ...message,
3687
- content
3688
- };
3689
- });
3690
- }
3691
- /**
3692
- * Centralize the non-secret product identity every provider request sends as `User-Agent`, keeping
3693
- * adapters from drifting. See
3694
- * `.agents/notes/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md`.
3695
- *
3696
- * App-attribution vocabulary for provider requests.
3697
- * @module @deepseek-ai/dsh-llm/attribution
3698
- */
3699
- const { version } = createRequire(import.meta.url)("../package.json");
3700
- /**
3701
- * LLM service: adapter registry with a waterfall-interceptable streaming call
3702
- * API. Exports the `LlmRuntime` default, the abstract `LlmAdapter` for
3703
- * provider backends, and `BlockAssembler` for chunk assembly.
3704
- *
3705
- * @module @deepseek-ai/dsh-llm
3706
- */
3707
- var __runInitializers = function(thisArg, initializers, value) {
3708
- var useValue = arguments.length > 2;
3709
- for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
3710
- return useValue ? value : void 0;
3711
- };
3712
- var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
3713
- function accept(f) {
3714
- if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
3715
- return f;
3716
- }
3717
- var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
3718
- var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
3719
- var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
3720
- var _, done = false;
3721
- for (var i = decorators.length - 1; i >= 0; i--) {
3722
- var context = {};
3723
- for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
3724
- for (var p in contextIn.access) context.access[p] = contextIn.access[p];
3725
- context.addInitializer = function(f) {
3726
- if (done) throw new TypeError("Cannot add initializers after decoration has completed");
3727
- extraInitializers.push(accept(f || null));
3728
- };
3729
- var result = (0, decorators[i])(kind === "accessor" ? {
3730
- get: descriptor.get,
3731
- set: descriptor.set
3732
- } : descriptor[key], context);
3733
- if (kind === "accessor") {
3734
- if (result === void 0) continue;
3735
- if (result === null || typeof result !== "object") throw new TypeError("Object expected");
3736
- if (_ = accept(result.get)) descriptor.get = _;
3737
- if (_ = accept(result.set)) descriptor.set = _;
3738
- if (_ = accept(result.init)) initializers.unshift(_);
3739
- } else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
3740
- else descriptor[key] = _;
3741
- }
3742
- if (target) Object.defineProperty(target, contextIn.name, descriptor);
3743
- done = true;
3744
- };
3745
- /**
3746
- * Typed error for LLM-related failures. Extends {@link HarnessError}, so the
3747
- * `code` string (e.g. `AUTH`, `RATE_LIMIT`, `NO_ADAPTER`) is shared taxonomy.
3748
- */
3749
- var LlmError = class extends HarnessError {
3750
- /** Serializable facts retained beside this live Error. */
3751
- failure;
3752
- /**
3753
- * @param message - non-empty human-readable failure summary.
3754
- * @param code - non-empty stable provider-neutral machine code.
3755
- * @param options - optional cause and validated serializable provider facts.
3756
- */
3757
- constructor(message, code, options) {
3758
- if (typeof message !== "string" || message.length === 0) throw new Error("LlmError message must be a non-empty string");
3759
- if (typeof code !== "string" || code.length === 0) throw new Error("LlmError code must be a non-empty string");
3760
- if (options?.status !== void 0 && (!Number.isInteger(options.status) || options.status < 100 || options.status > 599)) throw new Error("LlmError status must be an integer from 100 through 599");
3761
- if (options?.providerRetryAfterMs !== void 0 && (!Number.isFinite(options.providerRetryAfterMs) || options.providerRetryAfterMs <= 0)) throw new Error("LlmError providerRetryAfterMs must be a positive finite number");
3762
- if (options?.requestId !== void 0 && (typeof options.requestId !== "string" || options.requestId.length === 0)) throw new Error("LlmError requestId must be a non-empty string");
3763
- super(message, code, options);
3764
- this.name = "LlmError";
3765
- this.failure = Object.freeze({
3766
- message,
3767
- code,
3768
- ...options?.status === void 0 ? {} : { status: options.status },
3769
- ...options?.providerRetryAfterMs === void 0 ? {} : { providerRetryAfterMs: options.providerRetryAfterMs },
3770
- ...options?.requestId === void 0 ? {} : { requestId: options.requestId },
3771
- ...options?.offloadImages === void 0 ? {} : { offloadImages: options.offloadImages }
3772
- });
3773
- }
3774
- };
3775
- (() => {
3776
- let _classSuper = TypertRemoteService;
3777
- let _instanceExtraInitializers = [];
3778
- let _listProviders_decorators;
3779
- let _listConfigurableProviders_decorators;
3780
- let _remoteDiscoverModels_decorators;
3781
- return class LlmRuntime extends _classSuper {
3782
- static {
3783
- const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
3784
- _listProviders_decorators = [Remote];
3785
- _listConfigurableProviders_decorators = [Remote];
3786
- _remoteDiscoverModels_decorators = [Remote("discoverModels")];
3787
- __esDecorate(this, null, _listProviders_decorators, {
3788
- kind: "method",
3789
- name: "listProviders",
3790
- static: false,
3791
- private: false,
3792
- access: {
3793
- has: (obj) => "listProviders" in obj,
3794
- get: (obj) => obj.listProviders
3795
- },
3796
- metadata: _metadata
3797
- }, null, _instanceExtraInitializers);
3798
- __esDecorate(this, null, _listConfigurableProviders_decorators, {
3799
- kind: "method",
3800
- name: "listConfigurableProviders",
3801
- static: false,
3802
- private: false,
3803
- access: {
3804
- has: (obj) => "listConfigurableProviders" in obj,
3805
- get: (obj) => obj.listConfigurableProviders
3806
- },
3807
- metadata: _metadata
3808
- }, null, _instanceExtraInitializers);
3809
- __esDecorate(this, null, _remoteDiscoverModels_decorators, {
3810
- kind: "method",
3811
- name: "remoteDiscoverModels",
3812
- static: false,
3813
- private: false,
3814
- access: {
3815
- has: (obj) => "remoteDiscoverModels" in obj,
3816
- get: (obj) => obj.remoteDiscoverModels
3817
- },
3818
- metadata: _metadata
3819
- }, null, _instanceExtraInitializers);
3820
- if (_metadata) Object.defineProperty(this, Symbol.metadata, {
3821
- enumerable: true,
3822
- configurable: true,
3823
- writable: true,
3824
- value: _metadata
3825
- });
3826
- }
3827
- adapters = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Map());
3828
- directory = /* @__PURE__ */ new Map();
3829
- discoveries = /* @__PURE__ */ new Map();
3830
- constructor(ctx) {
3831
- super(ctx, "llm");
3832
- }
3833
- /** Notify topology observers without letting one broken listener veto the commit. */
3834
- emitAdaptersUpdated() {
3835
- let invariantFailure;
3836
- for (const listener of this.ctx.events.dispatch("emit", ["llm/adapters-updated"])) try {
3837
- const returned = listener();
3838
- if (returned != null && typeof returned.then === "function") Promise.resolve(returned).then(void 0, (error) => {
3839
- this.warnAdaptersListenerFailure(error);
3840
- });
3841
- } catch (error) {
3842
- if (error?.code === "INVARIANT") {
3843
- invariantFailure ??= error;
3844
- continue;
3845
- }
3846
- this.warnAdaptersListenerFailure(error);
3847
- }
3848
- if (invariantFailure !== void 0) throw invariantFailure;
3849
- }
3850
- /** Contained-listener diagnostic shared by the sync and async failure paths. */
3851
- warnAdaptersListenerFailure(error) {
3852
- this.ctx.logger.warn("llm: an llm/adapters-updated listener failed");
3853
- this.ctx.logger.warn(error);
3854
- }
3855
- /**
3856
- * Register an adapter for the given provider routes. Throws `LlmError` with code
3857
- * `DUPLICATE_ADAPTER` if any provider already has an adapter (all-or-nothing).
3858
- * Disposed with the fiber.
3859
- * @param providers - every provider route this adapter should serve.
3860
- * @param adapter - the adapter that streams calls for those providers.
3861
- * @returns the disposer, carrying {@link AdapterRegistrationHandle.replace}.
3862
- */
3863
- registerAdapter(providers, adapter) {
3864
- const owned = /* @__PURE__ */ new Set();
3865
- let released = false;
3866
- const dispose = this.ctx.effect(function* () {
3867
- if (providers.length === 0) throw new LlmError("an adapter must register at least one provider", "INVALID_ADAPTER");
3868
- this.commitRoutes(owned, this.prepareRoutes(providers, adapter, owned));
3869
- yield () => {
3870
- released = true;
3871
- for (const provider of owned) this.adapters.delete(provider);
3872
- owned.clear();
3873
- this.emitAdaptersUpdated();
3874
- };
3875
- }.bind(this), "llm.registerAdapter()");
3876
- const handle = (() => void dispose());
3877
- handle.replace = (next) => {
3878
- if (released) throw new LlmError("a disposed adapter registration cannot replace its routes", "REGISTRATION_DISPOSED");
3879
- this.commitRoutes(owned, this.prepareRoutes(next, adapter, owned));
3880
- };
3881
- return handle;
3882
- }
3883
- /**
3884
- * Validate one candidate route set for `adapter`, treating routes this
3885
- * registration already holds as available. Nothing is mutated: a rejected
3886
- * candidate leaves the registry exactly as it was.
3887
- */
3888
- prepareRoutes(providers, adapter, owned) {
3889
- const unique = /* @__PURE__ */ new Set();
3890
- const registrations = [];
3891
- for (const provider of providers) {
3892
- if (provider.length === 0) throw new LlmError("adapter provider names must be non-empty", "INVALID_ADAPTER");
3893
- if (unique.has(provider) || this.adapters.has(provider) && !owned.has(provider)) throw new LlmError(`an adapter for provider "${provider}" is already registered`, "DUPLICATE_ADAPTER");
3894
- const info = adapter.providerInfo(provider);
3895
- if (typeof info.id !== "string" || info.id !== provider || typeof info.name !== "string" || info.name.length === 0) throw new LlmError(`adapter metadata for provider "${provider}" must preserve its id and have a non-empty name`, "INVALID_ADAPTER");
3896
- unique.add(provider);
3897
- const retryPolicy = adapter.providerRetryPolicy(provider) ?? resolveRetryPolicy(void 0, `llm: provider "${provider}" retryPolicy`);
3898
- registrations.push({
3899
- adapter,
3900
- provider: {
3901
- id: info.id,
3902
- name: info.name
3903
- },
3904
- retryPolicy
3905
- });
3906
- }
3907
- return registrations;
3908
- }
3909
- /**
3910
- * Swap this registration's routes for the prepared ones in one synchronous
3911
- * section, so no observer can see the registry between the release and the
3912
- * re-registration. The route set's one mutation point is also where
3913
- * `llm/adapters-updated` is published, so a `replace` announces itself
3914
- * exactly like a first registration.
3915
- */
3916
- commitRoutes(owned, registrations) {
3917
- for (const provider of owned) this.adapters.delete(provider);
3918
- owned.clear();
3919
- for (const registration of registrations) {
3920
- this.adapters.set(registration.provider.id, registration);
3921
- owned.add(registration.provider.id);
3922
- }
3923
- this.emitAdaptersUpdated();
3924
- }
3925
- /**
3926
- * Describe provider routes with a registered adapter.
3927
- * @returns detached provider metadata in registration order.
3928
- */
3929
- listProviders() {
3930
- return [...this.adapters.values()].map(({ provider }) => ({ ...provider }));
3931
- }
3932
- /**
3933
- * Declare provider routes an adapter plugin can activate through
3934
- * configuration. Registration is all-or-nothing: an empty list, invalid
3935
- * entry, or a provider already declared by any registration throws
3936
- * `LlmError` without registering the rest. Disposed with the fiber.
3937
- * @param entries - every configurable provider this plugin owns.
3938
- * @returns a handle that withdraws all of them, and can atomically replace them.
3939
- */
3940
- registerConfigurableProviders(entries) {
3941
- let held = [];
3942
- let disposed = false;
3943
- /**
3944
- * Validate a candidate set in full against everything this registration
3945
- * does not already hold, then publish it. Nothing is written until the
3946
- * whole set passes, so a refused candidate leaves the current entries in
3947
- * place — the property that makes `replace` a swap rather than a
3948
- * delete-then-add that can strand the directory empty.
3949
- */
3950
- const commit = (candidates) => {
3951
- const detached = [];
3952
- const own = new Set(held.map((entry) => entry.provider));
3953
- for (const entry of candidates) {
3954
- if (entry.provider.length === 0 || entry.displayName.length === 0 || entry.settingsNs.length === 0) throw new LlmError("configurable providers need a non-empty provider, displayName, and settingsNs", "INVALID_DIRECTORY");
3955
- if (entry.settingsPath.some((segment) => segment.length === 0)) throw new LlmError(`configurable provider "${entry.provider}" has an empty settingsPath segment`, "INVALID_DIRECTORY");
3956
- if (this.directory.has(entry.provider) && !own.has(entry.provider) || detached.some((seen) => seen.provider === entry.provider)) throw new LlmError(`configurable provider "${entry.provider}" is already declared`, "DUPLICATE_DIRECTORY");
3957
- detached.push({
3958
- ...entry,
3959
- settingsPath: [...entry.settingsPath]
3960
- });
3961
- }
3962
- for (const entry of held) this.directory.delete(entry.provider);
3963
- for (const entry of detached) this.directory.set(entry.provider, entry);
3964
- held = detached;
3965
- this.emitAdaptersUpdated();
3966
- };
3967
- const dispose = this.ctx.effect(function* () {
3968
- if (entries.length === 0) throw new LlmError("a configurable-provider registration must declare at least one provider", "INVALID_DIRECTORY");
3969
- commit(entries);
3970
- yield () => {
3971
- disposed = true;
3972
- for (const entry of held) this.directory.delete(entry.provider);
3973
- held = [];
3974
- this.emitAdaptersUpdated();
3975
- };
3976
- }.bind(this), "llm.registerConfigurableProviders()");
3977
- const handle = (() => void dispose());
3978
- handle.replace = (next) => {
3979
- if (disposed) throw new LlmError("this configurable-provider registration was disposed", "REGISTRATION_DISPOSED");
3980
- commit(next);
3981
- };
3982
- return handle;
3983
- }
3984
- /**
3985
- * List every declared configurable provider, registered or dormant.
3986
- * @returns detached directory entries in declaration order.
3987
- */
3988
- listConfigurableProviders() {
3989
- return [...this.directory.values()].map((entry) => ({
3990
- ...entry,
3991
- settingsPath: [...entry.settingsPath]
3992
- }));
3993
- }
3994
- /**
3995
- * Offer to interrogate provider endpoints on behalf of the settings
3996
- * namespace this plugin owns. The namespace is the key because that is what
3997
- * a configuration surface already holds from the configurable-provider
3998
- * directory, and because a provider being *added* has no route to name yet.
3999
- * Disposed with the fiber.
4000
- * @param settingsNs - the namespace whose profiles this discovery serves.
4001
- * @param discover - interrogates one endpoint and must honor the supplied signal.
4002
- * @returns the disposer that withdraws the offer.
4003
- */
4004
- registerModelDiscovery(settingsNs, discover) {
4005
- const dispose = this.ctx.effect(function* () {
4006
- if (settingsNs.length === 0) throw new LlmError("model discovery needs a non-empty settings namespace", "INVALID_DISCOVERY");
4007
- if (this.discoveries.has(settingsNs)) throw new LlmError(`model discovery for "${settingsNs}" is already registered`, "DUPLICATE_DISCOVERY");
4008
- this.discoveries.set(settingsNs, discover);
4009
- yield () => {
4010
- this.discoveries.delete(settingsNs);
4011
- };
4012
- }.bind(this), "llm.registerModelDiscovery()");
4013
- return () => void dispose();
4014
- }
4015
- /**
4016
- * Interrogate one provider endpoint for the models it advertises. The
4017
- * request describes a draft, not a stored route, so nothing here reads or
4018
- * writes settings or credentials — the caller owns both, and the reply is
4019
- * candidate metadata a surface may offer for adoption.
4020
- * @param settingsNs - namespace whose registered discovery serves this draft.
4021
- * @param request - the endpoint, protocol, and one-shot credential to use.
4022
- * @param signal - caller cancellation.
4023
- * @returns the advertised models, deduplicated in endpoint order.
4024
- */
4025
- async discoverModels(settingsNs, request, signal) {
4026
- const discover = this.discoveries.get(settingsNs);
4027
- if (discover === void 0) throw new LlmError(`no model discovery is registered for "${settingsNs}"`, "NO_DISCOVERY");
4028
- if ((request.provider ?? "").length === 0 && (request.baseURL ?? "").length === 0) throw new LlmError("model discovery needs a provider route or a baseURL", "INVALID_DISCOVERY");
4029
- const discovered = signal === void 0 ? await discover(request) : await discover(request, signal);
4030
- const seen = /* @__PURE__ */ new Set();
4031
- const models = [];
4032
- for (const model of discovered) {
4033
- if (typeof model.id !== "string" || model.id.length === 0 || seen.has(model.id)) continue;
4034
- seen.add(model.id);
4035
- models.push({
4036
- id: model.id,
4037
- ...model.name === void 0 ? {} : { name: model.name },
4038
- ...model.contextWindow === void 0 ? {} : { contextWindow: model.contextWindow },
4039
- ...model.maxTokens === void 0 ? {} : { maxTokens: model.maxTokens },
4040
- ...model.inputModalities === void 0 ? {} : { inputModalities: [...model.inputModalities] }
4041
- });
4042
- }
4043
- return models;
4044
- }
4045
- /**
4046
- * Remote adapter for one draft provider interrogation.
4047
- * @param settingsNs - namespace whose registered discovery serves this draft.
4048
- * @param request - endpoint, protocol, and one-shot credential to use.
4049
- * @param signal - caller cancellation supplied by the Remote carrier.
4050
- * @returns advertised models in endpoint order.
4051
- * @throws RemoteError with `llm/model-discovery-rejected` when discovery refuses or fails.
4052
- */
4053
- async remoteDiscoverModels(settingsNs, request, signal) {
4054
- try {
4055
- return await this.discoverModels(settingsNs, request, signal);
4056
- } catch (error) {
4057
- throw new RemoteError("llm/model-discovery-rejected", error instanceof Error ? error.message : String(error), {
4058
- settingsNs,
4059
- ...request.baseURL === void 0 ? {} : { baseURL: request.baseURL }
4060
- }, { cause: error });
4061
- }
4062
- }
4063
- /**
4064
- * Resolve the retry policy captured when one provider route was registered.
4065
- * @param provider - registered provider route to inspect.
4066
- * @returns the provider-owned policy, with normal defaults already resolved.
4067
- */
4068
- providerRetryPolicy(provider) {
4069
- return this.registration(provider).retryPolicy;
4070
- }
4071
- /**
4072
- * Resolve provider-side request-image pricing for one exact route, or
4073
- * `undefined` when the provider is unregistered or declares none. Unknown
4074
- * providers degrade to `undefined` rather than throwing because callers
4075
- * price durable history whose route may no longer be mounted.
4076
- * @param provider - provider route named by a request header.
4077
- * @param model - exact model id named by the same header.
4078
- * @returns the owning adapter's image pricing for the route, when declared.
4079
- */
4080
- imageRequestPricing(provider, model) {
4081
- return this.adapters.get(provider)?.adapter.imageRequestPricing(provider, model);
4082
- }
4083
- /**
4084
- * Resolve the exact text one durable file occurrence contributes to every
4085
- * provider request in the current execution environment.
4086
- * @param ref - durable verbatim file reference from model history.
4087
- * @returns the same deterministic handle text used at adapter dispatch.
4088
- */
4089
- fileRequestText(ref) {
4090
- return fileHandleText(ref, this.fileReadPath(ref));
4091
- }
4092
- /** Detach typed adapter-owned modality metadata. */
4093
- detachedModalities(modalities) {
4094
- return modalities === void 0 ? void 0 : [...modalities];
4095
- }
4096
- /**
4097
- * Discover models advertised by one registered provider. Catalog membership
4098
- * is advisory and never changes routing or request validation.
4099
- * @param provider - registered provider route to inspect.
4100
- * @returns detached model metadata in adapter-preferred order.
4101
- */
4102
- async listModels(provider) {
4103
- const models = await this.registration(provider).adapter.listModels(provider);
4104
- const seen = /* @__PURE__ */ new Set();
4105
- return models.map((model) => {
4106
- if (typeof model.provider !== "string" || model.provider !== provider || typeof model.id !== "string" || model.id.length === 0 || typeof model.name !== "string" || model.name.length === 0 || model.description !== void 0 && typeof model.description !== "string" || seen.has(model.id)) throw new LlmError(`adapter returned invalid or duplicate model metadata for provider "${provider}"`, "INVALID_CATALOG");
4107
- seen.add(model.id);
4108
- const inputModalities = this.detachedModalities(model.inputModalities);
4109
- return {
4110
- provider: model.provider,
4111
- id: model.id,
4112
- name: model.name,
4113
- ...model.description === void 0 ? {} : { description: model.description },
4114
- ...inputModalities === void 0 ? {} : { inputModalities }
4115
- };
4116
- });
4117
- }
4118
- /**
4119
- * Resolve and validate all metadata from the adapter that owns one exact
4120
- * route. The result is detached from adapter-owned objects; catalog
4121
- * membership remains advisory and does not control request routing.
4122
- * @param provider - registered provider route to inspect.
4123
- * @param model - exact model id passed to the adapter.
4124
- * @param signal - optional cancellation for adapter-owned asynchronous lookup.
4125
- * @returns exact model identity plus available context and reasoning metadata.
4126
- */
4127
- async resolveModelInfo(provider, model, signal) {
4128
- return this.resolveModelInfoFor(this.registration(provider), model, signal);
4129
- }
4130
- async resolveModelInfoFor(registration, model, signal) {
4131
- const resolved = await registration.adapter.resolveModel(registration.provider.id, model, signal);
4132
- return this.normalizeModelInfo(registration, model, resolved);
4133
- }
4134
- /** Validate and detach one adapter-returned exact model result. */
4135
- normalizeModelInfo(registration, model, resolved) {
4136
- const provider = registration.provider.id;
4137
- if (typeof resolved.provider !== "string" || resolved.provider !== provider || typeof resolved.id !== "string" || resolved.id !== model || typeof resolved.name !== "string" || resolved.name.length === 0 || resolved.description !== void 0 && typeof resolved.description !== "string") throw new LlmError(`adapter returned invalid exact model metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
4138
- const context = resolved.context;
4139
- if (context !== void 0 && (!Number.isInteger(context.contextWindow) || context.contextWindow <= 0)) throw new LlmError(`adapter returned invalid context metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_CONTEXT");
4140
- const inputModalities = this.detachedModalities(resolved.inputModalities);
4141
- const systemPromptUpdate = resolved.systemPromptUpdate;
4142
- if (systemPromptUpdate !== void 0 && systemPromptUpdate !== "in-history") throw new LlmError(`adapter returned invalid system prompt update mode for provider "${provider}" model "${model}"`, "INVALID_MODEL_INFO");
4143
- const defaultMaxTokens = resolved.defaultMaxTokens;
4144
- if (defaultMaxTokens !== void 0 && (!Number.isSafeInteger(defaultMaxTokens) || defaultMaxTokens <= 0)) throw new LlmError(`adapter returned invalid default maxTokens for provider "${provider}" model "${model}"`, "INVALID_MODEL_MAX_TOKENS");
4145
- const info = {
4146
- provider,
4147
- id: model,
4148
- name: resolved.name,
4149
- ...resolved.description === void 0 ? {} : { description: resolved.description },
4150
- ...inputModalities === void 0 ? {} : { inputModalities },
4151
- ...context === void 0 ? {} : { context: { contextWindow: context.contextWindow } },
4152
- ...defaultMaxTokens === void 0 ? {} : { defaultMaxTokens },
4153
- ...resolved.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: resolved.systemPromptUpdate }
4154
- };
4155
- const reasoning = resolved.reasoning;
4156
- if (reasoning === void 0) return info;
4157
- if (reasoning.efforts.length === 0) throw new LlmError(`adapter returned invalid reasoning metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4158
- const seen = /* @__PURE__ */ new Set();
4159
- const efforts = reasoning.efforts.map((effort) => {
4160
- if (typeof effort.id !== "string" || effort.id.length === 0 || typeof effort.name !== "string" || effort.name.length === 0 || effort.description !== void 0 && typeof effort.description !== "string" || seen.has(effort.id)) throw new LlmError(`adapter returned invalid or duplicate reasoning effort metadata for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4161
- seen.add(effort.id);
4162
- return {
4163
- id: effort.id,
4164
- name: effort.name,
4165
- ...effort.description === void 0 ? {} : { description: effort.description }
4166
- };
4167
- });
4168
- if (reasoning.defaultEffort !== void 0 && !seen.has(reasoning.defaultEffort)) throw new LlmError(`adapter returned an unknown default reasoning effort for provider "${provider}" model "${model}"`, "INVALID_MODEL_REASONING");
4169
- return {
4170
- ...info,
4171
- reasoning: {
4172
- efforts,
4173
- ...reasoning.defaultEffort === void 0 ? {} : { defaultEffort: reasoning.defaultEffort }
4174
- }
4175
- };
4176
- }
4177
- /**
4178
- * Validate a conversation call config against its exact model capability and
4179
- * materialize adapter-configured defaults. Unsupported explicit efforts
4180
- * reject before provider I/O; no clamping or aliasing is performed. This
4181
- * standalone query does not bind a later dispatch; use {@link prepareCall}
4182
- * when logging and streaming must share one adapter registration.
4183
- * @param config - provider/model route and optional request controls.
4184
- * @param signal - optional cancellation for adapter-owned capability lookup.
4185
- * @returns a detached config only when a default must be materialized.
4186
- */
4187
- async resolveCallConfig(config, signal) {
4188
- return (await this.resolveCallFor(this.registration(config.provider), config, signal)).config;
4189
- }
4190
- async resolveCallFor(registration, config, signal) {
4191
- const info = await this.resolveModelInfoFor(registration, config.model, signal);
4192
- return this.resolveCallWithInfo(config, info);
4193
- }
4194
- /** Validate request controls against one already-bound exact model result. */
4195
- resolveCallWithInfo(config, info) {
4196
- const defaulted = config.maxTokens === void 0 && info.defaultMaxTokens !== void 0 ? {
4197
- ...config,
4198
- maxTokens: info.defaultMaxTokens
4199
- } : config;
4200
- const reasoning = info.reasoning;
4201
- const requested = defaulted.reasoningEffort;
4202
- let resolvedConfig = defaulted;
4203
- if (reasoning === void 0) {
4204
- if (requested !== void 0) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${requested}"`, "UNSUPPORTED_REASONING_EFFORT");
4205
- } else {
4206
- const effective = requested ?? reasoning.defaultEffort;
4207
- if (effective !== void 0) {
4208
- if (!reasoning.efforts.some((effort) => effort.id === effective)) throw new LlmError(`provider "${config.provider}" model "${config.model}" does not support reasoning effort "${effective}"`, "UNSUPPORTED_REASONING_EFFORT");
4209
- if (requested !== effective) resolvedConfig = {
4210
- ...defaulted,
4211
- reasoningEffort: effective
4212
- };
4213
- }
4214
- }
4215
- return {
4216
- config: resolvedConfig,
4217
- ...info.context === void 0 ? {} : { context: info.context },
4218
- modelInfo: info
4219
- };
4220
- }
4221
- /**
4222
- * Resolve one call under its current adapter registration. The returned
4223
- * one-shot handle keeps that registration across header logging and dispatch,
4224
- * so HMR cannot combine one adapter's capability result with another adapter.
4225
- * @param config - provider/model route and optional request controls.
4226
- * @param signal - optional cancellation for adapter-owned capability lookup.
4227
- * @returns a prepared config and its registration-bound stream entry point.
4228
- */
4229
- async prepareCall(config, signal) {
4230
- const registration = this.registration(config.provider);
4231
- const adapterCall = await registration.adapter.prepareCall(config.provider, config.model, signal);
4232
- const modelInfo = this.normalizeModelInfo(registration, config.model, adapterCall.model);
4233
- const resolved = this.resolveCallWithInfo(config, modelInfo);
4234
- const resolvedConfig = deepFreeze(structuredClone(resolved.config));
4235
- const context = resolved.context === void 0 ? void 0 : deepFreeze(structuredClone(resolved.context));
4236
- const adapterDefaults = deepFreeze({
4237
- ...config.reasoningEffort === void 0 && resolvedConfig.reasoningEffort !== void 0 ? { reasoningEffort: true } : {},
4238
- ...config.maxTokens === void 0 && resolvedConfig.maxTokens !== void 0 ? { maxTokens: true } : {}
4239
- });
4240
- let dispatched = false;
4241
- return Object.freeze({
4242
- config: resolvedConfig,
4243
- retryPolicy: registration.retryPolicy,
4244
- adapterDefaults,
4245
- ...context === void 0 ? {} : { context },
4246
- ...modelInfo.inputModalities === void 0 ? {} : { inputModalities: Object.freeze([...modelInfo.inputModalities]) },
4247
- ...modelInfo.systemPromptUpdate === void 0 ? {} : { systemPromptUpdate: modelInfo.systemPromptUpdate },
4248
- stream: (options) => {
4249
- if (dispatched) throw new LlmError("a prepared LLM call can only be dispatched once", "INVALID_PREPARED_CALL");
4250
- if (!callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
4251
- dispatched = true;
4252
- return this.streamWithRegistration(options, {
4253
- registration,
4254
- config: resolvedConfig,
4255
- modelInfo,
4256
- dispatch: (options) => adapterCall.stream(options)
4257
- });
4258
- }
4259
- });
4260
- }
4261
- registration(provider) {
4262
- const registration = this.adapters.get(provider);
4263
- if (!registration) throw new LlmError(`no adapter registered for provider "${provider}"`, "NO_ADAPTER");
4264
- return registration;
4265
- }
4266
- /** Remove replay state whose historical route is owned by another adapter. */
4267
- forAdapter(options, adapter) {
4268
- const messages = options.messages.map((message) => {
4269
- if (message.role !== "assistant") return message;
4270
- const source = message.source;
4271
- if (source.replayState === void 0) return message;
4272
- if (this.adapters.get(source.provider)?.adapter === adapter) return message;
4273
- return freezeMessage({
4274
- ...message,
4275
- source: {
4276
- kind: "model",
4277
- provider: source.provider,
4278
- model: source.model
4279
- }
4280
- });
4281
- });
4282
- if (messages.every((message, index) => message === options.messages[index])) return options;
4283
- const filtered = {
4284
- ...options,
4285
- messages
4286
- };
4287
- return Object.isFrozen(options) ? deepFreeze(filtered) : filtered;
4288
- }
4289
- /**
4290
- * Resolve the current execution-world read path of one durable file
4291
- * reference through the mounted attachment and filesystem providers.
4292
- */
4293
- fileReadPath(ref) {
4294
- let hostPath;
4295
- try {
4296
- hostPath = this.ctx.get("attachments")?.fileHostPath(ref);
4297
- } catch {
4298
- return;
4299
- }
4300
- if (hostPath === void 0) return void 0;
4301
- return this.ctx.get("fs")?.processPathFromHostPath(hostPath);
4302
- }
4303
- /**
4304
- * Final adapter boundary. Adapter selection, dispatch, iterator construction,
4305
- * and iteration failures become one terminal failure chunk. Middleware and
4306
- * downstream consumer failures remain thrown plugin or consumer errors.
4307
- */
4308
- async *adapterStream(options, prepared) {
4309
- let iterator;
4310
- try {
4311
- const registration = prepared?.registration ?? this.registration(options.provider);
4312
- const adapter = registration.adapter;
4313
- let modelInfo;
4314
- let resolvedConfig;
4315
- let dispatch;
4316
- if (prepared === void 0) {
4317
- const adapterCall = await adapter.prepareCall(options.provider, options.model, options.signal);
4318
- modelInfo = this.normalizeModelInfo(registration, options.model, adapterCall.model);
4319
- resolvedConfig = this.resolveCallWithInfo(options, modelInfo).config;
4320
- dispatch = (options) => adapterCall.stream(options);
4321
- } else {
4322
- modelInfo = prepared.modelInfo;
4323
- resolvedConfig = prepared.config;
4324
- dispatch = prepared.dispatch;
4325
- }
4326
- if (prepared !== void 0 && !callConfigEquals(options, resolvedConfig)) throw new LlmError("prepared LLM call config changed before adapter dispatch", "INVALID_PREPARED_CALL");
4327
- const resolvedOptions = callConfigEquals(options, resolvedConfig) ? options : Object.isFrozen(options) ? deepFreeze({
4328
- ...options,
4329
- ...resolvedConfig
4330
- }) : {
4331
- ...options,
4332
- ...resolvedConfig
4333
- };
4334
- let projectedMessages = resolvedOptions.messages;
4335
- if (projectedMessages.some((message) => contentHasFile(message.content))) projectedMessages = projectFilesToText(projectedMessages, (ref) => this.fileReadPath(ref));
4336
- if (modelInfo.inputModalities !== void 0 && !modelInfo.inputModalities.includes("image") && projectedMessages.some((message) => contentHasImage(message.content))) projectedMessages = projectImagesForTextModel(projectedMessages);
4337
- const projectedOptions = projectedMessages === resolvedOptions.messages ? resolvedOptions : Object.isFrozen(resolvedOptions) ? deepFreeze({
4338
- ...resolvedOptions,
4339
- messages: projectedMessages
4340
- }) : {
4341
- ...resolvedOptions,
4342
- messages: projectedMessages
4343
- };
4344
- iterator = dispatch(this.forAdapter(projectedOptions, adapter))[Symbol.asyncIterator]();
4345
- } catch (error) {
4346
- yield adapterFailureChunk(error, options.signal);
4347
- return;
4348
- }
4349
- let completed = false;
4350
- try {
4351
- while (true) {
4352
- let item;
4353
- try {
4354
- const next = await iterator.next();
4355
- item = next.done ? { done: true } : {
4356
- done: false,
4357
- value: next.value
4358
- };
4359
- } catch (error) {
4360
- completed = true;
4361
- yield adapterFailureChunk(error, options.signal);
4362
- return;
4363
- }
4364
- if (item.done) {
4365
- completed = true;
4366
- return;
4367
- }
4368
- yield item.value;
4369
- }
4370
- } finally {
4371
- if (!completed) {
4372
- const close = iterator.return?.bind(iterator);
4373
- if (close) await close();
4374
- }
4375
- }
4376
- }
4377
- /**
4378
- * Stream one model call as raw chunks (token-level deltas). Replay state is
4379
- * retained only when the same adapter instance owns its historical provider
4380
- * and the target provider. Final adapter selection remains fixed through
4381
- * asynchronous exact-model resolution and dispatch. Adapter selection,
4382
- * dispatch, and iteration failures become terminal `error` or `aborted`
4383
- * finish chunks; middleware, nested-call, cleanup, and consumer failures
4384
- * remain thrown.
4385
- * @param options - the full request; `options.provider` selects the adapter.
4386
- * @returns the chunk stream, possibly wrapped by `llm/stream` listeners.
4387
- */
4388
- stream(options) {
4389
- return this.streamWithRegistration(options);
4390
- }
4391
- streamWithRegistration(options, prepared) {
4392
- return this.ctx.waterfall(this, "llm/stream", options, () => this.adapterStream(options, prepared));
4393
- }
4394
- };
4395
- })();
4396
- /** Convert one adapter throw into the stream protocol's terminal outcome. */
4397
- function adapterFailureChunk(error, signal) {
4398
- const failure = normalizeLlmFailure(error);
4399
- return {
4400
- type: "finish",
4401
- reason: signal?.aborted || failure.code === "ABORTED" ? {
4402
- kind: "aborted",
4403
- failure
4404
- } : {
4405
- kind: "error",
4406
- failure
4407
- }
4408
- };
3971
+ if (!(typeof stream.listenerCount === "function" && stream.listenerCount("error") > 0)) stream.on("error", () => {});
3972
+ }
3973
+ }
3974
+ /**
3975
+ * Defensively log to console.error without letting stderr write failures
3976
+ * (e.g. ENOSPC from SyncWriteStream on redirected logs) crash the host process.
3977
+ */
3978
+ function safeConsoleError(message, ...args) {
3979
+ try {
3980
+ console.error(message, ...args);
3981
+ } catch {}
4409
3982
  }
4410
3983
  //#endregion
4411
3984
  //#region src/host-ai.ts
@@ -4554,6 +4127,780 @@ async function parseTaskDraft(llm, request, signal) {
4554
4127
  }
4555
4128
  }
4556
4129
  //#endregion
4130
+ //#region src/host/agent-tools.ts
4131
+ /** Model-facing JSON rendering shared by every tool. */
4132
+ function renderJson(_args, value) {
4133
+ return [{
4134
+ type: "text",
4135
+ text: JSON.stringify(value, null, 2)
4136
+ }];
4137
+ }
4138
+ /** Mark an already JSON-safe projection as the tool's canonical value. */
4139
+ function json(value) {
4140
+ return value;
4141
+ }
4142
+ /** A domain refusal the model is expected to read and act on. */
4143
+ function refused(code, message) {
4144
+ return json({
4145
+ ok: false,
4146
+ code,
4147
+ message
4148
+ });
4149
+ }
4150
+ function messageOf(error) {
4151
+ return error instanceof Error ? error.message : String(error);
4152
+ }
4153
+ function has(value, key) {
4154
+ return Object.prototype.hasOwnProperty.call(value, key);
4155
+ }
4156
+ /**
4157
+ * The calling session id, for the execution audit trail (the same stamp the
4158
+ * browser asserts). Absent when a call has no agent behind it.
4159
+ */
4160
+ function callingSessionId(exec) {
4161
+ const agent = exec.agent;
4162
+ const id = agent?.session?.id ?? agent?.session?.header?.id;
4163
+ return typeof id === "string" && id !== "" ? id : void 0;
4164
+ }
4165
+ function scheduleView(schedule) {
4166
+ return {
4167
+ enabled: schedule.enabled,
4168
+ cron: schedule.cron,
4169
+ ...schedule.nextRunAt === void 0 ? {} : { nextRunAt: schedule.nextRunAt },
4170
+ ...schedule.lastTriggeredAt === void 0 ? {} : { lastTriggeredAt: schedule.lastTriggeredAt }
4171
+ };
4172
+ }
4173
+ function tagView(tag) {
4174
+ return {
4175
+ name: tag.name,
4176
+ ...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
4177
+ };
4178
+ }
4179
+ function executionView(execution) {
4180
+ return {
4181
+ id: execution.id,
4182
+ ...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId },
4183
+ startedAt: execution.startedAt,
4184
+ ...execution.endedAt === void 0 ? {} : { endedAt: execution.endedAt },
4185
+ ...execution.result === void 0 ? {} : { result: execution.result },
4186
+ ...execution.error === void 0 ? {} : { error: execution.error },
4187
+ ...execution.initiatedBy === void 0 ? {} : { initiatedBy: execution.initiatedBy },
4188
+ ...execution.runGroupId === void 0 ? {} : { cascade: true },
4189
+ ...execution.ownResult === void 0 ? {} : {
4190
+ ownResult: execution.ownResult,
4191
+ awaitingSubtasks: execution.endedAt === void 0
4192
+ }
4193
+ };
4194
+ }
4195
+ /**
4196
+ * One compact task row for the model: identity, column, lineage, execution
4197
+ * targets, schedule, and the latest run. The prompt body rides only
4198
+ * task_board_get, so a fifty-task list stays readable.
4199
+ */
4200
+ function taskSummary(task, tasks, sessionDefault) {
4201
+ const latest = task.executions.at(-1);
4202
+ const subtaskCount = directSubtasks(tasks, task.id).length;
4203
+ return {
4204
+ id: task.id,
4205
+ title: task.title,
4206
+ status: task.status,
4207
+ description: task.description,
4208
+ ...task.parentId === void 0 ? {} : { parentId: task.parentId },
4209
+ ...subtaskCount === 0 ? {} : { subtaskCount },
4210
+ ...task.archivedAt === void 0 ? {} : { archivedAt: task.archivedAt },
4211
+ ...task.tags === void 0 ? {} : { tags: task.tags.map(tagView) },
4212
+ ...task.workspaceId === void 0 ? {} : { workspaceId: task.workspaceId },
4213
+ ...task.mode === void 0 ? {} : { mode: task.mode },
4214
+ ...task.permission === void 0 ? {} : { permission: task.permission },
4215
+ ...task.model === void 0 ? {} : { model: task.model },
4216
+ ...task.reuseSession === true ? { reuseSession: true } : {},
4217
+ ...task.teamRun === true ? { teamRun: true } : {},
4218
+ ...task.schedule === void 0 ? {} : { schedule: scheduleView(task.schedule) },
4219
+ ...task.freeze === void 0 ? {} : { continuationCard: true },
4220
+ ...task.permissionConfirmedAt === void 0 ? {} : { permissionConfirmedAt: task.permissionConfirmedAt },
4221
+ ...requiresPermissionConfirmation(task, sessionDefault ?? "read-only") ? { permissionPending: true } : {},
4222
+ executionCount: task.executions.length,
4223
+ ...latest === void 0 ? {} : { latestExecution: executionView(latest) },
4224
+ createdAt: task.createdAt,
4225
+ updatedAt: task.updatedAt
4226
+ };
4227
+ }
4228
+ /** One task's full detail: content, lineage, schedule, and recent runs. */
4229
+ function taskDetail(task, tasks, sessionDefault) {
4230
+ const parent = task.parentId === void 0 ? void 0 : tasks.find((item) => item.id === task.parentId);
4231
+ return {
4232
+ ...taskSummary(task, tasks, sessionDefault),
4233
+ prompt: task.prompt,
4234
+ depth: taskDepth(tasks, task.id),
4235
+ ...parent === void 0 ? {} : { parent: {
4236
+ id: parent.id,
4237
+ title: parent.title,
4238
+ status: parent.status
4239
+ } },
4240
+ ...task.freeze === void 0 ? {} : { freeze: {
4241
+ goal: task.freeze.goal,
4242
+ progress: task.freeze.progress,
4243
+ next: task.freeze.next,
4244
+ frozenAt: task.freeze.frozenAt,
4245
+ ...task.freeze.frozenBy === void 0 ? {} : { frozenBy: task.freeze.frozenBy },
4246
+ ...task.freeze.redacted === true ? { redacted: true } : {}
4247
+ } },
4248
+ ...task.handover === void 0 ? {} : { handover: {
4249
+ ...task.handover.workspaceId === void 0 ? {} : { workspaceId: task.handover.workspaceId },
4250
+ ...task.handover.mode === void 0 ? {} : { mode: task.handover.mode },
4251
+ ...task.handover.permission === void 0 ? {} : { permission: task.handover.permission },
4252
+ references: task.handover.references,
4253
+ bundledAt: task.handover.bundledAt
4254
+ } },
4255
+ subtasks: directSubtasks(tasks, task.id).map((child) => taskSummary(child, tasks, sessionDefault)),
4256
+ executions: task.executions.slice(-10).map(executionView)
4257
+ };
4258
+ }
4259
+ /** The executions a run opened: everything that was not in the ledger before. */
4260
+ function openedExecutions(before, after) {
4261
+ const known = new Set(before.map((execution) => execution.id));
4262
+ return after.filter((execution) => !known.has(execution.id));
4263
+ }
4264
+ /** Board summary attached to task_board_list. */
4265
+ function boardView(snapshot, maxSubtaskDepth) {
4266
+ const counts = {
4267
+ backlog: 0,
4268
+ todo: 0,
4269
+ running: 0,
4270
+ done: 0,
4271
+ failed: 0,
4272
+ archived: 0
4273
+ };
4274
+ for (const task of snapshot.tasks) if (task.archivedAt !== void 0) counts.archived += 1;
4275
+ else counts[task.status] += 1;
4276
+ return {
4277
+ revision: snapshot.revision,
4278
+ timeZone: snapshot.scheduler.timeZone,
4279
+ ...snapshot.scheduler.error === void 0 ? {} : { schedulerError: snapshot.scheduler.error },
4280
+ maxSubtaskDepth,
4281
+ sessionDefaultPermission: snapshot.sessionDefaultPermission ?? "read-only",
4282
+ runningSessions: snapshot.power.runningSessions,
4283
+ armedSchedules: snapshot.power.armedSchedules,
4284
+ counts
4285
+ };
4286
+ }
4287
+ /**
4288
+ * Build every task-board tool bound to one Host service.
4289
+ * @param host - the Host ledger/service face (TaskBoardHostService).
4290
+ * @returns the tool definitions, in TASK_BOARD_TOOL_NAMES order.
4291
+ */
4292
+ function buildTaskBoardTools(host) {
4293
+ return [
4294
+ buildListTool(host),
4295
+ buildGetTool(host),
4296
+ buildCreateTool(host),
4297
+ buildUpdateTool(host),
4298
+ buildSetParentTool(host),
4299
+ buildRunTool(host),
4300
+ buildManageTool(host),
4301
+ buildScheduleTool(host)
4302
+ ];
4303
+ }
4304
+ function buildSetParentTool(host) {
4305
+ return defineTool({
4306
+ name: "task_board_set_parent",
4307
+ description: [
4308
+ "Attach an existing task under a parent (making it a subtask) or detach it back to the root board by setting its parentId.",
4309
+ "Pass an empty parentId to detach.",
4310
+ "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).",
4311
+ "Running a task runs its whole subtree concurrently; a parent card settles only after its own turn and every direct subtask settle.",
4312
+ "Triggers: 任务看板, task board, 看板, 子任务, subtask, 关联任务, link task, 解除关联, detach."
4313
+ ].join(" "),
4314
+ parameters: {
4315
+ taskId: {
4316
+ type: "string",
4317
+ required: true,
4318
+ description: "The task whose parent link changes."
4319
+ },
4320
+ parentId: {
4321
+ type: "string",
4322
+ required: true,
4323
+ description: "Parent task id, or an empty string to detach the task back to the root board."
4324
+ }
4325
+ },
4326
+ output: {
4327
+ schema: { type: "json" },
4328
+ render: renderJson
4329
+ },
4330
+ async execute(args, exec) {
4331
+ try {
4332
+ const snapshot = host.apply(crypto.randomUUID(), {
4333
+ kind: "set-parent",
4334
+ taskId: args.taskId,
4335
+ parentId: args.parentId === "" ? null : args.parentId
4336
+ }, callingSessionId(exec));
4337
+ const task = snapshot.tasks.find((item) => item.id === args.taskId);
4338
+ if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
4339
+ return json({
4340
+ ok: true,
4341
+ task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4342
+ });
4343
+ } catch (error) {
4344
+ return refused("refused", messageOf(error));
4345
+ }
4346
+ }
4347
+ });
4348
+ }
4349
+ function buildRunTool(host) {
4350
+ return defineTool({
4351
+ name: "task_board_run",
4352
+ description: [
4353
+ "Run a task on the board now, or re-run a settled one.",
4354
+ "Running a task also runs its whole subtask tree CONCURRENTLY: the Host opens one real DSH session per member under one run group, each replaying its card pinned workspace, agent preset, permission and model; a subtask that does not pin them inherits its nearest ancestor.",
4355
+ "The parent card stays in the running column until its own turn and every direct subtask have settled, and it fails when any member failed; one failing subtask never stops the others.",
4356
+ "A card whose effective permission is above the deployment session default is refused with confirmation-required until a HUMAN confirms the binding in the board UI; this tool cannot confirm it.",
4357
+ "A card whose task carries teamRun instead starts ONE Team Lead session and the Host spawns a teammate per subtask inside it, so those cards run as an Agent Team rather than as independent sessions; that mode needs the deployment Agent Teams service and is refused when it is missing.",
4358
+ "Executions consume real API quota. Sessions are created asynchronously, so read their ids and outcomes back with task_board_get.",
4359
+ "Triggers: 任务看板, task board, 看板, 执行任务, run task, 重新执行, rerun, 子任务, subtask."
4360
+ ].join(" "),
4361
+ parameters: {
4362
+ taskId: {
4363
+ type: "string",
4364
+ required: true,
4365
+ description: "The task to run; running a root task runs its whole subtree."
4366
+ },
4367
+ rerun: {
4368
+ type: "boolean",
4369
+ description: "Re-run a settled task: reset its column to todo before starting (default false)."
4370
+ }
4371
+ },
4372
+ output: {
4373
+ schema: { type: "json" },
4374
+ render: renderJson
4375
+ },
4376
+ async execute(args, exec) {
4377
+ const before = host.snapshot();
4378
+ try {
4379
+ const snapshot = host.apply(crypto.randomUUID(), {
4380
+ kind: args.rerun === true ? "rerun" : "run",
4381
+ taskId: args.taskId
4382
+ }, callingSessionId(exec));
4383
+ const beforeById = new Map(before.tasks.map((task) => [task.id, task]));
4384
+ const started = snapshot.tasks.flatMap((task) => {
4385
+ return openedExecutions(beforeById.get(task.id)?.executions ?? [], task.executions).map((execution) => ({
4386
+ taskId: task.id,
4387
+ title: task.title,
4388
+ executionId: execution.id,
4389
+ ...execution.sessionId === void 0 ? {} : { sessionId: execution.sessionId }
4390
+ }));
4391
+ });
4392
+ const root = snapshot.tasks.find((task) => task.id === args.taskId);
4393
+ return json({
4394
+ ok: true,
4395
+ started,
4396
+ ...root === void 0 ? {} : { task: taskSummary(root, snapshot.tasks, snapshot.sessionDefaultPermission) },
4397
+ note: "Executions run asynchronously; call task_board_get to read session ids and the settled outcome. The parent settles only after its own turn and every direct subtask settle."
4398
+ });
4399
+ } catch (error) {
4400
+ const message = messageOf(error);
4401
+ return refused(message.startsWith("confirmation-required") ? "confirmation-required" : "refused", message);
4402
+ }
4403
+ }
4404
+ });
4405
+ }
4406
+ function buildManageTool(host) {
4407
+ return defineTool({
4408
+ name: "task_board_manage",
4409
+ description: [
4410
+ "Move, archive, restore, or delete one task board card.",
4411
+ "move-todo and move-backlog are the manual column moves; a running card cannot be moved.",
4412
+ "archive takes the whole subtask tree off the board and is refused while any member has an unsettled execution; restore brings the task, its ancestors and its subtree back; delete removes one card and is refused while it still has subtasks (detach or delete them first) or while it runs.",
4413
+ "It cannot confirm a permission binding: the confirmation gate is a human act performed in the board UI.",
4414
+ "Triggers: 任务看板, task board, 看板, 归档, archive, 删除任务, delete task, 移动任务, move task."
4415
+ ].join(" "),
4416
+ parameters: {
4417
+ taskId: {
4418
+ type: "string",
4419
+ required: true,
4420
+ description: "The task to act on."
4421
+ },
4422
+ action: {
4423
+ type: "string",
4424
+ required: true,
4425
+ enum: [
4426
+ "move-todo",
4427
+ "move-backlog",
4428
+ "archive",
4429
+ "restore",
4430
+ "delete"
4431
+ ],
4432
+ description: "The lifecycle operation to perform."
4433
+ }
4434
+ },
4435
+ output: {
4436
+ schema: { type: "json" },
4437
+ render: renderJson
4438
+ },
4439
+ async execute(args, exec) {
4440
+ const initiator = callingSessionId(exec);
4441
+ const action = {
4442
+ "move-todo": {
4443
+ kind: "move",
4444
+ taskId: args.taskId,
4445
+ status: "todo"
4446
+ },
4447
+ "move-backlog": {
4448
+ kind: "move",
4449
+ taskId: args.taskId,
4450
+ status: "backlog"
4451
+ },
4452
+ archive: {
4453
+ kind: "archive",
4454
+ taskId: args.taskId
4455
+ },
4456
+ restore: {
4457
+ kind: "restore",
4458
+ taskId: args.taskId
4459
+ },
4460
+ delete: {
4461
+ kind: "delete",
4462
+ taskId: args.taskId
4463
+ }
4464
+ }[args.action];
4465
+ if (action === void 0) return refused("unknown-action", "unsupported action " + args.action);
4466
+ if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
4467
+ try {
4468
+ const snapshot = host.apply(crypto.randomUUID(), action, initiator);
4469
+ if (args.action === "delete") return json({
4470
+ ok: true,
4471
+ action: args.action,
4472
+ taskId: args.taskId
4473
+ });
4474
+ const task = snapshot.tasks.find((item) => item.id === args.taskId);
4475
+ if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
4476
+ return json({
4477
+ ok: true,
4478
+ action: args.action,
4479
+ task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4480
+ });
4481
+ } catch (error) {
4482
+ return refused("refused", messageOf(error));
4483
+ }
4484
+ }
4485
+ });
4486
+ }
4487
+ function buildScheduleTool(host) {
4488
+ return defineTool({
4489
+ name: "task_board_schedule",
4490
+ description: [
4491
+ "Arm, change, or disarm a task scheduled runs (5-field cron in the Host local time zone; day-of-month and day-of-week use OR semantics).",
4492
+ "A due scheduled task runs the same cascade a manual run does, so a root task runs its whole subtask tree.",
4493
+ "A schedule whose tree contains an unconfirmed above-default permission is refused and rolls to its next occurrence; the reason is reported in the list board summary as schedulerError.",
4494
+ "Missed occurrences during Host downtime are skipped, never queued; a task that is already running skips its occurrence.",
4495
+ "Triggers: 任务看板, task board, 看板, 定时任务, schedule, cron, 每天执行."
4496
+ ].join(" "),
4497
+ parameters: {
4498
+ taskId: {
4499
+ type: "string",
4500
+ required: true,
4501
+ description: "The task whose schedule changes."
4502
+ },
4503
+ enabled: {
4504
+ type: "boolean",
4505
+ description: "Arm (true) or disarm (false) the schedule."
4506
+ },
4507
+ cron: {
4508
+ type: "string",
4509
+ description: "5-field cron expression: minute hour day-of-month month day-of-week."
4510
+ }
4511
+ },
4512
+ output: {
4513
+ schema: { type: "json" },
4514
+ render: renderJson
4515
+ },
4516
+ async execute(args, exec) {
4517
+ if (args.enabled === void 0 && args.cron === void 0) return refused("nothing-to-change", "pass enabled and/or cron");
4518
+ if (!host.snapshot().tasks.some((task) => task.id === args.taskId)) return refused("task-not-found", "no task with id " + args.taskId);
4519
+ const patch = {
4520
+ ...args.enabled === void 0 ? {} : { enabled: args.enabled },
4521
+ ...args.cron === void 0 ? {} : { cron: args.cron }
4522
+ };
4523
+ try {
4524
+ const snapshot = host.apply(crypto.randomUUID(), {
4525
+ kind: "set-schedule",
4526
+ taskId: args.taskId,
4527
+ patch
4528
+ }, callingSessionId(exec));
4529
+ const task = snapshot.tasks.find((item) => item.id === args.taskId);
4530
+ if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
4531
+ return json({
4532
+ ok: true,
4533
+ schedule: task.schedule === void 0 ? null : scheduleView(task.schedule),
4534
+ task: taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4535
+ });
4536
+ } catch (error) {
4537
+ return refused("refused", messageOf(error));
4538
+ }
4539
+ }
4540
+ });
4541
+ }
4542
+ function buildListTool(host) {
4543
+ return defineTool({
4544
+ name: "task_board_list",
4545
+ description: [
4546
+ "List tasks on the DSH task board (the Host-authoritative ledger behind the Web GUI task board).",
4547
+ "Filters: status column, parentId (direct subtasks), rootsOnly, tag, free-text query, and archived inclusion.",
4548
+ "Returns each task id, title, column, parent/subtask link, execution targets, schedule and latest run outcome, plus the board summary (revision, time zone, subtask depth limit, session-default permission).",
4549
+ "Use task_board_get for one task in full, task_board_create to add work, and task_board_run to execute it.",
4550
+ "Triggers: 任务看板, task board, 看板, 子任务, subtask, task list, 定时任务, scheduled task."
4551
+ ].join(" "),
4552
+ parameters: {
4553
+ status: {
4554
+ type: "string",
4555
+ enum: [
4556
+ "backlog",
4557
+ "todo",
4558
+ "running",
4559
+ "done",
4560
+ "failed"
4561
+ ],
4562
+ description: "Only cards in this column."
4563
+ },
4564
+ parentId: {
4565
+ type: "string",
4566
+ description: "Only the direct subtasks of this task id."
4567
+ },
4568
+ rootsOnly: {
4569
+ type: "boolean",
4570
+ description: "Only tasks that have no parent (default false)."
4571
+ },
4572
+ includeArchived: {
4573
+ type: "boolean",
4574
+ description: "Include archived tasks (default false)."
4575
+ },
4576
+ tag: {
4577
+ type: "string",
4578
+ description: "Only tasks carrying this label name."
4579
+ },
4580
+ query: {
4581
+ type: "string",
4582
+ description: "Case-insensitive match against title, description, prompt, and label names."
4583
+ },
4584
+ limit: {
4585
+ type: "integer",
4586
+ description: "Maximum rows to return (default 50, maximum 200)."
4587
+ }
4588
+ },
4589
+ output: {
4590
+ schema: { type: "json" },
4591
+ render: renderJson
4592
+ },
4593
+ isConcurrencySafe: () => true,
4594
+ async execute(args) {
4595
+ const snapshot = host.snapshot();
4596
+ const needle = typeof args.query === "string" ? args.query.trim().toLowerCase() : "";
4597
+ const limit = Math.min(Math.max(typeof args.limit === "number" ? Math.trunc(args.limit) : 50, 1), 200);
4598
+ const matched = snapshot.tasks.filter((task) => {
4599
+ if (args.includeArchived !== true && task.archivedAt !== void 0) return false;
4600
+ if (args.status !== void 0 && task.status !== args.status) return false;
4601
+ if (args.parentId !== void 0 && task.parentId !== args.parentId) return false;
4602
+ if (args.rootsOnly === true && task.parentId !== void 0) return false;
4603
+ if (args.tag !== void 0 && !(task.tags ?? []).some((tag) => tag.name === args.tag)) return false;
4604
+ if (needle !== "") {
4605
+ if (![
4606
+ task.title,
4607
+ task.description,
4608
+ task.prompt,
4609
+ ...(task.tags ?? []).map((tag) => tag.name)
4610
+ ].join("\n").toLowerCase().includes(needle)) return false;
4611
+ }
4612
+ return true;
4613
+ });
4614
+ return json({
4615
+ ok: true,
4616
+ board: boardView(snapshot, snapshot.maxSubtaskDepth ?? 1),
4617
+ total: matched.length,
4618
+ count: Math.min(matched.length, limit),
4619
+ tasks: matched.slice(0, limit).map((task) => taskSummary(task, snapshot.tasks, snapshot.sessionDefaultPermission))
4620
+ });
4621
+ }
4622
+ });
4623
+ }
4624
+ function buildGetTool(host) {
4625
+ return defineTool({
4626
+ name: "task_board_get",
4627
+ description: [
4628
+ "Read one task board card in full: prompt, description, labels, column, parent link, direct subtasks, execution targets, schedule, permission gate state, and the last ten execution attempts with their session ids and outcomes.",
4629
+ "Use it before running or editing a card, and after a run to read the settled outcome.",
4630
+ "Triggers: 任务看板, task board, 看板, 子任务, subtask, task detail."
4631
+ ].join(" "),
4632
+ parameters: { taskId: {
4633
+ type: "string",
4634
+ required: true,
4635
+ description: "Task id, as reported by task_board_list or task_board_create."
4636
+ } },
4637
+ output: {
4638
+ schema: { type: "json" },
4639
+ render: renderJson
4640
+ },
4641
+ isConcurrencySafe: () => true,
4642
+ async execute(args) {
4643
+ const snapshot = host.snapshot();
4644
+ const task = snapshot.tasks.find((item) => item.id === args.taskId);
4645
+ if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
4646
+ return json({
4647
+ ok: true,
4648
+ task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4649
+ });
4650
+ }
4651
+ });
4652
+ }
4653
+ function buildCreateTool(host) {
4654
+ return defineTool({
4655
+ name: "task_board_create",
4656
+ description: [
4657
+ "Create a task on the DSH task board, or a subtask by passing parentId.",
4658
+ "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.",
4659
+ "The deployment caps subtask depth (default one level, maximum three): a subtask of a subtask is refused unless the board is configured deeper.",
4660
+ "The task lands in the todo column and does NOT run; call task_board_run to execute it.",
4661
+ "Triggers: 任务看板, task board, 看板, 新建任务, create task, 子任务, subtask, 定时任务."
4662
+ ].join(" "),
4663
+ parameters: {
4664
+ title: {
4665
+ type: "string",
4666
+ required: true,
4667
+ description: "Short display title (one line)."
4668
+ },
4669
+ description: {
4670
+ type: "string",
4671
+ description: "Longer human description shown in the detail view."
4672
+ },
4673
+ prompt: {
4674
+ type: "string",
4675
+ description: "The instruction sent to the execution agent; the title is used when blank."
4676
+ },
4677
+ parentId: {
4678
+ type: "string",
4679
+ description: "Parent task id, making this a subtask. Omit for a root task."
4680
+ },
4681
+ workspaceId: {
4682
+ type: "string",
4683
+ description: "Workspace id the execution must run in; omit to inherit the parent value or use the most recent workspace."
4684
+ },
4685
+ mode: {
4686
+ type: "string",
4687
+ description: "Agent preset id the execution session is composed from; omit for the deployment default or the parent value."
4688
+ },
4689
+ permission: {
4690
+ type: "string",
4691
+ enum: [...TASK_PERMISSIONS],
4692
+ 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."
4693
+ },
4694
+ model: {
4695
+ type: "string",
4696
+ description: "Pinned model as provider/model (or a model id); omit for the host default or the parent value."
4697
+ },
4698
+ reuseSession: {
4699
+ type: "boolean",
4700
+ description: "Continue later runs in the previous execution session instead of a fresh conversation."
4701
+ },
4702
+ teamRun: {
4703
+ type: "boolean",
4704
+ description: "Run this task as an Agent Team: running it starts one Team Lead session and the Host spawns a teammate per subtask inside it. Omit or false for one independent session per member. Refused when the deployment serves no Agent Teams service."
4705
+ },
4706
+ tags: {
4707
+ type: "array",
4708
+ description: "Labels: the name renders as a badge and drives the board filter; promptPrefix is injected ahead of the execution prompt on every run.",
4709
+ items: {
4710
+ type: "object",
4711
+ additionalProperties: false,
4712
+ properties: {
4713
+ name: {
4714
+ type: "string",
4715
+ required: true,
4716
+ description: "Label name (unique within the task, at most 32 characters)."
4717
+ },
4718
+ promptPrefix: {
4719
+ type: "string",
4720
+ description: "Optional prompt line injected before the task prompt (at most 200 characters)."
4721
+ }
4722
+ }
4723
+ }
4724
+ },
4725
+ schedule: {
4726
+ type: "object",
4727
+ additionalProperties: false,
4728
+ description: "Arm a 5-field cron schedule at creation time (Host local time zone).",
4729
+ properties: {
4730
+ enabled: {
4731
+ type: "boolean",
4732
+ required: true,
4733
+ description: "Whether the schedule is armed."
4734
+ },
4735
+ cron: {
4736
+ type: "string",
4737
+ required: true,
4738
+ description: "5-field cron: minute hour day-of-month month day-of-week."
4739
+ }
4740
+ }
4741
+ }
4742
+ },
4743
+ output: {
4744
+ schema: { type: "json" },
4745
+ render: renderJson
4746
+ },
4747
+ async execute(args, exec) {
4748
+ if (args.title.trim() === "") return refused("title-required", "title must not be blank");
4749
+ const id = crypto.randomUUID();
4750
+ const tags = (args.tags ?? []).map((tag) => ({
4751
+ name: tag.name,
4752
+ ...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
4753
+ }));
4754
+ const input = {
4755
+ title: args.title,
4756
+ description: args.description ?? "",
4757
+ prompt: args.prompt ?? "",
4758
+ ...args.parentId === void 0 || args.parentId === "" ? {} : { parentId: args.parentId },
4759
+ ...args.workspaceId === void 0 || args.workspaceId === "" ? {} : { workspaceId: args.workspaceId },
4760
+ ...args.mode === void 0 || args.mode === "" ? {} : { mode: args.mode },
4761
+ ...args.permission === void 0 ? {} : { permission: args.permission },
4762
+ ...args.model === void 0 || args.model === "" ? {} : { model: args.model },
4763
+ ...args.reuseSession === true ? { reuseSession: true } : {},
4764
+ ...args.teamRun === true ? { teamRun: true } : {},
4765
+ ...tags.length === 0 ? {} : { tags },
4766
+ ...args.schedule === void 0 ? {} : { schedule: {
4767
+ enabled: args.schedule.enabled,
4768
+ cron: args.schedule.cron
4769
+ } }
4770
+ };
4771
+ try {
4772
+ const snapshot = host.apply(crypto.randomUUID(), {
4773
+ kind: "create",
4774
+ id,
4775
+ input
4776
+ }, callingSessionId(exec));
4777
+ const task = snapshot.tasks.find((item) => item.id === id);
4778
+ if (task === void 0) return refused("create-failed", "the Host did not confirm the new task");
4779
+ return json({
4780
+ ok: true,
4781
+ task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4782
+ });
4783
+ } catch (error) {
4784
+ return refused("refused", messageOf(error));
4785
+ }
4786
+ }
4787
+ });
4788
+ }
4789
+ function buildUpdateTool(host) {
4790
+ return defineTool({
4791
+ name: "task_board_update",
4792
+ description: [
4793
+ "Edit a task board card: title, description, prompt, labels, and the execution targets (workspace, agent preset, permission, model, session reuse).",
4794
+ "Only the fields you pass change; pass an empty string to clear a target, or an empty labels array to clear the labels.",
4795
+ "Title, description and prompt freeze once the task has started executing; the execution targets stay editable because they only affect future runs.",
4796
+ "Changing the pinned permission re-arms the permission confirmation gate.",
4797
+ "Triggers: 任务看板, task board, 看板, 修改任务, edit task, 子任务, 标签, tags."
4798
+ ].join(" "),
4799
+ parameters: {
4800
+ taskId: {
4801
+ type: "string",
4802
+ required: true,
4803
+ description: "Task id to edit."
4804
+ },
4805
+ title: {
4806
+ type: "string",
4807
+ description: "New title (a blank title is refused by the Host)."
4808
+ },
4809
+ description: {
4810
+ type: "string",
4811
+ description: "New description."
4812
+ },
4813
+ prompt: {
4814
+ type: "string",
4815
+ description: "New execution prompt."
4816
+ },
4817
+ workspaceId: {
4818
+ type: "string",
4819
+ description: "New workspace id; an empty string clears it."
4820
+ },
4821
+ mode: {
4822
+ type: "string",
4823
+ description: "New agent preset id; an empty string clears it."
4824
+ },
4825
+ permission: {
4826
+ type: "string",
4827
+ enum: [...TASK_PERMISSIONS, ""],
4828
+ description: "New permission preset; an empty string clears it back to the session default."
4829
+ },
4830
+ model: {
4831
+ type: "string",
4832
+ description: "New pinned model; an empty string clears it."
4833
+ },
4834
+ reuseSession: {
4835
+ type: "boolean",
4836
+ description: "Continue later runs in the previous execution session."
4837
+ },
4838
+ teamRun: {
4839
+ type: "boolean",
4840
+ description: "Switches this task between a plain cascade (one session per member) and an Agent Team run (Lead session plus a teammate per subtask)."
4841
+ },
4842
+ tags: {
4843
+ type: "array",
4844
+ description: "Replacement label set; an empty array clears all labels.",
4845
+ items: {
4846
+ type: "object",
4847
+ additionalProperties: false,
4848
+ properties: {
4849
+ name: {
4850
+ type: "string",
4851
+ required: true,
4852
+ description: "Label name."
4853
+ },
4854
+ promptPrefix: {
4855
+ type: "string",
4856
+ description: "Optional prompt line injected before the task prompt."
4857
+ }
4858
+ }
4859
+ }
4860
+ }
4861
+ },
4862
+ output: {
4863
+ schema: { type: "json" },
4864
+ render: renderJson
4865
+ },
4866
+ async execute(args, exec) {
4867
+ const patch = {};
4868
+ if (has(args, "title") && typeof args.title === "string") patch.title = args.title;
4869
+ if (has(args, "description") && typeof args.description === "string") patch.description = args.description;
4870
+ if (has(args, "prompt") && typeof args.prompt === "string") patch.prompt = args.prompt;
4871
+ if (has(args, "workspaceId") && typeof args.workspaceId === "string") patch.workspaceId = args.workspaceId;
4872
+ if (has(args, "mode") && typeof args.mode === "string") patch.mode = args.mode;
4873
+ if (has(args, "model") && typeof args.model === "string") patch.model = args.model;
4874
+ if (has(args, "permission") && typeof args.permission === "string") patch.permission = args.permission === "" ? void 0 : args.permission;
4875
+ if (has(args, "reuseSession")) patch.reuseSession = args.reuseSession === true;
4876
+ if (has(args, "teamRun")) patch.teamRun = args.teamRun === true;
4877
+ if (has(args, "tags")) {
4878
+ const tags = args.tags ?? [];
4879
+ patch.tags = tags.length === 0 ? null : tags.map((tag) => ({
4880
+ name: tag.name,
4881
+ ...tag.promptPrefix === void 0 ? {} : { promptPrefix: tag.promptPrefix }
4882
+ }));
4883
+ }
4884
+ if (Object.keys(patch).length === 0) return refused("nothing-to-change", "pass at least one field to update");
4885
+ try {
4886
+ const snapshot = host.apply(crypto.randomUUID(), {
4887
+ kind: "update",
4888
+ taskId: args.taskId,
4889
+ patch
4890
+ }, callingSessionId(exec));
4891
+ const task = snapshot.tasks.find((item) => item.id === args.taskId);
4892
+ if (task === void 0) return refused("task-not-found", "no task with id " + args.taskId);
4893
+ return json({
4894
+ ok: true,
4895
+ task: taskDetail(task, snapshot.tasks, snapshot.sessionDefaultPermission)
4896
+ });
4897
+ } catch (error) {
4898
+ return refused("refused", messageOf(error));
4899
+ }
4900
+ }
4901
+ });
4902
+ }
4903
+ //#endregion
4557
4904
  //#region src/http.ts
4558
4905
  /** Family-default JSON response headers; callers may append or override. */
4559
4906
  const JSON_HEADERS = {
@@ -4656,13 +5003,47 @@ function resolveAccess(access) {
4656
5003
  };
4657
5004
  }
4658
5005
  /**
5006
+ * Harness browser-auth cookie prefix (dsh-client-connection): the
5007
+ * authority-bound signed cookie the Host mints in exchange for its launch
5008
+ * token. dsh-web's remote channel redeems that token before re-issuing paired
5009
+ * traffic to 127.0.0.1 (packages/dsh-remote-web-ui/src/inner-auth.ts), and the
5010
+ * official DSH Desktop shell keeps its own copy from the same exchange and
5011
+ * attaches it to every request it forwards for the dsh-app://app/ page. The
5012
+ * two constants describe one fact and move together.
5013
+ */
5014
+ const BROWSER_AUTH_COOKIE_PREFIX = "dsh-auth-";
5015
+ /**
5016
+ * Whether a Cookie header carries the Host's browser-auth credential. Only an
5017
+ * application on this machine can hold it: the desktop shell never lets it
5018
+ * reach the page's cookie jar, and SameSite=Strict keeps a cross-site page
5019
+ * from attaching it.
5020
+ * @param header - the raw Cookie header value.
5021
+ */
5022
+ function carriesBrowserAuthCookie(header) {
5023
+ if (header === void 0) return false;
5024
+ return header.split(";").some((segment) => segment.trim().startsWith(BROWSER_AUTH_COOKIE_PREFIX));
5025
+ }
5026
+ /**
4659
5027
  * Browser-signal tripwire, NOT an authority check: a bare curl sends neither
4660
5028
  * header and is refused, but a curl with a forged Origin passes this too.
4661
5029
  * The real boundary is the loopback socket + Host + origin-equality checks
4662
5030
  * in isTrustedTaskBoardRequest below; do not rely on this marker alone.
5031
+ *
5032
+ * A first-party client that presents NEITHER header must still pass. The DSH
5033
+ * Desktop shell serves the Web GUI from dsh-app://app/ and forwards that
5034
+ * page's Host requests itself, deleting origin and sec-fetch-site on the
5035
+ * way (dsh-desktop-host's forwardWebRequest), so the panel's own fetch
5036
+ * arrives marker-less and every route behind this guard answered 403 - which
5037
+ * the panel renders as its board.hostError.unauthorized message. The shell
5038
+ * does attach the Host's browser-auth cookie, redeemed from the Host's launch
5039
+ * URL at startup and deliberately withheld from the page. That credential,
5040
+ * not a header the shell strips, is the browser signal of an application on
5041
+ * this machine; a marker-less, credential-less request stays refused.
4663
5042
  */
4664
5043
  function browserSameOriginMarker(req) {
4665
- return req.headers["sec-fetch-site"] === "same-origin" || typeof req.headers.origin === "string";
5044
+ if (req.headers["sec-fetch-site"] === "same-origin") return true;
5045
+ if (typeof req.headers.origin === "string") return true;
5046
+ return carriesBrowserAuthCookie(req.headers.cookie);
4666
5047
  }
4667
5048
  function sameAuthority(req, host) {
4668
5049
  if (req.headers["sec-fetch-site"] === "cross-site") return false;
@@ -4874,38 +5255,111 @@ function makeTaskBoardRoutes(service, access = {}, options = {}) {
4874
5255
  * the loader accepts a standalone install of the same package side by side;
4875
5256
  * without this guard the second instance would still re-register the same
4876
5257
  * webserver routes, tools, settings namespaces, and system-prompt sections
4877
- * and fail the boot. mountOnce makes the second host apply a no-op for the
4878
- * lifetime of the first instance (the browser half is already deduped by
4879
- * package name in the client module host).
5258
+ * and fail the boot. mountOnce makes a mount of an already-mounted package a
5259
+ * no-op for as long as the first instance lives (the browser half is already
5260
+ * deduped by package name in the client module host).
5261
+ *
5262
+ * A no-op is only safe while the holder is ALIVE. The holder can be disposed
5263
+ * long after the refused mount ran its course: the Host reloads a profile by
5264
+ * creating the new loader entries before the old ones are torn down (a
5265
+ * plugin-manager enable/disable/install write, a settings-driven row reload,
5266
+ * HMR), so the new aggregate shell entry mounts its family plugin while the
5267
+ * previous entry still owns the name. Dropping that refused mount lost the
5268
+ * plugin for good - the previous entry then disposed its own mount, releasing
5269
+ * the name with nobody left to take it, and the family row stayed listed as
5270
+ * active while its host routes 404ed (the task-board panel showed
5271
+ * "board.hostError.notMounted", no degraded record appeared, and only a Host
5272
+ * restart recovered it).
5273
+ *
5274
+ * The refused mount is therefore QUEUED, not dropped, and replayed the moment
5275
+ * the holder releases the name - if the waiting fiber is still alive then. The
5276
+ * single-instance guarantee is unchanged: exactly one mount is live per
5277
+ * package name, and the replay re-enters the guard so a later mount still
5278
+ * dedupes against it.
4880
5279
  *
4881
5280
  * The registry rides a global symbol so two module instances of the same
4882
- * package (npm copy vs repository link) still share one verdict. cordis
4883
- * `ctx.effect` runs its callback immediately and treats the callback's
5281
+ * package (npm copy vs repository link) still share one verdict. That symbol
5282
+ * is a CROSS-REPOSITORY contract, not this file's private state: the four
5283
+ * satellite packages (dsh-skins / dsh-pet / dsh-presets /
5284
+ * dsh-community-plugins) are separate repositories carrying their own copy of
5285
+ * this guard, rebuilt on their own schedule, so the value under `MOUNTED`
5286
+ * must keep the shape every published copy reads (a `Set` of package names
5287
+ * with `has`/`add`/`delete`). The wait queues this guard added therefore live
5288
+ * under their own additive key, and the registry reads back a `Set` even when
5289
+ * some other build left a different value there. Changing `MOUNTED`'s shape
5290
+ * in place broke that contract once: a satellite's legacy copy created a
5291
+ * `Set`, the family's new copy read it as a `Map`, and every family row
5292
+ * mounted after it failed with "claims.get is not a function".
5293
+ *
5294
+ * cordis `ctx.effect` runs its callback immediately and treats the callback's
4884
5295
  * return value as the fiber disposer, so the unmarker is returned, not run.
4885
5296
  */
5297
+ /** Published cross-repository contract: package names currently mounted. */
4886
5298
  const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
5299
+ /** Additive key this guard owns: refused mounts waiting for the name. */
5300
+ const WAITERS = Symbol.for("dsh-web.mounted-plugins.waiters");
5301
+ /**
5302
+ * The shared name registry, always a `Set` whatever another build stored here:
5303
+ * a foreign value (an interim shape, a hand-written global) must not take every
5304
+ * family plugin down with it.
5305
+ * @returns the process-wide set of mounted package names.
5306
+ */
4887
5307
  function mountedSet() {
4888
5308
  const registry = globalThis;
4889
- return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
5309
+ const existing = registry[MOUNTED];
5310
+ if (existing instanceof Set) return existing;
5311
+ const created = /* @__PURE__ */ new Set();
5312
+ registry[MOUNTED] = created;
5313
+ return created;
5314
+ }
5315
+ /** Queue per package name for mounts refused while a holder was alive. */
5316
+ function mountWaiters() {
5317
+ const registry = globalThis;
5318
+ return registry[WAITERS] ??= /* @__PURE__ */ new Map();
4890
5319
  }
4891
5320
  /**
4892
5321
  * Wrap a cordis plugin apply so the package runs at most once per process.
4893
- * The first mount registers normally and unmarks when its fiber disposes;
4894
- * any later mount of the same package name is a no-op.
5322
+ * The first mount registers normally and releases the name when its fiber
5323
+ * disposes; a mount refused while that name is held waits for the release and
5324
+ * then runs, unless its own fiber disposes first.
4895
5325
  * @param packageName - npm package identity shared by every install source.
4896
5326
  * @param fn - the original plugin apply.
4897
5327
  * @returns an apply of the same shape.
4898
5328
  */
4899
5329
  function mountOnce(packageName, fn) {
4900
- return ((...args) => {
5330
+ const mount = (...args) => {
4901
5331
  const mounted = mountedSet();
4902
- if (mounted.has(packageName)) return;
5332
+ const ctx = args[0];
5333
+ if (mounted.has(packageName)) {
5334
+ const waiters = mountWaiters();
5335
+ const queue = waiters.get(packageName) ?? [];
5336
+ let alive = true;
5337
+ const pending = { run: () => {
5338
+ if (alive) mount(...args);
5339
+ } };
5340
+ ctx?.effect?.(() => () => {
5341
+ alive = false;
5342
+ const index = queue.indexOf(pending);
5343
+ if (index >= 0) queue.splice(index, 1);
5344
+ });
5345
+ queue.push(pending);
5346
+ waiters.set(packageName, queue);
5347
+ return;
5348
+ }
4903
5349
  mounted.add(packageName);
4904
- args[0]?.effect?.(() => () => {
5350
+ ctx?.effect?.(() => () => {
4905
5351
  mounted.delete(packageName);
5352
+ const waiters = mountWaiters();
5353
+ const queue = waiters.get(packageName);
5354
+ if (queue === void 0) return;
5355
+ waiters.delete(packageName);
5356
+ for (const waiter of queue.splice(0)) queueMicrotask(() => {
5357
+ waiter.run();
5358
+ });
4906
5359
  });
4907
5360
  return fn(...args);
4908
- });
5361
+ };
5362
+ return mount;
4909
5363
  }
4910
5364
  //#endregion
4911
5365
  //#region src/index.ts
@@ -4922,7 +5376,12 @@ const inject = [
4922
5376
  "commands"
4923
5377
  ];
4924
5378
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
4925
- const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。";
5379
+ const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;任务可建子任务(深度上限可配 1..3,默认 1 层,子任务不能再带子任务),执行父任务会并发执行其子任务树,子任务可单独覆盖权限与模型;另注册 task_board_* agent 工具(list/get/create/update/set_parent/run/manage/schedule),任何会话都可直接读写看板、子任务与定时计划,但运行任务会真实执行并消耗额度,高于会话默认权限的绑定仍必须由用户在 GUI 人工确认(工具刻意不提供确认能力);支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。";
5380
+ /**
5381
+ * Continuable-subagent provider for fresh teammates, matching the Agent Teams
5382
+ * tool plugin's own `freshProvider` default.
5383
+ */
5384
+ const DEFAULT_TEAM_PROVIDER = "spawn";
4926
5385
  /**
4927
5386
  * The schema is left to inference rather than annotated with `z<Config>`: a
4928
5387
  * volatile field's parsed output is a `Volatile` reference while its accepted
@@ -4935,7 +5394,9 @@ const Config = z.object({
4935
5394
  preventIdleSleep: z.boolean().default(false).volatile(),
4936
5395
  trustedProxyHosts: z.array(z.string()).default([]),
4937
5396
  proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
4938
- sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION)
5397
+ sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
5398
+ maxSubtaskDepth: z.number().min(1).max(3).default(1).volatile(),
5399
+ teamProvider: z.string().min(1).default(DEFAULT_TEAM_PROVIDER)
4939
5400
  });
4940
5401
  /**
4941
5402
  * Read one config field's current value.
@@ -4966,9 +5427,105 @@ function resolveProxyAccess(config, env = process.env) {
4966
5427
  proxyToken
4967
5428
  };
4968
5429
  }
5430
+ /**
5431
+ * Resolve the optional agent-tool registry. The board deliberately does not
5432
+ * inject it: a deployment whose runtime serves no registry must still mount the
5433
+ * whole board and lose only the agent-tool surface, the same tolerance the
5434
+ * optional llm service gets.
5435
+ * @param ctx - the plugin context.
5436
+ * @returns the registry, or undefined when this deployment serves none.
5437
+ */
5438
+ function resolveToolRegistry(ctx) {
5439
+ try {
5440
+ const tools = ctx.get("tools");
5441
+ return tools !== void 0 && typeof tools.register === "function" ? tools : void 0;
5442
+ } catch {
5443
+ return;
5444
+ }
5445
+ }
5446
+ /**
5447
+ * Resolve the native cordis timer service (dsh-base's own `cordis-plugin-timer`
5448
+ * row). The board arms its schedule through it so unloading the row clears
5449
+ * every pending timer with the owning fiber, instead of the row tracking raw
5450
+ * process handles by hand. A composition that serves no timer service keeps
5451
+ * working on the process globals.
5452
+ * @param ctx - the plugin context.
5453
+ * @returns the timer face, or undefined when this deployment serves none.
5454
+ */
5455
+ function resolveHostTimers(ctx) {
5456
+ try {
5457
+ const timer = ctx.get("timer");
5458
+ if (timer === void 0) return void 0;
5459
+ return typeof timer.timeout === "function" && typeof timer.interval === "function" ? {
5460
+ timeout: timer.timeout.bind(timer),
5461
+ interval: timer.interval.bind(timer)
5462
+ } : void 0;
5463
+ } catch {
5464
+ return;
5465
+ }
5466
+ }
4969
5467
  /** Schema default, re-read for hand-built test contexts (the loader applies them normally). */
4970
5468
  const DEFAULT_ANNOUNCE = false;
4971
5469
  /**
5470
+ * How long one teammate may take to reach its durable active or failed edge.
5471
+ * Agent Teams provisioning normally resolves in seconds; the bound exists so a
5472
+ * hung spawn settles the subtask instead of holding the cascade open forever.
5473
+ */
5474
+ const TEAMMATE_PROVISION_TIMEOUT_MS = 6e5;
5475
+ /**
5476
+ * Resolve the optional Agent Teams service. Like the tool registry and the llm
5477
+ * runtime, it is deliberately not injected: a deployment without Agent Teams
5478
+ * must still mount the whole board and only lose team-mode runs.
5479
+ * @param ctx - the plugin context.
5480
+ * @returns the service, or undefined when this deployment serves none.
5481
+ */
5482
+ function resolveAgentTeams(ctx) {
5483
+ try {
5484
+ const teams = ctx.get("agentTeams");
5485
+ return teams !== void 0 && typeof teams.spawnTeammate === "function" ? teams : void 0;
5486
+ } catch {
5487
+ return;
5488
+ }
5489
+ }
5490
+ /**
5491
+ * Build the team dispatcher: the run's Lead session spawns one teammate per
5492
+ * subtask through the Agent Teams service, and the teammate's session id is
5493
+ * attached to that subtask's execution so the ordinary session monitor settles
5494
+ * it. Returns undefined when the deployment serves no Agent Teams service or
5495
+ * no live-agent registry, which makes a team run fail closed.
5496
+ * @param ctx - the plugin context.
5497
+ * @param provider - the continuable-subagent provider for fresh teammates.
5498
+ * @returns the dispatcher, or undefined when team runs cannot be served.
5499
+ */
5500
+ function buildTeamDispatcher(ctx, provider) {
5501
+ const teams = resolveAgentTeams(ctx);
5502
+ if (teams === void 0) return void 0;
5503
+ if (typeof ctx.agents?.get !== "function") return void 0;
5504
+ return { async spawn(input) {
5505
+ const lead = ctx.agents.get(input.leadSessionId);
5506
+ if (lead === void 0) return { error: `lead session ${input.leadSessionId} is not live` };
5507
+ try {
5508
+ const member = (await teams.spawnTeammate(lead, {
5509
+ name: input.name,
5510
+ description: input.description,
5511
+ prompt: [{
5512
+ type: "text",
5513
+ text: input.prompt
5514
+ }],
5515
+ context: "fresh",
5516
+ provider,
5517
+ signal: AbortSignal.timeout(TEAMMATE_PROVISION_TIMEOUT_MS)
5518
+ }))?.member;
5519
+ if (member === void 0) return { error: "Agent Teams returned no teammate" };
5520
+ if (member.phase === "failed") return { error: member.error ?? "teammate provisioning failed" };
5521
+ if (typeof member.id !== "string" || member.id === "") return { error: "Agent Teams returned no teammate session" };
5522
+ return { sessionId: member.id };
5523
+ } catch (error) {
5524
+ return { error: `teammate provisioning failed: ${error instanceof Error ? error.message : String(error)}` };
5525
+ }
5526
+ } };
5527
+ }
5528
+ /**
4972
5529
  * Read the optional `llm` service. The board deliberately does not inject it:
4973
5530
  * a deployment without a model must still mount the board, and the parse route
4974
5531
  * answers a typed failure instead of the plugin failing to load (issue #1540).
@@ -4987,8 +5544,8 @@ function resolveLlmRuntime(ctx) {
4987
5544
  * Activate the board's host half: ledger, routes, cron scheduler, power
4988
5545
  * inhibitor, and the model-facing announcement.
4989
5546
  *
4990
- * The effective settings are the config the Host hands this row. The three
4991
- * fields the browser card edits are schema-volatile, so the Loader commits an
5547
+ * The effective settings are the config the Host hands this row. The fields
5548
+ * the browser card edits are schema-volatile, so the Loader commits an
4992
5549
  * edit into the running fiber's references instead of remounting the row —
4993
5550
  * `sync` therefore reads them at use time and follows
4994
5551
  * `loader/volatile-update`, the event the Loader emits once it has committed
@@ -5004,9 +5561,14 @@ function applyImpl(ctx, config) {
5004
5561
  const announceToAgent = () => readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE);
5005
5562
  /** Current idle-sleep protection flag. */
5006
5563
  const preventIdleSleep = () => readConfigField(config?.preventIdleSleep, false);
5564
+ /** Current subtask depth limit (read live: the settings card edits it in place). */
5565
+ const maxSubtaskDepth = () => readConfigField(config?.maxSubtaskDepth, 1);
5007
5566
  const host = new TaskBoardHostService(ctx.typertGateway, {
5008
5567
  workspaceRegistry: ctx.workspaceRegistry,
5009
5568
  sessionDefaultPermission: config?.sessionDefaultPermission ?? "read-only",
5569
+ maxSubtaskDepth: maxSubtaskDepth(),
5570
+ timers: resolveHostTimers(ctx),
5571
+ team: buildTeamDispatcher(ctx, config?.teamProvider ?? "spawn"),
5010
5572
  commandDispatcher: { async execute(sessionId, line, signal) {
5011
5573
  const agent = ctx.agents.get(sessionId);
5012
5574
  if (agent === void 0) throw new Error(`execution session ${sessionId} is not available`);
@@ -5015,6 +5577,30 @@ function applyImpl(ctx, config) {
5015
5577
  });
5016
5578
  host.setConfiguration(enabled(), preventIdleSleep());
5017
5579
  host.start();
5580
+ let disposeTools;
5581
+ const setToolsEnabled = (active) => {
5582
+ if (!active) {
5583
+ disposeTools?.();
5584
+ disposeTools = void 0;
5585
+ return;
5586
+ }
5587
+ if (disposeTools !== void 0) return;
5588
+ const registry = resolveToolRegistry(ctx);
5589
+ if (registry === void 0) return;
5590
+ const disposers = buildTaskBoardTools(host).map((tool) => registry.register(tool));
5591
+ disposeTools = () => {
5592
+ for (const dispose of disposers.splice(0)) dispose();
5593
+ };
5594
+ };
5595
+ setToolsEnabled(enabled());
5596
+ const scopedInject = ctx.inject;
5597
+ if (typeof scopedInject === "function") scopedInject.call(ctx, ["tools"], () => {
5598
+ setToolsEnabled(enabled());
5599
+ return () => {
5600
+ disposeTools?.();
5601
+ disposeTools = void 0;
5602
+ };
5603
+ });
5018
5604
  ctx.effect(() => {
5019
5605
  const disposers = [];
5020
5606
  try {
@@ -5025,11 +5611,13 @@ function applyImpl(ctx, config) {
5025
5611
  } });
5026
5612
  for (const route of routes) disposers.push(ctx.webServer.register(route));
5027
5613
  } catch (error) {
5614
+ setToolsEnabled(false);
5028
5615
  for (const dispose of disposers) dispose();
5029
5616
  host.dispose();
5030
5617
  throw error;
5031
5618
  }
5032
5619
  return () => {
5620
+ setToolsEnabled(false);
5033
5621
  for (const dispose of disposers) dispose();
5034
5622
  host.dispose();
5035
5623
  };
@@ -5040,11 +5628,14 @@ function applyImpl(ctx, config) {
5040
5628
  const next = {
5041
5629
  enabled: enabled(),
5042
5630
  announceToAgent: announceToAgent(),
5043
- preventIdleSleep: preventIdleSleep()
5631
+ preventIdleSleep: preventIdleSleep(),
5632
+ maxSubtaskDepth: maxSubtaskDepth()
5044
5633
  };
5045
- if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep) return;
5634
+ if (applied !== void 0 && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep && applied.maxSubtaskDepth === next.maxSubtaskDepth) return;
5046
5635
  applied = next;
5636
+ host.ledger.setMaxSubtaskDepth(next.maxSubtaskDepth);
5047
5637
  host.setConfiguration(next.enabled, next.preventIdleSleep);
5638
+ setToolsEnabled(next.enabled);
5048
5639
  if (disposeSection !== void 0) {
5049
5640
  disposeSection();
5050
5641
  disposeSection = void 0;
@@ -5062,4 +5653,4 @@ function applyImpl(ctx, config) {
5062
5653
  sync();
5063
5654
  }
5064
5655
  //#endregion
5065
- export { Config, DEFAULT_PROXY_TOKEN_ENV, TASK_BOARD_GUIDANCE, apply, inject, readConfigField, resolveLlmRuntime, resolveProxyAccess };
5656
+ export { Config, DEFAULT_PROXY_TOKEN_ENV, DEFAULT_TEAM_PROVIDER, TASK_BOARD_GUIDANCE, apply, buildTeamDispatcher, inject, readConfigField, resolveAgentTeams, resolveHostTimers, resolveLlmRuntime, resolveProxyAccess, resolveToolRegistry };