@aeondave/pi-persona 1.17.3 → 1.17.5

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 (93) hide show
  1. package/AGENTS.md +57 -36
  2. package/CHANGELOG.md +106 -1
  3. package/README.md +32 -16
  4. package/docs/ARCHITECTURE.md +199 -48
  5. package/docs/COLLABORATION.md +110 -0
  6. package/docs/EXPERIENCE.md +33 -8
  7. package/docs/README.md +12 -0
  8. package/docs/REFERENCE.md +65 -14
  9. package/docs/STRATEGIES.md +6 -0
  10. package/package.json +12 -6
  11. package/src/agents/agent.ts +6 -0
  12. package/src/bridge.ts +33 -23
  13. package/src/bus/broker/client.ts +151 -30
  14. package/src/bus/broker/host.ts +130 -11
  15. package/src/bus/broker/messages.ts +35 -5
  16. package/src/bus/contact-transport.ts +8 -0
  17. package/src/bus/contact.ts +118 -40
  18. package/src/bus/peers.ts +23 -6
  19. package/src/bus/result-reader.ts +88 -0
  20. package/src/core/brief.ts +14 -4
  21. package/src/core/capabilities.ts +1 -1
  22. package/src/core/companion.ts +64 -0
  23. package/src/core/leg-thinking.ts +57 -0
  24. package/src/core/ownership.ts +1 -2
  25. package/src/core/pi-compat.ts +5 -12
  26. package/src/core/session-runs.ts +1 -1
  27. package/src/core/types.ts +1 -1
  28. package/src/engine/adapter.ts +100 -20
  29. package/src/engine/async.ts +53 -11
  30. package/src/engine/child.ts +11 -2
  31. package/src/engine/inproc.ts +98 -47
  32. package/src/engine/result-grant.ts +62 -0
  33. package/src/engine/session-tools.ts +6 -0
  34. package/src/engine/stream.ts +6 -0
  35. package/src/engine/worktree.ts +17 -7
  36. package/src/exocom/collaboration-delivery.ts +29 -0
  37. package/src/exocom/collaboration.ts +148 -0
  38. package/src/exocom/envelope.ts +43 -4
  39. package/src/exocom/gate.ts +7 -2
  40. package/src/exocom/install.ts +45 -17
  41. package/src/exocom/ledger.ts +58 -8
  42. package/src/exocom/plane.ts +32 -1
  43. package/src/exocom/profile.ts +40 -0
  44. package/src/exocom/registry.ts +5 -0
  45. package/src/exocom/status.ts +11 -1
  46. package/src/exocom/write-guard.ts +7 -0
  47. package/src/extension/broker-host.ts +61 -9
  48. package/src/extension/companion.ts +38 -0
  49. package/src/extension/engine.ts +40 -29
  50. package/src/extension/hooks.ts +954 -931
  51. package/src/extension/identity.ts +41 -75
  52. package/src/extension/memory-owner.ts +54 -0
  53. package/src/extension/mind-worker.ts +129 -0
  54. package/src/extension/results.ts +138 -0
  55. package/src/extension/shared.ts +17 -0
  56. package/src/extension.ts +2922 -2849
  57. package/src/loader.ts +11 -3
  58. package/src/orchestration/agent-attempt.ts +15 -0
  59. package/src/orchestration/flow-journal.ts +7 -1
  60. package/src/orchestration/flow-run.ts +125 -19
  61. package/src/orchestration/flow.ts +11 -0
  62. package/src/orchestration/roster.ts +71 -23
  63. package/src/orchestration/sdk.ts +61 -19
  64. package/src/orchestration/types.ts +10 -0
  65. package/src/persona/model-participants.ts +20 -7
  66. package/src/persona/orchestrate.ts +6 -7
  67. package/src/persona/persona.ts +1 -1
  68. package/src/results/archive.ts +281 -0
  69. package/src/results/record.ts +166 -0
  70. package/src/results/retrieval.ts +142 -0
  71. package/src/results/types.ts +128 -0
  72. package/src/telemetry/contract.ts +1 -1
  73. package/src/tools/council.ts +40 -23
  74. package/src/tools/delegate-tool.ts +50 -14
  75. package/src/tools/delegate.ts +13 -6
  76. package/src/tools/exocom-collaboration-input.ts +168 -0
  77. package/src/tools/exocom-collaboration.ts +315 -0
  78. package/src/tools/exocom-profile.ts +102 -0
  79. package/src/tools/exocom.ts +20 -3
  80. package/src/tools/flow.ts +24 -8
  81. package/src/tools/intercom-tool.ts +98 -14
  82. package/src/tools/models.ts +15 -0
  83. package/src/tools/monitor.ts +20 -1
  84. package/src/tools/read-result.ts +52 -0
  85. package/src/tools/timer.ts +23 -2
  86. package/src/ui/activity-rows.ts +34 -0
  87. package/src/ui/agent-overlay.ts +23 -13
  88. package/src/ui/agent-tree.ts +67 -6
  89. package/src/ui/literal-tool-result.ts +1 -1
  90. package/src/ui/model-picker.ts +1 -1
  91. package/src/ui/orchestration-launch.ts +49 -0
  92. package/src/ui/session-cards.ts +191 -14
  93. package/src/ui/strategy-leg.ts +40 -0
@@ -81,9 +81,10 @@ These are the guardrails a contributor must not violate. They are enforced in co
81
81
  by the SDK on every `agent()` call and by the engine per child. Concurrency is shared across all
82
82
  calls in one SDK instance; token admission uses completed usage and is rechecked after queueing, so
83
83
  active legs can overshoot it. Nesting depth is not one of these numeric knobs — it is structural,
84
- capped at 1: a child's whole pi-persona extension activation short-circuits under
85
- `PI_PERSONA_DISABLE=1` (the **fork-bomb guard**), so it registers NO tools at all —
86
- `delegate`/`council`/`orchestrate`/`flow` included. On the child engine this is a one-shot env var
84
+ capped at 1: full pi-persona activation short-circuits under `PI_PERSONA_DISABLE=1` (the
85
+ **fork-bomb guard**), so no supervisor orchestration tools (`delegate`/`council`/`flow`) are
86
+ registered. A broker-enabled child instead loads the minimal bridge, which registers only
87
+ granted communication and scoped result-reader tools. On the child engine this is a one-shot env var
87
88
  set on each spawned process; the in-process engine instead shares one Node process across
