pi-better-harness 0.44.0 → 0.45.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.45.0] - 2026-11-12
4
+
5
+ ### Changed
6
+
7
+ - Bundle Subagents 0.32.0 for session-scoped historical wait warnings, original-session inspection, durable cancellation, and dismissal of unchanged notices. (#719)
8
+
3
9
  ## [0.44.0] - 2026-11-12
4
10
 
5
11
  ### Changed
@@ -1085,3 +1085,17 @@ Near-term:
1085
1085
  twice ([#2](https://github.com/1aboveio/pi-better-subagents/issues/2)) — **done**.
1086
1086
  - Named agent-definition files (per-agent system prompt + tool allowlist) and
1087
1087
  chain/parallel orchestration.
1088
+ # Historical Waits
1089
+
1090
+ `/subagents history` lists retained waits from other sessions in this workspace.
1091
+ Each entry identifies its originating session and offers inspection or explicit
1092
+ cancellation. Inspection does not adopt the assignment, acknowledge its evidence,
1093
+ or start a continuation. Cancellation confirms the durable result; it does not
1094
+ claim that an uncertain writer has stopped. Failures leave the wait available
1095
+ for inspection and retry.
1096
+
1097
+ `/subagents history dismiss` durably dismisses the current historical-attention
1098
+ notice without changing any wait or evidence. A changed wait generation, phase,
1099
+ publication or attention reason can produce a new notice. Ordinary task navigation
1100
+ remains scoped to the active session.
1101
+
@@ -0,0 +1,35 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { type RunCallbackOrigin } from "./registry.ts";
5
+ import { durableSubagentRoot } from "./session-store.ts";
6
+ import { parkedWaits, type ParkedWait } from "./worker-waits.ts";
7
+
8
+ /** Discovery is not adoption: the captured owner and wait evidence stay untouched. */
9
+ export function historicalWaits(origin: RunCallbackOrigin | undefined): ParkedWait[] {
10
+ if (!origin) return [];
11
+ return parkedWaits().filter(({ record }) => record.ownerWorkspace === origin.cwd
12
+ && record.ownerSessionId !== origin.sessionId);
13
+ }
14
+
15
+ export function attentionRevision(waits: ParkedWait[]): string {
16
+ return createHash("sha256").update(JSON.stringify(waits.map(({ runId, record }) => [
17
+ runId, record.waitId, record.generation, record.phase, record.heldReason,
18
+ record.cancelReason, record.resumedBy, record.resumeLaunchToken,
19
+ ]).sort((a, b) => String(a[0]).localeCompare(String(b[0]))))).digest("hex");
20
+ }
21
+
22
+ function receiptPath(workspace: string): string {
23
+ return join(durableSubagentRoot(), `historical-attention-${createHash("sha256").update(workspace).digest("hex")}.json`);
24
+ }
25
+ export function attentionDismissed(workspace: string, revision: string): boolean {
26
+ try { return JSON.parse(readFileSync(receiptPath(workspace), "utf8")).revision === revision; }
27
+ catch { return false; }
28
+ }
29
+ export function dismissAttention(workspace: string, revision: string): void {
30
+ mkdirSync(durableSubagentRoot(), { recursive: true });
31
+ const path = receiptPath(workspace);
32
+ const temp = `${path}.${randomUUID()}.tmp`;
33
+ writeFileSync(temp, JSON.stringify({ version: 1, revision }), { mode: 0o600 });
34
+ renameSync(temp, path);
35
+ }
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { assignmentFor } from "./assignment-projection.mjs";
15
+ import { historicalWaits, attentionRevision, attentionDismissed, dismissAttention } from "./historical-attention.ts";
15
16
  import { registerAssignmentAttempt, commitAssignmentTerminal, readOutcome, receiptOutcome, outcomeTranscriptReceipt, inspectOutcome, createOutcomeHost, closeOutcomeHost, claimOutcome, type AssignmentBinding } from "./assignment-outcomes.ts";
16
17
  import { currentSandboxPermissions } from "./permission-policy.ts";
17
18
  import { requestResumeAdmission } from "./worker-waits.ts";
@@ -238,6 +239,8 @@ import { defaultUserRoot, type CatalogSnapshot } from "./catalog-store.ts";
238
239
  import { restrictContextFilesToWorktree } from "./shared-worktree-context.ts";
239
240
 
240
241
  const SUBAGENT_MODE_ACTIONS: readonly AutocompleteItem[] = [
242
+ { value: "history", label: "history", description: "Inspect or cancel waits from other sessions" },
243
+ { value: "history dismiss", label: "history dismiss", description: "Dismiss unchanged historical attention notices" },
241
244
  { value: "settings", label: "settings", description: "Open delegation, concurrency, and timing settings" },
242
245
  { value: "cap", label: "cap <number>", description: "Set a positive concurrent subagent limit for this session" },
243
246
  { value: "tool-wait", label: "tool-wait <minutes>", description: "Set the session tool-wait alert time; 0 turns it off" },
@@ -1438,20 +1441,22 @@ function navigatorDetail(id: string, now: number = Date.now()) {
1438
1441
  * Set by the active extension instance: cancel a parked assignment's pending
1439
1442
  * wait. Returns false when the run is not parked, so the ordinary stop path runs.
1440
1443
  */
1441
- let cancelParkedForUi: ((id: string, reason: string) => boolean) | undefined;
1444
+ let cancelParkedForUi: ((id: string, reason: string) => Promise<boolean>) | undefined;
1442
1445
 
1443
1446
  /** Shared #44 stop+dismiss path used by navigator Close (#47). */
1444
1447
  function navigatorCloseRun(id: string) {
1445
1448
  // A parked attempt's child already exited cleanly, so "close" must cancel the
1446
1449
  // pending automatic resume instead of pretending to stop a live process.
1447
- if (cancelParkedForUi?.(id, "closed from the navigator; the automatic resume is cancelled")) {
1448
- try { dismissRun(id); } catch { /* dismissal is best effort */ }
1449
- spendCache.delete(id);
1450
- healthLogCache.delete(id);
1451
- resetChildEventLogCursor(id);
1452
- resetParseRunCursor(id);
1453
- syncHealthTicker();
1454
- return { action: "stopped", id };
1450
+ if (parkedWaitForRun(id) && cancelParkedForUi) {
1451
+ void cancelParkedForUi(id, "closed from the navigator; the automatic resume is cancelled")
1452
+ .then((cancelled) => {
1453
+ if (!cancelled) throw new Error("The pending wait changed; inspect it again.");
1454
+ dismissRun(id);
1455
+ uiCtx?.ui.notify(`Cancelled the pending wait of ${id}. Writer termination may remain unproven.`, "info");
1456
+ })
1457
+ .catch((error) => uiCtx?.ui.notify(`Cancellation failed for ${id}: ${String(error)}. The wait remains available for inspection and retry.`, "warning"));
1458
+ // The synchronous navigator must retain the row until the durable operation settles.
1459
+ return { action: "missing", id };
1455
1460
  }
1456
1461
  const outcome = executeNavigatorClose(id, {
1457
1462
  readMeta,
@@ -2300,18 +2305,21 @@ export default function (pi: ExtensionAPI) {
2300
2305
  childExtensionPath: childBridgeExtensionPath(selfDir()),
2301
2306
  });
2302
2307
  let resumeOrchestration: ResumeOrchestration | undefined;
2308
+ const notifyOwnedWait = (message: string, level: "warning" | "info" = "warning") => {
2309
+ try { uiCtx?.ui?.notify?.(message, level); } catch { /* no UI */ }
2310
+ };
2303
2311
  /**
2304
2312
  * Stop a continuation attempt only when this parent owns it and the token
2305
2313
  * published with it matches. A pid is never signalled on a guess.
2306
2314
  */
2307
2315
  const stopPublishedAttempt = createOwnedAttemptStopper();
2308
- cancelParkedForUi = (id, reason) => {
2316
+ cancelParkedForUi = async (id, reason) => {
2309
2317
  const parked = parkedWaitForRun(id);
2310
2318
  if (!parked) return false;
2311
- void cancelParkedWait(workerOperations.service, parked, reason, { stopAttempt: stopPublishedAttempt })
2312
- .then(() => { if (parked.meta.assignment) enqueueCompletionCallback(pi, id); })
2313
- .catch(() => { /* the durable record keeps the wait visible */ })
2314
- .finally(() => syncHealthTicker());
2319
+ try {
2320
+ await cancelParkedWait(workerOperations.service, parked, reason, { stopAttempt: stopPublishedAttempt });
2321
+ if (parked.meta.assignment) enqueueCompletionCallback(pi, id);
2322
+ } finally { syncHealthTicker(); }
2315
2323
  return true;
2316
2324
  };
2317
2325
  const effectiveSettings = () => {
@@ -3482,9 +3490,8 @@ export default function (pi: ExtensionAPI) {
3482
3490
  capacity: resumeCapacity(),
3483
3491
  launch: launchParkedResume,
3484
3492
  stopAttempt: stopPublishedAttempt,
3485
- notify: (message, level) => {
3486
- try { uiCtx?.ui?.notify?.(message, level); } catch { /* ignore UI failures */ }
3487
- },
3493
+ notifyWhen: (meta) => Boolean(activeCallbackOrigin && belongsToOrigin(meta, activeCallbackOrigin)),
3494
+ notify: notifyOwnedWait,
3488
3495
  });
3489
3496
  return resumeOrchestration;
3490
3497
  }
@@ -3509,10 +3516,13 @@ export default function (pi: ExtensionAPI) {
3509
3516
  try {
3510
3517
  await reconcileParkedWaits(service, {
3511
3518
  resume: (park) => orchestration.resume(park),
3512
- notify: (message) => {
3513
- try { uiCtx?.ui?.notify?.(message, "warning"); } catch { /* ignore UI failures */ }
3514
- },
3519
+ notifyWhen: (meta) => Boolean(activeCallbackOrigin && belongsToOrigin(meta, activeCallbackOrigin)),
3520
+ notify: notifyOwnedWait,
3515
3521
  });
3522
+ const historical = historicalWaits(activeCallbackOrigin);
3523
+ if (historical.length && activeCallbackOrigin && !attentionDismissed(activeCallbackOrigin.cwd, attentionRevision(historical))) {
3524
+ uiCtx?.ui?.notify?.(`${historical.length} historical subagent waits remain. Inspect or cancel with /subagents history; dismiss this notice with /subagents history dismiss.`, "warning");
3525
+ }
3516
3526
  } catch { /* reconciliation retries on the next session start */ }
3517
3527
  }
3518
3528
 
@@ -3990,23 +4000,25 @@ export default function (pi: ExtensionAPI) {
3990
4000
  enqueueCompletionCallback(pi, id);
3991
4001
  } });
3992
4002
  acceptanceStopToolRef = acceptanceStopTool;
3993
- // A parked assignment has no live child: `subagent_stop` is the explicit
3994
- // takeover that cancels its pending automatic resume, so the conversation can
3995
- // be continued manually. Without this, stop would report "not running" and
3996
- // leave the automatic resume armed for the same assignment.
4003
+ // A parked attempt may have exited, but its automatic resume is still armed.
4004
+ // Explicit stop must confirm durable cancellation, not just stop a process.
3997
4005
  const acceptanceStopToolWithParked = {
3998
4006
  ...acceptanceStopTool,
3999
4007
  async execute(toolCallId: string, params: unknown, signal?: AbortSignal, onUpdate?: unknown, ctx?: unknown) {
4000
4008
  const target = (params as { id?: unknown } | undefined)?.id;
4001
- if (typeof target === "string" && cancelParkedForUi?.(target, "cancelled by subagent_stop; the automatic resume is cancelled")) {
4002
- renderWidget();
4003
- syncHealthTicker();
4004
- return {
4005
- content: [{
4006
- type: "text",
4007
- text: `Cancelled the pending wait of parked subagent ${target}. Its conversation can now be continued with subagent_spawn session_id.`,
4008
- }],
4009
- };
4009
+ try {
4010
+ if (typeof target === "string" && await cancelParkedForUi?.(target, "cancelled by subagent_stop; the automatic resume is cancelled")) {
4011
+ renderWidget();
4012
+ syncHealthTicker();
4013
+ return {
4014
+ content: [{
4015
+ type: "text",
4016
+ text: `Durably cancelled the pending wait of parked subagent ${target}. Writer termination may remain unproven; ordinary continuation admission still applies.`,
4017
+ }],
4018
+ };
4019
+ }
4020
+ } catch (error) {
4021
+ return { isError: true, content: [{ type: "text", text: `Cancellation failed for ${target}: ${String(error)}. The retained wait remains available for inspection and retry.` }] };
4010
4022
  }
4011
4023
  return (acceptanceStopTool.execute as (
4012
4024
  id: string, p: unknown, s?: AbortSignal, u?: unknown, c?: unknown,
@@ -4100,10 +4112,36 @@ export default function (pi: ExtensionAPI) {
4100
4112
  if (typeof pi.registerCommand === "function") {
4101
4113
  agentOperations.registerCommands(pi);
4102
4114
  pi.registerCommand("subagents", {
4103
- description: "[settings|mode manual|adaptive|coordinator|cap <number>|tool-wait <minutes>|stuck <minutes>|save|reset] — Subagent settings",
4115
+ description: "[history [dismiss]|settings|mode manual|adaptive|coordinator|cap <number>|tool-wait <minutes>|stuck <minutes>|save|reset] — Subagent management",
4104
4116
  getArgumentCompletions: subagentsArgumentCompletions,
4105
4117
  async handler(args, ctx) {
4106
4118
  const tokens = args.trim().split(/\s+/).filter(Boolean);
4119
+ if (tokens[0] === "history") {
4120
+ try {
4121
+ const origin = { cwd: ctx.cwd, sessionId: ctx.sessionManager.getSessionId() };
4122
+ const waits = historicalWaits(origin);
4123
+ if (tokens[1] === "dismiss") {
4124
+ dismissAttention(ctx.cwd, attentionRevision(waits));
4125
+ ctx.ui.notify("Historical attention notice dismissed. Waits and evidence are unchanged.", "info");
4126
+ return;
4127
+ }
4128
+ const labels = waits.map((p) => `${p.runId} · session ${p.record.ownerSessionId ?? "unknown"} · ${p.phase}`);
4129
+ const selected = await ctx.ui.select("Historical subagent waits", labels);
4130
+ const park = waits[labels.indexOf(selected ?? "")];
4131
+ if (!park) return;
4132
+ const action = await ctx.ui.select(`${park.runId} · session ${park.record.ownerSessionId ?? "unknown"}`, ["Inspect", "Cancel pending wait"]);
4133
+ if (action === "Inspect") {
4134
+ ctx.ui.notify(JSON.stringify({ runId: park.runId, originatingSession: park.record.ownerSessionId,
4135
+ workspace: park.record.ownerWorkspace, waitId: park.waitId, generation: park.generation,
4136
+ phase: park.phase, reason: park.record.heldReason, cancellation: park.record.cancelReason,
4137
+ resumedBy: park.record.resumedBy, log: park.meta.logPath }), "info");
4138
+ } else if (action === "Cancel pending wait" && await ctx.ui.confirm("Cancel pending wait?", `${park.runId} belongs to session ${park.record.ownerSessionId ?? "unknown"}. This does not prove its writer has stopped.`)) {
4139
+ if (!await cancelParkedForUi?.(park.runId, "explicit cancellation from historical management")) throw new Error("The pending wait changed; inspect it again.");
4140
+ ctx.ui.notify(`Durably cancelled the pending wait of ${park.runId}. Writer termination may remain unproven.`, "info");
4141
+ }
4142
+ } catch (error) { ctx.ui.notify(`Historical wait management failed: ${String(error)}. Inspect or retry with /subagents history.`, "warning"); }
4143
+ return;
4144
+ }
4107
4145
  if (tokens.length === 0 || (tokens.length === 1 && tokens[0] === "settings")) {
4108
4146
  await openSettings(ctx);
4109
4147
  return;
@@ -4153,7 +4191,7 @@ export default function (pi: ExtensionAPI) {
4153
4191
  changeTiming(key, minutes);
4154
4192
  ctx.ui.notify(`${tokens[0] === "tool-wait" ? "Tool-wait" : "No-progress"} alert: ${minutes} min (current session). Running subagents are unchanged.`, "info");
4155
4193
  } else {
4156
- ctx.ui.notify("Usage: /subagents [settings|mode manual|adaptive|coordinator|cap <number>|tool-wait <minutes>|stuck <minutes>|save|reset]", "warning");
4194
+ ctx.ui.notify("Usage: /subagents [history [dismiss]|settings|mode manual|adaptive|coordinator|cap <number>|tool-wait <minutes>|stuck <minutes>|save|reset]", "warning");
4157
4195
  }
4158
4196
  refreshBackgroundWorkNavigator(ctx);
4159
4197
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-subagents",
3
- "version": "0.31.0",
3
+ "version": "0.32.0",
4
4
  "description": "Pi extension for detached, sandboxed subagent runs that keep the foreground session free.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -1670,6 +1670,7 @@ export interface ResumeOrchestrationOptions {
1670
1670
  /** Follow a cancelled generation to the attempt it already published. */
1671
1671
  stopAttempt?: StopPublishedAttempt;
1672
1672
  notify?(message: string, level: "info" | "warning"): void;
1673
+ notifyWhen?(meta: RunMeta): boolean;
1673
1674
  now?: () => number;
1674
1675
  }
1675
1676
 
@@ -1872,7 +1873,7 @@ export function createResumeOrchestration(options: ResumeOrchestrationOptions):
1872
1873
  } catch { /* the park record still shows the hold */ }
1873
1874
  if (park.meta.assignment) commitAssignmentTerminal({ ...park.meta, status: "failed", failureReason: reason }, true);
1874
1875
  try {
1875
- options.notify?.(`Subagent ${park.runId} wait ${park.waitId} is held: ${reason}`, "warning");
1876
+ if (!options.notifyWhen || options.notifyWhen(park.meta)) options.notify?.(`Subagent ${park.runId} wait ${park.waitId} is held: ${reason}`, "warning");
1876
1877
  } catch { /* ignore UI failures */ }
1877
1878
  return "held";
1878
1879
  };
@@ -2117,7 +2118,8 @@ export async function cancelParkedWait(
2117
2118
  const scope = backgroundScopeForMeta(park.meta, park.record);
2118
2119
  const at = (options.now ?? Date.now)();
2119
2120
  if (park.meta.assignment) {
2120
- commitAssignmentTerminal({ ...park.meta, status: "killed" });
2121
+ const outcome = commitAssignmentTerminal({ ...park.meta, status: "killed" });
2122
+ if (outcome?.outcome !== "cancelled") throw new Error("The assignment already has a non-cancelled terminal outcome; cancellation was not committed.");
2121
2123
  const cancellation = await stopPublishedAttempt(park, reason, options.stopAttempt, options.now);
2122
2124
  updateParkRecord(park.runId, { phase: "cancelled", cancelReason: cancellation === "foreign" ? `${reason} (published writer termination remains unproven)` : reason, cancelledAt: at });
2123
2125
  return;
@@ -2130,10 +2132,10 @@ export async function cancelParkedWait(
2130
2132
  updateParkRecord(park.runId, {
2131
2133
  phase: "held",
2132
2134
  cancelReason: `${reason} (the durable cancellation failed: ${reasonOf(error)})`,
2133
- cancelledAt: at,
2134
2135
  });
2135
- return;
2136
+ throw new Error(`Durable cancellation failed: ${reasonOf(error)}`);
2136
2137
  }
2138
+ if (view.phase !== "cancelled") throw new Error(`Durable cancellation was not confirmed (wait is ${view.phase}).`);
2137
2139
  const settled = readParkRecord(park.runId);
2138
2140
  if (settled?.phase === "cancelled" && settled.cancelReason) return;
2139
2141
  const cancellation = await stopPublishedAttempt(park, reason, options.stopAttempt, options.now);
@@ -2148,14 +2150,7 @@ export async function cancelParkedWait(
2148
2150
  });
2149
2151
  return;
2150
2152
  }
2151
- const cancellation = await stopPublishedAttempt(park, reason, options.stopAttempt, options.now);
2152
- updateParkRecord(park.runId, {
2153
- phase: "cancelled",
2154
- cancelReason: cancellation === "foreign"
2155
- ? `${reason} (the published continuation could not be attributed to this parent and was left running)`
2156
- : reason,
2157
- cancelledAt: at,
2158
- });
2153
+ throw new Error("Durable cancellation unavailable: the original worker scope and observed-waits service are required.");
2159
2154
  }
2160
2155
 
2161
2156
  /**
@@ -2493,17 +2488,22 @@ export async function settleResumedAttemptExit(input: ResumedAttemptExitInput):
2493
2488
 
2494
2489
  export async function reconcileParkedWaits(
2495
2490
  service: WorkerOperationService | undefined,
2496
- options: {
2491
+ outerOptions: {
2497
2492
  resume(park: ParkedWait): Promise<"resumed" | "held" | "skipped">;
2498
2493
  notify?(message: string, level: "warning"): void;
2494
+ notifyWhen?(meta: RunMeta): boolean;
2499
2495
  },
2500
2496
  ): Promise<{ resumed: number; held: number; skipped: number }> {
2501
2497
  let resumed = 0;
2502
2498
  let held = 0;
2503
2499
  let skipped = 0;
2500
+ const options = outerOptions;
2501
+ const scopedOptions = (meta: RunMeta) => ({ ...outerOptions,
2502
+ notify: outerOptions.notifyWhen && !outerOptions.notifyWhen(meta) ? undefined : outerOptions.notify });
2504
2503
  if (service?.outageReconciliation === 1) {
2505
2504
  try {
2506
2505
  for (const report of pendingObservations()) {
2506
+ const options = scopedOptions(report.meta);
2507
2507
  try {
2508
2508
  const scope = backgroundScopeForMeta(report.meta, report.record);
2509
2509
  if (!scope) continue;
@@ -2569,6 +2569,7 @@ export async function reconcileParkedWaits(
2569
2569
  }
2570
2570
  }
2571
2571
  for (const park of parkedWaits()) {
2572
+ const options = scopedOptions(park.meta);
2572
2573
  try {
2573
2574
  if (park.record.phase === "resuming") {
2574
2575
  const attemptId = park.record.resumedBy ?? "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.44.0",
3
+ "version": "0.45.0",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, model routing, and Outcome & Todo.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -64,7 +64,7 @@
64
64
  "pi-ollama-cloud": "0.13.1",
65
65
  "pi-better-sandbox": "0.14.1",
66
66
  "pi-better-ssh": "0.1.5",
67
- "pi-better-subagents": "0.31.0",
67
+ "pi-better-subagents": "0.32.0",
68
68
  "proper-lockfile": "^4.1.2",
69
69
  "shell-quote": "1.12.0",
70
70
  "smol-toml": "1.9.0",