@llblab/pi-kit 0.1.12 → 0.2.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 (73) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +2 -2
  3. package/node_modules/@llblab/pi-actors/AGENTS.md +5 -3
  4. package/node_modules/@llblab/pi-actors/BACKLOG.md +1 -1
  5. package/node_modules/@llblab/pi-actors/CHANGELOG.md +15 -0
  6. package/node_modules/@llblab/pi-actors/README.md +5 -3
  7. package/node_modules/@llblab/pi-actors/dist/index.js +4 -1
  8. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.d.ts +2 -2
  9. package/node_modules/@llblab/pi-actors/dist/lib/async-runs.js +42 -19
  10. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +2 -1
  11. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +7 -2
  12. package/node_modules/@llblab/pi-actors/dist/lib/limits.d.ts +9 -0
  13. package/node_modules/@llblab/pi-actors/dist/lib/limits.js +9 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/observability.d.ts +10 -11
  15. package/node_modules/@llblab/pi-actors/dist/lib/observability.js +81 -56
  16. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +31 -0
  17. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +180 -0
  18. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.d.ts +115 -0
  19. package/node_modules/@llblab/pi-actors/dist/lib/run-delivery.js +623 -0
  20. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +3 -0
  21. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +341 -13
  22. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.d.ts +1 -1
  23. package/node_modules/@llblab/pi-actors/dist/lib/runs-trace.js +5 -3
  24. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.d.ts +16 -0
  25. package/node_modules/@llblab/pi-actors/dist/lib/session-evidence.js +143 -0
  26. package/node_modules/@llblab/pi-actors/dist/lib/temp.js +1 -1
  27. package/node_modules/@llblab/pi-actors/dist/lib/tools-inspect.js +3 -1
  28. package/node_modules/@llblab/pi-actors/dist/scripts/async-runner.mjs +5 -19
  29. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +2 -2
  30. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  31. package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +1 -1
  32. package/node_modules/@llblab/pi-actors/docs/README.md +1 -0
  33. package/node_modules/@llblab/pi-actors/docs/async-runs.md +8 -4
  34. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +207 -0
  35. package/node_modules/@llblab/pi-actors/index.ts +4 -1
  36. package/node_modules/@llblab/pi-actors/lib/async-runs.ts +42 -21
  37. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +8 -3
  38. package/node_modules/@llblab/pi-actors/lib/limits.ts +9 -0
  39. package/node_modules/@llblab/pi-actors/lib/observability.ts +97 -78
  40. package/node_modules/@llblab/pi-actors/lib/pi.ts +210 -0
  41. package/node_modules/@llblab/pi-actors/lib/run-delivery.ts +800 -0
  42. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +370 -18
  43. package/node_modules/@llblab/pi-actors/lib/runs-trace.ts +6 -4
  44. package/node_modules/@llblab/pi-actors/lib/session-evidence.ts +153 -0
  45. package/node_modules/@llblab/pi-actors/lib/temp.ts +1 -1
  46. package/node_modules/@llblab/pi-actors/lib/tools-inspect.ts +4 -1
  47. package/node_modules/@llblab/pi-actors/package.json +3 -3
  48. package/node_modules/@llblab/pi-actors/scripts/async-runner.mjs +5 -19
  49. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +2 -2
  50. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  51. package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +1 -1
  52. package/node_modules/@llblab/pi-telegram/AGENTS.md +4 -4
  53. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +19 -0
  54. package/node_modules/@llblab/pi-telegram/README.md +1 -1
  55. package/node_modules/@llblab/pi-telegram/docs/activity.md +12 -5
  56. package/node_modules/@llblab/pi-telegram/docs/architecture.md +7 -6
  57. package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -2
  58. package/node_modules/@llblab/pi-telegram/docs/public-api.md +3 -4
  59. package/node_modules/@llblab/pi-telegram/docs/ui-style.md +4 -5
  60. package/node_modules/@llblab/pi-telegram/docs/voice.md +12 -2
  61. package/node_modules/@llblab/pi-telegram/index.ts +0 -1
  62. package/node_modules/@llblab/pi-telegram/lib/activity.ts +32 -5
  63. package/node_modules/@llblab/pi-telegram/lib/bindings.ts +59 -12
  64. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +1 -1
  65. package/node_modules/@llblab/pi-telegram/lib/config.ts +25 -25
  66. package/node_modules/@llblab/pi-telegram/lib/inbound.ts +1 -1
  67. package/node_modules/@llblab/pi-telegram/lib/lifecycle.ts +50 -0
  68. package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +10 -73
  69. package/node_modules/@llblab/pi-telegram/lib/pi.ts +15 -0
  70. package/node_modules/@llblab/pi-telegram/lib/status.ts +11 -1
  71. package/node_modules/@llblab/pi-telegram/package.json +4 -4
  72. package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +5 -3
  73. package/package.json +3 -3
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  All notable changes to `@llblab/pi-kit` are documented here.
4
4
 
5
+ ## 0.2.0 - 2026-08-31
6
+
7
+ - `Actors Minor Releases`: Advances `@llblab/pi-actors` to `0.52.0`, bringing root-owned follow-ups, durable completion epochs, presentation acknowledgment, recovery-safe scheduling, explicit urgent steering, and settled packed-package parity into the bundled kit.
8
+ - `Telegram Minor Release`: Advances `@llblab/pi-telegram` to `0.41.0`, keeping typing active through settled work and compaction, making connected companion projection always-on, and updating generated controls, connection notices, token telemetry, and voice setup guidance.
9
+
10
+ ## 0.1.13 - 2026-08-30
11
+
12
+ - `Telegram Minor Release`: Advances `@llblab/pi-telegram` to `0.40.0`, requiring Pi 0.84.4 and adopting native compaction-failure, mid-run compaction, terminal-message, and local UI-prompt lifecycle contracts while increasing bounded inbound text output to 24,000 characters.
13
+
5
14
  ## 0.1.12 - 2026-08-28
