@dougbots/avenor-pi 0.16.1 → 0.17.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/README.md CHANGED
@@ -14,6 +14,10 @@ which avenor
14
14
  export AVENOR_BIN=~/.botfiles/bin/avenor
15
15
  ```
16
16
 
17
+ You also need Pi 1.4.0 or newer (`pi --version`). Busy-session delivery depends on
18
+ the `agent_before_settle` hook, so a notice raised while you were busy needs that
19
+ version to reach you. A missing notification is not a lost run.
20
+
17
21
  ## Installation
18
22
 
19
23
  ### From npm (once published)
@@ -144,8 +148,9 @@ Each roster entry must contain `backend` and at least one of `agent` or `model`.
144
148
  Beyond the tools, the extension integrates with Pi's TUI and event system:
145
149
 
146
150
  - **Status widget** — persistent widget showing all active runs with status, phase, and permission state
147
- - **Footer status** — active runs and polling error counts shown in the Pi footer bar
148
- - **Polling diagnostics** — bounded polling errors are emitted on `avenor:poll:error` for companion extensions
151
+ - **Footer status** — the `avenor` status key carries active runs, the live agent-tree total, the unread notice count, and the current poll-failure count (see [Completion notices](#completion-notices))
152
+ - **Completion notices** — one notice per finished run, delivered immediately when your session is idle and batched into a single continuation when it was busy
153
+ - **Polling diagnostics** — bounded polling errors are emitted on `avenor:poll:error` for companion extensions; a completion-notice delivery that exhausts its retries is emitted separately on `avenor:delivery:error`
149
154
  - **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
150
155
  - **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
151
156
  - **Custom rendering** — The Pi extension renders all Avenor tool calls and results as themed, bounded summaries. Results have collapsed and expanded views.
@@ -155,6 +160,56 @@ Beyond the tools, the extension integrates with Pi's TUI and event system:
155
160
  - `/avenor-watch <run_id>` — open a live event feed overlay
156
161
  - `/avenor-cancel <run_id>` — cancel a running sub-agent
157
162
 
163
+ ### Completion notices
164
+
165
+ A fire-and-forget spawn (`wait: false`) promises one thing: you hear about the
166
+ run when it ends. Each finished run produces one notice — label, terminal
167
+ status, bounded output preview — and where it lands depends on your session at
168
+ that moment.
169
+
170
+ - **Idle.** The notice is sent as a user message and starts its own turn. It
171
+ counts as unread until Pi confirms the message landed, and while compaction is
172
+ running the send waits for it to finish. A send that is accepted but never
173
+ confirmed is sent at most three times; once that budget is spent the notice
174
+ stays unread for the next settlement and the failure is emitted on
175
+ `avenor:delivery:error`.
176
+ - **Busy.** The notice is queued and the footer shows `unread:N`. The next
177
+ settlement that completes normally appends each queued notice as an
178
+ `avenor-completion` custom message under a single continuation, so three runs
179
+ finishing while you work mean three messages and one wake-up. A settlement
180
+ that ends in abort or error delivers nothing; the notices wait.
181
+
182
+ Every terminal status queues a notice — `done`, `failed`, `timeout`, `killed`.
183
+
184
+ `avenor_result` and a blocking `avenor_spawn` (`wait: true`) own delivery while
185
+ they run; `avenor_result` takes the label you spawned with, so you need not keep
186
+ the id. A `ready` result consumes the run: the queued notice is dropped,
187
+ `unread:N` falls by one, and nothing arrives for it later. A result that is not
188
+ `ready` — interrupted by a sibling's stopping point, timed out, or reporting a
189
+ pending permission — consumes nothing, and its notice arrives on the normal
190
+ schedule. A blocking spawn that reaches completion returns the result inline and
191
+ removes the notice; the footer can show a transient `unread:N` for that run while
192
+ the wait is in flight.
193
+
194
+ #### Footer status
195
+
196
+ One string under the status key `avenor`, in this order:
197
+
198
+ | Token | Meaning |
199
+ |---|---|
200
+ | `🟢 label — running [phase] 🔒 +2` | one per non-terminal run: status emoji, label, status, phase label, `🔒` for a pending permission, `+N` for that run's active descendants |
201
+ | `total:N` | direct active runs plus discovered active descendants; present only when active descendants exist |
202
+ | `unread:N` | notices not yet delivered: queued for a settlement, or in flight for an idle session |
203
+ | `errors:N` | recent poll failures; cleared by a cycle that adds no new error |
204
+
205
+ Missing `unread:N` or `errors:N` means zero. Without `total:N`, count the active run lines. `unread:N` counts notices,
206
+ not runs, and can stay set after the last run exits — an otherwise idle session
207
+ can still advertise `unread:1` until you consume or receive that notice.
208
+ `total:N` tracks live runs and disappears with them. `errors:N` outlives them
209
+ too: it clears when a later poll cycle adds no new error, so it counts recent
210
+ failures rather than a consecutive streak. The bounded history behind
211
+ `/avenor-errors` is longer than `errors:N`; running that command clears both.
212
+
158
213
  ### Tool-result channels
159
214
 
160
215
  Pi returns JSON model content in `content[0].text` and structured `details` for
package/dist/index.d.ts CHANGED
@@ -37,7 +37,11 @@ declare const CHANNEL_POLL_COMPLETED: "avenor:poll:completed";
37
37
  declare const CHANNEL_RUN_TERMINAL: "avenor:run:terminal";
38
38
  /** Emitted when a best-effort polling operation fails. */
39
39
  declare const CHANNEL_POLL_ERROR: "avenor:poll:error";
40
+ /** Emitted when a completion-notice delivery fails permanently. */
41
+ declare const CHANNEL_DELIVERY_ERROR: "avenor:delivery:error";
40
42
  type PollErrorSource = 'singleton-list' | 'run-status' | 'spawn-status';
43
+ /** Delivery paths whose retries can be exhausted. */
44
+ type DeliveryErrorSource = 'idle-send';
41
45
  interface PollErrorPayload {
42
46
  /** Polling operation that failed. */
43
47
  readonly source: PollErrorSource;
@@ -60,6 +64,31 @@ interface PollErrorPayloadInput {
60
64
  readonly count: number;
61
65
  readonly timestamp: number;
62
66
  }
67
+ interface DeliveryErrorPayload {
68
+ /** Delivery path that failed. */
69
+ readonly source: DeliveryErrorSource;
70
+ /** Run whose completion notice is stuck unread. */
71
+ readonly runId: string;
72
+ /** Label of the run, as shown in completion notices. */
73
+ readonly label: string;
74
+ /** Human-readable failure description. */
75
+ readonly message: string;
76
+ /** Bounded, sanitized error text suitable for debug consumers. */
77
+ readonly error: string;
78
+ /** Delivery attempts made for this notice. */
79
+ readonly attempts: number;
80
+ /** Wall-clock timestamp in epoch milliseconds. */
81
+ readonly timestamp: number;
82
+ }
83
+ interface DeliveryErrorPayloadInput {
84
+ readonly source: DeliveryErrorSource;
85
+ readonly runId: string;
86
+ readonly label: string;
87
+ readonly message: string;
88
+ readonly error: unknown;
89
+ readonly attempts: number;
90
+ readonly timestamp: number;
91
+ }
63
92
  type AvenorRunStatus = Readonly<Omit<RunStatusEntry, 'supervisorId' | 'pid' | 'permissionDescription'> & {
64
93
  supervisorKey: string;
65
94
  }>;
@@ -79,7 +108,7 @@ interface RunTerminalPayload {
79
108
  readonly label: string;
80
109
  readonly agent: string;
81
110
  readonly status: string;
82
- /** Number of active descendants when the run was removed, if available. */
111
+ /** Number of active descendants last seen for the run, if available. */
83
112
  readonly nestedCount?: number;
84
113
  readonly backend?: string;
85
114
  }
@@ -98,6 +127,7 @@ interface AvenorEventMap {
98
127
  [CHANNEL_POLL_COMPLETED]: PollCompletedPayload;
99
128
  [CHANNEL_RUN_TERMINAL]: RunTerminalPayload;
100
129
  [CHANNEL_POLL_ERROR]: PollErrorPayload;
130
+ [CHANNEL_DELIVERY_ERROR]: DeliveryErrorPayload;
101
131
  }
102
132
  type AvenorEventChannel = keyof AvenorEventMap;
103
133
  type AvenorEventBus = Pick<EventBus, 'emit' | 'on'>;
@@ -113,6 +143,8 @@ declare function onAvenorEvent<K extends AvenorEventChannel>(bus: Pick<EventBus,
113
143
  declare function createPollCompletedPayload(entries: readonly RunStatusEntry[], timestamp: number, generation: number): PollCompletedPayload;
114
144
  /** Copy and freeze a terminal payload before publishing it. */
115
145
  declare function createRunTerminalPayload(payload: RunTerminalPayloadInput): RunTerminalPayload;
146
+ /** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
147
+ declare function createDeliveryErrorPayload(payload: DeliveryErrorPayloadInput): DeliveryErrorPayload;
116
148
  /** Copy, bound, sanitize, and freeze a polling error before publishing it. */
117
149
  declare function createPollErrorPayload(payload: PollErrorPayloadInput): PollErrorPayload;
118
150
  //#endregion
@@ -167,10 +199,13 @@ declare function buildInspectPayload(result: InspectResult): Record<string, unkn
167
199
  interface ExtensionOptions {
168
200
  /** Polling cadence; injectable so tests can drive deterministic ticks. */
169
201
  pollIntervalMs?: number;
202
+ /** How long an unconfirmed idle completion send waits before re-arming;
203
+ * injectable so tests can exercise the timeout without fixed sleeps. */
204
+ idleSendTimeoutMs?: number;
170
205
  /** Overrides broker-credential autodetection for sub-agent broker mode (tests). */
171
206
  subAgentEnv?: SubAgentEnv | null;
172
207
  }
173
208
  declare function createExtension(deps?: ExtensionDeps, options?: ExtensionOptions): (pi: ExtensionAPI) => Promise<void>;
174
209
  declare const _default: (pi: ExtensionAPI) => Promise<void>;
175
210
  //#endregion
176
- export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, ExtensionDeps, ExtensionOptions, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
211
+ export { type AvenorEventBus, type AvenorEventChannel, type AvenorEventMap, type AvenorRunStatus, CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, type DeliveryErrorPayload, type DeliveryErrorPayloadInput, type DeliveryErrorSource, ExtensionDeps, ExtensionOptions, type PollCompletedPayload, type PollErrorPayload, type PollErrorPayloadInput, type PollErrorSource, type RunTerminalPayload, type RunTerminalPayloadInput, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, _default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
package/dist/index.js CHANGED
@@ -106,6 +106,14 @@ async function postBroker(env, path, body, fetchImpl = fetch, signal) {
106
106
  if (!res.ok) throw await codedError(path, res);
107
107
  return res.json();
108
108
  }
109
+ /**
110
+ * Publish the harness's own user-defined session name for this run, or clear it
111
+ * with an empty name. The broker stores it apart from any model-set rename and
112
+ * prefers it, so peers see the name the human gave the session.
113
+ */
114
+ async function publishHarnessName(env, name, fetchImpl = fetch) {
115
+ await postBroker(env, "/session_name", { name: name ?? "" }, fetchImpl);
116
+ }
109
117
  /** Authenticated GET; broker credentials travel in headers, never the URL. */
110
118
  async function getBroker(env, path, fetchImpl = fetch, signal) {
111
119
  const res = await fetchImpl(`${env.brokerUrl}${path}`, {
@@ -536,7 +544,13 @@ function createBrokerChildBroker(env, fetchImpl = fetch) {
536
544
  }
537
545
  },
538
546
  async peers() {
539
- return { peers: peerRecords(await getBroker(env, "/sessions", fetchImpl)) };
547
+ const body = await getBroker(env, "/sessions", fetchImpl);
548
+ const peers = peerRecords(body);
549
+ const detail = body !== null && typeof body === "object" ? body.peers_error : void 0;
550
+ return typeof detail === "string" && detail ? {
551
+ peers,
552
+ detail
553
+ } : { peers };
540
554
  },
541
555
  async rename(req) {
542
556
  try {
@@ -1136,21 +1150,6 @@ function formatRunLine(entry) {
1136
1150
  const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
1137
1151
  return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
1138
1152
  }
1139
- /**
1140
- * decideCompletion determines whether to skip or send the completion
1141
- * steering message for a tracked run that has reached terminal status.
1142
- *
1143
- * The message is delivered on the same terminal tick — never deferred to a
1144
- * later cycle — so a run that is the last live one still gets its completion.
1145
- * `blocking` suppresses it while avenor_result is awaiting (it will deliver
1146
- * the result itself and set `consumed`), and `consumed` suppresses it once
1147
- * avenor_result has already returned the result successfully.
1148
- */
1149
- function decideCompletion(run) {
1150
- if (run.blocking) return "skip";
1151
- if (run.consumed) return "skip";
1152
- return "send";
1153
- }
1154
1153
  //#endregion
1155
1154
  //#region src/nested-count.ts
1156
1155
  const NESTED_DIAL_TIMEOUT_MS = 2e3;
@@ -1303,6 +1302,8 @@ const CHANNEL_POLL_COMPLETED = "avenor:poll:completed";
1303
1302
  const CHANNEL_RUN_TERMINAL = "avenor:run:terminal";
1304
1303
  /** Emitted when a best-effort polling operation fails. */
1305
1304
  const CHANNEL_POLL_ERROR = "avenor:poll:error";
1305
+ /** Emitted when a completion-notice delivery fails permanently. */
1306
+ const CHANNEL_DELIVERY_ERROR = "avenor:delivery:error";
1306
1307
  /** Maximum sanitized error text exposed to companion extensions. */
1307
1308
  const POLL_ERROR_TEXT_CHARS = 600;
1308
1309
  function sanitizeAndBoundText(value) {
@@ -1360,6 +1361,18 @@ function createRunTerminalPayload(payload) {
1360
1361
  ...payload.backend !== void 0 && { backend: payload.backend }
1361
1362
  });
1362
1363
  }
1364
+ /** Copy, bound, sanitize, and freeze a delivery failure before publishing it. */
1365
+ function createDeliveryErrorPayload(payload) {
1366
+ return Object.freeze({
1367
+ source: payload.source,
1368
+ runId: payload.runId,
1369
+ label: payload.label,
1370
+ message: sanitizeAndBoundText(payload.message),
1371
+ error: sanitizeAndBoundText(payload.error),
1372
+ attempts: payload.attempts,
1373
+ timestamp: payload.timestamp
1374
+ });
1375
+ }
1363
1376
  /** Copy, bound, sanitize, and freeze a polling error before publishing it. */
1364
1377
  function createPollErrorPayload(payload) {
1365
1378
  return Object.freeze({
@@ -1920,6 +1933,8 @@ function asStatusResult(raw) {
1920
1933
  return raw;
1921
1934
  }
1922
1935
  const POLL_INTERVAL_MS = 3e3;
1936
+ const IDLE_SEND_TIMEOUT_MS = 1e4;
1937
+ const MAX_IDLE_SEND_ATTEMPTS = 3;
1923
1938
  const INSPECT_LIMIT = 128;
1924
1939
  const COMPLETION_OUTPUT_CHARS = 600;
1925
1940
  const INSPECT_JSON_STRING_CHARS = 600;
@@ -2092,6 +2107,7 @@ function trackedRunKey(supervisorId, runId) {
2092
2107
  function createExtension(deps = defaultDeps, options = {}) {
2093
2108
  return async function(pi) {
2094
2109
  const pollIntervalMs = options.pollIntervalMs ?? POLL_INTERVAL_MS;
2110
+ const idleSendTimeoutMs = options.idleSendTimeoutMs ?? IDLE_SEND_TIMEOUT_MS;
2095
2111
  let telemetryGeneration = 0;
2096
2112
  const trackedRuns = /* @__PURE__ */ new Map();
2097
2113
  let pollingActive = false;
@@ -2101,6 +2117,8 @@ function createExtension(deps = defaultDeps, options = {}) {
2101
2117
  let pollingErrorCount = 0;
2102
2118
  const pollingErrors = [];
2103
2119
  let lastStatusEntries = [];
2120
+ let sessionGeneration = 0;
2121
+ let compacting = false;
2104
2122
  let lastTurnSocketClosed = false;
2105
2123
  const pendingWaits = /* @__PURE__ */ new Set();
2106
2124
  /** The agent-facing prose clause for a stopping point. A synthetic status
@@ -2174,12 +2192,16 @@ function createExtension(deps = defaultDeps, options = {}) {
2174
2192
  };
2175
2193
  }
2176
2194
  /** Build the early return for a wait interrupted by a sibling stopping
2177
- * point. When the awaited run itself has since reached a stopping point
2178
- * (possible when the caller omitted supervisor_id and the registry key
2179
- * diverged), deliver its real state instead of an interruption notice. */
2195
+ * point. When the awaited run itself has since reached a stopping point,
2196
+ * deliver its real state instead of an interruption notice. The probe and
2197
+ * redelivery use the caller's original run reference, matching the non-
2198
+ * interrupted path: a label may resolve canonically to a newer run than
2199
+ * the one this call first bound to, and the reconciliation below handles
2200
+ * that divergence the same way. A caller that omitted supervisor_id still
2201
+ * probes through the bound run's supervisor. */
2180
2202
  async function resultAfterInterrupt(params, stop, tracked) {
2181
- const runId = tracked?.runId ?? params.run_id;
2182
- const supervisorId = tracked?.supervisorId ?? params.supervisor_id;
2203
+ const runId = params.run_id;
2204
+ const supervisorId = params.supervisor_id ?? tracked?.supervisorId;
2183
2205
  let current;
2184
2206
  try {
2185
2207
  current = asStatusResult(await deps.statusTool({
@@ -2229,6 +2251,35 @@ function createExtension(deps = defaultDeps, options = {}) {
2229
2251
  subAgentPollTimer = null;
2230
2252
  }
2231
2253
  }
2254
+ let publishedSessionName = null;
2255
+ let publishChain = Promise.resolve();
2256
+ let refusedSessionName = null;
2257
+ function publishSessionName(ctx) {
2258
+ if (!subAgentEnv) return publishChain;
2259
+ const env = subAgentEnv;
2260
+ const manager = ctx.sessionManager;
2261
+ if (!manager || typeof manager.getSessionName !== "function") return publishChain;
2262
+ const name = manager.getSessionName();
2263
+ if (publishedSessionName && publishedSessionName.name === name) return publishChain;
2264
+ if (refusedSessionName === name) return publishChain;
2265
+ const attempt = { name };
2266
+ publishedSessionName = attempt;
2267
+ publishChain = publishChain.then(async () => {
2268
+ try {
2269
+ await publishHarnessName(env, name);
2270
+ refusedSessionName = null;
2271
+ } catch (error) {
2272
+ if (error instanceof BrokerHttpError && error.status === 400 && error.code === "invalid-name") {
2273
+ refusedSessionName = name;
2274
+ console.error("[avenor] session_name_set refused:", error.message);
2275
+ return;
2276
+ }
2277
+ if (publishedSessionName === attempt) publishedSessionName = null;
2278
+ console.error("[avenor] session_name_set failed:", error instanceof Error ? error.message : error);
2279
+ }
2280
+ });
2281
+ return publishChain;
2282
+ }
2232
2283
  async function startSubAgentPoll() {
2233
2284
  if (!subAgentEnv) return;
2234
2285
  stopSubAgentPoll();
@@ -2313,6 +2364,40 @@ function createExtension(deps = defaultDeps, options = {}) {
2313
2364
  if (pollingErrors.length > MAX_POLL_ERRORS) pollingErrors.shift();
2314
2365
  emitAvenorEvent(pi.events, CHANNEL_POLL_ERROR, payload);
2315
2366
  }
2367
+ /** A delivery that exhausted its retries is not a polling failure: emit
2368
+ * it on its own channel so `avenor:poll:error` consumers keep a pure
2369
+ * polling contract. The unread notice is the persistent user-facing
2370
+ * surface; this event is the companion-extension signal. */
2371
+ function recordDeliveryError(run, attempts) {
2372
+ emitAvenorEvent(pi.events, CHANNEL_DELIVERY_ERROR, createDeliveryErrorPayload({
2373
+ source: "idle-send",
2374
+ runId: run.runId,
2375
+ label: run.label,
2376
+ message: "idle completion send was never confirmed",
2377
+ error: /* @__PURE__ */ new Error(`no message_end after ${attempts} attempts`),
2378
+ attempts,
2379
+ timestamp: Date.now()
2380
+ }));
2381
+ console.error(`[avenor] idle completion send for run "${run.label}" was never confirmed after ${attempts} attempts; the notice stays unread`);
2382
+ }
2383
+ /** Latch the per-notice retry budget as spent and report the exhausted
2384
+ * delivery. Idempotent, so the timeout re-arm and the compaction
2385
+ * hand-back cannot double-report the same notice. */
2386
+ function capIdleSend(run) {
2387
+ if (!run.idleSend || run.idleSend.capped) return;
2388
+ run.idleSend.capped = true;
2389
+ recordDeliveryError(run, run.idleSend.attempts);
2390
+ }
2391
+ /** Hand an unconfirmed idle send back to the poll: clear the in-flight
2392
+ * mark and latch the budget when it is spent. Shared by the poll-tick
2393
+ * timeout and the compaction hand-back, which must stay in lockstep. */
2394
+ function rearmIdleSend(run) {
2395
+ const idle = run.idleSend;
2396
+ if (!idle) return;
2397
+ idle.inFlight = false;
2398
+ idle.sentAt = void 0;
2399
+ if (idle.attempts >= MAX_IDLE_SEND_ATTEMPTS) capIdleSend(run);
2400
+ }
2316
2401
  function clearPollingErrors() {
2317
2402
  pollingErrorCount = 0;
2318
2403
  pollingErrors.length = 0;
@@ -2321,38 +2406,75 @@ function createExtension(deps = defaultDeps, options = {}) {
2321
2406
  function upsertTrackedRun(run, status, blocking = false) {
2322
2407
  const key = trackedRunKey(run.supervisorId, run.runId);
2323
2408
  const current = trackedRuns.get(key);
2324
- trackedRuns.set(key, {
2409
+ const tracked = current ?? {
2325
2410
  runId: run.runId,
2326
- agent: run.agent ?? status?.agent ?? current?.agent ?? "unknown",
2327
- label: run.label ?? status?.label ?? current?.label ?? run.runId,
2328
- supervisorId: run.supervisorId ?? current?.supervisorId,
2329
- runtimeId: run.runtimeId ?? status?.runtime_id ?? current?.runtimeId,
2330
- startTime: current?.startTime ?? Date.now(),
2331
- lastStatus: status ?? current?.lastStatus,
2332
- blocking: current?.blocking ?? blocking,
2333
- permissionNotified: current?.permissionNotified,
2334
- consumed: current?.consumed
2335
- });
2411
+ startTime: Date.now(),
2412
+ agent: "unknown",
2413
+ label: run.label ?? run.runId
2414
+ };
2415
+ tracked.runId = run.runId;
2416
+ tracked.agent = run.agent ?? status?.agent ?? current?.agent ?? tracked.agent;
2417
+ tracked.label = run.label ?? status?.label ?? current?.label ?? tracked.label;
2418
+ tracked.supervisorId = run.supervisorId ?? current?.supervisorId;
2419
+ tracked.runtimeId = run.runtimeId ?? status?.runtime_id ?? current?.runtimeId;
2420
+ if (!current) tracked.startTime = Date.now();
2421
+ tracked.lastStatus = status ?? current?.lastStatus;
2422
+ tracked.blocking = current?.blocking ?? blocking;
2423
+ trackedRuns.set(key, tracked);
2424
+ }
2425
+ function findTrackedByLabel(label, supervisorId) {
2426
+ let match;
2427
+ for (const run of trackedRuns.values()) {
2428
+ if (supervisorId !== void 0 && run.supervisorId !== supervisorId) continue;
2429
+ if (run.label !== label) continue;
2430
+ if (match) return void 0;
2431
+ match = run;
2432
+ }
2433
+ return match;
2336
2434
  }
2337
2435
  /**
2338
- * Look a run up in the per-supervisor tracking namespace. When a
2339
- * supervisor is supplied, resolve exactly. Without one, prefer the
2436
+ * Resolve a canonical run id in the per-supervisor tracking namespace.
2437
+ * When a supervisor is supplied, resolve exactly. Without one, prefer the
2340
2438
  * singleton namespace and only fall back to a run id walk when it is
2341
2439
  * unambiguous across supervisors. Duplicate run ids across supervisors
2342
2440
  * without an explicit supervisor return undefined so callers never bind
2343
2441
  * flags or telemetry to the wrong supervisor's run.
2344
2442
  */
2345
- function getTrackedRun(runId, supervisorId) {
2346
- if (supervisorId !== void 0) return trackedRuns.get(trackedRunKey(supervisorId, runId));
2347
- const singleton = trackedRuns.get(trackedRunKey(void 0, runId));
2348
- if (singleton) return singleton;
2349
- let match;
2350
- for (const run of trackedRuns.values()) {
2351
- if (run.runId !== runId) continue;
2352
- if (match) return void 0;
2353
- match = run;
2443
+ function trackedRunsById(runId, supervisorId) {
2444
+ if (supervisorId !== void 0) {
2445
+ const exact = trackedRuns.get(trackedRunKey(supervisorId, runId));
2446
+ return exact ? [exact] : [];
2354
2447
  }
2355
- return match;
2448
+ const singleton = trackedRuns.get(trackedRunKey(void 0, runId));
2449
+ if (singleton) return [singleton];
2450
+ const matches = [];
2451
+ for (const run of trackedRuns.values()) if (run.runId === runId) matches.push(run);
2452
+ return matches;
2453
+ }
2454
+ function getTrackedRunById(runId, supervisorId) {
2455
+ const matches = trackedRunsById(runId, supervisorId);
2456
+ return matches.length === 1 ? matches[0] : void 0;
2457
+ }
2458
+ /**
2459
+ * Resolve a user-supplied run reference. A canonical identity always wins;
2460
+ * a label is only a fallback, and an ambiguous canonical id never falls
2461
+ * through to a label.
2462
+ */
2463
+ function getTrackedRun(runId, supervisorId) {
2464
+ const exact = getTrackedRunById(runId, supervisorId);
2465
+ if (exact) return exact;
2466
+ if (trackedRunsById(runId, supervisorId).length > 1) return void 0;
2467
+ return findTrackedByLabel(runId, supervisorId);
2468
+ }
2469
+ /** Resolve the tracked run a successful result belongs to from the
2470
+ * canonical run id the result tool reports, accepting a runtime-id alias
2471
+ * for the label-resolved run. */
2472
+ function reconcileResultRun(tracked, result, supervisorId) {
2473
+ const runId = result.run_id;
2474
+ if (!runId) return tracked;
2475
+ if (tracked && (tracked.runId === runId || tracked.runtimeId === runId)) return tracked;
2476
+ const namespace = tracked?.supervisorId ?? supervisorId;
2477
+ return trackedRuns.get(trackedRunKey(namespace, runId));
2356
2478
  }
2357
2479
  async function resolveFinalOutput(run, status) {
2358
2480
  const preview = finalOutputPreview(status);
@@ -2368,6 +2490,67 @@ function createExtension(deps = defaultDeps, options = {}) {
2368
2490
  return;
2369
2491
  }
2370
2492
  }
2493
+ /**
2494
+ * Fire-and-forget delivery. Pi's extension-facing `sendUserMessage` returns
2495
+ * void and the runtime swallows async rejections, so a send is only known to
2496
+ * have landed when the user message comes back through `message_end`. Only a
2497
+ * synchronous throw is observable here.
2498
+ */
2499
+ function sendUserMessageLogged(text, failureLog) {
2500
+ try {
2501
+ pi.sendUserMessage(text, { deliverAs: "followUp" });
2502
+ } catch (err) {
2503
+ console.error(failureLog, err);
2504
+ }
2505
+ }
2506
+ function clearPendingNotices() {
2507
+ for (const run of trackedRuns.values()) {
2508
+ run.pendingNotice = void 0;
2509
+ run.idleSend = void 0;
2510
+ }
2511
+ }
2512
+ /**
2513
+ * Emit the single terminal event for a tracked run. Every terminal path
2514
+ * (poll, blocking spawn, avenor_result) goes through here so the event-once
2515
+ * guard and payload shape stay identical. A tracked run emits at most once;
2516
+ * an untracked run emits with the canonical result identity, unguarded.
2517
+ *
2518
+ * @returns whether the event was emitted, so callers can gate one-time
2519
+ * side effects.
2520
+ */
2521
+ function emitRunTerminal(input) {
2522
+ const { run } = input;
2523
+ if (!run) {
2524
+ if (!input.runId) return false;
2525
+ emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
2526
+ runId: input.runId,
2527
+ supervisorId: input.supervisorId,
2528
+ runtimeId: input.runtimeId,
2529
+ label: input.label,
2530
+ agent: input.agent,
2531
+ status: input.status,
2532
+ nestedCount: input.nestedCount,
2533
+ backend: input.backend
2534
+ }));
2535
+ return true;
2536
+ }
2537
+ if (run.terminalEventEmitted) return false;
2538
+ run.terminalEventEmitted = true;
2539
+ emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
2540
+ runId: run.runId,
2541
+ supervisorId: run.supervisorId,
2542
+ runtimeId: input.runtimeId,
2543
+ label: input.label,
2544
+ agent: input.agent,
2545
+ status: input.status,
2546
+ nestedCount: input.nestedCount,
2547
+ backend: input.backend
2548
+ }));
2549
+ return true;
2550
+ }
2551
+ function sendCompletionNotice(notice) {
2552
+ sendUserMessageLogged(notice.text, "avenor tick: sendUserMessage failed");
2553
+ }
2371
2554
  async function pollRuns() {
2372
2555
  const errorCountAtPollStart = pollingErrorCount;
2373
2556
  const liveMap = /* @__PURE__ */ new Map();
@@ -2460,7 +2643,11 @@ function createExtension(deps = defaultDeps, options = {}) {
2460
2643
  if (nestedPids.length > 0) {
2461
2644
  const nestedCounts = await Promise.all(nestedPids.map((pid) => countNestedRuns(pid, deps.dial)));
2462
2645
  let idx = 0;
2463
- for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) entry.nestedCount = nestedCounts[idx++] ?? 0;
2646
+ for (const entry of entries) if (entry.backend === "pi" && entry.pid && !isTerminalStatus(entry.status)) {
2647
+ entry.nestedCount = nestedCounts[idx++] ?? 0;
2648
+ const tracked = trackedRuns.get(trackedRunKey(entry.supervisorId, entry.runId));
2649
+ if (tracked) tracked.nestedCount = entry.nestedCount;
2650
+ }
2464
2651
  }
2465
2652
  if (pollingErrorCount === errorCountAtPollStart) pollingErrorCount = 0;
2466
2653
  return entries;
@@ -2469,17 +2656,24 @@ function createExtension(deps = defaultDeps, options = {}) {
2469
2656
  lastStatusEntries = entries;
2470
2657
  if (!sessionCtx) return;
2471
2658
  const lines = renderStatusLines(entries);
2659
+ let unread = 0;
2660
+ for (const run of trackedRuns.values()) if (run.pendingNotice) unread++;
2472
2661
  const errorSuffix = pollingErrorCount > 0 ? `errors:${pollingErrorCount}` : "";
2662
+ const unreadSuffix = unread > 0 ? `unread:${unread}` : "";
2473
2663
  if (lines.length > 0) {
2474
2664
  sessionCtx.ui.setWidget("avenor-status", lines);
2475
2665
  const statusStr = lines.map((line) => line.slice(0, 60)).join(" ");
2476
2666
  const nestedTotal = entries.filter((e) => !isTerminalStatus(e.status)).reduce((sum, e) => sum + (e.nestedCount ?? 0), 0);
2477
2667
  const total = lines.length + nestedTotal;
2478
- const statusWithTotal = [nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr, errorSuffix].filter(Boolean).join(" ");
2668
+ const statusWithTotal = [
2669
+ nestedTotal > 0 ? `${statusStr} total:${total}` : statusStr,
2670
+ unreadSuffix,
2671
+ errorSuffix
2672
+ ].filter(Boolean).join(" ");
2479
2673
  sessionCtx.ui.setStatus("avenor", statusWithTotal);
2480
2674
  } else {
2481
2675
  sessionCtx.ui.setWidget("avenor-status", void 0);
2482
- sessionCtx.ui.setStatus("avenor", errorSuffix);
2676
+ sessionCtx.ui.setStatus("avenor", [unreadSuffix, errorSuffix].filter(Boolean).join(" "));
2483
2677
  }
2484
2678
  }
2485
2679
  function startPolling() {
@@ -2500,12 +2694,20 @@ function createExtension(deps = defaultDeps, options = {}) {
2500
2694
  });
2501
2695
  if (!pollingActive || generation !== pollingGeneration) return;
2502
2696
  updateWidget(entries);
2697
+ let widgetDirty = false;
2698
+ const sessionAtPollStart = sessionGeneration;
2503
2699
  emitAvenorEvent(pi.events, CHANNEL_POLL_COMPLETED, createPollCompletedPayload(entries, Date.now(), ++telemetryGeneration));
2504
2700
  for (const entry of entries) {
2505
2701
  const entryKey = trackedRunKey(entry.supervisorId, entry.runId);
2506
2702
  const run = trackedRuns.get(entryKey);
2507
2703
  if (!run) continue;
2508
2704
  const prevStatus = prevStatuses.get(entryKey);
2705
+ const idle = run.idleSend;
2706
+ if (idle?.inFlight && idle.sentAt !== void 0 && Date.now() - idle.sentAt >= idleSendTimeoutMs) {
2707
+ rearmIdleSend(run);
2708
+ widgetDirty = true;
2709
+ }
2710
+ if (run.idleSend?.inFlight && isTerminalStatus(entry.status)) continue;
2509
2711
  const wasStopped = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status) || !!prevStatus?.pendingPermission;
2510
2712
  if ((entry.pendingPermission || isTerminalStatus(entry.status)) && !wasStopped) interruptPendingWaits({
2511
2713
  runId: entry.runId,
@@ -2513,45 +2715,61 @@ function createExtension(deps = defaultDeps, options = {}) {
2513
2715
  status: isTerminalStatus(entry.status) ? entry.status : "waiting"
2514
2716
  }, entryKey);
2515
2717
  if (isTerminalStatus(entry.status)) {
2516
- if (decideCompletion(run) === "skip") {
2517
- if (run.consumed) trackedRuns.delete(entryKey);
2518
- continue;
2519
- }
2520
- if (!(prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status))) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
2521
- const finalOutput = await resolveFinalOutput(run, run.lastStatus);
2522
- try {
2523
- pi.sendUserMessage(buildCompletionText({
2524
- runId: run.runId,
2525
- label: run.label
2526
- }, run.lastStatus, finalOutput), { deliverAs: "followUp" });
2527
- } catch (err) {
2528
- console.error("avenor tick: sendUserMessage failed", err);
2529
- }
2530
- emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
2531
- runId: entry.runId,
2532
- supervisorId: entry.supervisorId,
2718
+ const finalOutput = run.pendingNotice ? void 0 : await resolveFinalOutput(run, run.lastStatus);
2719
+ if (sessionAtPollStart !== sessionGeneration) continue;
2720
+ if (trackedRuns.get(entryKey) !== run) continue;
2721
+ const wasTerminalBefore = prevStatus?.status !== void 0 && isTerminalStatus(prevStatus.status);
2722
+ if (emitRunTerminal({
2723
+ run,
2533
2724
  runtimeId: entry.runtimeId,
2534
2725
  label: entry.label,
2535
2726
  agent: entry.agent,
2536
2727
  status: entry.status,
2537
- nestedCount: entry.nestedCount,
2728
+ nestedCount: entry.nestedCount ?? run.nestedCount,
2538
2729
  backend: entry.backend
2539
- }));
2540
- trackedRuns.delete(entryKey);
2541
- } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
2542
- run.permissionNotified = true;
2543
- if (run.lastStatus) try {
2544
- pi.sendUserMessage(buildWaitingText({
2730
+ })) {
2731
+ if (!wasTerminalBefore && !run.blocking) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
2732
+ }
2733
+ const notice = run.pendingNotice ?? {
2734
+ status: entry.status,
2735
+ text: buildCompletionText({
2545
2736
  runId: run.runId,
2546
- label: run.label,
2547
- agent: run.agent
2548
- }, run.lastStatus), { deliverAs: "followUp" });
2549
- } catch (err) {
2550
- console.error("avenor tick: sendUserMessage for permission failed", err);
2737
+ label: run.label
2738
+ }, run.lastStatus, finalOutput)
2739
+ };
2740
+ if (run.blocking) {
2741
+ if (!run.pendingNotice) {
2742
+ run.pendingNotice = notice;
2743
+ widgetDirty = true;
2744
+ }
2745
+ continue;
2746
+ }
2747
+ if (sessionCtx?.isIdle() === true && !compacting && !run.idleSend?.capped) {
2748
+ run.pendingNotice = notice;
2749
+ run.idleSend = {
2750
+ inFlight: true,
2751
+ sentAt: Date.now(),
2752
+ attempts: (run.idleSend?.attempts ?? 0) + 1,
2753
+ capped: false
2754
+ };
2755
+ widgetDirty = true;
2756
+ sendCompletionNotice(notice);
2757
+ continue;
2758
+ } else if (!run.pendingNotice) {
2759
+ run.pendingNotice = notice;
2760
+ widgetDirty = true;
2551
2761
  }
2762
+ } else if (entry.pendingPermission && !run.blocking && !run.permissionNotified) {
2763
+ run.permissionNotified = true;
2764
+ if (run.lastStatus) sendUserMessageLogged(buildWaitingText({
2765
+ runId: run.runId,
2766
+ label: run.label,
2767
+ agent: run.agent
2768
+ }, run.lastStatus), "avenor tick: sendUserMessage for permission failed");
2552
2769
  } else if (!entry.pendingPermission && run.permissionNotified) run.permissionNotified = false;
2553
2770
  }
2554
- if (entries.some((entry) => !isTerminalStatus(entry.status)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
2771
+ if (widgetDirty) updateWidget(entries);
2772
+ if ((entries.some((entry) => !isTerminalStatus(entry.status)) || Array.from(trackedRuns.values()).some((run) => run.idleSend?.inFlight)) && pollingActive && generation === pollingGeneration) setTimeout(() => {
2555
2773
  tick().catch((err) => {
2556
2774
  pollingActive = false;
2557
2775
  console.error("avenor polling loop failed", err);
@@ -2846,10 +3064,16 @@ function createExtension(deps = defaultDeps, options = {}) {
2846
3064
  });
2847
3065
  pi.on("session_start", async (_event, ctx) => {
2848
3066
  sessionCtx = ctx;
3067
+ sessionGeneration++;
3068
+ compacting = false;
3069
+ clearPendingNotices();
3070
+ if (trackedRuns.size > 0) startPolling();
2849
3071
  lastStatusEntries = [];
2850
3072
  lastTurnSocketClosed = false;
2851
3073
  pollingErrorCount = 0;
2852
3074
  pollingErrors.length = 0;
3075
+ publishedSessionName = null;
3076
+ publishSessionName(ctx);
2853
3077
  if (!isSubAgentBrokerMode) startHostAskPoll();
2854
3078
  startSubAgentPoll();
2855
3079
  });
@@ -2859,6 +3083,8 @@ function createExtension(deps = defaultDeps, options = {}) {
2859
3083
  disposeHostProviders();
2860
3084
  hostPendingAsks.clear();
2861
3085
  subAgentPendingAsks.clear();
3086
+ sessionGeneration++;
3087
+ clearPendingNotices();
2862
3088
  await stopPolling();
2863
3089
  for (const wait of [...pendingWaits]) {
2864
3090
  pendingWaits.delete(wait);
@@ -2870,7 +3096,36 @@ function createExtension(deps = defaultDeps, options = {}) {
2870
3096
  }
2871
3097
  sessionCtx = null;
2872
3098
  });
2873
- pi.on("before_agent_start", async () => {
3099
+ pi.on("message_end", (event) => {
3100
+ if (event.message?.role !== "user") return;
3101
+ const text = extractTurnText([event.message]).text;
3102
+ if (text === "") return;
3103
+ const matches = [...trackedRuns].filter(([, run]) => run.pendingNotice?.text === text);
3104
+ let targets = matches.filter(([, run]) => run.idleSend?.inFlight);
3105
+ if (targets.length === 0) targets = matches.filter(([, run]) => run.idleSend !== void 0);
3106
+ for (const [key, run] of targets) {
3107
+ run.pendingNotice = void 0;
3108
+ run.idleSend = void 0;
3109
+ trackedRuns.delete(key);
3110
+ }
3111
+ if (targets.length > 0) updateWidget(lastStatusEntries);
3112
+ });
3113
+ pi.on("session_before_compact", () => {
3114
+ compacting = true;
3115
+ });
3116
+ const resumeAfterCompaction = () => {
3117
+ compacting = false;
3118
+ for (const run of trackedRuns.values()) {
3119
+ if (run.permissionNotified && run.lastStatus?.pending_permission) run.permissionNotified = false;
3120
+ if (!run.idleSend) continue;
3121
+ rearmIdleSend(run);
3122
+ }
3123
+ if (Array.from(trackedRuns.values()).some((run) => run.pendingNotice || !run.permissionNotified && !!run.lastStatus?.pending_permission)) startPolling();
3124
+ };
3125
+ pi.on("session_compact", resumeAfterCompaction);
3126
+ pi.on("before_agent_start", async (_event, ctx) => {
3127
+ publishSessionName(ctx);
3128
+ compacting = false;
2874
3129
  if (trackedRuns.size === 0) return;
2875
3130
  const active = (await pollRuns()).filter((entry) => !isTerminalStatus(entry.status));
2876
3131
  if (active.length === 0) return;
@@ -2884,6 +3139,38 @@ function createExtension(deps = defaultDeps, options = {}) {
2884
3139
  display: true
2885
3140
  } };
2886
3141
  });
3142
+ pi.on("agent_before_settle", async (event) => {
3143
+ if (event.outcome !== "completed") return;
3144
+ const deliverable = [];
3145
+ for (const run of trackedRuns.values()) {
3146
+ if (!run.pendingNotice) continue;
3147
+ if (run.idleSend?.inFlight) continue;
3148
+ deliverable.push({
3149
+ run,
3150
+ notice: run.pendingNotice
3151
+ });
3152
+ }
3153
+ if (deliverable.length === 0) return;
3154
+ for (const { run } of deliverable) {
3155
+ run.pendingNotice = void 0;
3156
+ trackedRuns.delete(trackedRunKey(run.supervisorId, run.runId));
3157
+ }
3158
+ updateWidget(lastStatusEntries);
3159
+ return {
3160
+ entries: [...event.entries, ...deliverable.map(({ run, notice }) => ({
3161
+ type: "custom_message",
3162
+ customType: "avenor-completion",
3163
+ content: notice.text,
3164
+ display: true,
3165
+ details: {
3166
+ run_id: run.runId,
3167
+ supervisor_id: run.supervisorId,
3168
+ status: notice.status
3169
+ }
3170
+ }))],
3171
+ continue: true
3172
+ };
3173
+ });
2887
3174
  pi.registerMessageRenderer("avenor-active-runs", (message, _options, theme) => {
2888
3175
  const lines = message.content.split("\n");
2889
3176
  let text = "";
@@ -3022,7 +3309,7 @@ function createExtension(deps = defaultDeps, options = {}) {
3022
3309
  }).catch(() => void 0));
3023
3310
  if (probed && (isTerminalStatus(probed.status) || probed.status === "waiting")) waitStatus = probed;
3024
3311
  else {
3025
- const tracked = getTrackedRun(result.run_id, supervisorId);
3312
+ const tracked = getTrackedRunById(result.run_id, supervisorId);
3026
3313
  if (tracked) tracked.blocking = false;
3027
3314
  return {
3028
3315
  content: [{
@@ -3041,7 +3328,8 @@ function createExtension(deps = defaultDeps, options = {}) {
3041
3328
  };
3042
3329
  }
3043
3330
  } else {
3044
- trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
3331
+ const runKey = trackedRunKey(supervisorId, result.run_id);
3332
+ trackedRuns.delete(runKey);
3045
3333
  return {
3046
3334
  content: [{
3047
3335
  type: "text",
@@ -3066,10 +3354,13 @@ function createExtension(deps = defaultDeps, options = {}) {
3066
3354
  ...spawnIdentityMetadata(hostParams)
3067
3355
  }
3068
3356
  };
3069
- upsertTrackedRun(runRef, status, wait);
3357
+ if (trackedRuns.has(trackedRunKey(supervisorId, result.run_id))) upsertTrackedRun(runRef, status, wait);
3070
3358
  if (status.status === "waiting") {
3071
- const tracked = getTrackedRun(result.run_id, supervisorId);
3072
- if (tracked) tracked.permissionNotified = true;
3359
+ const tracked = getTrackedRunById(result.run_id, supervisorId);
3360
+ if (tracked) {
3361
+ tracked.permissionNotified = true;
3362
+ tracked.blocking = false;
3363
+ }
3073
3364
  return {
3074
3365
  content: [{
3075
3366
  type: "text",
@@ -3093,16 +3384,19 @@ function createExtension(deps = defaultDeps, options = {}) {
3093
3384
  runId: result.run_id,
3094
3385
  supervisorId
3095
3386
  }, status);
3096
- emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
3097
- runId: result.run_id,
3098
- supervisorId,
3387
+ const runKey = trackedRunKey(supervisorId, result.run_id);
3388
+ const tracked = trackedRuns.get(runKey);
3389
+ emitRunTerminal({
3390
+ run: tracked,
3099
3391
  runtimeId: status.runtime_id ?? runRef.runtimeId,
3100
3392
  label,
3101
3393
  agent: params.agent,
3102
3394
  status: status.status,
3395
+ nestedCount: tracked?.nestedCount,
3103
3396
  backend: status.backend
3104
- }));
3105
- trackedRuns.delete(trackedRunKey(supervisorId, result.run_id));
3397
+ });
3398
+ trackedRuns.delete(runKey);
3399
+ updateWidget(lastStatusEntries);
3106
3400
  return {
3107
3401
  content: [{
3108
3402
  type: "text",
@@ -3120,6 +3414,8 @@ function createExtension(deps = defaultDeps, options = {}) {
3120
3414
  }
3121
3415
  };
3122
3416
  }
3417
+ const fallthrough = getTrackedRunById(result.run_id, supervisorId);
3418
+ if (fallthrough) fallthrough.blocking = false;
3123
3419
  return {
3124
3420
  content: [{
3125
3421
  type: "text",
@@ -3205,6 +3501,7 @@ function createExtension(deps = defaultDeps, options = {}) {
3205
3501
  const previousBlocking = tracked?.blocking;
3206
3502
  if (tracked) tracked.blocking = true;
3207
3503
  let completed = false;
3504
+ let resultRun = tracked;
3208
3505
  const blockingWait = params.wait ?? true ? createInterruptibleWait(trackedRunKey(tracked?.supervisorId ?? params.supervisor_id, tracked?.runId ?? params.run_id), signal) : void 0;
3209
3506
  try {
3210
3507
  let result;
@@ -3221,18 +3518,30 @@ function createExtension(deps = defaultDeps, options = {}) {
3221
3518
  result = await resultAfterInterrupt(params, blockingWait.stopped, tracked);
3222
3519
  }
3223
3520
  completed = result.ready;
3521
+ resultRun = reconcileResultRun(tracked, result, params.supervisor_id);
3224
3522
  if (completed) {
3225
- if (tracked) tracked.consumed = true;
3226
- emitAvenorEvent(pi.events, CHANNEL_RUN_TERMINAL, createRunTerminalPayload({
3227
- runId: params.run_id,
3228
- supervisorId: tracked?.supervisorId ?? params.supervisor_id,
3229
- runtimeId: tracked?.runtimeId ?? result.runtime_id,
3230
- label: tracked?.label ?? result.label ?? params.run_id,
3231
- agent: tracked?.agent ?? "unknown",
3232
- status: result.status ?? "done"
3233
- }));
3234
- if (tracked) trackedRuns.delete(trackedRunKey(tracked.supervisorId, tracked.runId));
3235
- } else if (tracked && result.status === "waiting") tracked.permissionNotified = true;
3523
+ const idMatches = trackedRunsById(params.run_id, params.supervisor_id).length;
3524
+ const labelMatches = [...trackedRuns.values()].filter((run) => (params.supervisor_id === void 0 || run.supervisorId === params.supervisor_id) && run.label === params.run_id).length;
3525
+ emitRunTerminal({
3526
+ run: resultRun,
3527
+ runId: idMatches > 1 || idMatches === 0 && labelMatches > 1 ? void 0 : result.run_id ?? params.run_id,
3528
+ supervisorId: params.supervisor_id,
3529
+ runtimeId: resultRun?.runtimeId ?? result.runtime_id,
3530
+ label: resultRun?.label ?? result.label ?? params.run_id,
3531
+ agent: resultRun?.agent ?? "unknown",
3532
+ status: result.status ?? "done",
3533
+ nestedCount: resultRun?.nestedCount,
3534
+ backend: resultRun?.lastStatus?.backend
3535
+ });
3536
+ if (resultRun) {
3537
+ trackedRuns.delete(trackedRunKey(resultRun.supervisorId, resultRun.runId));
3538
+ updateWidget(lastStatusEntries);
3539
+ if (tracked && resultRun !== tracked) {
3540
+ const trackedKey = trackedRunKey(tracked.supervisorId, tracked.runId);
3541
+ if (trackedRuns.get(trackedKey) === tracked) tracked.blocking = previousBlocking ?? false;
3542
+ }
3543
+ } else if (tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
3544
+ } else if (resultRun && result.status === "waiting") resultRun.permissionNotified = true;
3236
3545
  return {
3237
3546
  content: [{
3238
3547
  type: "text",
@@ -3242,7 +3551,7 @@ function createExtension(deps = defaultDeps, options = {}) {
3242
3551
  };
3243
3552
  } finally {
3244
3553
  blockingWait?.dispose();
3245
- if (!completed && tracked && getTrackedRun(params.run_id, params.supervisor_id) === tracked) tracked.blocking = previousBlocking ?? false;
3554
+ if (!completed && tracked && trackedRuns.get(trackedRunKey(tracked.supervisorId, tracked.runId)) === tracked) tracked.blocking = previousBlocking ?? false;
3246
3555
  }
3247
3556
  },
3248
3557
  renderCall(args, theme, context) {
@@ -3832,4 +4141,4 @@ function createExtension(deps = defaultDeps, options = {}) {
3832
4141
  }
3833
4142
  var src_default = createExtension();
3834
4143
  //#endregion
3835
- export { CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
4144
+ export { CHANNEL_DELIVERY_ERROR, CHANNEL_POLL_COMPLETED, CHANNEL_POLL_ERROR, CHANNEL_RUN_TERMINAL, buildCompletionText, buildInspectPayload, compactWhitespace, createDeliveryErrorPayload, createExtension, createPollCompletedPayload, createPollErrorPayload, createRunTerminalPayload, src_default as default, emitAvenorEvent, isTerminalStatus, onAvenorEvent, renderStatusLines, spawnIdentityMetadata, statusSupervisorId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-pi",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "type": "module",
5
5
  "keywords": [
6
6
  "pi-package"
@@ -35,15 +35,15 @@
35
35
  "dev": "tsdown --watch"
36
36
  },
37
37
  "dependencies": {
38
- "@dougbots/avenor-core": "^0.16.1"
38
+ "@dougbots/avenor-core": "^0.17.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-coding-agent": ">=0.74.0",
41
+ "@earendil-works/pi-coding-agent": ">=1.4.0",
42
42
  "@earendil-works/pi-tui": "*"
43
43
  },
44
44
  "devDependencies": {
45
- "@earendil-works/pi-coding-agent": "*",
46
- "@earendil-works/pi-tui": "*",
45
+ "@earendil-works/pi-coding-agent": "^1.4.0",
46
+ "@earendil-works/pi-tui": "^1.4.0",
47
47
  "@types/bun": "^1.3.12",
48
48
  "@types/node": "^25.8.0",
49
49
  "tsdown": "^0.22.0",