@llblab/pi-kit 0.35.5 → 0.36.1

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 (34) hide show
  1. package/AGENTS.md +2 -1
  2. package/BACKLOG.md +0 -5
  3. package/CHANGELOG.md +12 -0
  4. package/README.md +33 -31
  5. package/node_modules/@llblab/pi-actors/AGENTS.md +3 -1
  6. package/node_modules/@llblab/pi-actors/CHANGELOG.md +4 -0
  7. package/node_modules/@llblab/pi-actors/README.md +2 -2
  8. package/node_modules/@llblab/pi-actors/dist/index.js +1 -0
  9. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.d.ts +1 -0
  10. package/node_modules/@llblab/pi-actors/dist/lib/extension-runtime.js +3 -0
  11. package/node_modules/@llblab/pi-actors/dist/lib/pi.d.ts +7 -2
  12. package/node_modules/@llblab/pi-actors/dist/lib/pi.js +20 -6
  13. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.d.ts +1 -0
  14. package/node_modules/@llblab/pi-actors/dist/lib/run-ui-runtime.js +74 -74
  15. package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +1 -1
  16. package/node_modules/@llblab/pi-actors/dist/skills/actors/references/runs.md +1 -1
  17. package/node_modules/@llblab/pi-actors/docs/README.md +1 -1
  18. package/node_modules/@llblab/pi-actors/docs/async-runs.md +3 -1
  19. package/node_modules/@llblab/pi-actors/docs/coordinator-delivery.md +22 -20
  20. package/node_modules/@llblab/pi-actors/index.ts +1 -0
  21. package/node_modules/@llblab/pi-actors/lib/extension-runtime.ts +4 -0
  22. package/node_modules/@llblab/pi-actors/lib/pi.ts +33 -9
  23. package/node_modules/@llblab/pi-actors/lib/run-ui-runtime.ts +72 -69
  24. package/node_modules/@llblab/pi-actors/package.json +1 -1
  25. package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +1 -1
  26. package/node_modules/@llblab/pi-actors/skills/actors/references/runs.md +1 -1
  27. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +4 -0
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +5 -4
  29. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  30. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +1 -1
  31. package/node_modules/@llblab/pi-state-flow/docs/usage.md +1 -0
  32. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +5 -4
  33. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  34. package/package.json +3 -3
@@ -7,7 +7,7 @@ Living index of all documentation in the `/docs` directory.
7
7
  - [command-templates.md](./command-templates.md) — Portable synchronous command execution standard
8
8
  - [template-recipes.md](./template-recipes.md) — Saved JSON/Markdown recipe standard, imports, and reusable command-template graph composition
9
9
  - [async-runs.md](./async-runs.md) — Run lifecycle, state, Control, Trace, cancellation, and terminal reconciliation
10
- - [coordinator-delivery.md](./coordinator-delivery.md) — Accepted next-minor design for durable terminal batching and explicit urgent Pi steering
10
+ - [coordinator-delivery.md](./coordinator-delivery.md) — Active model-step completion overlays, idle batching, durable acceptance, and explicit urgent Pi steering
11
11
  - [actor-inspector.md](./actor-inspector.md) — Owner-filtered actor-instance navigation through Recipe, Trace, and Control
12
12
  - [inspection.md](./inspection.md) — Complete `inspect` target/view matrix, authorization boundaries, and diagnostic routes
13
13
  - [tool-registry.md](./tool-registry.md) — Local `pi-actors` registry storage and `register_tool` adaptation
@@ -110,7 +110,9 @@ Statuses include `running`, `done`, `failed`, `exited`, `cancelled`, and `killed
110
110
 
111
111
  Ambient observation detects root terminal transitions and explicit retained Trace attention. Terminal transitions reconcile before semantic attention. Canonical attention is an in-memory wake hint, not a durable queue: observers prime retained ids at startup, deliver each later retained unseen id once, and bound memory to the current retained set across compaction. Persist durable recovery state or an artifact before emitting attention; compaction may discard older hints and its marker makes that history loss explicit. Terminal follow-up delivery persists handled/failure evidence so reloads retry unhandled transitions without duplicating completed notifications.
112
112
 
113
- Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its containing top-level Run is terminal; descendant Pi sessions never schedule completion follow-ups. After `agent_settled`, session recovery, or an idle debounce, the root scheduler snapshots the ready trees plus concurrently completed top-level Runs into one owner-fenced immutable batch of at most 256 exact generations. One batch causes one automatic agent turn, renders one visible gray operator card, exposes at most 64 bounded parent-child rows with semantic outputs to the model, and marks member terminals handled after Pi durably accepts the exact batch. Pending send failures retain bounded retry evidence. The narrow crash boundary between acceptance and finalization remains recoverable from bounded active-session evidence, but normal hot delivery never waits on a later context callback and therefore cannot block subsequent epochs. Duplicate exact context envelopes collapse before presentation.
113
+ Ordinary finite Runs project terminal results through one root-coordinator completion scheduler. Every detached runner passes its delivery-root owner and exact parent Run generation to nested actor processes. A descendant terminal remains authoritative but ineligible until its complete containing top-level tree is terminal; descendant Pi sessions never schedule completion delivery. At an active completed `turn_end`, the root scheduler snapshots all ready trees into one owner-fenced immutable batch of at most 256 exact generations, preserves preceding boundary entries, and returns one visible `custom_message` plus `continue: true`. The full tool batch finishes before insertion, and the next model request receives fresh results within the same agent run without using native steering or follow-up queues. Aborted/error boundaries and pending native input defer insertion. Already-idle Pi still wakes through `agent_settled`, session recovery, or a short idle debounce. One batch renders one gray operator card and exposes at most 64 bounded parent-child rows with semantic outputs.
114
+
115
+ Boundary proposals remain pending until exact model context or bounded active-session evidence proves the entry was committed; returning a draft never marks member terminals handled. An idle send finalizes immediately on Pi acceptance, and durable queued phase also proves acceptance. Recovery retries a pending batch only after proving absence from the bounded active session chain; incomplete or conflicting evidence stays pending with diagnostics. Exact acceptance marks the member generations handled and releases the next epoch without depending on one later context callback. Pending send failures retain bounded retry evidence. Duplicate exact context envelopes collapse before presentation. See [Coordinator Delivery](./coordinator-delivery.md) for scheduling and recovery details.
114
116
 
115
117
  Sequence, parallel, repeat, and imported branches are internal execution topology and never own branch-level turns. Each separately launched Run owns its own generation and terminal lifecycle; compatible singleton reuse is not a new launch. Explicit semantic attention may intentionally add a checkpoint turn; Runs marked silent and synchronously acknowledged stop outcomes suppress automatic projection. Large semantic results stay outside compact completion rows and remain available in structured details, execution captures, or artifacts.
116
118
 
@@ -8,10 +8,10 @@ Separate durable Run completion truth from the scheduling of model turns:
8
8
 
9
9
  ```text