6
15
 
7
16
  - `Codex Usage Release`: Advances `@llblab/pi-codex-usage` to `0.9.4`, reducing automatic OpenAI quota polling by increasing the refresh interval from 30 to 60 seconds while preserving the local countdown responsiveness.
package/README.md CHANGED
@@ -6,10 +6,10 @@
6
6
 
7
7
  | Package | Version | Resources |
8
8
  | --- | ---: | --- |
9
- | `@llblab/pi-actors` | `0.50.0` | Extension and Skills |
9
+ | `@llblab/pi-actors` | `0.52.0` | Extension and Skills |
10
10
  | `@llblab/pi-codex-usage` | `0.9.4` | Extension |
11
11
  | `@llblab/pi-grow-loop` | `0.7.3` | Extension and Skills |
12
- | `@llblab/pi-telegram` | `0.39.5` | Extension and Skills |
12
+ | `@llblab/pi-telegram` | `0.41.0` | Extension and Skills |
13
13
 
14
14
  Versions are exact by design. Updating an extension does not change an installed kit until this repository explicitly advances that dependency and publishes a new kit version.
15
15
 
@@ -84,7 +84,7 @@ Canonical event:
84
84
  {"id":"…","ts":"…","kind":"…","summary":"…","data":{},"level":"info","attention":"notify"}
85
85
  ```
86
86
 
87
- Trace is a bounded retained suffix, not an audit archive. Every first-party writer must call `appendRunTraceEvent`; under the canonical token-owned lock it appends within both fixed bounds or atomically retains the newest suffix plus one cumulative warning-only `runtime.trace_compacted` marker. The marker means older history was discarded; terminal/result/execution/artifact files remain authoritative independently. Reads are newline-safe and order equal timestamps by same-source ordinal, fixed source rank, then stable id without exposing ordering metadata. Never write `trace.jsonl` directly. Persist durable state or an artifact before attention; `attention: "followup"` remains rare.
87
+ Trace is a bounded retained suffix, not an audit archive. Every first-party writer must call `appendRunTraceEvent`; under the canonical token-owned lock it appends within both fixed bounds or atomically retains the newest suffix plus one cumulative warning-only `runtime.trace_compacted` marker. The marker means older history was discarded; terminal/result/execution/artifact files remain authoritative independently. Reads are newline-safe and order equal timestamps by same-source ordinal, fixed source rank, then stable id without exposing ordering metadata. Never write `trace.jsonl` directly. Generic command lifecycle is Trace-only: `command.done` preserves complete bounded execution evidence but never requests or projects any attention value, and Recipe grammar has no command-completion delivery switch. Semantic attention is explicit: `notify` is visible status, `followup` is an ordinary checkpoint hint, and actor-authored `steer` is durable urgent delivery at Pi's next safe boundary. Never infer attention from status or exit code. Admit steer by exact event and Run generation before `deliverAs: "steer"`; acknowledge only its exact model-bound context envelope, and preserve the later root-terminal completion batch. Sequence, parallel, repeat, and import branches remain internal, while each separately launched Run owns a separate generation.
88
88
 
89
89
  ### Control
90
90
 
@@ -134,9 +134,11 @@ Automatic review receives value-free structural projections, not executable cont
134
134
 
135
135
  Tool result/error text contributes exactly one leading line break. Keep model-facing responses compact and state-backed. Preserve complete byte-exact command streams in bounded spill files while returning bounded tails; never feed truncated tails into pipeline stdin.
136
136
 
137
- File watchers accelerate reconciliation; a bounded interval recovers missed terminal and retained-attention events. Canonical attention observation uses stable retained ids, primes them at startup without replay, and bounds seen memory to the current suffix across compaction; only allowlisted legacy outbox fallback uses line offsets. Attention is a wake hint, so persist durable recovery state before emitting it. Terminal follow-ups contain only Run id, status, one base path, and relative artifact names in visible content; semantic details remain structured. Delivery remains honestly at-least-once across the send/handled-marker crash window.
137
+ File watchers accelerate reconciliation; a bounded interval recovers missed terminal and retained-attention events. Canonical attention observation uses stable retained ids and bounds seen memory to the current suffix across compaction; only allowlisted legacy outbox fallback uses line offsets. Ordinary `notify` and `followup` hints prime at startup without replay and require durable state or artifacts for recovery. Explicit `steer` instead re-enters through the owner journal until exact presentation records its newer generation-fenced Trace marker.
138
138
 
139
- When a deferred Run result gates the next step, wait for its terminal follow-up. Inspect early only for operator request, meaningful attention, or diagnosis of an overdue Run.
139
+ Ordinary root terminals remain authoritative in Run state until the owner-scoped completion scheduler snapshots an immutable batch at a settled or idle boundary. Batch delivery is generation-fenced and bounded at 256 durable members, 64 model-facing rows, 16 KiB of context, and a 1 MiB journal. `sendMessage()` acceptance advances only to queued; only the exact batch id and content appearing in model-bound `context` advances to presented and authorizes exact-generation handled markers. Restart recovery walks the bounded active Pi session parent chain: exact queued evidence waits without resend, proven absence returns the envelope to pending, and incomplete or conflicting evidence remains queued with diagnostics. Retry duplicates collapse before presentation, malformed envelopes fail closed, and automatic review waits behind an active completion batch.
140
+
141
+ When deferred Run results gate the next step, wait for their completion batch. Inspect early only for operator request, meaningful attention, or diagnosis of an overdue Run.
140
142
 
141
143
  ## Documentation and Release Discipline
142
144
 
@@ -1,6 +1,6 @@
1
1
  # Project Backlog
2
2
 
3
- - [ ] `Future minor — Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority; keep this feature outside the 0.49.2 and 0.50.0 cohorts.
3
+ - [ ] `Next minor — Linux MPRIS media integration`: Expose the active `music-player/playback` singleton as one optional generation-fenced MPRIS2 player so GNOME and compatible desktop shells can show current media and native controls without making D-Bus a second playback authority.
4
4
  - [ ] Publish `PlaybackStatus`, track metadata, duration, read-time position, volume, and supported capabilities under one stable session-scoped bus identity; disappear cleanly when the Run stops or its generation is replaced, and fail soft when the user D-Bus session is unavailable.