88
89
  concurrently-building sub-sessions, so it ref-counts the guard (`inproc.ts`) to keep it set until the
89
90
  LAST one finishes. The in-process engine also excludes `ORCHESTRATION_TOOLS` from the child session
@@ -151,6 +152,12 @@ These are the guardrails a contributor must not violate. They are enforced in co
151
152
 
152
153
  ## Module layout
153
154
 
155
+ The supported host is the latest stable Pi (currently 1.4.0). Runtime code uses its public APIs
156
+ directly; obsolete host facades, ignored SDK options and compatibility-only aliases are removed
157
+ with their callers. Genuine host failures remain explicit rather than fabricating session identity
158
+ or model availability. One-way user-data migrations and retained-history readers are separate data
159
+ contracts: they remain only while serving stored user data, not to support an older Pi runtime.
160
+
154
161
  Domain dependencies remain downward-only and acyclic: `core ← all`; `engine`/`bus → core`;
155
162
  `orchestration → engine + bus + core`; `persona → orchestration + core`; the Exocom transport and
156
163
  ledger → `core + bus`; `tools`/`ui → lower layers`. `src/exocom/install.ts` is deliberately the
@@ -281,10 +288,18 @@ up to 16 names. Same idea, two layers, two caps — see docs/REFERENCE.md's exam
281
288
 
282
289
  `isolation: worktree` is fail-closed. It requires a clean Git checkout so the detached `HEAD` view
283
290
  cannot silently omit staged/unstaged/untracked supervisor work; a non-repository, dirty checkout,
284
- worktree creation failure, missing diff artifact, or an artifact over the return limit fails the leg —
285
- the base engine is never invoked against the real tree as a fallback. The diff returns to the
286
- supervisor before the temporary tree is removed, and every git call is asynchronous so the extension
287
- host keeps rendering, ticking timers, and serving broker sockets while a large checkout runs.
291
+ worktree creation failure, or a successful leg with neither a captured diff nor a returned unified
292
+ patch fails the leg — the base engine is never invoked against the real tree as a fallback. The
293
+ created worktree starts from the committed `HEAD` only: local ignored files are not copied into it,
294
+ and untracked ignored outputs are not exported. The original checkout's ignored files remain
295
+ untouched; tasks that need local-only inputs must recreate or provide them explicitly. The
296
+ worktree's initial commit is pinned before the worker starts. Export compares the final file state
297
+ against that immutable commit, so worker commits, later unstaged changes and new files are included
298
+ together. The diff is captured before the temporary tree is removed, then returned to the supervisor;
299
+ commit history is not applied to the main checkout. If Git cannot capture the diff but the worker
300
+ returned a unified patch, that patch is retained with a warning that it was worker-provided and not
301
+ verified against the worktree state. Every git call is asynchronous so the extension host keeps
302
+ rendering, ticking timers, and serving broker sockets while a large checkout runs.
288
303
 
289
304
  Transient retries inside one agent session belong to the host **Pi runtime**'s `retry.*` settings, not
290
305
  a persona contract — pi-persona neither parses retry notation nor schedules its own backoff. Both
@@ -307,10 +322,19 @@ models, repeated same-model attempts, stop/timeout/contract/agent/infrastructure
307
322
  child/token budgets do not recover. Both attempts are accounted, and a warning plus result metadata
308
323
  identify the switch. A per-role model assignment can supplement the legacy per-agent assignment;
309
324
  preflight model selection includes external arbiters and other declared agent-valued parameters.
310
- Live tree and telemetry labels use those participant identities for auxiliary actors too, with
311
- inline model pins taking precedence. If distinct full roles share a short display hint, the display
312
- keeps the hint (with `#N` suffixes for repeated node keys) but omits the uncertain model label rather
313
- than borrowing another role's choice. Routing still resolves the full role key, not the display hint.
325
+ Live tree and telemetry labels resolve auxiliary actors and every repeated roster occurrence from
326
+ the SDK's exact, immutable attempt identity (`agent`, full `role`, explicit `model` and `thinking`),
327
+ alongside its unique node key. Short role hints and `#N` suffixes are presentation only: colliding
328
+ hints never discard a known model or borrow another occurrence's inline pins. Model assignments
329
+ and agent defaults are copied after setup when the engine is constructed, so a queued leg's display
330
+ cannot change with later persona/settings edits. Seeds use each actual roster member; lifecycle
331
+ updates use the actual attempt, including custom strategy calls and auxiliary actors in any order.
332
+ A recovery keeps the node key, changes its requested model, and clears the preceding attempt's Pi
333
+ effort observation. Terminal `modelUsed` and validated effort observations override intent only when
334
+ reported; an absent observation retains the current attempt's intent. Initial effort remains the
335
+ explicit spec request, never inferred provider cognition. Clearing an unknown model emits an empty
336
+ `model` string in a telemetry update, preserving the existing string patch contract without leaving
337
+ a stale previous model in consumers.
314
338
 
315
339
  **Model assignment repair is configuration, not execution recovery.** `/persona models [profile]`
316
340
  is an explicit user edit in the Pi adapter: it reuses `resolveCouncilInvocation`,