10
10
  one Run generation -> one terminal record with inherited tree lineage
11
- one ready completion forest per bounded epoch -> one root-coordinator turn
11
+ one ready completion forest per bounded epoch -> one root-coordinator model step
12
12
  ```
13
13
 
14
- Ordinary terminals accumulate across nested actor sessions while Pi is active. Descendants wait for their containing top-level Run, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch after Pi settles. Only an explicitly actor-authored urgent semantic checkpoint may steer an active agent loop.
14
+ Ordinary terminals accumulate across nested actor sessions. Descendants wait for their complete containing top-level tree, then all ready trees and concurrently completed top-level Runs reach only the root coordinator in one bounded batch. During active work, a public `turn_end` overlay supplies fresh results to the next model request after the full tool batch, without waiting for idle or using native steering. Already-idle Pi retains debounced follow-up wakeups. Explicit actor-authored urgent semantic checkpoints remain a separate native-steer delivery class.
15
15
 
16
16
  ## Non-Goals
17
17
 
@@ -27,7 +27,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
27
27
  - `attention: "notify"` remains visible UI status without a model turn.
28
28
  - `attention: "followup"` retains its existing explicit semantic follow-up behavior.
29
29
  - New `attention: "steer"` requests urgent semantic delivery at Pi's next safe assistant/tool boundary.
30
- - Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees instead of sending one follow-up per Run or descendant session.
30
+ - Terminal transitions inherit exact parent-generation and root-owner lineage; only the root session batches ready completion trees into a model-step overlay or an already-idle follow-up, never one turn per Run or descendant session.
31
31
 
32
32
  `command.done` remains non-projectable even if malformed or legacy Trace attaches any attention value.
33
33
 
@@ -37,7 +37,7 @@ Ordinary terminals accumulate across nested actor sessions while Pi is active. D
37
37
  - `runs-trace.ts` owns admission of the new `steer` attention value.
38
38
  - `observability.ts` discovers terminal candidates and explicit semantic attention without deciding Pi delivery timing.
39
39
  - A new `run-delivery.ts` domain owns the owner-scoped delivery journal, batch construction, bounds, phases, formatting, recovery, and acknowledgments.
40
- - `run-ui-runtime.ts` owns idle detection, debounce, reconciliation, flushing, and stale-context containment.
40
+ - `run-ui-runtime.ts` owns active model-step boundaries, idle detection, debounce, reconciliation, flushing, and stale-context containment.
41
41
  - `extension-runtime.ts` orders completion flushes before automatic Recipe review.
42
42
  - `index.ts` remains a thin registration root and adds only the required Pi lifecycle adapter.
43
43
  - `pi.ts` exposes narrow ports for batched follow-up and urgent steer delivery.
@@ -75,13 +75,13 @@ Completion batch shape:
75
75
 
76
76
  Phases are monotonic:
77
77
 
78
- 1. `pending`: The exact member snapshot is durable but no Pi message has been accepted.
78
+ 1. `pending`: The exact member snapshot is durable but Pi acceptance is unproven. Returning a boundary draft does not advance this phase.
79
79
  2. `queued`: Pi accepted a custom message carrying the exact batch or steer ID.
80
- 3. `presented`: A Pi `context` event observed that ID in messages being supplied to an LLM call.
80
+ 3. `presented`: Exact completion acceptance is proven, or a Pi `context` event observed the urgent-steer ID in messages being supplied to an LLM call.
81
81
 
82
82
  Every journal mutation uses the canonical token-owned lock, expected-phase fencing, owner and generation validation, and atomic replacement. Repeated transitions are idempotent. Corrupt, oversized, foreign-owner, or stale-generation state fails closed with bounded diagnostics.
83
83
 
84
- Completion and steer acknowledgments intentionally differ. Successful `sendMessage()` acceptance finalizes an ordinary completion batch immediately and marks its members through the existing terminal-handled authority; this prevents a missed context callback from blocking every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after queueing, the bounded delivery snapshot remains sufficient and the missing state write becomes a diagnostic rather than invalidating acceptance.
84
+ Completion and steer acknowledgments intentionally differ. An active completion overlay remains pending until exact model context or bounded active-session evidence proves its committed entry. Successful idle `sendMessage()` acceptance finalizes an ordinary completion batch immediately; durable queued phase also proves acceptance. Both paths mark members through the existing terminal-handled authority, so a missed context callback cannot block every later epoch. Urgent steer still requires exact model-context presentation. If a completion member was synchronously archived or pruned after acceptance, the bounded delivery snapshot remains sufficient and the missing state write becomes a diagnostic rather than invalidating acceptance.
85
85
 
86
86
  ## Completion Collection
87
87
 
@@ -97,17 +97,20 @@ It excludes:
97
97
 
98
98
  Candidates sort by terminal timestamp, then stable Run identity, then `run_instance_id`. Replacement generations with the same logical Run id remain distinct internal members.
99
99
 
100
- While `ctx.isIdle()` is false, candidates remain durable in their Run state and no terminal follow-up is sent. A flush snapshots eligible candidates into one immutable batch. While transport acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
100
+ While Pi is active, candidates remain durable until the next completed `turn_end`; watchers do not enqueue ordinary terminal follow-ups into the native message queue. The boundary itself reads current Run state, so a missed watcher cannot make the next step stale. A flush snapshots eligible candidates into one immutable batch. While acceptance is pending, newer terminals stay unhandled for the next bounded completion epoch.
101
101
 
102
102
  ## Batch Flush
103
103
 
104
104
  Flush one batch when:
105
105
 
106
- 1. `agent_settled` fires for the still-active context and `ctx.isIdle()` remains true;
107
- 2. terminals arrive while Pi is already idle and survive one short debounce window;
108
- 3. session restoration discovers unhandled terminal generations or recoverable queued delivery state.
106
+ 1. A completed active `turn_end` reaches the exact root owner without an aborted signal or native pending input;
107
+ 2. `agent_settled` fires for the still-active owner and `ctx.isIdle()` remains true;
108
+ 3. Terminals arrive while Pi is already idle and survive the 100 ms default debounce window;
109
+ 4. Session restoration discovers unhandled terminal generations or recoverable delivery state.
109
110
 
110
- The custom message uses `customType: "pi-actors-run-batch"`, `display: true`, `deliverAs: "followUp"`, and `triggerTurn: true`. It restores the visible gray completion card for the operator while supplying the same single tree-compressed prompt to the model. The bounded content retains the exact batch identity for session evidence and deduplication even though Pi omits private message details from model context. It includes:
111
+ Active delivery returns `{ entries: [...event.entries, entry], continue: true }`, where `entry` is one `custom_message` with `customType: "pi-actors-run-batch"` and `display: true`. It preserves previous extensions' boundary entries and requests one next model step in the same agent run after the entire tool batch. Aborted/error boundaries, queued native input, and outstanding urgent steer defer the overlay. It never uses `sendMessage(..., { deliverAs: "steer" })` or a native follow-up queue for active completion delivery.
112
+
113
+ Already-idle delivery uses the same custom message with `deliverAs: "followUp"` and `triggerTurn: true` to wake Pi. Both paths render one gray operator card and supply the same tree-compressed prompt to the model. The bounded content retains the exact batch identity for session evidence and deduplication even though Pi omits private message details from model context. It includes:
111
114
 
112
115
  - batch ID and completion window;
113
116
  - counts by terminal status;
@@ -120,14 +123,14 @@ Completion member details remain redacted through existing terminal projection r
120
123
 
121
124
  ## Acceptance, Presentation, And Recovery
122
125
 
123
- Ordinary completion delivery atomically advances through queued acceptance to finalization immediately after `sendMessage()` returns, marks every still-present member generation terminal-handled, and retains a bounded owner receipt. A `context` lifecycle adapter remains for exact urgent-steer presentation and harmless completion deduplication; presented steer writes `delivery.steer_presented` in the exact Run generation.
126
+ Returning boundary drafts is a proposal, not an SDK receipt. An active completion batch advances through queued acceptance to finalization only when exact model-bound `context` or a bounded active-session parent-chain read proves the committed entry. If another extension replaces or invalidates the proposed draft chain, the absent batch remains pending and can retry with the same identity. Idle delivery advances immediately after `sendMessage()` returns. Acceptance marks every still-present member generation terminal-handled and retains a bounded owner receipt. The `context` lifecycle adapter also deduplicates completion envelopes and handles exact urgent-steer presentation; presented steer writes `delivery.steer_presented` in the exact Run generation.
124
127
 
125
128
  The generation-fenced Trace marker prevents a retained historical steer from replaying after bounded owner receipts rotate: suffix compaction cannot retain the older steer while discarding its newer presentation marker. Missing, archived, pruned, or replaced Run state needs no marker because it can no longer replay that original generation.
126
129
 
127
130
  Recovery rules:
128
131
 
129
132
  - Send failure: keep `pending`, record failure evidence, and retry.
130
- - Crash after queueing: inspect existing owned Pi session evidence for the exact custom message ID.
133
+ - Pending batch after a boundary attempt or restart: inspect the bounded active Pi session chain for the exact batch ID and content. Proven presence finalizes; proven absence permits retry; incomplete or conflicting evidence stays pending with a diagnostic.
131
134
  - Completion send accepted: finalize immediately and release the next epoch.
132
135
  - Crash leaves a queued completion: queued itself proves `sendMessage()` acceptance, so finalize without resend or content reformatting.
133
136
  - Presented urgent steer: never resend.
@@ -179,12 +182,12 @@ Another extension may start work during `agent_settled`; recheck `ctx.isIdle()`
179
182
 
180
183
  Implementation is complete only when source and packed-extension tests prove:
181
184
 
182
- 1. Multiple Runs finishing during one agent run cause no immediate terminal turns and one settled batch.
185
+ 1. Multiple Runs finishing during active work produce one boundary entry and one next model request before idle, after all tools in the current batch finish; no native steering/follow-up enqueue occurs.
183
186
  2. Idle completions inside the debounce window form one batch.
184
187
  3. Completion/settled races project every generation exactly once.
185
188
  4. Send failure and restart before queueing retry without a handled marker.
186
189
  5. Restart in the narrow queue-acceptance/finalization boundary neither loses nor duplicates the batch.
187
- 6. Successful completion acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
190
+ 6. Boundary proposals do not acknowledge terminals early. Exact committed-entry evidence or idle send acceptance acknowledges members atomically and idempotently; urgent steer still requires exact `context` presentation.
188
191
  7. Replacement generations sharing a Run id remain distinct.
189
192
  8. Silent, stopped, cancelled, handled, and foreign-owner Runs remain excluded.
190
193
  9. Legacy or malformed `command.done` attention, including `steer`, remains non-projectable.
@@ -192,11 +195,10 @@ Implementation is complete only when source and packed-extension tests prove:
192
195
  11. Overflow, corruption, journal backpressure, archive/prune races, and stale contexts fail safely.
193
196
  12. Completion flushing precedes automatic Recipe review.
194
197
  13. Pi 1.0.0 is the minimum source and packed lifecycle baseline; coding-agent and TUI peers share that floor.
198
+ 14. The packed extension runs through the public SDK with a synthetic provider, proving one batch reaches the next request before settlement for both a full tool batch and a tool-free final answer, while preserving preceding boundary entries.
195
199
 
196
200
  Focused observability and delivery tests precede TypeScript/build/import checks. The acceptance checkpoint then runs full product validation, dependency audit, package dry-run, and ABCd context validation.
197
201
 
198
- ## Rollout
199
-
200
- This is one minor release because durable batching, acceptance acknowledgment, lifecycle ordering, and explicit steer share one model-delivery invariant. Completion acceptance must release later epochs without depending on a context callback; steer must not ship before its durable presentation-deduplication path exists.
202
+ ## Product Boundaries
201
203
 
202
- Update README and Run documentation only when implementation establishes the new public behavior. Move the accepted outcome from BACKLOG to CHANGELOG only after complete validation.
204
+ Active overlays and idle follow-ups share the same batch identity, bounds, lineage, terminal authority, and acceptance journal. They differ only in the Pi scheduling port and its acceptance evidence. Urgent semantic steer remains independent; completion status never implies urgency. The extension has no dependency on Telegram and does not modify Telegram reply or preview ownership.
@@ -17,6 +17,7 @@ export default function toolRegistryExtension(pi: Pi.ExtensionAPI) {
17
17
  );
18
18
  }
19
19
  pi.on("session_start", async (_event, ctx) => runtime.onSessionStart(ctx));
20
+ pi.on("turn_end", (event, ctx) => runtime.onTurnEnd(event, ctx));
20
21
  pi.on("agent_settled", async (_event, ctx) => runtime.onAgentSettled(ctx));
21
22
  pi.on("context", async (event, ctx) => ({
22
23
  messages: runtime.onContext(event.messages, ctx) as typeof event.messages,
@@ -26,6 +26,7 @@ export interface ActorExtensionRuntime {
26
26
  discoverResources(metaUrl: string): { skillPaths: string[] } | undefined;
27
27
  getRunOwnerId(ctx: Pi.ExtensionContext): string;
28
28
  onAgentSettled(ctx: Pi.ExtensionContext): void;
29
+ onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
29
30
  onContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
30
31
  onSessionShutdown(reason: string, ctx: Pi.ExtensionContext): void;
31
32
  onSessionStart(ctx: Pi.ExtensionContext): Promise<void>;
@@ -146,6 +147,9 @@ export function createActorExtensionRuntime(
146
147
  if (!activeRunOwnerId || getRunOwnerId(ctx) !== activeRunOwnerId) return;
147
148
  if (!runUiRuntime.flushCompletionBatch(ctx)) automaticReview.schedule();
148
149
  },
150
+ onTurnEnd(event, ctx) {
151
+ return runUiRuntime.onTurnEnd(event, ctx);
152
+ },
149
153
  onContext(messages, ctx) {
150
154
  return runUiRuntime.projectContext(messages, ctx);
151
155
  },
@@ -7,11 +7,14 @@
7
7
  import type {
8
8
  ExtensionAPI,
9
9
  ExtensionContext,
10
+ CustomMessageEntryDraft,
11
+ TurnEndEvent,
12
+ TurnEndEventResult,
10
13
  } from "@earendil-works/pi-coding-agent";
11
14
 
12
15
  import * as SessionEvidence from "./session-evidence.ts";
13
16
 
14
- export type { ExtensionAPI, ExtensionContext };
17
+ export type { ExtensionAPI, ExtensionContext, TurnEndEvent, TurnEndEventResult };
15
18
 
16
19
  export const RUN_COMPLETION_BATCH_CUSTOM_TYPE = "pi-actors-run-batch";
17
20
  export const RUN_STEER_CUSTOM_TYPE = "pi-actors-run-steer";
@@ -44,12 +47,12 @@ export function createNotificationSink(
44
47
  };
45
48
  }
46
49
 
47
- export function sendRunCompletionBatch(
48
- pi: ExtensionAPI,
50
+ export function createRunCompletionBatchEntry(
49
51
  batchId: string,
50
52
  content: string,
51
- ): void {
52
- pi.sendMessage({
53
+ ): CustomMessageEntryDraft {
54
+ return {
55
+ type: "custom_message",
53
56
  customType: RUN_COMPLETION_BATCH_CUSTOM_TYPE,
54
57
  content,
55
58
  display: true,
@@ -59,10 +62,16 @@ export function sendRunCompletionBatch(
59
62
  kind: "completion_batch",
60
63
  },
61
64
  },
62
- }, {
63
- deliverAs: "followUp",
64
- triggerTurn: true,
65
- });
65
+ };
66
+ }
67
+
68
+ export function sendRunCompletionBatch(
69
+ pi: ExtensionAPI,
70
+ batchId: string,
71
+ content: string,
72
+ ): void {
73
+ const { type: _type, ...message } = createRunCompletionBatchEntry(batchId, content);
74
+ pi.sendMessage(message, { deliverAs: "followUp", triggerTurn: true });
66
75
  }
67
76
 
68
77
  export function sendRunSteer(
@@ -219,6 +228,21 @@ export function removeRunSteerFromContext(
219
228
  return messages.filter((message) => steerMessage(message)?.steerId !== steerId);
220
229
  }
221
230
 
231
+ export function inspectRunCompletionBatchSessionEvidence(
232
+ ctx: ExtensionContext,
233
+ input: { batchId: string; content: string },
234
+ ): SessionEvidence.ActiveSessionEntryEvidence {
235
+ return SessionEvidence.inspectBoundedActiveSessionEntries({
236
+ getEntry: (id) => ctx.sessionManager.getEntry(id),
237
+ leaf: ctx.sessionManager.getLeafEntry(),
238
+ match: (entry) => {
239
+ const batch = completionBatchMessage(entry);
240
+ if (!batch || batch.batchId !== input.batchId) return undefined;
241
+ return batch.content === input.content ? "present" : "conflict";
242
+ },
243
+ });
244
+ }
245
+
222
246
  export function inspectRunSteerSessionEvidence(
223
247
  ctx: ExtensionContext,
224
248
  input: { content: string; eventId: string; steerId: string },
@@ -15,6 +15,7 @@ import * as RunDeliveryLineage from "./run-delivery-lineage.ts";
15
15
  export interface RunUiRuntime {
16
16
  close(): void;
17
17
  flushCompletionBatch(ctx: Pi.ExtensionContext): boolean;
18
+ onTurnEnd(event: Pi.TurnEndEvent, ctx: Pi.ExtensionContext): Pi.TurnEndEventResult | undefined;
18
19
  projectContext(messages: unknown[], ctx: Pi.ExtensionContext): unknown[];
19
20
  shutdown(
20
21
  eventReason: string,
@@ -51,6 +52,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
51
52
  const deliveryObservation = Observability.createRunUiObservationState();
52
53
  const retirementAttempts = new Set<string>();
53
54
  const steerDiagnostics = new Set<string>();
55
+ const completionDiagnostics = new Set<string>();
54
56
 
55
57
  const close = (): void => {
56
58
  running = false;
@@ -72,6 +74,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
72
74
  deliveryTimeout = undefined;
73
75
  recoverQueuedSteers = false;
74
76
  steerDiagnostics.clear();
77
+ completionDiagnostics.clear();
75
78
  if (animationInterval) clearInterval(animationInterval);
76
79
  animationInterval = undefined;
77
80
  };
@@ -242,6 +245,53 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
242
245
  });
243
246
  return true;
244
247
  };
248
+ const acceptCompletionBatch = (ownerId: string, batchId: string): boolean => {
249
+ const input = { batchId, ownerId, tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir };
250
+ if (!RunDelivery.markRunCompletionBatchQueued(input)) return false;
251
+ if (!RunDelivery.markRunCompletionBatchPresented(input)) return false;
252
+ const accepted = journal(ownerId).completion_batch;
253
+ return accepted ? finishPresentedBatch(ownerId, accepted) : false;
254
+ };
255
+ const reconcileCompletionBatch = (ctx: Pi.ExtensionContext, ownerId: string): boolean => {
256
+ const batch = journal(ownerId).completion_batch;
257
+ if (!batch) return false;
258
+ if (batch.phase === "pending") {
259
+ // Returning boundary drafts is not acceptance: prove the exact entry before acknowledging it.
260
+ const evidence = Pi.inspectRunCompletionBatchSessionEvidence(ctx, {
261
+ batchId: batch.batch_id,
262
+ content: RunDelivery.formatRunCompletionBatchMessage(batch),
263
+ });
264
+ if (evidence.status === "absent") return false;
265
+ if (evidence.status !== "present") {
266
+ const key = `${batch.batch_id}:${evidence.status}:${evidence.reason ?? ""}`;
267
+ if (!completionDiagnostics.has(key)) {
268
+ completionDiagnostics.add(key);
269
+ ctx.ui.notify(
270
+ `Actor completion recovery is ${evidence.status}: ${evidence.reason ?? "conflicting session evidence"}. Batch ${batch.batch_id} remains pending.`,
271
+ "warning",
272
+ );
273
+ }
274
+ return true;
275
+ }
276
+ }
277
+ return !acceptCompletionBatch(ownerId, batch.batch_id);
278
+ };
279
+ const pendingCompletionBatch = (ownerId: string): RunDelivery.RunCompletionBatch | undefined => {
280
+ const existing = journal(ownerId).completion_batch;
281
+ if (existing) return existing;
282
+ const snapshot = Observability.readRunDeliverySnapshot(deliveryObservation, ownerId);
283
+ admitCompletionTransitions(ownerId, snapshot);
284
+ Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
285
+ return journal(ownerId).completion_batch;
286
+ };
287
+ const isCurrentOwner = (ctx: Pi.ExtensionContext): boolean => {
288
+ if (!running || !activeOwnerId) return false;
289
+ try {
290
+ return Pi.getSessionId(ctx) === activeOwnerId;
291
+ } catch {
292
+ return false;
293
+ }
294
+ };
245
295
  const isIdle = (ctx: Pi.ExtensionContext): boolean =>
246
296
  typeof ctx.isIdle !== "function" || ctx.isIdle();
247
297
  const isDeliveryRoot = (ownerId: string): boolean => {
@@ -422,48 +472,14 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
422
472
 
423
473
  flushCompletionBatch = (ctx: Pi.ExtensionContext): boolean => {
424
474
  const ownerId = activeOwnerId;
425
- if (!running || !ownerId) return false;
426
- try {
427
- if (Pi.getSessionId(ctx) !== ownerId) return false;
428
- } catch {
429
- return false;
430
- }
475
+ if (!ownerId || !isCurrentOwner(ctx)) return false;
431
476
  if (flushSteers(ctx, ownerId)) return true;
432
477
  if (!isDeliveryRoot(ownerId)) return false;
433
- let batch = journal(ownerId).completion_batch;
434
- if (!batch) {
435
- const snapshot = Observability.readRunDeliverySnapshot(
436
- deliveryObservation,
437
- ownerId,
438
- );
439
- admitCompletionTransitions(ownerId, snapshot);
440
- Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
441
- batch = journal(ownerId).completion_batch;
442
- }
478
+ if (reconcileCompletionBatch(ctx, ownerId)) return true;
479
+ const batch = pendingCompletionBatch(ownerId);
443
480
  if (!batch) return false;
444
- if (batch.phase === "presented") {
445
- finishPresentedBatch(ownerId, batch);
446
- const snapshot = Observability.readRunDeliverySnapshot(
447
- deliveryObservation,
448
- ownerId,
449
- );
450
- admitCompletionTransitions(ownerId, snapshot);
451
- Observability.pruneRunUiObservationState(deliveryObservation, snapshot);
452
- batch = journal(ownerId).completion_batch;
453
- if (!batch) return false;
454
- }
455
481
  if (!isIdle(ctx)) return true;
456
482
  const content = RunDelivery.formatRunCompletionBatchMessage(batch);
457
- if (batch.phase === "queued") {
458
- if (!RunDelivery.markRunCompletionBatchPresented({
459
- batchId: batch.batch_id,
460
- ownerId,
461
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
462
- })) return true;
463
- const accepted = journal(ownerId).completion_batch;
464
- if (accepted) finishPresentedBatch(ownerId, accepted);
465
- return flushCompletionBatch(ctx);
466
- }
467
483
  if (!isIdle(ctx)) return true;
468
484
  try {
469
485
  Pi.sendRunCompletionBatch(deps.pi, batch.batch_id, content);
@@ -476,22 +492,9 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
476
492
  });
477
493
  throw error;
478
494
  }
479
- if (!RunDelivery.markRunCompletionBatchQueued({
480
- batchId: batch.batch_id,
481
- ownerId,
482
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
483
- })) {
484
- throw new Error("Completion batch changed before queue acknowledgment");
485
- }
486
- if (!RunDelivery.markRunCompletionBatchPresented({
487
- batchId: batch.batch_id,
488
- ownerId,
489
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
490
- })) {
495
+ if (!acceptCompletionBatch(ownerId, batch.batch_id)) {
491
496
  throw new Error("Completion batch changed before durable acceptance");
492
497
  }
493
- const accepted = journal(ownerId).completion_batch;
494
- if (accepted) finishPresentedBatch(ownerId, accepted);
495
498
  ctx.ui.notify(
496
499
  `Actor completions ready: ${batch.members.length}`,
497
500
  "info",
@@ -502,14 +505,25 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
502
505
  return {
503
506
  close,
504
507
  flushCompletionBatch,
508
+ onTurnEnd(event, ctx) {
509
+ const ownerId = activeOwnerId;
510
+ if (
511
+ !ownerId || !isCurrentOwner(ctx) || !isDeliveryRoot(ownerId) ||
512
+ event.outcome !== "completed" || isIdle(ctx) || ctx.signal?.aborted ||
513
+ ctx.hasPendingMessages?.() || event.context.pendingMessages.length > 0
514
+ ) return undefined;
515
+ if (flushSteers(ctx, ownerId) || reconcileCompletionBatch(ctx, ownerId)) return undefined;
516
+ const batch = pendingCompletionBatch(ownerId);
517
+ if (!batch) return undefined;
518
+ const entry = Pi.createRunCompletionBatchEntry(
519
+ batch.batch_id,
520
+ RunDelivery.formatRunCompletionBatchMessage(batch),
521
+ );
522
+ return { entries: [...event.entries, entry], continue: true };
523
+ },
505
524
  projectContext(messages, ctx) {
506
525
  const ownerId = activeOwnerId;
507
- if (!running || !ownerId) return messages;
508
- try {
509
- if (Pi.getSessionId(ctx) !== ownerId) return messages;
510
- } catch {
511
- return messages;
512
- }
526
+ if (!ownerId || !isCurrentOwner(ctx)) return messages;
513
527
  const steerContext = Pi.dedupeRunSteerContext(messages);
514
528
  let contextMessages = steerContext.messages;
515
529
  for (const steer of journal(ownerId).steers) {
@@ -549,18 +563,7 @@ export function createRunUiRuntime(deps: RunUiRuntimeDeps): RunUiRuntime {
549
563
  batch.batch_id,
550
564
  );
551
565
  }
552
- if (!RunDelivery.markRunCompletionBatchQueued({
553
- batchId: batch.batch_id,
554
- ownerId,
555
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
556
- })) return projected.messages;
557
- if (!RunDelivery.markRunCompletionBatchPresented({
558
- batchId: batch.batch_id,
559
- ownerId,
560
- tempDir: Paths.EXTENSION_RUNTIME_PATHS.tempDir,
561
- })) return projected.messages;
562
- const presented = journal(ownerId).completion_batch;
563
- if (presented) finishPresentedBatch(ownerId, presented);
566
+ acceptCompletionBatch(ownerId, batch.batch_id);
564
567
  return projected.messages;
565
568
  },
566
569
  shutdown(eventReason, ownerId, ctx) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-actors",
3
- "version": "0.54.0",
3
+ "version": "0.55.0",
4
4
  "private": false,
5
5
  "description": "Local Actor Kernel for Pi",
6
6
  "keywords": [
@@ -107,7 +107,7 @@ Run = Recipe + Trace + Control
107
107
  ```
