pi-subagents 0.65.1 → 0.67.0

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 (148) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/README.md +5 -4
  3. package/agents/evidence-auditor.md +34 -0
  4. package/agents/researcher.md +23 -13
  5. package/agents/reviewer.md +3 -2
  6. package/docs/agents.md +20 -3
  7. package/docs/configuration.md +25 -5
  8. package/docs/extension-api.md +124 -18
  9. package/docs/missions.md +8 -0
  10. package/docs/models.md +59 -2
  11. package/docs/observability.md +46 -6
  12. package/docs/standalone-background.md +49 -0
  13. package/docs/tool-reference.md +20 -10
  14. package/docs/watchdog.md +35 -4
  15. package/docs/workflows.md +40 -19
  16. package/inspector-runner.mjs +2 -2
  17. package/package.json +2 -1
  18. package/prompts/parallel-review.md +1 -1
  19. package/{runner-server-preload.mjs → runner-peer-preload.mjs} +8 -3
  20. package/skills/pi-subagents/SKILL.md +14 -0
  21. package/skills/pi-subagents/references/execution-controls.md +20 -5
  22. package/skills/pi-subagents/references/management-authoring-rpc.md +2 -1
  23. package/skills/pi-subagents/references/prompting-and-roles.md +2 -2
  24. package/src/agents/advertised-agent-prompt.ts +94 -0
  25. package/src/agents/agent-management.ts +14 -1
  26. package/src/agents/agent-serializer.ts +2 -0
  27. package/src/agents/agents.ts +14 -0
  28. package/src/agents/builtin-names.ts +1 -0
  29. package/src/api/delegation.ts +4 -0
  30. package/src/api/preflight.ts +76 -45
  31. package/src/api/shared-types.ts +3 -1
  32. package/src/api/workflow-resources.ts +6 -0
  33. package/src/extension/fanout-child.ts +63 -4
  34. package/src/extension/index.ts +58 -8
  35. package/src/extension/public-execution.ts +4 -3
  36. package/src/extension/rpc.ts +8 -21
  37. package/src/extension/schemas.ts +71 -80
  38. package/src/extension/tool-description.ts +29 -81
  39. package/src/inspectors/actions.ts +148 -0
  40. package/src/inspectors/ghostty/actions.ts +74 -0
  41. package/src/inspectors/ghostty/plugin.ts +17 -0
  42. package/src/inspectors/herdr/actions.ts +99 -179
  43. package/src/inspectors/herdr/plugin.ts +20 -0
  44. package/src/inspectors/herdr/project-panes.ts +1 -1
  45. package/src/inspectors/{herdr/inspector-runner.ts → inspector-runner.ts} +12 -12
  46. package/src/inspectors/plugins.ts +8 -0
  47. package/src/inspectors/{herdr/session-roots-codec.ts → session-roots-codec.ts} +3 -14
  48. package/src/inspectors/types.ts +51 -0
  49. package/src/intercom/intercom-bridge.ts +50 -8
  50. package/src/intercom/native-supervisor-channel.ts +104 -67
  51. package/src/runs/background/active-async-capacity.ts +22 -18
  52. package/src/runs/background/async-execution.ts +45 -56
  53. package/src/runs/background/async-job-tracker.ts +35 -3
  54. package/src/runs/background/async-resume.ts +5 -9
  55. package/src/runs/background/async-status-snapshot.ts +10 -12
  56. package/src/runs/background/async-status.ts +17 -9
  57. package/src/runs/background/auto-drain.ts +44 -30
  58. package/src/runs/background/binary-bootstrap.ts +33 -0
  59. package/src/runs/background/chain-root-attachment.ts +8 -0
  60. package/src/runs/background/control-channel.ts +78 -44
  61. package/src/runs/background/fleet-view.ts +30 -2
  62. package/src/runs/background/notify.ts +117 -13
  63. package/src/runs/background/owned-process-tree.ts +35 -8
  64. package/src/runs/background/process-terminal.ts +23 -23
  65. package/src/runs/background/run-child-session.ts +121 -36
  66. package/src/runs/background/run-status.ts +78 -5
  67. package/src/runs/background/runner-aliases.ts +28 -9
  68. package/src/runs/background/runner-child-launch.ts +88 -0
  69. package/src/runs/background/runner-child-sessions.ts +5 -4
  70. package/src/runs/background/scheduled-runs.ts +40 -13
  71. package/src/runs/background/stale-run-reconciler.ts +3 -1
  72. package/src/runs/background/steering.ts +20 -2
  73. package/src/runs/background/subagent-runner.ts +458 -239
  74. package/src/runs/background/subagent-wait.ts +54 -8
  75. package/src/runs/background/wait-completions.ts +4 -0
  76. package/src/runs/background/wait-tool.ts +1 -1
  77. package/src/runs/foreground/async-steering-action.ts +37 -7
  78. package/src/runs/foreground/execution.ts +145 -56
  79. package/src/runs/foreground/prompt-audit.ts +3 -1
  80. package/src/runs/foreground/subagent-executor.ts +584 -297
  81. package/src/runs/foreground/workflow-detach-reconcile.ts +10 -5
  82. package/src/runs/foreground/workflow-foreground-steering.ts +57 -2
  83. package/src/runs/shared/acceptance.ts +7 -4
  84. package/src/runs/shared/agent-contract.ts +1 -1
  85. package/src/runs/shared/async-status-projection.ts +51 -47
  86. package/src/runs/shared/capability-ceiling.ts +2 -0
  87. package/src/runs/shared/child-hooks.ts +167 -3
  88. package/src/runs/shared/child-launch.ts +28 -13
  89. package/src/runs/shared/child-lifecycle.ts +6 -3
  90. package/src/runs/shared/child-runtime-config.ts +3 -1
  91. package/src/runs/shared/child-session.ts +75 -8
  92. package/src/runs/shared/child-tool-plan.ts +124 -5
  93. package/src/runs/shared/completion-evidence.ts +2 -2
  94. package/src/runs/shared/completion-guard.ts +6 -3
  95. package/src/runs/shared/effective-system-prompt.ts +33 -0
  96. package/src/runs/shared/external-cli-runner.ts +9 -7
  97. package/src/runs/shared/host-step-status.ts +11 -11
  98. package/src/runs/shared/llm-intent-arbiter.ts +21 -11
  99. package/src/runs/shared/model-fallback.ts +12 -6
  100. package/src/runs/shared/nested-events.ts +5 -5
  101. package/src/runs/shared/orca-progress-tabs.ts +7 -1
  102. package/src/runs/shared/parallel-handoff.ts +57 -12
  103. package/src/runs/shared/parallel-utils.ts +2 -2
  104. package/src/runs/shared/pi-spawn.ts +10 -0
  105. package/src/runs/shared/readonly-drain-observation.ts +42 -0
  106. package/src/runs/shared/readonly-model-continuation.ts +69 -0
  107. package/src/runs/shared/readonly-session-evidence.ts +307 -0
  108. package/src/runs/shared/run-fanout-budget.ts +8 -8
  109. package/src/runs/shared/runtime-acknowledged-extensions.ts +3 -3
  110. package/src/runs/shared/subagent-prompt-runtime.ts +20 -4
  111. package/src/runs/shared/task-intent.ts +46 -13
  112. package/src/runs/shared/workflow-async-child-guidance.ts +18 -0
  113. package/src/runs/shared/worktree-setup-command.ts +190 -0
  114. package/src/runs/shared/worktree.ts +366 -208
  115. package/src/shared/fork-context.ts +15 -72
  116. package/src/shared/launch-contract.ts +65 -2
  117. package/src/shared/opencode-session-headers.ts +30 -0
  118. package/src/shared/types.ts +85 -61
  119. package/src/shared/utils.ts +7 -2
  120. package/src/shared/workflow-child-permit.ts +18 -13
  121. package/src/slash/delegation-adapters.ts +3 -1
  122. package/src/slash/delegation-request.ts +14 -0
  123. package/src/slash/slash-commands.ts +2 -1
  124. package/src/slash/subagents-admin.ts +11 -4
  125. package/src/tui/fleet-status.ts +164 -19
  126. package/src/tui/fleet.ts +27 -19
  127. package/src/tui/render.ts +172 -33
  128. package/src/watchdog/child-status.ts +8 -0
  129. package/src/watchdog/model-selection.ts +20 -0
  130. package/src/watchdog/permission-arbiter.ts +3 -1
  131. package/src/watchdog/register-child.ts +1 -0
  132. package/src/watchdog/register-main.ts +31 -27
  133. package/src/watchdog/review.ts +132 -67
  134. package/src/watchdog/runtime.ts +82 -20
  135. package/src/watchdog/scope.ts +1 -1
  136. package/src/watchdog/settings.ts +9 -3
  137. package/src/watchdog/tool-actions.ts +13 -12
  138. package/src/watchdog/turn-delta.ts +23 -0
  139. package/src/watchdog/types.ts +4 -0
  140. package/src/workflows/chat-progress.ts +3 -3
  141. package/src/workflows/scripted-workflow.ts +275 -17
  142. package/src/workflows/workflow-checklist.ts +13 -17
  143. package/src/workflows/workflow-child-summary.ts +57 -8
  144. package/src/workflows/workflow-preflight.ts +19 -19
  145. package/src/workflows/workflow-receipt.ts +3 -3
  146. package/src/workflows/workflow-resources.ts +96 -21
  147. package/src/workflows/workflow-settlement.ts +3 -0
  148. /package/src/inspectors/{herdr/shell-command.ts → shell-command.ts} +0 -0