@@ -487,8 +511,9 @@ never simultaneously a telemetry event, a routed message, and a UI source of tru
487
511
  usage`). Feeds derived UI; never routed as an agent message.
488
512
  - **Bus Msg** — semantic agent↔agent communication (`progress · decision · interview`, and sibling
489
513
  peer sends). `src/bus`. `progress_update` is a Bus message only, not an EngineEvent.
490
- - **ProgressView** — a derived, read-only UI view computed *from* EngineEvents. Never authored, never
491
- a source of truth. The `f9` agent tree and `peek` digest are ProgressViews.
514
+ - **ProgressView** — a derived, read-only UI view computed from observed runtime lifecycles:
515
+ EngineEvents, wake-source lifecycles, or the bounded local companion lifecycle below. Never a
516
+ source of truth. The `f9` agent tree and `peek` digest are ProgressViews.
492
517
 
493
518
  Steering is a supervisor-to-child control action, distinct from semantic Bus messages; requesting
494
519
  steering does not establish that a blocking headless delegate call has been interrupted. The peek digest is
@@ -498,15 +523,83 @@ External telemetry is not a fourth communication plane: it is a generic observer
498
523
  future plugins may consume or produce, not an exclusive pi-persona protocol. It observes projected
499
524
  events and cannot route, reply, steer, or otherwise control agents.
500
525
 
526
+ ### Loaded companions and local activity
527
+
528
+ The local EventBus request `pi-persona:companion:v1` accepts `{ version: 1, respond }` and calls
529
+ `respond` synchronously with `{ version: 1, provider: "pi-persona", role, capabilities }`.
530
+ `role` is `supervisor`, `leg`, or `disabled`; `capabilities.memory` is `1` for a loaded supervisor or
531
+ delegated leg and `false` for a disabled supervisor. `capabilities.activity` is `1` only for an enabled
532
+ supervisor. Persona registers this responder before its bridge and disable guards. Pi owns the
533
+ subscription lifetime and revokes it on disposal/reload. Companions query the loaded extension;
534
+ settings files, package installation, and environment flags cannot establish its presence. This is
535
+ a cooperative local extension contract, not authentication or an OS permission boundary. The
536
+ contract is available from Persona 1.17.5, paired with Mind 0.8.4 or later; qualify both together.
537
+
538
+ Memory ownership is queried through `pi-persona:memory-owner:v1` with
539
+ `{ version: 1, sessionId, respond }`. Its synchronous response is
540
+ `{ version: 1, provider: "pi-persona", sessionId, ready, persona }`, where `persona` is the actual
541
+ live selection or `null` for Persona off. The native manager/id/file must still match, and `ready`
542
+ remains false until Persona's session startup finishes. `pi-persona:memory-owner-changed:v1`
543
+ broadcasts that same descriptor on readiness, selection and shutdown. A companion loaded first
544
+ must not wait inside its startup hook for a later extension's hook; it can activate on the readiness
545
+ broadcast or its next fresh query. Canonical settings retain startup preferences, not live ownership:
546
+ `persist: false` selections still change this descriptor. Unavailable ownership never authorizes a
547
+ fallback to another persona's saved selection.
548
+
549
+ Each engine freezes the parent's memory persona when it is built. In-process children receive a
550
+ private ResourceLoader factory and their own native manager/EventBus responder; child processes
551
+ receive the versioned `PI_PERSONA_MEMORY_PERSONA` record only in that spawn's environment. Inherited
552
+ values are scrubbed before spawning. Neither route mutates a shared process-wide persona value.
553
+ Delegated legs expose this frozen owner while retaining their lean memory-only role, including an
554
+ explicit `null` owner when the parent had Persona off.
555
+
556
+ `pi-persona:activity:v1` contributes operator-only local-work rows through a closed record:
557
+ `{ version: 1, producerId: "pi-persona-mind", activityId, sessionId, phase, state, target }`.
558
+ `phase` is `inspect|stage|apply`, `state` is `running|staged|done|failed`, and `target` is
559
+ `project|persona|shared`; `staged` is valid only for `stage`. No memory text, paths, models, token
560
+ counts, or free-form output are accepted. Ingress matches the active native session and checks its
561
+ manager's current id/file. The producer also captures its native owner and scope across awaits.
562
+ The resulting `Mind` row uses `kind: "activity"`: it has no agent count, steer/stop handle, wake,
563
+ stall badge, or `agent.*` telemetry. The F9 drill-in explains the local phase. Up to eight rows are
564
+ retained, with completed/staged rows evicted before live work; teardown and explicit persona
565
+ selection revoke old rows without cancelling workers. A staged row states that a proposal was
566
+ prepared and requires explicit apply; it does not claim that the plan remains valid indefinitely or
567
+ that model review is running.
568
+ Receipts remain in Mind's session-native cards; local activity events are not replayed on restart.
569
+
570
+ Mind may request one real async maintenance worker through `pi-persona:mind-worker:v1` with
571
+ the closed record `{ version: 1, sessionId, persona, task, respond }` (task at most 48,000 characters).
572
+ Persona checks the current native session and ready memory owner, actual persona, delegation and
573
+ memory permissions, the persona's raw delegate target rules for `mind`, and idle/no-pending-message
574
+ state. A strict target allowlist must include `mind`; explicit memory/delegate/target denies win.
575
+ No installed agent is required. Accepted
576
+ responses are synchronous `{ ok: true, runId, model, thinking, completion, cancel }`; rejection is
577
+ `{ ok: false, reason }`. `completion` resolves with `{ ok, output, error?, resultRef?, model?, thinking? }`.
578
+ Output remains untrusted data for Mind's bounded proposal parser, never executable code.
579
+
580
+ This is an ordinary tracked `Mind` sub-agent in the tree and F9, sharing the launch pool, usage,
581
+ progress, stop/steer controls and terminal-result archive. Its private definition has an explicit empty
582
+ tool allowlist and no worker spine. Provider/model and thinking are pinned to the accepting session;
583
+ provider fallback is disabled. The worker has a 180-second cancellation ceiling; owner readiness or
584
+ selection changes cancel it. Mind can also cancel through its accepted handle. Settlement returns
585
+ to the requesting companion and retains operator failure reporting, but never wakes a supervisor
586
+ model turn. Native session teardown cancels it through the ordinary stop registry. Local inspect,
587
+ stage and apply operations continue to use activity rows; model work uses the real agent view.
588
+
501
589
  ## The comm plane in practice
502
590
 
503
591
  - **In-process bus** (`bus/inproc.ts`) — a handle-based mailbox: `send` (one-way), `ask` (blocks for a
504
- reply), `reply`, `onMessage`. `contact_supervisor` (child→supervisor, gated by `coaching: on`) and
592
+ reply), `reply`, `onMessage`. `contact_supervisor` (child→supervisor, gated by `canUseBus` and,
593
+ for in-process children, `coaching: on`) and
505
594
  `contact_peer` (sibling→sibling) are the child-side tools bound onto it. Each unread inbox holds at
506
595
  most 200 messages; overflow evicts an old one-way note first, and an inbox full of live asks rejects
507
596
  new delivery rather than stranding a sender. Ask settlement (reply, timeout, abort, departure, or
508
597
  failed delivery) releases its timer/listener and unread entry; a previously retained question stays
509
598
  readable with `expectsReply: false`, so old questions never advertise an active reply id.
599
+ Tool outcomes distinguish `answered` from unavailable, timed-out, cancelled and failed requests;
600
+ delivery evidence is never approval. Headless questions remain nonblocking and unresolved, allowing
601
+ only independent already-authorized work or a blocked/partial handoff. Pre-aborted calls perform no
602
+ bus action, and replies racing cancellation are not reported as answers.
510
603
  - **Sibling peer comm** — a strategy opts a run in via `AgentRunSpec.peers` (gated by `canUseBus`). The
511
604
  child gets `contact_peer` (`list`/`send`, ONE-WAY so peers can never deadlock; per-engine-instance
512
605
  scoping; a 20-send budget and an 8,000-character body limit, enforced before consuming a send). The
@@ -524,11 +617,19 @@ events and cannot route, reply, steer, or otherwise control agents.
524
617
  notifier, f9, peek) is unchanged by construction. Off ⇒ the host never starts and the child spawns
525
618
  byte-identical to pre-broker pi-persona. Ask failures echo the originating `msgId` so the client
526
619
  rejects the right request immediately, and a client cancellation propagates only to that connection's
527
- own host ask. `extension/broker-host.ts`'s `SupervisorBroker` owns the lifecycle: it starts the host
528
- lazily on the first child-engine build, warns once on a failed bind (children built while it's down
529
- spawn without a bus endpoint, so they never burn connect backoff against a dead socket), and the next
530
- build retries; `/doctor` surfaces its state on one line. A dropped client connection flips the
620
+ own host ask. A one-way send gets a UUID-correlated host receipt; `delivered: true` means admitted
621
+ to the bus, not read or acted on by the peer. Each child is limited to 64 pending receipts with a
622
+ maximum 10-second wait. Abort, disconnect, a full or missing inbox, or a lost receipt reports
623
+ `delivered: false`. It never waits for a recipient answer or approval and does not replay.
624
+ `extension/broker-host.ts`'s `SupervisorBroker` owns the lifecycle: it starts
625
+ the host lazily on the first child-engine build and warns once on a failed bind. Children built while
626
+ it's down spawn without a bus endpoint, so they never burn connect backoff against a dead socket;
627
+ the next build retries; `/doctor` surfaces its state on one line. A dropped client connection flips the
531
628
  child-side bridge to `⇄ offline` and fails a fresh ask fast rather than hanging.
629
+ A connected child exposes `contact_supervisor` when `canUseBus` is allowed; `contact_peer` also
630
+ requires the run's peer opt-in. Granted custom tool names join a nonempty plain allowlist so Pi's
631
+ host filter does not hide them. Explicit `tools: []` and exclusions remain restrictive. A denied
632
+ semantic bus does not remove supervisor runtime steering or an independently scoped result reader.
532
633
 
533
634
  ### Presentation is a projection, not another comm plane
534
635
 
@@ -627,6 +728,30 @@ for the supervisor and for explicit retrieval; the default TUI projection is del
627
728
  unreadable versions. Session-file deletion owns log deletion; ordinary shutdown saves diagnostics,
628
729
  not deletes them. Operator-only expandable log entries do not create model context or extra wakes.
629
730
 
731
+ **Terminal result archive** (`src/results/`, `src/extension/results.ts`) applies the same
732
+ ownership model to settled agent work: every terminal delegate/council/flow outcome (and each final
733
+ SDK leg result, after any model recovery) is projected — once, idempotently by immutable origin —
734
+ into a versioned session entry with an opaque `result:<UUID>` reference, independent from runtime
735
+ run ids. Budgets: 128 entries, 8 MiB cumulative serialized payload per owning session file, 512 KiB
736
+ per record; bodies cap at 262,144 UTF-16 units with honest truncation metadata. Unknown, malformed
737
+ and duplicate history still consumes quota; unaccountable history disables new persistence instead
738
+ of resetting it. A fork/copy is a new owning file charged only for the entries it actually carries.
739
+ `storage: "session-entry"` means the host accepted the entry — never an fsync guarantee; memory-only
740
+ retention is labelled as such. Retrieval is read-only: `intercom results` lists, `intercom result
741
+ { ref, offset, limit }` pages (8,000 default / 16,000 max units, surrogate-safe), with no respawn,
742
+ rebilling, collection or extra wake; reading a page never returns the whole run hidden in metadata.
743
+ A saved result is not a live worker: the restore notice says runtime handles are gone while retained
744
+ reports remain readable. Archive listeners belong to the extension factory/tracker lifetime and are
745
+ installed exactly once. `session_shutdown` revokes that session's archive owner and clears its
746
+ captures. A later activation creates a fresh owner and capture context before the ordinary completion
747
+ listener observes terminal results for that activation; a late callback bound to the old owner cannot
748
+ append into the new session.
749
+
750
+ Flow reference handoff needs durable session-entry storage when a phase's result feeds a dependent
751
+ phase. In a no-session run, a source phase can finish with a memory-only receipt, but the dependent's
752
+ reader preflight rejects it before that dependent is spawned. Use a persisted session for reference
753
+ handoff across phases, or use inline handoff when no durable reader is available.
754
+
630
755
  This is a UI invariant only: truncating a collapsed card must never be confused with truncating the
631
756
  underlying result or changing a strategy's contract.
632
757
 
@@ -640,17 +765,22 @@ and message peer-to-peer, with no parent/child relationship. (intercom = interna
640
765
  external comm.)
641
766
 
642
767
  Exocom supplies fenced one-way postcards and presence; chat `message` never mutates shared work
643
- state. Separately, a workspace work ledger (`ledger.ts`, tools in `tools/exocom-work.ts`) records
644
- claims/asks/answers/progress/releases. `exocom_ask` canonicalizes its target through
645
- `plane.resolvePeer` (raw session ids on the ledger, never the display name); registry display name is
646
- keyed by `from_session`, never the envelope's self-reported `from_name`. A pending ask constrains the
647
- turn (`gate.ts`: answer/decline plus read-only tools) behind a bounded `pendingAskPrompt`.
648
- `exocom_wait` is non-blocking. Clean shutdown releases this session's claims/asks; if that best-effort
649
- write fails, the vanished owner is pruned on the next ledger transaction (live registry sessions are
650
- the lease), and `expireWaiters` wakes any waiter left behind instead of leaving `exocom_wait` armed
651
- forever. This gate is cooperative coordination for participating local Pi processes — not filesystem
652
- authorization, not isolation from another same-user process, and not a delegate/council or task/run
653
- replacement.
768
+ state. `src/exocom/ledger.ts` stores claims, questions, answers and progress; `src/exocom/collaboration.ts`
769
+ adds voluntary tasks, optional offers and explicit evidence excerpts. Tool adapters live in
770
+ `src/tools/exocom.ts`, `src/tools/exocom-collaboration.ts`, and `src/tools/exocom-profile.ts`.
771
+ `exocom_ask` resolves its target through the live registry to a raw session id, never a display name.
772
+ Attribution is resolved by `from_session`, never the envelope's self-reported `from_name`.
773
+
774
+ A pending ask gates the participant to `exocom_answer`/`exocom_decline`, `exocom_name` identity
775
+ metadata, `exocom_status`, explicit collaboration reads (`exocom_task` list/get, `exocom_publish`
776
+ read, `exocom_profile` get), and read-only filesystem tools. Profile `set`/`clear` and all other
777
+ mutations remain unavailable until the ask is settled. Naming does not settle an ask. The bounded
778
+ `pendingAskPrompt` explains the outstanding obligation. `exocom_wait` is non-blocking. Clean shutdown
779
+ releases this session's claims/asks; if that best-effort write fails, the vanished owner is pruned on
780
+ the next ledger transaction (live registry sessions are the lease), and `expireWaiters` wakes any
781
+ waiter left behind instead of leaving `exocom_wait` armed forever. This gate is cooperative
782
+ coordination for participating local Pi processes — not filesystem authorization, not isolation from
783
+ another same-user process, and not a delegate/council or task/run replacement.
654
784
 
655
785
  - **Opt-in, OFF by default.** `PI_PERSONA_EXOCOM=1` or bare `--exocom` joins the current workspace;
656
786
  `--exocom=Ab0T` joins an existing scope by its exact, case-sensitive four-character Base62 alias
@@ -677,18 +807,31 @@ replacement.
677
807
  standing brief, and `/exocom` label a peer as same-workspace or external without exposing its
678
808
  absolute `cwd`. External peers are full Pi instances that can inspect files in their own workspace;
679
809
  paths are never implicitly shared.
680
- - **Postcards plus a durable, non-blocking join.** `exocom_send` returns a `msg_id` immediately; a
681
- reply is another `exocom_send` with `in_reply_to` set. `target: "*"` broadcasts postcards
682
- best-effort (one unreachable peer doesn't fail the rest). Work coordination is separate:
683
- `exocom_ask` commits an obligation to the ledger, `answer`/`decline` settles it, and `exocom_wait`
684
- arms a bounded idle wake rather than blocking. The signed frame is a wake signal; the ledger remains
685
- the source of truth if delivery is deferred.
810
+ - **Postcards, asks and offers have different obligations.** `exocom_send` returns a `msg_id`
811
+ immediately; a reply is another `exocom_send` with `in_reply_to` set. `target: "*"` broadcasts
812
+ postcards best-effort. `exocom_ask` commits a targeted question that must be answered or declined;
813
+ `exocom_wait` arms a bounded idle wake rather than blocking. `exocom_offer` is an optional,
814
+ expiring invitation to consider an open task, targeted to one peer or the pool; it creates no ask,
815
+ no answer obligation and no work authorization. Its default lifetime is one hour, capped at 24 hours.
816
+ Signed immediate-wake frames are signals; the ledger remains authoritative when delivery is deferred.
817
+ - **Task generations and atomic pickup.** `exocom_task create` records a voluntary objective and
818
+ declared write set; creation neither launches work nor claims paths. The task's immutable
819
+ `task_id` is its creation frame's `msg_id`; later take/progress/finish, offer and publish actions
820
+ carry both `work_key` and `task_id`, so reusing a key cannot attach a delayed action to a newer
821
+ task. A write-set task's pickup and overlapping write claim commit together under the ledger lock;
822
+ competing overlapping pickups are rejected. Read-only tasks can be taken by a peer in another
823
+ workspace scope. A task whose owning registry lease disappears becomes stale and cannot be taken
824
+ over automatically. `exocom_task list` is paginated; `get` returns the complete bounded task detail,
825
+ including the exact write set. `exocom_publish` attaches an explicitly authored bounded excerpt to
826
+ one task generation; discovery pages titles and message ids, and the full excerpt is read by id.
827
+ These excerpts do not grant access to a peer's terminal-result archive.
686
828
  - **Cross-workspace writes are advisory-only in v1.** A repository-relative claim path has meaning
687
829
  only in the workspace that owns the selected scope, so a member whose home workspace differs from
688
- the scope cannot `exocom_claim` (the tool and the signed-frame receiver both reject a foreign
689
- claim) — it can still send, inspect its own files, ask, answer/decline, wait, and release its own
690
- asks. Multi-repository write coordination would need a versioned resource namespace; paths from
691
- unrelated repositories are never compared as if they named the same files.
830
+ the scope cannot `exocom_claim` or create/pick up a write-set task (the tool and signed-frame
831
+ transaction both enforce the scope). It can still read shared tasks/evidence, take read-only tasks,
832
+ send postcards, ask, answer/decline, and release its own asks. Multi-repository write coordination
833
+ would need a versioned resource namespace; paths from unrelated repositories are never compared as
834
+ if they named the same files.
692
835
  - **Reply routing is session-stable.** `exocom_list` shows human display names (`name`/`name#2`);