5
5
  - [ ] Map `Play`, `Pause`, `PlayPause`, `Next`, `Previous`, `Stop`, `Seek`, `SetPosition`, and `Volume` back into the existing generation-fenced music-player Control/helper contract rather than signaling the backend or editing Run state directly.
6
6
  - [ ] Validate deterministic D-Bus contract behavior plus a live GNOME smoke showing the media surface, metadata, progress, volume, and controls while preserving backend independence and exact Actor ownership.
@@ -2,6 +2,21 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.52.0: Durable Coordinator Delivery
6
+
7
+ - `Completion Epochs`: Replaces per-Run terminal turns with one immutable owner-fenced completion batch per idle/settled epoch. Batches retain exact Run generations and terminal timestamps, bound durable and model-facing members, preserve silent and synchronous-stop semantics, and remain eligible after urgent steering.
8
+ - `Presentation Acknowledgment`: Persists completion delivery through `pending`, `queued`, and `presented`; Pi transport acceptance advances only to `queued`, while exact model-bound `context` presentation authorizes generation-fenced terminal handled markers. Duplicate envelopes collapse and altered or conflicting content fails closed.
9
+ - `Recovery-Safe Scheduling`: Stores owner-hashed atomic delivery journals with bounded receipts, attempts, and errors. Restart recovery inspects only the bounded active Pi session chain, retries proven-absent envelopes, preserves uncertain queued evidence, rejects malformed, oversized, duplicate, and symlinked state, and keeps the scheduler directory through stale-temp cleanup.
10
+ - `Explicit Urgent Steering`: Adds durable actor-authored Trace `attention: "steer"` envelopes delivered only through Pi's safe steer boundary. Exact presentation writes generation-fenced `delivery.steer_presented` evidence, suppresses historical replay, and never suppresses the later root-terminal completion batch; `command.done` remains Trace-only regardless of legacy attention fields.
11
+ - `Settled Packed Parity`: Exercises the compiled package against the Pi 0.84.4 lifecycle contract, proving urgent steer presentation precedes its exact completion epoch, with `agent_settled`, follow-up/steer delivery modes, model-bound context acknowledgment, terminal fencing, and durable steer evidence.
12
+
13
+ ## 0.51.0: Monotonic Run Follow-Ups
14
+
15
+ - `Root-Owned Follow-Up`: A normal finite Run now produces one automatic agent turn from its root terminal result. Sequence, parallel, repeat, and imported branches remain internal execution topology; each separately launched Run still owns a separate generation and terminal lifecycle.
16
+ - `Trace-Only Command Lifecycle`: Consolidates each command completion into one complete bounded `command.done` observation with level, captures, session provenance, artifacts, and execution evidence, but no attention. Legacy `command.done` attention remains non-projectable, and the removed Recipe-level delivery grammar stays removed.
17
+ - `Monotonic Reconciliation`: Delivers terminal transitions before explicit semantic attention, preserves silent Runs, synchronous stop behavior, generation fencing, terminal retry evidence, and semantic checkpoints, and removes branch exit-code heuristics that could wake the coordinator from stale process-level events.
18
+ - `Settled Pi Baseline`: Requires Pi and Pi TUI 0.84.4 or newer and schedules automatic Recipe review on `agent_settled`, after queued follow-ups, retries, and compaction complete. Source and packed-package regressions pin the lifecycle and minimum peer contract.
19
+
5
20
  ## 0.50.0: Hardened Actor Baseline
6
21
 
7
22
  - `Template Recipe Standard`: Rebuilt the Recipe authoring guide around one current-state contract: formats, identity, uniformly detailed file-level and command-node field tables, resolution precedence, imports, async/singleton lifecycle, Control, artifacts, runtime origins, provenance, authoring workflow, and validation. Removed migration history and legacy-reference narration from the normative document.
@@ -15,11 +15,11 @@ An **actor** is any runnable local capability: a script, tool, service, pipeline
15
15
 
16
16
  Multi-instance systems commonly put instance creation and routing in an external gateway. pi-actors supports a different topology: the current Pi instance remains the coordinator, companion extensions such as Telegram provide presence, and explicit Runs perform bounded delegated work. The coordinator receives high-level outcomes, decomposes them, stays available for decisions, and owns integration plus final validation instead of becoming another undifferentiated worker.
17
17
 
18
- This topology does not require every task to become a subagent. Short work with one natural validation boundary stays inline; delegation pays when clean context, asynchronous execution, independent judgement, parallel ownership, or coordinator availability exceeds its coordination cost. Bounded implementation can run with reasoning off while the coordinator selectively launches clean-context reasoning-enabled reviewers; several independent reviews can provide broader evidence than one author self-review. Terminal follow-ups, durable Trace, and declared artifacts replace tight polling loops.
18
+ This topology does not require every task to become a subagent. Short work with one natural validation boundary stays inline; delegation pays when clean context, asynchronous execution, independent judgement, parallel ownership, or coordinator availability exceeds its coordination cost. Bounded implementation can run with reasoning off while the coordinator selectively launches clean-context reasoning-enabled reviewers; several independent reviews can provide broader evidence than one author self-review. Settled completion batches, durable Trace, and declared artifacts replace tight polling loops.
19
19
 