108
108
 
109
109
  1. Spawn with the exact logical Recipe identity and caller-owned values.
110
- 2. Retain the returned `run:<id>` and normally wait for the root coordinator's settled completion batch instead of polling. Nested actor completions accumulate under their containing top-level Run and arrive in that one tree-compressed batch; descendant sessions do not need notification options.
110
+ 2. Retain the returned `run:<id>` and normally wait for the root coordinator's completion batch instead of polling. While Pi is active, ready results arrive together at the next safe model-step boundary after the full tool batch; already-idle Pi wakes through a debounced follow-up. Nested actor completions wait for their complete containing top-level tree and arrive in that one compressed batch; descendant sessions do not need notification options.
111
111
  3. Inspect `view=trace` when retained observations or attention matter.
112
112
  4. Inspect `view=control` before diagnosing service readiness, stale work, or saturation.
113
113
  5. Send `message` only for an action declared and consumed by that controlled Recipe.
@@ -14,7 +14,7 @@ A rare Skill Recipe may declare `singleton: true`. Do not pass `as`: the runtime
14
14
 
15
15
  ## Observe
16
16
 
17
- Normally wait for the settled completion batch. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
17
+ Normally wait for the completion batch: ready trees reach an active root coordinator at the next safe model-step boundary after the full tool batch, or wake an already-idle coordinator through a debounced follow-up. Inspect only when requested, when meaningful attention arrives, or when the Run is overdue or blocked:
18
18
 