@@ -5,11 +5,12 @@
5
5
  * the parent aliases that specifier (and the other host peer packages) to the
6
6
  * installed pi package through `JITI_ALIAS` when it spawns the runner, see
7
7
  * `runner-aliases.ts`. Tests replace the factory with a scripted one by
8
- * naming a module in the runner config.
8
+ * naming a module in the runner config. The binary bootstrap supplies its
9
+ * embedded SDK through the default factory's existing loader option instead.
9
10
  */
10
11
  import * as path from "node:path";
11
12
  import { pathToFileURL } from "node:url";
12
- import { createDefaultChildSessionFactory, type ChildSessionFactory } from "../shared/child-session.ts";
13
+ import { createDefaultChildSessionFactory, type ChildSessionFactory, type DefaultChildSessionFactoryOptions } from "../shared/child-session.ts";
13
14
 
14
15
  export interface RunnerChildSessionConfig {
15
16
  /** Test seam: module whose default export is a `ChildSessionFactory`, or a function returning one. */
@@ -20,8 +21,8 @@ function isChildSessionFactory(value: unknown): value is ChildSessionFactory {
20
21
  return Boolean(value) && typeof value === "object" && typeof (value as ChildSessionFactory).create === "function" && typeof (value as ChildSessionFactory).dispose === "function";
21
22
  }
22
23
 
23
- export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig): Promise<ChildSessionFactory> {
24
- if (!config.childSessionFactoryModule) return createDefaultChildSessionFactory();
24
+ export async function loadRunnerChildSessionFactory(config: RunnerChildSessionConfig, options?: DefaultChildSessionFactoryOptions): Promise<ChildSessionFactory> {
25
+ if (!config.childSessionFactoryModule) return createDefaultChildSessionFactory(options);
25
26
  const loaded = await import(pathToFileURL(path.resolve(config.childSessionFactoryModule)).href) as { default?: unknown };
26
27
  const candidate = typeof loaded.default === "function" ? (loaded.default as () => unknown)() : loaded.default;
27
28
  if (!isChildSessionFactory(candidate)) {
@@ -54,6 +54,7 @@ export interface ScheduleRecord {
54
54
  timeoutMs?: number;
55
55
  paused: boolean;
56
56
  sessionOnly?: boolean;
57
+ quiet?: boolean;
57
58
  ownerSessionFile?: string;
58
59
  createdAt: string;
59
60
  updatedAt: string;
@@ -308,6 +309,7 @@ function parseSchedule(value: unknown, file: string): ScheduleRecord {
308
309
  if (typeof record.trigger.every !== "string" || typeof record.trigger.everyMs !== "number" || typeof record.trigger.anchorAt !== "string" || typeof record.trigger.nextRunAt !== "string") throw new Error(`Schedule record '${file}' has an invalid interval trigger.`);
309
310
  } else throw new Error(`Schedule record '${file}' has an unsupported trigger.`);
310
311
  if (record.sessionOnly !== undefined && typeof record.sessionOnly !== "boolean") throw new Error(`Schedule record '${file}' has invalid sessionOnly.`);
312
+ if (record.quiet !== undefined && typeof record.quiet !== "boolean") throw new Error(`Schedule record '${file}' has invalid quiet.`);
311
313
  if (record.sessionOnly === true && (typeof record.ownerSessionFile !== "string" || !record.ownerSessionFile.trim())) throw new Error(`Schedule record '${file}' is session-only but has no owner session file.`);
312
314
  return { ...record, target: parseScheduleTarget(record.target, file) } as ScheduleRecord;
313
315
  }
@@ -448,7 +450,7 @@ function sanitizeTarget(params: SubagentParamsLike): { target?: ScheduleTarget;
448
450
  return { target: { workflowScript: params.workflowScript.trim(), ...(baseRef === undefined ? {} : { baseRef }) } };
449
451
  }
450
452
 
451
- function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
453
+ function executionParams(schedule: ScheduleRecord, quiet = false): SubagentParamsLike {
452
454
  return {
453
455
  ...schedule.target,
454
456
  async: true,
@@ -456,7 +458,7 @@ function executionParams(schedule: ScheduleRecord): SubagentParamsLike {
456
458
  cwd: schedule.cwd,
457
459
  mission: false,
458
460
  // Scheduled fires have no operator watching, so completions must name the origin.
459
- scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}) },
461
+ scheduleOrigin: { id: schedule.id, ...(schedule.name ? { name: schedule.name } : {}), ...(quiet ? { quiet: true } : {}) },
460
462
  ...(schedule.timeoutMs === undefined ? {} : { timeoutMs: schedule.timeoutMs }),
461
463
  };
462
464
  }
@@ -612,6 +614,8 @@ export class ScheduledRunManager {
612
614
  if (params.catchUp !== undefined && params.catchUp !== "none" && params.catchUp !== "latest") return textResult("catchUp must be 'none' or 'latest'.", undefined, undefined, true);
613
615
  if (params.missionId !== undefined || params.mission !== undefined || params.missionUpdate !== undefined || params.missionStatus !== undefined || params.missionScope !== undefined) return textResult("Mission attachment is deferred from this first schedule slice.", undefined, undefined, true);
614
616
  if (params.on !== undefined || params.timezone !== undefined || every === "day" || every === "week" || every === "month" || every === "year") return textResult("Calendar schedules are deferred from this first safe slice. Use a fixed interval such as every:'24h' or every:'7d'.", undefined, undefined, true);
617
+ if (params.quiet !== undefined && typeof params.quiet !== "boolean") return textResult("quiet must be a boolean.", undefined, undefined, true);
618
+ if (at && params.quiet === true) return textResult("quiet is only supported for recurring schedules.", undefined, undefined, true);
615
619
  const sessionOnly = params.sessionOnly === true;
616
620
  if (sessionOnly && params.cwd !== undefined && !samePath(params.cwd, ctx.cwd)) return textResult("sessionOnly schedules cannot use an explicit cross-project cwd.", undefined, undefined, true);
617
621
  const ownerSessionFile = sessionOnly ? ctx.sessionManager.getSessionFile() : undefined;
@@ -644,13 +648,14 @@ export class ScheduledRunManager {
644
648
  ...(params.timeoutMs === undefined ? {} : { timeoutMs: params.timeoutMs }),
645
649
  paused: false,
646
650
  ...(sessionOnly ? { sessionOnly: true, ownerSessionFile: path.resolve(ownerSessionFile!) } : {}),
651
+ ...(trigger.kind === "interval" && params.quiet === true ? { quiet: true } : {}),
647
652
  createdAt: timestamp(now),
648
653
  updatedAt: timestamp(now),
649
654
  };
650
655
  store.write(schedule);
651
656
  store.appendEvent(schedule, "schedule.created");
652
657
  this.arm(schedule, store);
653
- return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
658
+ return textResult(`Created schedule ${id}.\nName: ${schedule.name}\nTrigger: ${at ? `at ${at}` : `every ${every}`}\nSession only: ${schedule.sessionOnly === true ? "yes" : "no"}\nQuiet: ${schedule.quiet === true ? "yes" : "no"}\nNext: ${schedule.trigger.nextRunAt}\nTarget: ${targetLabel(schedule.target)}`, [schedule]);
654
659
  }
655
660
 
656
661
  private list(): AgentToolResult<Details> {
@@ -661,7 +666,7 @@ export class ScheduledRunManager {
661
666
 
662
667
  private show(params: SubagentParamsLike): AgentToolResult<Details> {
663
668
  const schedule = this.resolve(params);
664
- return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
669
+ return textResult([`Schedule: ${schedule.id}`, `Name: ${schedule.name}`, `State: ${schedule.paused ? "paused" : schedule.activeRunId ? "running" : "scheduled"}`, `Session only: ${schedule.sessionOnly === true ? "yes" : "no"}`, `Quiet: ${schedule.quiet === true ? "yes" : "no"}`, `Target: ${targetLabel(schedule.target)}`, `CWD: ${shortenPath(schedule.cwd)}`, `Next: ${schedule.trigger.nextRunAt ?? "none"}`, `Catch up: ${schedule.catchUp}`, schedule.activeRunId ? `Active run: ${schedule.activeRunId}` : undefined].filter(Boolean).join("\n"), [schedule]);
665
670
  }
666
671
 
667
672
  private history(params: SubagentParamsLike): AgentToolResult<Details> {
@@ -689,7 +694,18 @@ export class ScheduledRunManager {
689
694
  if (!scheduleBelongsToSession(schedule, context)) {
690
695
  return textResult(`Skipped schedule ${schedule.id}: current session is not its owner.`, [schedule]);
691
696
  }
692
- const run = await this.launch(store, schedule, this.now(), "manual", false);
697
+ if (params.quiet !== undefined && typeof params.quiet !== "boolean") return textResult("quiet must be a boolean.", undefined, undefined, true);
698
+ const run = await this.launch(store, schedule, this.now(), "manual", false, params.quiet === true);
699
+ const updated = store.get(schedule.id);
700
+ if (run.state === "running") {
701
+ const now = this.now();
702
+ if (updated.trigger.kind === "interval") updated.trigger.nextRunAt = timestamp(now + updated.trigger.everyMs);
703
+ else updated.trigger.nextRunAt = undefined;
704
+ updated.updatedAt = timestamp(now);
705
+ store.write(updated);
706
+ store.appendEvent(updated, "schedule.manual_satisfied");
707
+ this.arm(updated, store);
708
+ }
693
709
  return textResult(`Manual schedule run ${run.id}: ${run.state}${run.asyncId ? ` (async ${run.asyncId})` : ""}.`, [store.get(schedule.id)], [run], run.state === "failed_launch");
694
710
  }
695
711
 
@@ -708,8 +724,16 @@ export class ScheduledRunManager {
708
724
 
709
725
  private remove(params: SubagentParamsLike): AgentToolResult<Details> {
710
726
  const schedule = this.resolve(params);
711
- if (schedule.activeRunId) return textResult(`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`, [schedule], undefined, true);
712
727
  const store = this.requireStore();
728
+ if (schedule.activeRunId) {
729
+ const run = store.history(schedule.id).find((item) => item.id === schedule.activeRunId);
730
+ let terminal = false;
731
+ if (run?.scheduleId === schedule.id && run.state === "running" && run.asyncId && run.asyncDir) {
732
+ const status = readJson(path.join(run.asyncDir, "status.json"), "async status") as Partial<AsyncStatus>;
733
+ terminal = status.runId === run.asyncId && typeof status.state === "string" && ["complete", "failed", "stopped", "rejected"].includes(status.state);
734
+ }
735
+ if (!terminal) return textResult(`Schedule ${schedule.id} has active run ${schedule.activeRunId}; stop that run before deleting the schedule.`, [schedule], undefined, true);
736
+ }
713
737
  this.clearTimer(store, schedule.id);
714
738
  store.appendEvent(schedule, "schedule.deleted");
715
739
  store.delete(schedule.id);
@@ -807,8 +831,9 @@ export class ScheduledRunManager {
807
831
  await this.launch(store, schedule, planned, "timer", true);
808
832
  }
809
833
 
810
- private async launch(store: ScheduleStore, schedule: ScheduleRecord, planned: number, dueReason: ScheduleRunRecord["dueReason"], advance: boolean): Promise<ScheduleRunRecord> {
834
+ private async launch(store: ScheduleStore, schedule: ScheduleRecord, planned: number, dueReason: ScheduleRunRecord["dueReason"], advance: boolean, quiet?: boolean): Promise<ScheduleRunRecord> {
811
835
  const now = this.now();
836
+ const nextRunAtBeforeClaim = schedule.trigger.nextRunAt;
812
837
  const run: ScheduleRunRecord = { schemaVersion: 1, id: this.randomId(), scheduleId: schedule.id, plannedAt: timestamp(planned), dueReason, state: "running", startedAt: timestamp(now) };
813
838
  if (schedule.activeRunId) {
814
839
  run.state = "skipped";
@@ -849,7 +874,7 @@ export class ScheduledRunManager {
849
874
  store.write(schedule);
850
875
  store.writeRun(schedule, run, "schedule.run.started");
851
876
  try {
852
- const result = await this.deps.launch(executionParams(schedule), this.requireContext(store), new AbortController().signal);
877
+ const result = await this.deps.launch(executionParams(schedule, dueReason === "manual" ? quiet === true : schedule.quiet === true), this.requireContext(store), new AbortController().signal);
853
878
  const asyncId = result.details?.asyncId ?? result.details?.runId;
854
879
  if (result.isError || !asyncId) throw new Error(result.content.find((item) => item.type === "text")?.text ?? "Scheduled launch failed.");
855
880
  run.asyncId = asyncId;
@@ -862,12 +887,14 @@ export class ScheduledRunManager {
862
887
  run.state = "failed_launch";
863
888
  run.completedAt = timestamp(this.now());
864
889
  run.error = error instanceof Error ? error.message : String(error);
865
- schedule.activeRunId = undefined;
866
- schedule.updatedAt = timestamp(this.now());
867
- store.write(schedule);
868
- store.writeRun(schedule, run, "schedule.run.failed");
890
+ const latest = store.get(schedule.id);
891
+ latest.activeRunId = undefined;
892
+ if (!advance && nextRunAtBeforeClaim) latest.trigger.nextRunAt = nextRunAtBeforeClaim;
893
+ latest.updatedAt = timestamp(this.now());
894
+ store.write(latest);
895
+ store.writeRun(latest, run, "schedule.run.failed");
869
896
  fs.rmSync(lockPath, { force: true });
870
- this.arm(schedule, store);
897
+ this.arm(latest, store);
871
898
  return run;
872
899
  }
873
900
  }
@@ -9,6 +9,7 @@ import { resolveEffectiveThinking } from "../../shared/model-info.ts";
9
9
  import { normalizeParallelGroups } from "./parallel-groups.ts";
10
10
  import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent, type NestedRoute } from "../shared/nested-events.ts";
11
11
  import { assertWorkflowGraphHostSteps } from "../shared/host-step-status.ts";
12
+ import type { RawDrainStatusObserver } from "../shared/readonly-drain-observation.ts";
12
13
 
13
14
  export type PidLiveness = "alive" | "dead" | "unknown";
14
15
 
@@ -352,9 +353,10 @@ export function checkPidLiveness(pid: number, kill: KillFn = process.kill): PidL
352
353
  }
353
354
  }
354
355
 
355
- export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}): ReconcileAsyncRunResult {
356
+ export function reconcileAsyncRun(asyncDir: string, options: ReconcileAsyncRunOptions = {}, observeStatus?: RawDrainStatusObserver): ReconcileAsyncRunResult {
356
357
  const now = options.now?.() ?? Date.now();
357
358
  const status = readStatus(asyncDir);
359
+ observeStatus?.(status);
358
360
  const startedStatus = !status && options.startedRun ? buildStartedStatus(asyncDir, options.startedRun, now) : undefined;
359
361
  const effectiveStatus = status ?? startedStatus;
360
362
  if (!effectiveStatus) return { status: null, repaired: false };
@@ -14,6 +14,7 @@ import type {
14
14
  import { readStatus } from "../../shared/utils.ts";
15
15
  import { previewDisplayText } from "../../shared/display-text.ts";
16
16
  import { redactSecretValues } from "../shared/permissions.ts";
17
+ import type { SteerDeliveryMode } from "./control-channel.ts";
17
18
 
18
19
  export const MAX_STEERING_REQUESTS = 20;
19
20
  export const STEERING_MESSAGE_PREVIEW_LIMIT = 160;
@@ -22,6 +23,21 @@ export function steeringMessagePreview(message: string): string {
22
23
  return previewDisplayText(redactSecretValues(message), STEERING_MESSAGE_PREVIEW_LIMIT);
23
24
  }
24
25
 
26
+ /** FIFO match of one accepted steer to an emitted user message; equal text claims the oldest entry. */
27
+ export function takeMatchingAcceptedSteer<T extends { text: string }>(accepted: T[], messageText: string): T | undefined {
28
+ const index = accepted.findIndex((entry) => entry.text === messageText);
29
+ if (index < 0) return undefined;
30
+ const [entry] = accepted.splice(index, 1);
31
+ return entry;
32
+ }
33
+
34
+ /** Settlement reason for one accepted request that never got a matching user `message_end`. */
35
+ export function unconsumedSteerReason(mode?: SteerDeliveryMode): string {
36
+ return mode === "follow_up"
37
+ ? "child completed before consuming follow-up"
38
+ : "child completed before consuming steering";
39
+ }
40
+
25
41
  export function steeringReceipt(message: string, receipt: string): string {
26
42
  const preview = steeringMessagePreview(message);
27
43
  const longestFence = Math.max(2, ...[...preview.matchAll(/`{3,}/g)].map((match) => match[0]!.length));
@@ -93,7 +109,9 @@ export function updateSteeringTarget(
93
109
  if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
94
110
  return target;
95
111
  }
96
- if ((target.state === "routed" || target.state === "queued") && state !== "routed" && state !== "queued") status.pending = Math.max(0, status.pending - 1);
112
+ const wasPending = target.state === "routed" || target.state === "queued";
113
+ const nowPending = state === "routed" || state === "queued";
114
+ if (wasPending && !nowPending) status.pending = Math.max(0, status.pending - 1);
97
115
  target.state = state;
98
116
  if (state === "routed") target.routedAt = now;
99
117
  if (state === "delivered") {
@@ -108,7 +126,7 @@ export function updateSteeringTarget(
108
126
  if (state === "recovered") target.recoveredAt = now;
109
127
  if (fields.reason) target.reason = fields.reason;
110
128
  if (fields.replacementRunId) target.replacementRunId = fields.replacementRunId;
111
- incrementStateCount(status, state);
129
+ if (!wasPending || !nowPending) incrementStateCount(status, state);
112
130
  return target;
113
131
  }
114
132