20
20
  ## Install
21
21
 
22
- Requires Node.js 22.19.0 or newer.
22
+ Requires Node.js 22.19.0 or newer and Pi 0.84.4 or newer.
23
23
 
24
24
  ```bash
25
25
  pi install npm:@llblab/pi-actors
@@ -151,7 +151,9 @@ String command-template leaves execute directly without shell interpretation. Us
151
151
 
152
152
  Trace fields are exact: `id`, `ts`, `kind`, and optional `summary`, `data`, `level`, `attention`. Address, sender, recipient, reply, and routing fields fail validation. Trace is a bounded retained suffix, not an audit archive: the canonical authority appends within 2,048 events and 4 MiB or atomically keeps the newest suffix plus one warning-only `runtime.trace_compacted` marker. That marker means older history was discarded; `result.json`, `execution.json`, terminal state, and declared artifacts retain their own authority. `inspect view=trace` reports whether retained history is complete. Reads are newline-safe and deterministic; first-party scripts never write this file directly.
153
153
 
154
- Attention is a live wake hint, not a durable queue: persist recovery state or an artifact first. Use `attention: "notify"` for visible status and `attention: "followup"` only when the coordinator needs semantic follow-up context; compaction may discard older hints. Store large evidence in artifacts or bounded execution captures.
154
+ Generic command lifecycle is Trace-only: runner-owned `command.done` records preserve completion and execution evidence but never request or project attention, including malformed legacy records carrying `steer`. There is no Recipe-level command-completion delivery switch. Semantic attention is explicit: `notify` shows visible status without a model turn, `followup` supplies ordinary checkpoint context, and `steer` requests urgent delivery at Pi's next safe assistant/tool boundary. `notify` and `followup` remain retained wake hints that may compact away, so persist recovery state or an artifact first. `steer` is admitted to the bounded owner delivery journal before `deliverAs: "steer"`, acknowledged only in exact model-bound context, and never suppresses the eventual root-terminal batch. Store large evidence in artifacts or bounded execution captures.
155
+
156
+ Ordinary finite Runs project root terminal results through one owner-fenced completion scheduler. Completions that accumulate while Pi is active or inside the idle debounce window enter one immutable bounded batch and cause one automatic agent turn after Pi settles; terminal generations are acknowledged only when that exact batch reaches model-bound context. Sequence, parallel, repeat, and imported branches remain internal and never create branch-level turns, while separately launched Runs retain exact independent generations. Silent Runs and synchronous stop acknowledgments remain non-projecting.
155
157
 
156
158
  ## Control
157
159
 
@@ -9,7 +9,10 @@ export default function toolRegistryExtension(pi) {
9
9
  const runtime = ExtensionRuntime.createActorExtensionRuntime(pi);
10
10
  pi.on("resources_discover", async () => runtime.discoverResources(import.meta.url));
11
11
  pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
12
- pi.on("agent_end", async (_event, ctx) => runtime.onAgentEnd(ctx));
12
+ pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
13
+ pi.on("context", async (event, ctx) => ({
14
+ messages: runtime.onContext(event.messages, ctx),
15
+ }));
13
16
  pi.on("session_shutdown", async (event, ctx) => runtime.onSessionShutdown(event.reason, ctx));
14
17
  pi.on("before_agent_start", async (event, ctx) => runtime.beforeAgentStart(event.systemPrompt, event.systemPromptOptions.skills ?? [], ctx));
15
18
  InspectorCommand.registerActorInspectorCommand(pi, runtime.getRunOwnerId);
@@ -136,8 +136,8 @@ export interface RunControlExpectation {
136
136
  ownerId?: string;
137
137
  runInstanceId?: string;
138
138
  }
139
- export declare function markRunTerminalNotificationHandled(stateDir: string, status: string): void;
140
- export declare function recordRunTerminalDeliveryFailure(stateDir: string, status: string, error: unknown): void;
139
+ export declare function markRunTerminalNotificationHandled(stateDir: string, status: string, expectedRunInstanceId: string): boolean;
140
+ export declare function markRunSteerPresentationHandled(stateDir: string, expectedRunInstanceId: string, eventId: string, steerId: string): boolean;
141
141
  export declare function cancelRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
142
142
  export declare function archiveRun(runOrDir: string, expected?: RunControlExpectation): Record<string, unknown>;
143
143
  export declare function pruneRun(runOrDir: string, options?: {
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { spawn } from "node:child_process";
6
6
  import { randomUUID } from "node:crypto";
7
- import { closeSync, existsSync, mkdirSync, openSync, readdirSync, statSync, } from "node:fs";
7
+ import { closeSync, existsSync, mkdirSync, openSync, readdirSync, rmSync, statSync, } from "node:fs";
8
8
  import { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
  import { isDeepStrictEqual } from "node:util";
@@ -719,25 +719,48 @@ function stopRun(runOrDir, signal, event, expected = {}) {
719
719
  releaseControlLock();
720
720
  }
721
721
  }
722
- export function markRunTerminalNotificationHandled(stateDir, status) {
723
- markTerminalHandled(stateDir, {
724
- event: "run.notification",
725
- status,
726
- });
722
+ export function markRunTerminalNotificationHandled(stateDir, status, expectedRunInstanceId) {
723
+ const releaseLock = RunsStart.acquireStateStartLock(stateDir);
724
+ try {
725
+ if (!existsSync(join(stateDir, "run.json")))
726
+ return false;
727
+ const current = getRunStatus(stateDir);
728
+ if (current.run_instance_id !== expectedRunInstanceId ||
729
+ current.status !== status)
730
+ return false;
731
+ markTerminalHandled(stateDir, {
732
+ event: "run.notification",
733
+ run_instance_id: expectedRunInstanceId,
734
+ status,
735
+ });
736
+ rmSync(join(stateDir, "terminal-delivery-failure.json"), { force: true });
737
+ return true;
738
+ }
739
+ finally {
740
+ releaseLock();
741
+ }
727
742
  }
728
- export function recordRunTerminalDeliveryFailure(stateDir, status, error) {
729
- const path = join(stateDir, "terminal-delivery-failure.json");
730
- const previous = readJson(path);
731
- const message = (error instanceof Error ? error.message : String(error))
732
- .replaceAll(/\s+/g, " ")
733
- .trim()
734
- .slice(0, 500);
735
- writeJsonAtomic(path, {
736
- attempts: Math.max(0, Number(previous?.attempts ?? 0)) + 1,
737
- error: message || "unknown delivery failure",
738
- status,
739
- ts: new Date().toISOString(),
740
- });
743
+ export function markRunSteerPresentationHandled(stateDir, expectedRunInstanceId, eventId, steerId) {
744
+ const releaseLock = RunsStart.acquireStateStartLock(stateDir);
745
+ try {
746
+ if (!existsSync(join(stateDir, "run.json")))
747
+ return false;
748
+ const current = getRunStatus(stateDir);
749
+ if (current.run_instance_id !== expectedRunInstanceId)
750
+ return false;
751
+ appendRunTraceEvent(stateDir, {
752
+ data: {
753
+ event_id: eventId,
754
+ run_instance_id: expectedRunInstanceId,
755
+ steer_id: steerId,
756
+ },
757
+ kind: "delivery.steer_presented",
758
+ });
759
+ return true;
760
+ }
761
+ finally {
762
+ releaseLock();
763
+ }
741
764
  }
742
765
  export function cancelRun(runOrDir, expected = {}) {
743
766
  const result = stopRun(runOrDir, "SIGTERM", "run.cancel", expected);
@@ -13,7 +13,8 @@ export interface ActorExtensionRuntime {
13
13
  skillPaths: string[];
14
14
  } | undefined;
15
15
  getRunOwnerId(ctx: Pi.ExtensionContext): string;
16
- onAgentEnd(ctx: Pi.ExtensionContext): void;
16
+ onAgentSettled(ctx: Pi.ExtensionContext): void;
17
+ onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
17
18
  onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
18
19
  onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
19
20
  registerCoreTools(): void;
@@ -112,10 +112,15 @@ export function createActorExtensionRuntime(pi) {
112
112
  return skillPaths.length > 0 ? { skillPaths } : undefined;
113
113
  },
114
114
  getRunOwnerId,
115
- onAgentEnd(ctx) {
116
- if (activeRunContext === ctx)
115
+ onAgentSettled(ctx) {
116
+ if (activeRunContext !== ctx)
117
+ return;
118
+ if (!runUiRuntime.flushCompletionBatch(ctx))
117
119
  automaticReview.schedule();
118
120
  },
121
+ onContext(messages, ctx) {
122
+ return runUiRuntime.projectContext(messages, ctx);
123
+ },
119
124
  onSessionShutdown(reason, ctx) {
120
125
  const ownerId = runOwnerIdsByContext.get(ctx);
121
126
  runOwnerIdsByContext.delete(ctx);
@@ -27,3 +27,12 @@ export declare const RUN_CONTROL_ERROR_MAX_BYTES: number;
27
27
  export declare const RUN_CONTROL_JOURNAL_MAX_BYTES: number;
28
28
  export declare const RUN_RETENTION_MAX_RECORDS = 256;
29
29
  export declare const RUN_RETENTION_MAX_BYTES: number;
30
+ export declare const RUN_DELIVERY_BATCH_MAX_MEMBERS = 256;
31
+ export declare const RUN_DELIVERY_MODEL_MAX_MEMBERS = 64;
32
+ export declare const RUN_DELIVERY_MODEL_MAX_BYTES: number;
33
+ export declare const RUN_DELIVERY_RECEIPT_LIMIT = 128;
34
+ export declare const RUN_DELIVERY_SESSION_MAX_ENTRIES = 256;
35
+ export declare const RUN_DELIVERY_SESSION_MAX_BYTES: number;
36
+ export declare const RUN_DELIVERY_STEER_MAX_ENVELOPES = 64;
37
+ export declare const RUN_DELIVERY_STEER_MAX_BYTES: number;
38
+ export declare const RUN_DELIVERY_JOURNAL_MAX_BYTES: number;
@@ -27,3 +27,12 @@ export const RUN_CONTROL_ERROR_MAX_BYTES = 4 * 1024;
27
27
  export const RUN_CONTROL_JOURNAL_MAX_BYTES = 1024 * 1024;
28
28
  export const RUN_RETENTION_MAX_RECORDS = 256;
29
29
  export const RUN_RETENTION_MAX_BYTES = 1024 * 1024;
30
+ export const RUN_DELIVERY_BATCH_MAX_MEMBERS = 256;
31
+ export const RUN_DELIVERY_MODEL_MAX_MEMBERS = 64;
32
+ export const RUN_DELIVERY_MODEL_MAX_BYTES = 16 * 1024;
33
+ export const RUN_DELIVERY_RECEIPT_LIMIT = 128;
34
+ export const RUN_DELIVERY_SESSION_MAX_ENTRIES = 256;
35
+ export const RUN_DELIVERY_SESSION_MAX_BYTES = 1024 * 1024;
36
+ export const RUN_DELIVERY_STEER_MAX_ENVELOPES = 64;
37
+ export const RUN_DELIVERY_STEER_MAX_BYTES = 16 * 1024;
38
+ export const RUN_DELIVERY_JOURNAL_MAX_BYTES = 1024 * 1024;
@@ -5,8 +5,9 @@
5
5
  */
6
6
  import { type FSWatcher } from "node:fs";
7
7
  import * as AsyncRuns from "./async-runs.ts";
8
+ import type { RunCompletionBatchMember } from "./run-delivery.ts";
8
9
  export type RunObservedStatus = "running" | "done" | "failed" | "exited" | "cancelled" | "killed";
9
- export type RunTraceAttention = "log" | "notify" | "followup";
10
+ export type RunTraceAttention = "log" | "notify" | "followup" | "steer";
10
11
  export type RunTraceLevel = "info" | "warning" | "error";
11
12
  export interface RunObservation {
12
13
  activeSubagents?: number;
@@ -23,6 +24,7 @@ export interface RunObservation {
23
24
  terminalHandled?: boolean;
24
25
  retireWhen?: string;
25
26
  run: string;
27
+ runInstanceId?: string;
26
28
  semanticResult?: RunTerminalSemanticResult;
27
29
  tool?: string;
28
30
  stateDir?: string;
@@ -68,15 +70,6 @@ export declare function readRunUiSnapshot(state: RunUiObservationState, ownerId:
68
70
  stateRoot?: string;
69
71
  }): RunUiSnapshot;
70
72
  export declare function pruneRunUiObservationState(state: RunUiObservationState, snapshot: Pick<RunUiSnapshot, "summary" | "transitions">): void;
71
- export declare function deliverRunTransitionNotifications(transitions: RunTransition[], sink: RunUiNotificationSink, inFlight?: Set<string>): void;
72
- export declare function reconcileRunTerminalNotifications(input: {
73
- inFlight?: Set<string>;
74
- ownerId: string;
75
- sink: RunUiNotificationSink;
76
- state: RunUiObservationState;
77
- stateRoot?: string;
78
- includeAttention?: boolean;
79
- }): RunUiSnapshot;
80
73
  export declare function deliverRunAttentionNotifications(events: RunAttentionEvent[], sink: RunUiNotificationSink): void;
81
74
  export interface RunRetirementCandidate {
82
75
  activeSubagents: number;
@@ -133,7 +126,9 @@ export interface RunRetirementExecutorOptions {
133
126
  export interface RunTransition {
134
127
  from: RunObservedStatus;
135
128
  run: string;
129
+ runInstanceId?: string;
136
130
  stateDir?: string;
131
+ terminalAt?: string;
137
132
  artifacts?: Record<string, string>;
138
133
  launchCorrelation?: Record<string, string>;
139
134
  launchSource?: AsyncRuns.AsyncRunLaunchSource;
@@ -161,6 +156,7 @@ export interface RunAttentionEvent {
161
156
  level: RunTraceLevel;
162
157
  metadata?: Record<string, unknown>;
163
158
  run: string;
159
+ runInstanceId?: string;
164
160
  stateDir: string;
165
161
  summary: string;
166
162
  ts: string;
@@ -178,9 +174,12 @@ export declare function pruneRunObservationState(previousStatuses: Map<string, R
178
174
  export declare function detectRunAttentionEvents(legacyLineCounts: Map<string, number>, summary: RunSummary, seenEventIds?: Map<string, Set<string>>, prime?: boolean): RunAttentionEvent[];
179
175
  export declare function getRunAttentionNotificationType(event: RunAttentionEvent): RunTransitionNotificationType;
180
176
  export declare function shouldNotifyRunAttentionEvent(event: RunAttentionEvent): boolean;
177
+ export declare function isRunSteerAttentionEvent(event: RunAttentionEvent): boolean;
178
+ export declare function retryRunAttentionEvent(state: RunUiObservationState, event: Pick<RunAttentionEvent, "id" | "stateDir">): void;
181
179
  export declare function shouldSendRunAttentionFollowUp(event: RunAttentionEvent): boolean;
182
180
  export declare function formatRunAttentionMessage(event: RunAttentionEvent): string;
183
181
  export declare function getRunTransitionNotificationType(transition: RunTransition): RunTransitionNotificationType;
184
182
  export declare function shouldNotifyRunTransition(transition: RunTransition): boolean;
185
- export declare function shouldSendRunTransitionFollowUp(transition: RunTransition): boolean;
183
+ /** Build exact immutable generation members for owner-journal admission. */
184
+ export declare function collectRunCompletionBatchMembers(transitions: RunTransition[]): RunCompletionBatchMember[];
186
185
  export declare function formatRunTransitionMessage(transition: RunTransition): string;
@@ -35,52 +35,6 @@ export function readRunUiSnapshot(state, ownerId, options = {}) {
35
35
  export function pruneRunUiObservationState(state, snapshot) {
36
36
  pruneRunObservationState(state.observed, state.legacyEventLines, snapshot.summary, snapshot.transitions.map((transition) => transition.stateDir ?? transition.run), state.attentionEventIds);
37
37
  }
38
- export function deliverRunTransitionNotifications(transitions, sink, inFlight = new Set()) {
39
- for (const transition of transitions) {
40
- if (!shouldNotifyRunTransition(transition))
41
- continue;
42
- const key = transition.stateDir ?? transition.run;
43
- if (inFlight.has(key))
44
- continue;
45
- inFlight.add(key);
46
- try {
47
- const text = formatRunTransitionMessage(transition);
48
- sink.notify(text, getRunTransitionNotificationType(transition));
49
- if (!shouldSendRunTransitionFollowUp(transition))
50
- continue;
51
- sink.sendFollowUp({
52
- customType: "pi-actors-run",
53
- content: text,
54
- display: false,
55
- details: transition,
56
- });
57
- if (transition.stateDir) {
58
- AsyncRuns.markRunTerminalNotificationHandled(transition.stateDir, transition.to);
59
- }
60
- }
61
- catch (error) {
62
- if (transition.stateDir) {
63
- AsyncRuns.recordRunTerminalDeliveryFailure(transition.stateDir, transition.to, error);
64
- }
65
- const message = error instanceof Error ? error.message : String(error);
66
- sink.notify(`Actor terminal delivery failed for run:${transition.run}: ${message.replaceAll(/\s+/g, " ").slice(0, 240)}`, "error");
67
- }
68
- finally {
69
- inFlight.delete(key);
70
- }
71
- }
72
- }
73
- export function reconcileRunTerminalNotifications(input) {
74
- const snapshot = readRunUiSnapshot(input.state, input.ownerId, {
75
- includeAttention: input.includeAttention,
76
- stateRoot: input.stateRoot,
77
- });
78
- if (input.includeAttention)
79
- deliverRunAttentionNotifications(snapshot.attentionEvents, input.sink);
80
- deliverRunTransitionNotifications(snapshot.transitions, input.sink, input.inFlight);
81
- pruneRunUiObservationState(input.state, snapshot);
82
- return snapshot;
83
- }
84
38
  export function deliverRunAttentionNotifications(events, sink) {
85
39
  for (const event of events) {
86
40
  if (!shouldNotifyRunAttentionEvent(event))
@@ -271,11 +225,18 @@ function getProgress(status) {
271
225
  }
272
226
  function getUpdatedAt(status) {
273
227
  const progress = getProgress(status);
274
- return typeof progress.updatedAt === "string"
275
- ? progress.updatedAt
276
- : typeof status.createdAt === "string"
277
- ? status.createdAt
278
- : undefined;
228
+ const result = status.result &&
229
+ typeof status.result === "object" &&
230
+ !Array.isArray(status.result)
231
+ ? status.result
232
+ : {};
233
+ return typeof result.completed_at === "string"
234
+ ? result.completed_at
235
+ : typeof progress.updatedAt === "string"
236
+ ? progress.updatedAt
237
+ : typeof status.createdAt === "string"
238
+ ? status.createdAt
239
+ : undefined;
279
240
  }
280
241
  function scanRunStateDirs(stateRoot, depth = 0, seen = new Set()) {
281
242
  if (!existsSync(stateRoot) || seen.has(stateRoot))
@@ -409,6 +370,9 @@ function observeRun(stateDir) {
409
370
  ...(typeof status.recipe_file === "string"
410
371
  ? { recipeFile: status.recipe_file }
411
372
  : {}),
373
+ ...(typeof status.run_instance_id === "string"
374
+ ? { runInstanceId: status.run_instance_id }
375
+ : {}),
412
376
  ...(status.terminal_handled ? { terminalHandled: true } : {}),
413
377
  ...(typeof status.retire_when === "string"
414
378
  ? { retireWhen: status.retire_when }
@@ -725,8 +689,10 @@ export function detectRunTransitions(previous, summary) {
725
689
  ...(run.launchSource ? { launchSource: run.launchSource } : {}),
726
690
  ...(run.modelPolicy ? { modelPolicy: run.modelPolicy } : {}),
727
691
  ...(run.recipeFile ? { recipeFile: run.recipeFile } : {}),
692
+ ...(run.runInstanceId ? { runInstanceId: run.runInstanceId } : {}),
728
693
  ...(run.semanticResult ? { semanticResult: run.semanticResult } : {}),
729
694
  ...(run.terminalHandled ? { terminalHandled: true } : {}),
695
+ ...(run.updatedAt ? { terminalAt: run.updatedAt } : {}),
730
696
  to: run.status,
731
697
  ...(run.tool ? { tool: run.tool } : {}),
732
698
  });
@@ -761,7 +727,9 @@ function parseAttentionRecord(raw, run, index) {
761
727
  return {
762
728
  ...(raw.body !== undefined ? { body: raw.body } : {}),
763
729
  ...(raw.data !== undefined ? { data: raw.data } : {}),
764
- attention: raw.attention === "notify" || raw.attention === "followup"
730
+ attention: raw.attention === "notify" ||
731
+ raw.attention === "followup" ||
732
+ raw.attention === "steer"
765
733
  ? raw.attention
766
734
  : normalizeTraceAttention(raw.delivery),
767
735
  id,
@@ -773,6 +741,7 @@ function parseAttentionRecord(raw, run, index) {
773
741
  ? { metadata: raw.metadata }
774
742
  : {}),
775
743
  run: run.run,
744
+ ...(run.runInstanceId ? { runInstanceId: run.runInstanceId } : {}),
776
745
  stateDir: run.stateDir,
777
746
  summary,
778
747
  ts,
@@ -811,6 +780,15 @@ export function detectRunAttentionEvents(legacyLineCounts, summary, seenEventIds
811
780
  const read = readTraceAttentionRecords(run);
812
781
  const retained = new Set();
813
782
  const seen = seenEventIds.get(key) ?? new Set();
783
+ const presentedSteerIds = new Set(read.records.flatMap((record) => {
784
+ if (record.kind !== "delivery.steer_presented" ||
785
+ !record.data ||
786
+ typeof record.data !== "object" ||
787
+ Array.isArray(record.data))
788
+ return [];
789
+ const eventId = record.data.event_id;
790
+ return typeof eventId === "string" && eventId ? [eventId] : [];
791
+ }));
814
792
  const start = read.canonical ? 0
815
793
  : Math.min(legacyLineCounts.get(key) ?? 0, read.records.length);
816
794
  for (const [index, record] of read.records.entries()) {
@@ -819,8 +797,16 @@ export function detectRunAttentionEvents(legacyLineCounts, summary, seenEventIds
819
797
  event.kind === "runtime.trace_compacted")
820
798
  continue;
821
799
  retained.add(event.id);
822
- if (prime || run.notificationPolicy === "silent")
800
+ if (isRunSteerAttentionEvent(event) && presentedSteerIds.has(event.id)) {
823
801
  seen.add(event.id);
802
+ continue;
803
+ }
804
+ if (run.notificationPolicy === "silent")
805
+ seen.add(event.id);
806
+ else if (prime) {
807
+ if (!isRunSteerAttentionEvent(event))
808
+ seen.add(event.id);
809
+ }
824
810
  else if (index >= start && !seen.has(event.id)) {
825
811
  events.push(event);
826
812
  seen.add(event.id);
@@ -838,7 +824,17 @@ export function getRunAttentionNotificationType(event) {
838
824
  return event.level;
839
825
  }
840
826
  export function shouldNotifyRunAttentionEvent(event) {
841
- return event.attention === "notify" || event.attention === "followup";
827
+ if (event.kind === "command.done")
828
+ return false;
829
+ return event.attention === "notify" ||
830
+ event.attention === "followup" ||
831
+ event.attention === "steer";
832
+ }
833
+ export function isRunSteerAttentionEvent(event) {
834
+ return event.kind !== "command.done" && event.attention === "steer";
835
+ }
836
+ export function retryRunAttentionEvent(state, event) {
837
+ state.attentionEventIds.get(event.stateDir)?.delete(event.id);
842
838
  }
843
839
  export function shouldSendRunAttentionFollowUp(event) {
844
840
  return event.attention === "followup";
@@ -925,8 +921,37 @@ export function shouldNotifyRunTransition(transition) {
925
921
  transition.to === "killed" ||
926
922
  transition.to === "exited");
927
923
  }
928
- export function shouldSendRunTransitionFollowUp(transition) {
929
- return shouldNotifyRunTransition(transition);
924
+ /** Build exact immutable generation members for owner-journal admission. */
925
+ export function collectRunCompletionBatchMembers(transitions) {
926
+ return transitions.flatMap((transition) => {
927
+ if (!shouldNotifyRunTransition(transition) ||
928
+ !transition.stateDir ||
929
+ !transition.runInstanceId ||
930
+ !transition.terminalAt ||
931
+ Number.isNaN(Date.parse(transition.terminalAt)))
932
+ return [];
933
+ const artifactEntries = Object.entries(transition.artifacts ?? {})
934
+ .filter((entry) => typeof entry[1] === "string" && Boolean(entry[1]))
935
+ .slice(0, 4);
936
+ const rawSummary = transition.semanticResult?.summary.trim() ||
937
+ `Run ${transition.to}.`;
938
+ return [{
939
+ ...(artifactEntries.length > 0
940
+ ? { artifacts: Object.fromEntries(artifactEntries) }
941
+ : {}),
942
+ run: transition.run,
943
+ run_instance_id: transition.runInstanceId,
944
+ state_dir: transition.stateDir,
945
+ status: transition.to,
946
+ summary: rawSummary.length > 1_000
947
+ ? `${rawSummary.slice(0, 999)}…`
948
+ : rawSummary,
949
+ terminal_at: transition.terminalAt,
950
+ }];
951
+ }).sort((left, right) => left.terminal_at.localeCompare(right.terminal_at) ||
952
+ left.run.localeCompare(right.run) ||
953
+ left.run_instance_id.localeCompare(right.run_instance_id) ||
954
+ left.state_dir.localeCompare(right.state_dir));
930
955
  }
931
956
  const TERMINAL_FOLLOW_UP_ARTIFACT_LIMIT = 4;
932
957
  const TERMINAL_FOLLOW_UP_IDENTIFIER_CHARS = 120;
@@ -4,7 +4,10 @@
4
4
  * Owns direct pi SDK imports and exposes narrow pi-actors-facing helpers/types for the composition root.
5
5
  */
6
6
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import * as SessionEvidence from "./session-evidence.ts";
7
8
  export type { ExtensionAPI, ExtensionContext };
9
+ export declare const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
10
+ export declare const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
8
11
  export interface PiNotificationSink {
9
12
  notify(message: string, level: "info" | "warning" | "error"): void;
10
13
  sendFollowUp(message: {
@@ -16,4 +19,32 @@ export interface PiNotificationSink {
16
19
  }
17
20
  export declare function getSessionId(ctx: ExtensionContext): string;
18
21
  export declare function createNotificationSink(pi: ExtensionAPI, ctx: ExtensionContext): PiNotificationSink;
22
+ export declare function sendRunCompletionBatch(pi: ExtensionAPI, batchId: string, content: string): void;
23
+ export declare function sendRunSteer(pi: ExtensionAPI, input: {
24
+ content: string;
25
+ eventId: string;
26
+ steerId: string;
27
+ }): void;
28
+ /** Collapse exact retry duplicates and remove conflicting delivery envelopes. */
29
+ export declare function dedupeRunCompletionBatchContext(messages: unknown[]): {
30
+ batches: Map<string, string>;
31
+ conflicts: Set<string>;
32
+ messages: unknown[];
33
+ };
34
+ export declare function removeRunCompletionBatchFromContext(messages: unknown[], batchId: string): unknown[];
35
+ export declare function dedupeRunSteerContext(messages: unknown[]): {
36
+ conflicts: Set<string>;
37
+ messages: unknown[];
38
+ steers: Map<string, {
39
+ content: string;
40
+ eventId: string;
41
+ }>;
42
+ };
43
+ export declare function removeRunSteerFromContext(messages: unknown[], steerId: string): unknown[];
44
+ export declare function inspectRunSteerSessionEvidence(ctx: ExtensionContext, input: {
45
+ content: string;
46
+ eventId: string;
47
+ steerId: string;
48
+ }): SessionEvidence.ActiveSessionEntryEvidence;
49
+ export declare function inspectRunCompletionBatchSessionEvidence(ctx: ExtensionContext, batchId: string, content: string): SessionEvidence.ActiveSessionEntryEvidence;
19
50
  export declare function registerToolDefinitions(pi: ExtensionAPI, definitions: Iterable<unknown>): void;