19
19
  ```text
20
20
  inspect target=run:<id> view=recipe
@@ -2,6 +2,10 @@
2
2
 
3
3
  > Each release keeps at most 8 outcome records of at most 512 characters.
4
4
 
5
+ ## 0.26.5: Telegram Mode Receipt Hotfix
6
+
7
+ - `Telegram mode controls`: Passive now refreshes the selected button on the first accepted click after Off, including existing-memory restoration and write-fence recovery during an active run. Accepted controls retain their renewed presentation receipt instead of being mistaken for canceled work; later mode choices, branch changes and shutdown still suppress obsolete updates. Memory storage and mode semantics are unchanged; no migration is required.
8
+
5
9
  ## 0.26.4: Passive Compaction Continuity
6
10
 
7
11
  - `Passive continuation`: After idle Stop, native compaction can replace the later user anchor with a summary. Passive now retains that summary, the available tool calls/results and later refinements instead of reducing requests to a frozen handoff. Reload/resume preserve the same continuation; discarded raw history is not reconstructed. Native compaction, memory scopes and stored state are unchanged; no migration or configuration change is required.
@@ -1214,11 +1214,11 @@ export default function stateFlowExtension(pi, options = {}) {
1214
1214
  });
1215
1215
  if (!modePersistenceError) {
1216
1216
  appendCheckpoint();
1217
- return { ok: true, message: `State Flow ${selectedMode}` };
1217
+ return { ok: true, message: `State Flow ${selectedMode}`, signal: sharedInspectionLifetime.signal };
1218
1218
  }
1219
1219
  const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
1220
1220
  notifyProblem(ctx, message, "warning");
1221
- return { ok: true, message };
1221
+ return { ok: true, message, signal: sharedInspectionLifetime.signal };
1222
1222
  };
1223
1223
  let accepted = false;
1224
1224
  try {
@@ -1291,10 +1291,11 @@ export default function stateFlowExtension(pi, options = {}) {
1291
1291
  const operation = selectMode(activeContext, mode);
1292
1292
  if (mode === "active")
1293
1293
  return operation;
1294
- // Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
1294
+ // Native-only outcomes use the selected lifetime; async acceptance may renew and return its own receipt.
1295
1295
  const signal = sharedInspectionLifetime.signal;
1296
1296
  try {
1297
- return { ...await operation, signal };
1297
+ const result = await operation;
1298
+ return { ...result, signal: result.signal ?? signal };
1298
1299
  }
1299
1300
  catch (error) {
1300
1301
  return { ok: false, message: diagnosticText(error), signal };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.26.4",
3
+ "version": "0.26.5",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
@@ -138,7 +138,7 @@ The optional adapter presents one Off | Passive | Active row and uses the same m
138
138
  - Inspection returns coherent state plus matching revisions, without publication.
139
139
  - Controls and inspections acknowledge callbacks before waiting, suppress revoked results and escape late failures in the current menu.
140
140
  - Synchronous mode-selection ports are supported.
141
- - A first Passive selection may install a read-only shared cache without invalidating its own success receipt. Later controls, branch changes and cancellation still revoke obsolete presentation.
141
+ - Passive acceptance returns its current presentation receipt after any internal restoration or lifetime renewal, including existing-memory reacquisition after Off and write-fence recovery. The adapter preserves that accepted receipt, so the first click refreshes the button in both busy and idle sessions. Pre-runtime shared-cache loading also retains its receipt. Later controls, branch changes and shutdown still revoke obsolete presentation, including a change away and back to the same mode while callback acknowledgement waits.
142
142
 
143
143
  Adapter tests establish these contracts with isolated transport fixtures; they are not a live Telegram smoke test. Missing or unready transport remains fail-open and cannot change core memory behavior.
144
144
 
@@ -211,6 +211,7 @@ When `pi-telegram` is available, its main-menu section shows `State Flow: active
211
211
 