693
836
  sends and inbound reply hints use `name@<96-bit session hash>`, so a retained qualified target
694
837
  stays valid after a rename. Ambiguous suffix matches fail closed rather than guess. The
@@ -727,15 +870,23 @@ replacement.
727
870
  double-trigger a turn. An exchange's LENGTH is instead bounded prompt-side: the per-turn peer brief
728
871
  (`core/brief.ts`) carries a relevance bound — send only what changes what someone does — deliberately
729
872
  not a round count, since a counter can't see whether a round still serves the work.
730
- - **Tools are lazy and fail closed.** `exocom_list({ offset?, limit? })` exposes bounded, paginated
731
- presence; `exocom_send({ target, message, in_reply_to? })` sends one-way (a session-qualified-looking
732
- target resolves only as one, never falling back to a display name, since names are self-chosen);
733
- `exocom_name({ name })` rebrands the display call-sign without moving any state. The ledger tools —
734
- `exocom_claim`, `exocom_ask`, `exocom_answer`, `exocom_decline`, `exocom_wait`, `exocom_progress`,
735
- `exocom_release` — are capability-gated with the postcard tools; the live accessor, capability gate,
736
- and active-tool set all deny them once the plane stops, since Pi has no dynamic tool-unregister API.
737
- Admission requires at least one obligation closer, because every published peer may receive a
738
- durable ask and must retain a way to settle it.
873
+ - **Fifteen tools are lazy and fail closed.** `EXOCOM_TOOL_NAMES` lists `exocom_list`, `exocom_send`,
874
+ `exocom_name`, `exocom_claim`, `exocom_ask`, `exocom_answer`, `exocom_decline`, `exocom_wait`,
875
+ `exocom_release`, `exocom_progress`, `exocom_status`, `exocom_task`, `exocom_offer`,
876
+ `exocom_publish`, and `exocom_profile`. Each tool rechecks effective persona capabilities at call
877
+ time; the live accessor and active-tool set also deny calls once the plane stops, since Pi has no
878
+ dynamic tool-unregister API. A joined foreign-scope peer has no `exocom_claim`. Admission requires
879
+ `canUseBus` and at least one callable ask closer, because every published peer may receive a durable
880
+ ask and must retain a way to settle it.
881
+ - **Collaboration and profile retention are bounded.** The ledger retains at most 64 tasks, 64 offers,
882
+ and 64 evidence excerpts with a 128 KiB aggregate collaboration-state budget. Task descriptions cap
883
+ at 2,048 characters; write sets at 64 paths of 256 characters each; progress/finish notes at 4,096.
884
+ Evidence titles cap at 160 characters and excerpts at 4,096 characters / 8,192 UTF-8 bytes. Finished
885
+ or stale tasks remain for up to one hour; expired offers remain for up to one hour after expiry, and
886
+ evidence for up to one hour after publication. Active tasks remain while their owner lease is live.
887
+ `exocom_profile` contains up to eight self-described capability labels (48 characters each) and
888
+ optional `available`, `busy` or `away` status. Missing status is unknown. Profiles exist only for
889
+ their live Exocom session and never prove skills, permission or availability.
739
890
  - **Inbound delivery is bounded without loss.** Each message is injected under the same byte cap