212
212
  - A horizontal radio row presents `Off | Passive | Active`. The selected option uses 🟣, 🟡 or 🟢 respectively; each inactive option uses ⚫️.
213
213
  - Telegram Active requested during a run waits for settlement; Passive and Off apply immediately.
214
+ - The first accepted Passive click refreshes the selected button after memory restoration or write-fence recovery, including while the agent is busy. A later mode or branch selection still suppresses an obsolete menu update.
214
215
  - Ordinary successful controls add no redundant mode receipt; diagnostic outcomes stay visible.
215
216
  - Off exposes neither tool nor State Flow model context.
216
217
 
@@ -1200,11 +1200,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1200
1200
  });
1201
1201
  if (!modePersistenceError) {
1202
1202
  appendCheckpoint();
1203
- return { ok: true, message: `State Flow ${selectedMode}` };
1203
+ return { ok: true, message: `State Flow ${selectedMode}`, signal: sharedInspectionLifetime.signal };
1204
1204
  }
1205
1205
  const message = conciseDiagnostic(`State Flow ${selectedMode}; memory writes paused: ${modePersistenceError}`);
1206
1206
  notifyProblem(ctx, message, "warning");
1207
- return { ok: true, message };
1207
+ return { ok: true, message, signal: sharedInspectionLifetime.signal };
1208
1208
  };
1209
1209
  let accepted = false;
1210
1210
  try {
@@ -1272,10 +1272,11 @@ export default function stateFlowExtension(pi: ExtensionAPI, options: StateFlowE
1272
1272
  if (!activeContext) throw new Error("State Flow is not attached to an active session yet");
1273
1273
  const operation = selectMode(activeContext, mode);
1274
1274
  if (mode === "active") return operation;
1275
- // Inactive choices revoke older presentation at once; the receipt belongs to the new lifetime.
1275
+ // Native-only outcomes use the selected lifetime; async acceptance may renew and return its own receipt.
1276
1276
  const signal = sharedInspectionLifetime.signal;
1277
1277
  try {
1278
- return { ...await operation, signal };
1278
+ const result = await operation;
1279
+ return { ...result, signal: result.signal ?? signal };
1279
1280
  } catch (error) {
1280
1281
  return { ok: false, message: diagnosticText(error), signal };
1281
1282
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-state-flow",
3
- "version": "0.26.4",
3
+ "version": "0.26.5",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "description": "Incremental scoped state/context/memory compiler for Pi, inspired by SKILL.state",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.35.5",
3
+ "version": "0.36.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -44,12 +44,12 @@
44
44
  "banner.jpg"
45
45
  ],
46
46
  "dependencies": {
47
- "@llblab/pi-actors": "0.54.0",
47
+ "@llblab/pi-actors": "0.55.0",
48
48
  "@llblab/pi-claude-usage": "0.2.2",
49
49
  "@llblab/pi-clean-room": "0.3.0",
50
50
  "@llblab/pi-codex-usage": "0.13.1",
51
51
  "@llblab/pi-grow-loop": "0.9.0",
52
- "@llblab/pi-state-flow": "0.26.4",
52
+ "@llblab/pi-state-flow": "0.26.5",
53
53
  "@llblab/pi-telegram": "0.57.3",
54
54
  "@llblab/skills": "1.15.0"
55
55
  },