740
891
  whether plain text or an artifact descriptor. Bursts stay FIFO-queued; each rate-limited wake drains
741
892
  as many whole messages as fit, leaving the rest queued. A presentation cap on the collapsed card
@@ -0,0 +1,110 @@
1
+ # Supervised workers and independent colleagues
2
+
3
+ Use **Intercom** for workers launched by this session. Use **Exocom** for independent Pi sessions
4
+ that choose to collaborate. An Exocom peer is responsible to its own user; messages and shared tasks
5
+ do not make it a subordinate or grant it permission to act.
6
+
7
+ ## Supervising a leg
8
+
9
+ Choose `thinking` independently of `model` in a delegate or roster entry. The runtime preserves an
10
+ explicit provider choice and reports requested and observed thinking separately. A compact tree
11
+ label is only a projection; it does not prove the provider's internal reasoning effort.
12
+
13
+ Use `intercom peek` to inspect a particular uncertainty, `steer` to queue an instruction, and `stop`
14
+ to stop work. A steering receipt proves acceptance, not that the worker acted on it. Interactive
15
+ completions wake the supervisor automatically; end the turn instead of polling or sleeping.
16
+
17
+ `intercom results` lists terminal reports. `intercom result { ref, offset, limit }` reads a bounded
18
+ page after reload. A `result:<UUID>` identifies stored evidence, not a live worker. The receipt says
19
+ whether storage is durable or memory-only and whether retention truncated the original report.
20
+ Paging to the end cannot restore text that exceeded the retention ceiling.
21
+
22
+ An unanswered decision remains unresolved. Timeout, cancellation, disconnection and headless delivery
23
+ never imply approval. Continue only independent work already authorized, or return a partial result.
24
+
25
+ ## Sharing an objective without a leader
26
+
27
+ Start participating sessions with `--exocom` in the same workspace, or join an existing scope with
28
+ its code as described in [the user guide](EXPERIENCE.md#work-with-another-pi-through-exocom).
29
+ Use `exocom_list` to find stable qualified targets and self-reported profiles. A profile's capabilities
30
+ and availability are suggestions for choosing a colleague, not verified skills, reservations or tool
31
+ permissions. Missing availability is **unknown**. Profiles disappear when the Exocom plane stops.
32
+
33
+ 1. Describe an independent slice with `exocom_task { action: "create", work_key, description,
34
+ write_set }`. An empty write set means read-only work. Creation is an invitation to collaborate,
35
+ not an assignment. Keep tasks within the users' authorized objective.
36
+ 2. Inspect tasks using `exocom_task { action: "list", offset, limit, detail }`. Every task carries a
37
+ `task_id`, the immutable creation message ID, alongside its human-selected `work_key`. Before
38
+ taking a write task, use `exocom_task { action: "get", work_key, task_id }` to inspect the complete
39
+ bounded write set; list previews may shorten paths.
40
+ 3. Optionally send `exocom_offer { work_key, task_id, target, expiresInMinutes }`. Omitting target
41
+ records a pool invitation for discovery; it does not broadcast a model wake. A targeted offer uses
42
+ the existing signed wake path. Default expiry is one hour, maximum 24 hours. It never creates a
43
+ response obligation; expiration is not acceptance or rejection.
44
+ 4. A colleague voluntarily calls `exocom_task { action: "take", work_key, task_id }`. Pickup and the
45
+ declared file claim commit atomically; a second pickup or overlapping claim fails. A stale task ID
46
+ cannot act on a newly created task that reused the same key. Foreign-workspace members may take
47
+ read-only tasks, but cannot create or take write tasks in the joined scope.
48
+ 5. The taker posts `progress` with a bounded `note`, then `finish` with `outcome: "done"` or
49
+ `"abandoned"` and a note, always supplying the same `work_key` and `task_id`. The latest progress
50
+ note is retained. Finishing releases the task's claim; it never merges or approves code.
51
+
52
+ Task division does not need leader election. Each task has one taker and its own claim. Peers choose
53
+ separate open tasks; availability and execution remain up to each session. A peer may propose a different split, decline by doing
54
+ nothing with an offer, or discuss a conflict in a postcard. If a decision belongs to a user, bring it
55
+ to that user; there is no automatic arbitration or authority transfer between peers.
56
+
57
+ Use `exocom_ask` only when a peer's answer must gate the next action. Unlike an offer, an ask constrains
58
+ its addressee until `exocom_answer` or `exocom_decline`. Identity metadata and explicit read actions
59
+ remain available. `exocom_wait` arms a nonblocking wake; it does not block the current tool call.
60
+
61
+ ## Publishing evidence
62
+
63
+ `exocom_publish { action: "publish", work_key, task_id, title, excerpt }` copies an explicit excerpt
64
+ into the shared ledger. A title is at most 160 characters; an excerpt is at most 4,096 UTF-16 units
65
+ and 8,192 UTF-8 bytes. No tool follows paths or archive references in that text. To share a retained
66
+ leg report, read the needed page through Intercom, select the relevant excerpt, then publish that text.
67
+
68
+ Discover evidence with `exocom_publish { action: "read", work_key, offset, limit }`, then retrieve one
69
+ excerpt with `msg_id`. Publisher identity is resolved from the registry or retained session ID, never
70
+ the envelope's claimed display name. The excerpt remains fenced peer data. Verify its claims before
71
+ using it; a peer's `done` means that peer finished its task, not that checks passed or a reviewer approved.
72
+
73
+ ## Failure, retention and controls
74
+
75
+ `exocom_status` and `/exocom` show claims, questions, shared task states and invitations. Task lists
76
+ default to 10 entries (maximum 20); detailed pages default to 5 and allow at most 5. Evidence-title pages are bounded.
77
+ Tool cards collapse long content and expand using Pi's normal keybinding; this never changes the
78
+ stored evidence or the text supplied to the model.
79
+
80
+ Loss of a task owner's registry lease or explicit release marks unfinished work **stale**. It never
81
+ silently reopens the task. Inspect the work and create a newly scoped task if reassignment is needed.
82
+ Clean shutdown releases both write and read-only task ownership. `exocom_release` intentionally
83
+ releases this session's current ownership and outbound asks on a work key.
84
+
85
+ The ledger retains at most 64 tasks, 64 offers and 64 evidence excerpts in a 128 KiB collaboration
86
+ budget, including lifecycle headroom. Terminal/stale tasks remain visible for the existing one-hour
87
+ result retention window; offers remain until one hour after expiry, evidence for one hour after
88
+ publication. Removing a task also removes its related offers/evidence. Live open/taken tasks are not
89
+ evicted to admit new work. Limits fail explicitly; there is no silent ownership loss or global archive
90
+ access. Transactions perform durable cleanup; reads project current expiry without starting workers.
91
+
92
+ ## Contract boundaries
93
+
94
+ | Boundary | Required meaning | Enforcement |
95
+ |---|---|---|
96
+ | Intercom vs Exocom | Worker supervision vs independent peers | Existing capability gates, attribution and distinct transports |
97
+ | Delivery vs decision | Accepted/delivered is not answered, acted or approved | Explicit outcomes and unresolved guidance |
98
+ | Task identity | A reused key cannot retarget old intent | Creation `task_id` checked inside the ledger transaction and signed on frames |
99
+ | Pickup and ownership | One taker, claim admitted together, no overlap | Atomic ledger transaction; scope/capability admission inside its lock |
100
+ | Optional offer vs ask | Invitations never gate; targeted asks do | Separate records, tool actions and pending-ask gate |
101
+ | Evidence | Explicit bounded copied text, no cross-session archive access | Schema validation, ledger budgets, registry attribution and fences |
102
+ | Persistence | Versioned authoritative state must not become an empty ledger on error | Snapshot validation, retained-message idempotency and lifecycle budget reservation |
103
+ | Retention | Stale is visible, expiry never implies takeover | First-stale timestamp, bounded retention, fresh generation on recreation |
104
+ | Profiles and views | Self-report/projection cannot grant authority | Separate presence metadata and runtime-owned status |
105
+ | Flow references | Only compatible, successful archived phases can resume | Journal-version/status checks, scoped reads, explicit unavailable/truncated outcomes |
106
+
107
+ These are runtime contracts. The existing `outputContract` facility validates a worker's final JSON
108
+ report with scalar fields; a model-written `proof` is not a command receipt. It is not the right place
109
+ to encode task ownership or ledger transitions. Nested report schemas and host-executed acceptance
110
+ receipts remain separate product work; this collaboration protocol does not claim to implement them.
@@ -80,6 +80,10 @@ receipt previews the message that was actually queued; expand it (`Ctrl+O` by de
80
80
  configured key) to read the full message and diagnostic run ID. Aliases are display names, not
81
81
  addresses: tools still route by run ID, and duplicate aliases show a secondary ID to distinguish them.
82
82
  Saved launch/control receipts keep their display names even after the runtime tracker is gone.
83
+ Council and flow launch receipts also retain each participant's model and requested effort, with
84
+ phase and auxiliary roles identified. Expand the card for the full panel. A pending model choice
85
+ is marked explicitly; launch selections are not proof of the effective model or effort after
86
+ setup, provider recovery or Pi's effort adjustment. Later settings changes do not rewrite history.
83
87
 
84
88
  A truncated message includes a message id. The supervisor can retrieve its retained body with
85
89
  `intercom { action: "message", messageId: "m1" }`. A worker's final report has a run id instead:
@@ -92,14 +96,35 @@ outcomes that make sense without remembering those names.
92
96
 
93
97
  ## Work with another Pi through Exocom
94
98
 
95
- Use Intercom for this supervisor's workers and Exocom for independent Pi sessions. Ask a peer for
96
- a relevant second opinion or coordinate work already in progress. A bounded implementation task
97
- belongs in a worker brief with explicit ownership and a success criterion.
98
-
99
- When Exocom is enabled, use its listed targets to keep messages attached to the right session.
100
- Exchange decisions and evidence when they affect the work; acknowledgement-only conversations
101
- add noise. If a peer has joined from another workspace, agree which workspace owns each artifact.
102
- That peer's membership does not grant it a write claim in the selected workspace.
99
+ Use Intercom for this supervisor's workers and Exocom for independent Pi sessions. Use
100
+ `exocom_list` or `/exocom` to find the intended session-qualified target before sending a postcard
101
+ or asking a question.
102
+
103
+ `exocom_task create` records a voluntary objective and declared write paths; it does not start
104
+ another Pi or require a peer to accept. The receipt gives a `task_id`; keep it with the
105
+ `work_key` when inviting, taking, reporting progress, finishing or publishing evidence. A work key
106
+ can be reused after retention, so the task id identifies the exact generation. The peer chooses
107
+ whether to take the task. Write-set pickup and its advisory claim happen atomically, but a claim is
108
+ not an OS file lock. A peer that joined from another workspace can take read-only tasks, not tasks
109
+ that claim paths in the selected workspace. Use `exocom_task get` to inspect the complete write set;
110
+ the paginated list is a summary.
111
+
112
+ Use `exocom_offer` for a non-blocking invitation that expires by itself; it creates no obligation
113
+ to answer. Use `exocom_ask` when a specific decision is needed and the recipient must answer or
114
+ decline. A pending ask temporarily permits settlement, `exocom_name`, `exocom_status`, explicit
115
+ read actions (task list/get, evidence read and profile get), and read-only filesystem tools.
116
+ Changing `exocom_profile` is not allowed while an ask is pending. Changing the name does not answer
117
+ the ask.
118
+
119
+ Share only a bounded excerpt through `exocom_publish`; it does not grant access to another session's
120
+ terminal-result archive. Peer text is untrusted and appears fenced with registry-resolved attribution.
121
+ Capability labels and availability in `exocom_profile` are self-reported metadata, not verified
122
+ skills or permissions. Exchange decisions and evidence when they affect the work; acknowledgement-only
123
+ conversations add noise. If a peer joined from another workspace, agree which workspace owns each
124
+ artifact.
125
+
126
+ See the [cross-session collaboration guide](./COLLABORATION.md) for command shapes, retention limits,
127
+ and detailed examples.
103
128
 
104
129
  `exocom_status` (or `/exocom`) shows who owns what right now: your open claims, a peer's open
105
130
  claims, and any asks waiting on either side. Claims are advisory, not a lock — if you write into a
package/docs/README.md CHANGED
@@ -16,6 +16,8 @@ introduced a behavior. An `Unreleased` entry is not yet part of a published rele
16
16
  - Vendor-neutral observer contract, privacy boundary, persistence, and consumer conformance.
17
17
  - [`EXPERIENCE.md`](./EXPERIENCE.md)
18
18
  - Choosing a persona, supervising background work, and collaborating through Exocom.
19
+ - [`COLLABORATION.md`](./COLLABORATION.md)
20
+ - Intercom supervision, voluntary peer tasks, invitations, evidence, retention and runtime contracts.
19
21
  - [`MONITORS.md`](./MONITORS.md)
20
22
  - Clock snapshots, timers, and background event sources that wake the supervisor.
21
23
  - [`REFERENCE.md`](./REFERENCE.md)
@@ -28,6 +30,16 @@ introduced a behavior. An `Unreleased` entry is not yet part of a published rele
28
30
  - **For behavior-layer changes:** consult `SPINE.md` and `src/core` / `src/persona/spine.ts` together.
29
31
  - **For telemetry producers or consumers:** start with `TELEMETRY.md` and its conformance corpus.
30
32
 
33
+ ## Research and verification records
34
+
35
+ - [`The pi-subagents comparison`](https://github.com/AeonDave/pi-persona/blob/main/docs/reviews/2026-10-24-pi-subagents.md) records the bounded source
36
+ study and links its evidence and source notes.
37
+ - [`Current-host cleanup and final requalification`](https://github.com/AeonDave/pi-persona/blob/main/docs/reviews/2026-10-25-current-host-cleanup.md)
38
+ is the latest qualification report and links the preceding implementation review for its distinct
39
+ behavioral and live-run evidence.
40
+ - [`ROADMAP.md`](https://github.com/AeonDave/pi-persona/blob/main/ROADMAP.md) tracks the remaining work, including the unqualified
41
+ [compaction proposal](https://github.com/AeonDave/pi-persona/blob/main/docs/plans/2026-10-20-compaction-evaluation-proposal.md).
42
+
31
43
  ## Documentation maintenance notes
32
44
 
33
45
  When touching this folder: