agent-tempo 1.7.0-beta.8 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/CLAUDE.md +29 -6
  2. package/README.md +2 -1
  3. package/dashboard/package.json +1 -1
  4. package/dist/activities/maestro.d.ts +46 -1
  5. package/dist/activities/maestro.js +69 -20
  6. package/dist/activities/outbox.d.ts +14 -1
  7. package/dist/activities/outbox.js +71 -10
  8. package/dist/activities/resolve.d.ts +118 -17
  9. package/dist/activities/resolve.js +256 -28
  10. package/dist/activities/schedule-fire.js +5 -2
  11. package/dist/adapters/base.d.ts +2 -0
  12. package/dist/adapters/base.js +9 -0
  13. package/dist/adapters/claude-api/adapter.js +26 -4
  14. package/dist/adapters/claude-code/adapter.js +15 -9
  15. package/dist/adapters/claude-code-headless/adapter.js +22 -4
  16. package/dist/adapters/copilot/adapter.js +58 -8
  17. package/dist/adapters/opencode/adapter.js +26 -6
  18. package/dist/adapters/sdk/base.d.ts +60 -0
  19. package/dist/adapters/sdk/base.js +83 -0
  20. package/dist/adapters/sdk/doorbell-client.d.ts +106 -0
  21. package/dist/adapters/sdk/doorbell-client.js +261 -0
  22. package/dist/adapters/sdk/idle-backoff.d.ts +71 -0
  23. package/dist/adapters/sdk/idle-backoff.js +136 -0
  24. package/dist/cli/command-center-command.js +25 -2
  25. package/dist/cli/commands.d.ts +28 -0
  26. package/dist/cli/commands.js +111 -20
  27. package/dist/cli/config-command.js +14 -0
  28. package/dist/cli/daemon-command.js +41 -5
  29. package/dist/cli/daemon.d.ts +245 -5
  30. package/dist/cli/daemon.js +505 -44
  31. package/dist/cli/help-text.js +3 -1
  32. package/dist/cli/mcp.d.ts +26 -2
  33. package/dist/cli/mcp.js +33 -4
  34. package/dist/cli/sa-preflight.d.ts +27 -1
  35. package/dist/cli/sa-preflight.js +29 -5
  36. package/dist/cli/startup.js +8 -2
  37. package/dist/cli/upgrade-to-2-command.d.ts +10 -0
  38. package/dist/cli/upgrade-to-2-command.js +159 -0
  39. package/dist/cli.js +37 -5
  40. package/dist/client/core.d.ts +6 -0
  41. package/dist/client/core.js +81 -34
  42. package/dist/client/subscribe.d.ts +10 -0
  43. package/dist/client/subscribe.js +2 -0
  44. package/dist/config.d.ts +53 -0
  45. package/dist/config.js +70 -1
  46. package/dist/daemon.d.ts +78 -7
  47. package/dist/daemon.js +313 -29
  48. package/dist/ensemble/saver.d.ts +10 -0
  49. package/dist/ensemble/saver.js +18 -6
  50. package/dist/http/aggregate.d.ts +88 -1
  51. package/dist/http/aggregate.js +157 -13
  52. package/dist/http/deliverability.d.ts +68 -0
  53. package/dist/http/deliverability.js +78 -0
  54. package/dist/http/doorbell-routes.d.ts +32 -0
  55. package/dist/http/doorbell-routes.js +72 -0
  56. package/dist/http/doorbell.d.ts +45 -0
  57. package/dist/http/doorbell.js +162 -0
  58. package/dist/http/inner-loop-routes.d.ts +9 -0
  59. package/dist/http/inner-loop-routes.js +4 -0
  60. package/dist/http/qa.js +8 -1
  61. package/dist/http/server.d.ts +9 -0
  62. package/dist/http/server.js +45 -1
  63. package/dist/http/snapshot.d.ts +28 -5
  64. package/dist/http/snapshot.js +16 -6
  65. package/dist/http/writes.js +24 -2
  66. package/dist/pi/cue-pump.d.ts +107 -7
  67. package/dist/pi/cue-pump.js +131 -24
  68. package/dist/pi/extension.js +11 -0
  69. package/dist/pi/headless.js +49 -13
  70. package/dist/pi/install.d.ts +15 -0
  71. package/dist/pi/install.js +35 -0
  72. package/dist/pi/mission-control/actions.d.ts +52 -3
  73. package/dist/pi/mission-control/actions.js +112 -3
  74. package/dist/pi/mission-control/board.d.ts +104 -0
  75. package/dist/pi/mission-control/board.js +103 -2
  76. package/dist/pi/mission-control/extension.d.ts +166 -1
  77. package/dist/pi/mission-control/extension.js +477 -15
  78. package/dist/pi/mission-control/render.d.ts +25 -0
  79. package/dist/pi/mission-control/render.js +142 -8
  80. package/dist/pi/workflow-client.d.ts +29 -0
  81. package/dist/pi/workflow-client.js +87 -3
  82. package/dist/reconcile/orphans.js +8 -9
  83. package/dist/server-tools.js +1 -1
  84. package/dist/server.js +11 -1
  85. package/dist/spawn.d.ts +78 -26
  86. package/dist/spawn.js +69 -37
  87. package/dist/tools/broadcast.js +15 -1
  88. package/dist/tools/cue.js +52 -11
  89. package/dist/tools/ensemble.js +43 -3
  90. package/dist/tools/recruit.js +11 -29
  91. package/dist/tools/who-am-i.d.ts +3 -2
  92. package/dist/tools/who-am-i.js +9 -2
  93. package/dist/types.d.ts +42 -0
  94. package/dist/upgrade/phase-engine.d.ts +160 -0
  95. package/dist/upgrade/phase-engine.js +555 -0
  96. package/dist/upgrade/snapshot-v1.d.ts +214 -0
  97. package/dist/upgrade/snapshot-v1.js +165 -0
  98. package/dist/utils/action-counters.d.ts +75 -0
  99. package/dist/utils/action-counters.js +279 -0
  100. package/dist/utils/format-hosts.js +7 -0
  101. package/dist/utils/hosts.js +5 -0
  102. package/dist/utils/sdk-probe.d.ts +12 -0
  103. package/dist/utils/sdk-probe.js +28 -0
  104. package/dist/utils/search-attributes.d.ts +76 -4
  105. package/dist/utils/search-attributes.js +89 -4
  106. package/dist/utils/suspension.d.ts +99 -0
  107. package/dist/utils/suspension.js +128 -0
  108. package/dist/utils/visibility-deadline.js +5 -0
  109. package/dist/worker.d.ts +16 -1
  110. package/dist/worker.js +25 -4
  111. package/dist/workflows/maestro.d.ts +10 -0
  112. package/dist/workflows/maestro.js +126 -21
  113. package/dist/workflows/scheduler.js +17 -2
  114. package/dist/workflows/session.js +251 -14
  115. package/dist/workflows/signals.d.ts +8 -0
  116. package/dist/workflows/signals.js +3 -2
  117. package/package.json +8 -4
  118. package/workflow-bundle.js +580 -40
package/CLAUDE.md CHANGED
@@ -41,7 +41,8 @@ src/
41
41
  │ ├── sa-preflight.ts # search-attribute preflight — REQUIRED_SEARCH_ATTRIBUTES list (single source of truth), registerSearchAttribute, verifySearchAttributes, assertSearchAttributesOrExit
42
42
  │ ├── scenarios-command.ts # scenarios subcommand (dev mode only) — list/show shipped YAML scenario library (ADR 0014 §4.8)
43
43
  │ ├── startup.ts # auto-provisioning bootstrap state machine (#289) — six-step idempotent sequence used by bare `agent-tempo` invocation
44
- └── upgrade-command.ts # upgrade subcommand — crash-proof; dynamic-imports Temporal only for active-session warning
44
+ ├── upgrade-command.ts # upgrade subcommand — crash-proof; dynamic-imports Temporal only for active-session warning
45
+ │ └── upgrade-to-2-command.ts # upgrade-to-2 cutover verb (#785) — crash-proof; dynamic-imports the phase-engine + Temporal inside try/catch; --yes/--dry-run/--force-drain
45
46
  ├── adapters/
46
47
  │ ├── README.md # Adapter contract documentation
47
48
  │ ├── index.ts # Adapter registry bootstrap + barrel exports (mock registered iff isDevMode())
@@ -53,7 +54,7 @@ src/
53
54
  │ ├── opencode/ # OpenCodeAttachment — headless multi-provider adapter via SST OpenCode subprocess (#449)
54
55
  │ ├── pi/ # Headless Pi adapter — descriptor + spawn entry (Phase 3a). No BaseAttachment; the Pi extension singleton owns lifecycle (claim/heartbeat/tools/cue pump). `adapter.ts` is the process entry; `index.ts` is the registry descriptor.
55
56
  │ ├── mock/ # MockAttachment — dev-mode-only SDK adapter (ADR 0014 PR-2). prepack strips dist/adapters/mock from npm tarball.
56
- │ └── sdk/ # SDK-style adapter base (used by Copilot bridge and opencode)
57
+ │ └── sdk/ # SDK-style adapter base (used by Copilot bridge and opencode). Key files: `idle-backoff.ts` (T0.2 IdleBackoff helper — base 2s, cap 30s/60s, reset-on-delivery), `doorbell-client.ts` (T1.1 DoorbellClient + WakeableSleep — reconnecting SSE consumer for `/doorbell`; ding→reset()+wake(); connected ceiling 60s; disconnected falls back to 30s T0.2 floor)
57
58
  ├── client/
58
59
  │ ├── interface.ts # TempoClient TypeScript interface and related types
59
60
  │ └── index.ts # TempoClient factory implementation and barrel re-exports
@@ -85,12 +86,17 @@ src/
85
86
  │ ├── inner-loop.ts # InnerLoopRegistry + InnerSubscription — daemon-local fine-tail sink (3c MD-F); drop-oldest bounded queue (256) + `compacted{dropped,sinceTs}` marker; NOT on Temporal/bus, ephemeral no-replay
86
87
  │ ├── ingest-registry.ts # IngestTokenRegistry — per-player ingest token (mint-on-pi-spawn / revoke-on-destroy / revokeAll-on-shutdown); timing-safe validation; cross-player-spoof guard
87
88
  │ ├── inner-loop-routes.ts # 3 inner-loop HTTP routes: POST /inner/ingest + GET /inner/presence (INGRESS, loopback + X-Ingest-Token, uniform 403); GET /inner (EGRESS operator SSE, requireTier(3))
89
+ │ ├── doorbell.ts # DoorbellRegistry — in-process Map<"{ensemble}:{playerId}", Set<waiter>>; ring-with-no-listener drops on floor; level-triggered coalescing; never throws (T1.1 PR-1, #776)
90
+ │ ├── doorbell-routes.ts # GET /doorbell/:ensemble/:playerId SSE route — content-free ding events; loopback + X-Ingest-Token auth (same ingress model as inner-loop); NO event IDs / Last-Event-ID / replay by design; :ka keepalive every 15s (T1.1 PR-1, #776)
88
91
  │ ├── auth.ts # 3e MD-E RBAC: two-token model (readToken T1 / adminToken T1+T2+T3 env-var-only); loadRbacTokens; requireTier(tier, input) → TierGuardResult; tierForToken; loadReadToken (env>config>legacy httpToken>auto-gen); loadAdminToken (env-only); TLS/legacy startup warnings in startHttpServer
89
92
  │ ├── cors.ts / responses.ts / event-id.ts / port-file.ts / index.ts
90
93
  ├── reconcile/
91
94
  │ └── orphans.ts # Shared orphan-query helper (daemon reconcile-on-boot + CLI restore)
95
+ ├── upgrade/ # 1.x→2.0 cutover (#785)
96
+ │ ├── snapshot-v1.ts # VERSIONED upgrade-snapshot-v1.json schema + atomic persistence — the cross-release interface #786 imports (Temporal-free)
97
+ │ └── phase-engine.ts # 6-phase resumable cutover engine (preflight→pause→drain→snapshot→destroy→done); snapshot strictly precedes destroy
92
98
  ├── ensemble/
93
- │ ├── schema.ts / loader.ts / saver.ts # Lineup type definitions, load, save
99
+ │ ├── schema.ts / loader.ts / saver.ts # Lineup type definitions, load, save (saver exports buildLineupFromCluster — shared by save_lineup + #785 snapshot)
94
100
  │ └── agent-types.ts # Agent type discovery, resolution, and lineup resolution
95
101
  ├── tools/ # One file per MCP tool — see docs/tools.md for full reference
96
102
  │ ├── ensemble.ts / cue.ts / recruit.ts / report.ts / broadcast.ts / recall.ts / listen.ts
@@ -108,7 +114,7 @@ src/
108
114
  │ └── descriptor.ts # Transport-neutral tool descriptor (TempoToolDescriptor) + renderToMcp; per-tool `build*Tool` factories live in each tool file (MD-B, Phase 1)
109
115
  ├── pi/ # Pi-native integration — a Pi session as a first-class player over the Temporal core
110
116
  │ ├── extension.ts # `export default function(pi)` — interactive runtime entry. Holds the MODULE-SCOPE singleton `Map<workflowId, PiPlayerRuntime>` that survives Pi's per-switch instance rebuild (rebind, not re-claim); full tool surface via renderToPi; Option-C reason-discriminated teardown
111
- │ ├── phase-driver.ts / workflow-client.ts / cue-pump.ts # Pi-event→attachment-phase machine, thin client-side WorkflowClient (lease/heartbeat 90/30, handle getter), cue pump (D10 steer/followUp; one 1s loop with a second pendingReset intake S3 merge, D14 clean-wipe + /tempo-reset operator notice)
117
+ │ ├── phase-driver.ts / workflow-client.ts / cue-pump.ts # Pi-event→attachment-phase machine, thin client-side WorkflowClient (lease/heartbeat 90/30, handle getter), cue pump (D10 steer/followUp; IdleBackoff loop 1s base→30s disconnected/60s connected ceiling + ding wake via DoorbellClient; pendingIntake combined query T0.3; S3 merge, D14 clean-wipe + /tempo-reset operator notice)
112
118
  │ ├── lazy-proxy.ts # D11 createLazyProxy — Client/WorkflowHandle proxy resolving the live module-scope target per call (survives instance rebuild)
113
119
  │ ├── headless.ts # Headless Pi runtime (Phase 3a) — boots Pi's createAgentSession with inline extension; `noExtensions: true` closes S2 exec-tool bypass; SIGTERM/SIGINT shutdown → reliable detach + dispose
114
120
  │ ├── render-tools.ts # renderToPi — registers the shared tool descriptors on Pi's ExtensionAPI (TypeBox params via the converter)
@@ -168,6 +174,22 @@ npm run check:all # runs every CI gate locally (build, tests, drift checks, li
168
174
  > surveys or migrations, always grep **both** `test/` and `tests/` or you will miss
169
175
  > mocks and assertions that only live in one directory.
170
176
 
177
+ > **Windows `**`-glob false-negative (#707) — a zero-match can read as "clean".**
178
+ > On Windows, a ripgrep glob with a **leading directory segment before `**`**
179
+ > silently matches **zero** files: `Grep ... glob="src/**/*.ts"` returns *"No
180
+ > files found"* even when matches exist — a false negative that looks identical
181
+ > to "searched and found nothing." This bites call-site surveys, drift checks,
182
+ > and any "prove a symbol is absent" decision. **Reliable forms** (use these):
183
+ > path-scope instead — `Grep path="src" type="ts"` — or drop the leading dir —
184
+ > `glob="**/*.ts"`; or just `Read` the file when you know the path. A bare
185
+ > recursive `**/*.ext` works; `dir/**/*.ext` is the broken shape.
186
+ > The bug is in the search-tool layer, not this repo — **no repo check shells a
187
+ > `**` glob.** Repo lint/drift/conformance checks enumerate via `fs` recursion
188
+ > or `git ls-files` (never a shelled `**` glob) and now **assert a non-zero file
189
+ > count** (#707) so a future enumeration breakage fails loud instead of passing
190
+ > as "clean." Hold that bar for any new source-scanning check: *a check that
191
+ > scans nothing must never report success.*
192
+
171
193
  > **Test-only hooks live with the module they reset and follow the
172
194
  > `__<verb><Noun>ForTests` naming convention** — see
173
195
  > [docs/adr/0006-test-hooks-naming.md](docs/adr/0006-test-hooks-naming.md). The
@@ -208,14 +230,15 @@ daemon worker notes, `npx ts-node` dev runner).
208
230
  - **Claude API adapter** (`agent: 'claude-api'`, #131): Headless adapter that drives sessions via the Anthropic Messages API (`@anthropic-ai/sdk`) — no terminal, no Claude Code CLI. Requires `ANTHROPIC_API_KEY` env var and the `@anthropic-ai/sdk` optional dependency. Default model `claude-opus-4-7` (overridable via `model` recruit arg or `CLAUDE_TEMPO_API_MODEL` env). Claude-API players have access to agent-tempo MCP tools (cue, report, recall, ensemble, …) but not file-edit/shell/web tools. See `src/adapters/claude-api/`.
209
231
  - **OpenCode adapter** (`agent: 'opencode'`, #449): Headless multi-provider adapter that drives sessions via [SST OpenCode](https://opencode.ai) as a managed subprocess — supports Anthropic, OpenAI, Bedrock, Vertex, Ollama, and ~70 other providers via OpenCode's `provider/model` selector. Requires OpenCode CLI (`npm install -g opencode-ai`) and the `@opencode-ai/sdk` optional dependency. Recruit with `model: 'provider/name'` (e.g. `'anthropic/claude-opus-4-7'`). Tool bridging is MCP-native — OpenCode spawns `dist/server.js` as its own stdio MCP child. Session state is persisted server-side by OpenCode; the adapter stashes the session id on workflow metadata for reconnect across `opencode serve` restarts. See `src/adapters/opencode/`.
210
232
  - **Claude Code headless adapter** (`agent: 'claude-code-headless'`, #520): Headless adapter that drives sessions via the official `claude` CLI as a per-turn `claude -p --output-format stream-json` subprocess. The whole point: turns bill against the host's existing Claude Code subscription extra-usage credits (Pro / Max plans) rather than a Console workspace API key — the only ToS-clean way for a third-party tool to tap that pool. Requires the `claude` binary on PATH AND a logged-in Claude Code session (`claude auth login`); recruit pre-flight rejects with an actionable error otherwise. Tool surface is the union of full Claude Code built-ins (Bash / Read / Write / Edit / Glob / Grep / WebSearch / WebFetch) and the agent-tempo MCP surface — registered via inline `--mcp-config` so `claude` spawns `dist/server.js` as its own MCP child (no in-process bridge). Recruit knobs: `permissionMode` (default `'acceptEdits'`) or `dangerouslySkipPermissions: true` (mutually exclusive). Sessions resume across restart via the existing `sessionId` metadata field — the same UUID is shared with the interactive `claude-code` adapter (per-cwd JSONL is per-cwd, not per-adapter). See `src/adapters/claude-code-headless/` and `examples/ensembles/tempo-headless-jam.yaml`.
211
- - **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) is a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). Pi players run the full tool surface — incl. shell — with no permission layer, like the other adapters (the former `toolAccess`/`guardrailPolicy` knobs were removed). `noExtensions: true` stays: it blocks third-party disk/package extensions from loading into a recruited player (supply-chain hygiene). See `src/adapters/pi/` and `src/pi/headless.ts`.
233
+ - **Pi adapter** (`agent: 'pi'`, #632 / #666): Two modes. **(1) Interactive conductor** (#666): `agent-tempo up --agent pi --ensemble <name>` launches `pi` in a real terminal with the agent-tempo extension auto-loaded (`pi -e dist/pi/extension.js`); the Pi session self-bootstraps its Temporal workflow and attaches as a conductor/player — no separate recruiter step. From the TUI, `/recruit-conductor` relaunches the active ensemble's conductor — set `conductor.agent: pi` in that ensemble's lineup to make it a Pi conductor. Requires `@earendil-works/pi-coding-agent` on Node ≥ 22.19. Recommended: `ANTHROPIC_API_KEY` (without it the session falls back to Pi's own auth/default model). The `AGENT_TEMPO_*` env is auto-wired by `up`; power users can invoke the extension directly with `pi -e dist/pi/extension.js`. The interactive `--model provider/model` selector (e.g. `'github-copilot/gpt-4o'`) remains a fast-follow. **(2) Headless player** (Phase 3a): `recruit` with `agent: 'pi'` — no terminal, no BaseAttachment; runs `createAgentSession` with an in-memory `SessionManager`; the module-scope singleton owns claim/heartbeat/tools/cue pump (MD-D). Per-player `model: 'provider/model'` on recruit (#734): built-in providers AND custom `~/.pi/agent/models.json` providers (lmstudio, ollama, vllm, …) both resolve, falling back to `AGENT_TEMPO_PI_MODEL` — enables hybrid ensembles (frontier-model conductor, local-model workers). Pi players run the full tool surface — incl. shell — with no permission layer, like the other adapters (the former `toolAccess`/`guardrailPolicy` knobs were removed). `noExtensions: true` stays: it blocks third-party disk/package extensions from loading into a recruited player (supply-chain hygiene). See `src/adapters/pi/` and `src/pi/headless.ts`.
212
234
  - **Mission-control / Command-center** (3f, #700 P2): An interactive Pi TUI that is both a live ensemble board + operator controller and an LLM planner. Board side: HTTP-drives the daemon — coarse ensemble view via `/v1/events/:ensemble` SSE + fine per-player tail via `/inner` (T3); operator controls (cue/pause/play/restart/destroy) POST to the daemon write surface using `AGENT_TEMPO_HTTP_ADMIN_TOKEN`. Planner side: registers LLM tools (`ask` / `handoff` / `cue` / `recruit` / `observe_board`) via `MissionControlActions` — HTTP-backed, distinct from the player extension's `renderToPi` MCP surface. **Never claims attachment or registers as a player** — invisible to the ensemble. Launch with **`agent-tempo command-center [ensemble]`** (aliases: `cc`, `board`) — sets `AGENT_TEMPO_MISSION_CONTROL=1`. **Loopback auto-token** (#736): a local (loopback) daemon grants full trust without `AGENT_TEMPO_HTTP_ADMIN_TOKEN`; only a remote daemon requires explicit token configuration. **Role-gated and mutually exclusive** with the player extension (`resolvePiRole` discriminator: explicit `AGENT_TEMPO_PI_ROLE` → `PLAYER_NAME` present → `AGENT_TEMPO_MISSION_CONTROL` → `none`): a bare `pi` keeps both extensions dormant (plain coding session); a player session (`up --agent pi` / `recruit`) keeps the board dormant. Power users invoking `pi -e dist/pi/extension.js` directly without `PLAYER_NAME` also resolve to `none` (dormant) — set `AGENT_TEMPO_PI_ROLE=player` to force player mode. See `src/pi/mission-control/`.
213
235
  - **Command-center planner** (#700 P2): An inbox-less interactive Pi session (the operator's planning seat) that routes questions to players via correlated `cue` tags (`[Q <questionId>]`). Players answer with the `respond` MCP tool, which parks the answer on the per-ensemble maestro Q&A mailbox (TTL 1h, 20-slot cap). The planner is woken by an `answer` SSE event when the answer lands (`docs/SSE-PROTOCOL.md` §6). `/handoff` cues hand active work to a conductor (a registered player with a Temporal inbox). See `docs/concepts.md` for the Q&A mechanics.
214
236
  - **Mock adapter** (`agent: 'mock'`, dev mode only): Four modes: `echo` (echoes input), `scripted` (replays YAML scenario rules), `silent` (drains messages without replying — heartbeat-stale validation), `chaos` (probabilistic fail/crash injection via seeded PRNG). Only registered when `isDevMode()` is true; stripped from the npm tarball by `prepack`. See `src/adapters/mock/`.
215
237
  - **Saveable state** (#334, ADR 0011): Per-player curated state slots — the player itself decides what context survives a restart. Three MCP tools: `save_state` (owner-only write, max 4 slots × 32 KiB), `fetch_state` (read self or peer; audit identity recorded on each entry's `savedBy`), `clear_state` (owner-only). `restart` accepts `loadFromState: true | 'someKey'` to seed the new session from a saved-state slot instead of (or, with `transcript: 'replay'`, alongside) transcript replay. Saved-state delivery uses `from: 'self-restart'` as a stable system identity. Empty-slot fallback: graceful — falls through to transcript replay with a log line. See [docs/design/334-player-saveable-state.md](docs/design/334-player-saveable-state.md).
238
+ - **`upgrade-to-2` cutover** (#785, ratified migration protocol A2 — `docs/design/v2-scoping.md` §A.3): The 1.x → 2.0 migration verb. A 2.0 worker can never replay a 1.x-recorded run, so 2.0 ships behind a clean cutover: `agent-tempo upgrade-to-2` runs a six-phase protocol — **preflight** (enumerate ensembles; refuse if any connected daemon is below the 1.7.x version floor — `hostProfile.version` on the global maestro; covers same-host-stale-daemon, #801) → **pause** (brake the work *sources* — maestro + scheduler — leaving sessions live to drain) → **drain** (poll outboxes ≤60s; `--force-drain` records stragglers instead of stopping) → **snapshot** (freeze session dispatch, then capture continuity to `~/.agent-tempo/upgrade-snapshot-v1.json`) → **destroy** (idempotent teardown: peers → scheduler+maestro → conductor) → **done**. **Load-bearing invariant: SNAPSHOT strictly precedes DESTROY** (`destroyAndFinish` takes a persisted snapshot as a required arg); a crash leaves either everything intact or a durable snapshot + partial teardown, never destruction without capture. Resumable via the snapshot's phase stamp. The snapshot captures schedules (durable intent), #334 state slots (continuity), `sessionId` (resume pointer), the non-default recruit `model` (ad-hoc claude-api/opencode/pi continuity), and undelivered cues (operator review, NOT redelivered); coat-check is dropped (TTL-transient); gates/stages/worktrees are intentionally not captured (transient coordination state; worktree continuity rides `workDir`). **`src/upgrade/snapshot-v1.ts` is the cross-release interface the 2.0-side `up --from-upgrade` (#786) imports** — it is VERSIONED and deliberately Temporal-free; additive optional fields do NOT bump the version, only removed/renamed/retyped required fields do (with a 2.0 reader migration). CLI flags: `--yes` (skip confirm), `--dry-run` (print snapshot + destroy list, exit before pausing), `--force-drain`. The verb is crash-proof (`src/cli/upgrade-to-2-command.ts` dynamic-imports the Temporal-touching engine). See `src/upgrade/` and issue #785.
216
239
  - **Coat-check** (#318, ADR 0008): Per-ensemble transient content store on Maestro state. Solves the 100 KB cue body cap — stash a large artifact with `coat_check_put` (returns a ticket id) and attach the ticket to a `cue` via `attachmentTicket`; the recipient calls `coat_check_get` to pull the full body. Four MCP tools: `coat_check_put` (any player; max 32 KiB per entry, 20 slots per ensemble, TTL 7d default), `coat_check_get` (any player; bumps fetch-audit counters), `coat_check_list` (read-only survey; headers only, content omitted), `coat_check_evict` (owner or conductor). Saturation rejects with `CoatCheckSlotsFull` (no LRU eviction). See `src/tools/coat-check-*.ts` and [docs/adr/0008-coat-check-pattern.md](docs/adr/0008-coat-check-pattern.md).
217
240
  - **Lineup examples**: Six pre-built ensemble YAML files in `examples/ensembles/` — `tempo-big-band`, `tempo-dev-team`, `tempo-review-squad`, `tempo-jam-session`, `tempo-mock-jam` (dev-mode all-mock ensemble), `tempo-headless-jam` (#520 — all-`claude-code-headless` subscription-billed ensemble). Load with `agent-tempo up --lineup <name>` or the `load_lineup` tool.
218
- - **GitHub App identity** (`agent-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `agent-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
241
+ - **GitHub App identity** (`agent-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `agent-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). On Windows, `./scripts/ensemble-gh` works directly from PowerShell via the `scripts/ensemble-gh.cmd` shim (#741); the `bash ./scripts/ensemble-gh` form also still works as an explicit fallback. Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
219
242
 
220
243
  See [docs/concepts.md](docs/concepts.md) for the full glossary (Adapter, Attachment phases, Restart, Detach/Destroy, Migrate, Broadcast, Recall, Schedule, Lineup, Quality Gate, Worktree, Stage, Hold/Release, Pause/Resume, Maestro, TempoClient, Command-center planner, and more).
221
244
 
package/README.md CHANGED
@@ -148,12 +148,13 @@ agent-tempo # launch TUI (auto-provisions on first run)
148
148
  agent-tempo up [ensemble] # provision infrastructure and launch conductor
149
149
  agent-tempo down [--destroy] # tear down infrastructure (--destroy also terminates workflows)
150
150
  agent-tempo status [ensemble] # list active sessions
151
- agent-tempo destroy <ensemble> # terminate all sessions in an ensemble
151
+ agent-tempo destroy [ensemble] # terminate all sessions in an ensemble (defaults to "default")
152
152
  agent-tempo restore <ensemble> # restore orphaned sessions on this host
153
153
  agent-tempo hosts # list daemons polling this Temporal namespace (--all/--json)
154
154
  agent-tempo recall <name> # read a player's message history (--limit/--offset/--preview/--json)
155
155
  agent-tempo attachment-info <name> # inspect a session's phase, holder, lease, and heartbeat age
156
156
  agent-tempo release [ensemble] # release held players (unlock + deliver tasks)
157
+ agent-tempo dashboard # open the web dashboard (--pair for QR-code cross-device access)
157
158
  agent-tempo daemon <sub> # manage the worker daemon
158
159
  agent-tempo upgrade # update to latest
159
160
  ```
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "agent-tempo-dashboard",
3
3
  "private": true,
4
- "version": "1.7.0-beta.8",
4
+ "version": "1.7.0",
5
5
  "type": "module",
6
6
  "description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
7
7
  "scripts": {
@@ -55,9 +55,54 @@ export interface FetchEnsembleChatResult {
55
55
  hasConductor: boolean;
56
56
  error?: string;
57
57
  }
58
+ /** T0.1 (#748) — result of the V2 refresh (cloud-profile maestros). */
59
+ export interface RefreshEnsembleStateV2Result {
60
+ players: MaestroPlayerInfo[];
61
+ /**
62
+ * Whether the daemon currently has any live SSE subscriber (TUI, web
63
+ * dashboard, mission-control board). Read in-process from the
64
+ * AggregateRunner — zero Temporal cost. The maestro workflow stretches
65
+ * its next refresh timer when nobody is watching. `true` when no
66
+ * presence source is wired (fail-open: never stretch by accident).
67
+ */
68
+ observersPresent: boolean;
69
+ }
70
+ /**
71
+ * T0.1 (#748) — daemon-side observer presence source. A mutable holder
72
+ * because worker/activity construction happens BEFORE the daemon's
73
+ * AggregateRunner exists; daemon.ts fills `current` in once the HTTP/SSE
74
+ * plane is up (same late-wiring pattern as IngestTokenRegistry).
75
+ */
76
+ export interface ObserverPresenceSource {
77
+ current: (() => number) | null;
78
+ }
79
+ /** Options for {@link createMaestroActivities} (T0.1, #748). */
80
+ export interface MaestroActivityOptions {
81
+ /** Daemon cost profile — drives the V2 scan strategy. Default 'local'. */
82
+ costProfile?: 'local' | 'cloud';
83
+ /** Late-wired SSE subscriber count source (see {@link ObserverPresenceSource}). */
84
+ observerPresence?: ObserverPresenceSource;
85
+ }
58
86
  /** Activity interface — used by proxyActivities in the Maestro workflow. */
59
87
  export interface MaestroActivities {
88
+ /**
89
+ * Legacy V1 refresh — the `costProfile: 'local'` path AND the replay
90
+ * path for every maestro started before #748.
91
+ * TODO(next major, #748): remove once the minimum deployment age
92
+ * exceeds the longest-lived pre-#748 maestro history (they replay V1);
93
+ * remove together with the V1 branch in workflows/maestro.ts.
94
+ */
60
95
  refreshEnsembleState(ensemble: string): Promise<MaestroPlayerInfo[]>;
96
+ /**
97
+ * T0.1 (#748) — additive V2: called only by maestros started with
98
+ * `costProfile: 'cloud'` in their input. SA/memo-based ensemble-scoped
99
+ * scan + in-process observer presence for workflow-side cadence
100
+ * stretching. V1 stays for in-flight pre-#748 maestros (replay safety)
101
+ * and the local profile.
102
+ */
103
+ refreshEnsembleStateV2(input: {
104
+ ensemble: string;
105
+ }): Promise<RefreshEnsembleStateV2Result>;
61
106
  fetchConductorHistory(input: FetchConductorHistoryInput): Promise<FetchConductorHistoryResult>;
62
107
  relayCommandToConductor(input: RelayCommandInput): Promise<RelayCommandResult>;
63
108
  discoverEnsembles(): Promise<string[]>;
@@ -69,4 +114,4 @@ export interface MaestroActivities {
69
114
  * Create the Maestro activity implementations bound to a Temporal client.
70
115
  * Registered with the shared worker.
71
116
  */
72
- export declare function createMaestroActivities(client: Client): MaestroActivities;
117
+ export declare function createMaestroActivities(client: Client, opts?: MaestroActivityOptions): MaestroActivities;
@@ -5,40 +5,89 @@ const activity_1 = require("@temporalio/activity");
5
5
  const config_1 = require("../config");
6
6
  const types_1 = require("../types");
7
7
  const resolve_1 = require("./resolve");
8
+ const action_counters_1 = require("../utils/action-counters");
8
9
  const visibility_deadline_1 = require("../utils/visibility-deadline");
9
10
  const log = (...args) => console.error('[agent-tempo:maestro]', ...args);
10
11
  /**
11
12
  * Create the Maestro activity implementations bound to a Temporal client.
12
13
  * Registered with the shared worker.
13
14
  */
14
- function createMaestroActivities(client) {
15
- return {
15
+ function createMaestroActivities(client, opts = {}) {
16
+ /** Shared row-mapper for both refresh shapes. */
17
+ const toPlayerInfo = (ensemble) => (s) => ({
18
+ playerId: s.playerId,
19
+ ensemble,
20
+ part: s.part,
21
+ hostname: s.hostname,
22
+ workDir: s.workDir,
23
+ gitRoot: s.gitRoot,
24
+ gitBranch: s.gitBranch,
25
+ isConductor: s.isConductor,
26
+ agentType: s.agentType,
27
+ playerType: s.playerType,
28
+ phase: s.phase,
29
+ // #399 W1 — forward the activity-counter pair so the maestro's
30
+ // tempo bucket can diff across refreshes.
31
+ activityCount: s.activityCount,
32
+ lastActivityAt: s.lastActivityAt,
33
+ });
34
+ // #753 — attribute every Temporal call made by these activities (however
35
+ // deep, e.g. scanEnsembleSessions → queryHandleWithTimeout) to the maestro.
36
+ //
37
+ // T0.6/#774 (architect verification prerequisite) — the tag is SPLIT by
38
+ // the CALLING workflow, resolved per invocation from the activity
39
+ // context: the same factory serves the per-ensemble maestro (chat-gated)
40
+ // and the global maestro (never gated — the prime unwatched-residual
41
+ // suspect), and the meter must tell them apart. Anything else (incl.
42
+ // direct test invocation outside an activity context) lands in
43
+ // 'maestro-session'. NOTE: bySource report keys for the maestro line
44
+ // changed from 'maestro' to these three.
45
+ const maestroSource = () => {
46
+ try {
47
+ switch ((0, activity_1.activityInfo)().workflowType) {
48
+ case 'agentMaestroWorkflow': return 'maestro-ensemble';
49
+ case 'agentGlobalMaestroWorkflow': return 'maestro-global';
50
+ default: return 'maestro-session';
51
+ }
52
+ }
53
+ catch {
54
+ return 'maestro-session'; // outside an activity context (tests, direct calls)
55
+ }
56
+ };
57
+ return (0, action_counters_1.tagActionSource)(maestroSource, {
16
58
  async refreshEnsembleState(ensemble) {
17
59
  try {
18
60
  const sessions = await (0, resolve_1.scanEnsembleSessions)(client, ensemble);
19
- return sessions.map((s) => ({
20
- playerId: s.playerId,
21
- ensemble,
22
- part: s.part,
23
- hostname: s.hostname,
24
- workDir: s.workDir,
25
- gitRoot: s.gitRoot,
26
- gitBranch: s.gitBranch,
27
- isConductor: s.isConductor,
28
- agentType: s.agentType,
29
- playerType: s.playerType,
30
- phase: s.phase,
31
- // #399 W1 — forward the activity-counter pair so the maestro's
32
- // tempo bucket can diff across refreshes.
33
- activityCount: s.activityCount,
34
- lastActivityAt: s.lastActivityAt,
35
- }));
61
+ return sessions.map(toPlayerInfo(ensemble));
36
62
  }
37
63
  catch (err) {
38
64
  log('refreshEnsembleState failed:', err);
39
65
  throw activity_1.ApplicationFailure.nonRetryable(`Failed to scan ensemble: ${err instanceof Error ? err.message : String(err)}`);
40
66
  }
41
67
  },
68
+ async refreshEnsembleStateV2(input) {
69
+ try {
70
+ // Honor the DAEMON's configured profile for the scan strategy: a
71
+ // cloud-input maestro on a daemon flipped back to 'local' degrades
72
+ // gracefully to the legacy scan (still on the stretched cadence
73
+ // until its next restart).
74
+ const sessions = opts.costProfile === 'cloud'
75
+ ? await (0, resolve_1.scanEnsembleSessionsCloud)(client, input.ensemble, log)
76
+ : await (0, resolve_1.scanEnsembleSessions)(client, input.ensemble);
77
+ const count = opts.observerPresence?.current?.();
78
+ return {
79
+ players: sessions.map(toPlayerInfo(input.ensemble)),
80
+ // Fail-open: unknown presence (no aggregate wired yet, e.g. during
81
+ // daemon boot) counts as "observers present" — never stretch the
82
+ // cadence on missing information.
83
+ observersPresent: count === undefined ? true : count > 0,
84
+ };
85
+ }
86
+ catch (err) {
87
+ log('refreshEnsembleStateV2 failed:', err);
88
+ throw activity_1.ApplicationFailure.nonRetryable(`Failed to scan ensemble: ${err instanceof Error ? err.message : String(err)}`);
89
+ }
90
+ },
42
91
  async fetchConductorHistory(input) {
43
92
  try {
44
93
  const wfId = (0, config_1.conductorWorkflowId)(input.ensemble);
@@ -250,5 +299,5 @@ function createMaestroActivities(client) {
250
299
  };
251
300
  }
252
301
  },
253
- };
302
+ });
254
303
  }
@@ -218,4 +218,17 @@ export interface OutboxActivities {
218
218
  * claim — phase is legitimately live) → never skipped. Pure + exported for tests.
219
219
  */
220
220
  export declare function shouldSkipDuplicateSpawn(attachmentId: string | undefined, phase: AttachmentPhase): boolean;
221
- export declare function createOutboxActivities(client: Client, config: Config, ingestTokens?: IngestTokenRegistry): OutboxActivities;
221
+ /**
222
+ * T1.1 PR-1 — late-wired doorbell sink (the ObserverPresenceSource pattern:
223
+ * activity construction happens before the HTTP plane exists, so the daemon
224
+ * fills `current` at boot). A worker process that is not the daemon keeps
225
+ * `current: null` — ring() is a no-op and fallback polling covers it (§2.1).
226
+ * Defined here (not in src/http) so activities never import the HTTP plane.
227
+ */
228
+ export interface DoorbellSink {
229
+ current: {
230
+ ring(ensemble: string, playerId: string): void;
231
+ closePlayer(ensemble: string, playerId: string): void;
232
+ } | null;
233
+ }
234
+ export declare function createOutboxActivities(client: Client, config: Config, ingestTokens?: IngestTokenRegistry, doorbells?: DoorbellSink): OutboxActivities;
@@ -46,6 +46,9 @@ const git_info_1 = require("../git-info");
46
46
  const spawn_1 = require("../spawn");
47
47
  const config_2 = require("../config");
48
48
  const resolve_1 = require("./resolve");
49
+ const visibility_deadline_1 = require("../utils/visibility-deadline");
50
+ const action_counters_1 = require("../utils/action-counters");
51
+ const search_attributes_1 = require("../utils/search-attributes");
49
52
  const agent_types_1 = require("../ensemble/agent-types");
50
53
  const default_part_1 = require("../utils/default-part");
51
54
  const adapters_1 = require("../adapters");
@@ -84,6 +87,15 @@ function isRetryableTemporalError(err) {
84
87
  // to call unconditionally.
85
88
  if (err instanceof activity_1.ApplicationFailure)
86
89
  return false;
90
+ // #845 Mode A: a truncated visibility scan (resolveSession's deadline
91
+ // tripped mid-scan, #336/#529) is a LATENCY failure — the target may well
92
+ // exist; the scan just didn't finish. Treat it as transient so the
93
+ // activity retry policy re-runs resolveSession with a fresh 10s deadline
94
+ // (backoff-bounded) instead of collapsing it into a permanent
95
+ // "No active session found". This must NOT re-add an in-resolver retry
96
+ // loop — the bounding lives in Temporal's policy, not a hot-path scan.
97
+ if ((0, visibility_deadline_1.isVisibilityTimeout)(err))
98
+ return true;
87
99
  const e = err;
88
100
  const name = e?.name ?? '';
89
101
  const msg = e?.message ?? '';
@@ -156,8 +168,24 @@ function shouldSkipDuplicateSpawn(attachmentId, phase) {
156
168
  return false; // restart/migrate handoff — must spawn
157
169
  return phase === 'attached' || phase === 'processing' || phase === 'awaiting';
158
170
  }
159
- function createOutboxActivities(client, config, ingestTokens) {
160
- return {
171
+ /**
172
+ * Ring the doorbell sink, swallowing EVERYTHING — a ring must never fail or
173
+ * retry the delivery activity it follows (t11 §1: doorbell loss must be
174
+ * indistinguishable from doorbell-never-sent).
175
+ */
176
+ function ringDoorbell(doorbells, ensemble, playerId) {
177
+ try {
178
+ doorbells?.current?.ring(ensemble, playerId);
179
+ }
180
+ catch {
181
+ /* never propagate — §1 */
182
+ }
183
+ }
184
+ function createOutboxActivities(client, config, ingestTokens, doorbells) {
185
+ // #753 — attribute every Temporal call made by these activities (however
186
+ // deep, e.g. deliverCue → resolveSession → queryHandleWithTimeout) to the
187
+ // outbox dispatch plane.
188
+ return (0, action_counters_1.tagActionSource)('outbox', {
161
189
  async deliverCue(input) {
162
190
  const { ensemble, fromPlayerId, targetPlayerId, message, broadcastId, attachmentTicket } = input;
163
191
  try {
@@ -174,6 +202,10 @@ function createOutboxActivities(client, config, ingestTokens) {
174
202
  ...(broadcastId !== undefined ? { broadcastId } : {}),
175
203
  ...(attachmentTicket !== undefined ? { attachmentTicket } : {}),
176
204
  });
205
+ // T1.1 — the signal above has landed durably in history; ring the
206
+ // doorbell so a connected adapter polls now instead of at its
207
+ // backoff ceiling. Fire-and-forget: never fails this activity.
208
+ ringDoorbell(doorbells, ensemble, targetPlayerId);
177
209
  return { success: true };
178
210
  }
179
211
  catch (err) {
@@ -305,12 +337,19 @@ function createOutboxActivities(client, config, ingestTokens) {
305
337
  taskQueue,
306
338
  args: [sessionInput],
307
339
  workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
340
+ // T0.5 (#747) — read-only fields ride the MEMO, not search
341
+ // attributes (fresh namespaces register only the 5 filter SAs).
308
342
  searchAttributes: {
309
- ...(gitRoot ? { AgentTempoGitRoot: [gitRoot] } : {}),
310
343
  AgentTempoHostname: [os.hostname()],
311
344
  AgentTempoEnsemble: [ensemble],
312
345
  AgentTempoPlayerId: [targetName],
313
346
  },
347
+ memo: {
348
+ ...(gitRoot ? { [search_attributes_1.MEMO_KEYS.gitRoot]: gitRoot } : {}),
349
+ ...(agentDefinition ? { [search_attributes_1.MEMO_KEYS.playerType]: agentDefinition } : {}),
350
+ [search_attributes_1.MEMO_KEYS.isConductor]: isConductor,
351
+ [search_attributes_1.MEMO_KEYS.part]: sessionInput.autoSummary,
352
+ },
314
353
  });
315
354
  log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${sessionId}, held=${!!held})`);
316
355
  return { success: true, sessionId };
@@ -357,6 +396,20 @@ function createOutboxActivities(client, config, ingestTokens) {
357
396
  }
358
397
  }
359
398
  try {
399
+ // T1.1 PR-1 — mint a per-player ingest token for every SDK-family
400
+ // spawn (was Pi-only since 3c Tier-2). The token authenticates the
401
+ // adapter's loopback HTTP calls: `POST /inner/ingest` (Pi fine-tail)
402
+ // and `GET /doorbell/:e/:p` (PR-2's DoorbellClient, all SDK
403
+ // adapters). Single-token-per-workflowId (mint REPLACES) means a
404
+ // restart re-mints and naturally revokes the stale token. Injected
405
+ // into the subprocess env as AGENT_TEMPO_INGEST_TOKEN. Interactive
406
+ // terminal spawns are deliberately excluded — no doorbell client
407
+ // exists for them (PR-2's consumers are SdkAttachment + the Pi
408
+ // runtime); minting extends trivially if that ever changes.
409
+ const ingestToken = agent === 'mock' || agent === 'copilot' || agent === 'claude-api' ||
410
+ agent === 'opencode' || agent === 'claude-code-headless' || agent === 'pi'
411
+ ? ingestTokens?.mint((0, config_1.sessionWorkflowId)(ensemble, targetName))
412
+ : undefined;
360
413
  if (agent === 'mock') {
361
414
  // ADR 0014 PR-2 — mock adapter spawns headless. No terminal,
362
415
  // no Claude binary, no MCP server child. Talks to Temporal
@@ -377,6 +430,7 @@ function createOutboxActivities(client, config, ingestTokens) {
377
430
  attachmentId,
378
431
  attachmentRunId,
379
432
  adapterId,
433
+ ingestToken,
380
434
  });
381
435
  log(`Spawned mock adapter (pid ${pid}) in ${workDir} as "${targetName}" (mode=${mockMode ?? 'echo'}${attachmentId ? `, attachmentId=${attachmentId}` : ''})`);
382
436
  }
@@ -398,6 +452,7 @@ function createOutboxActivities(client, config, ingestTokens) {
398
452
  attachmentId,
399
453
  attachmentRunId,
400
454
  adapterId,
455
+ ingestToken,
401
456
  });
402
457
  log(`Spawned copilot-bridge (pid ${pid}) in ${workDir} as "${targetName}"${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
403
458
  }
@@ -424,6 +479,7 @@ function createOutboxActivities(client, config, ingestTokens) {
424
479
  attachmentId,
425
480
  attachmentRunId,
426
481
  adapterId,
482
+ ingestToken,
427
483
  });
428
484
  log(`Spawned claude-api adapter (pid ${pid}) in ${workDir} as "${targetName}"${model ? ` (model=${model})` : ''}${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
429
485
  }
@@ -452,6 +508,7 @@ function createOutboxActivities(client, config, ingestTokens) {
452
508
  attachmentId,
453
509
  attachmentRunId,
454
510
  adapterId,
511
+ ingestToken,
455
512
  });
456
513
  log(`Spawned opencode adapter (pid ${pid}) in ${workDir} as "${targetName}"${model ? ` (model=${model})` : ''}${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
457
514
  }
@@ -481,6 +538,7 @@ function createOutboxActivities(client, config, ingestTokens) {
481
538
  attachmentId,
482
539
  attachmentRunId,
483
540
  adapterId,
541
+ ingestToken,
484
542
  });
485
543
  log(`Spawned claude-code-headless adapter (pid ${pid}) in ${workDir} as "${targetName}"${permissionMode ? ` (permissionMode=${permissionMode})` : ''}${dangerouslySkipPermissions ? ' (dangerouslySkipPermissions=true)' : ''}${attachmentId ? ` (attachmentId=${attachmentId})` : ''}`);
486
544
  }
@@ -492,12 +550,6 @@ function createOutboxActivities(client, config, ingestTokens) {
492
550
  if (allowedTools && allowedTools.length > 0) {
493
551
  log(`Warning: allowedTools [${allowedTools.join(', ')}] specified for pi agent "${targetName}" — Pi players run the full tool surface, skipping allowedTools`);
494
552
  }
495
- // 3c Tier-2 — mint a per-player ingest token scoped to this player's
496
- // session workflowId so the headless Pi subprocess can authenticate its
497
- // `POST /inner/ingest` frames. Single-token-per-workflowId (mint
498
- // REPLACES) means a restart re-mints and naturally revokes the stale
499
- // token. Injected into the subprocess env as AGENT_TEMPO_INGEST_TOKEN.
500
- const ingestToken = ingestTokens?.mint((0, config_1.sessionWorkflowId)(ensemble, targetName));
501
553
  const { pid } = (0, spawn_1.spawnPiHeadless)({
502
554
  name: targetName,
503
555
  ensemble,
@@ -680,6 +732,12 @@ function createOutboxActivities(client, config, ingestTokens) {
680
732
  // revokeIngestToken(workflowId) on detach — deferred; residual surface
681
733
  // negligible (dead holder + loopback-only + single-token replacement).
682
734
  ingestTokens?.revoke((0, config_1.sessionWorkflowId)(ensemble, targetPlayerId));
735
+ // T1.1 — end the player's doorbell streams too (the registry-side
736
+ // lifecycle mirror of the token revoke; swallow-everything wrapper).
737
+ try {
738
+ doorbells?.current?.closePlayer(ensemble, targetPlayerId);
739
+ }
740
+ catch { /* never propagate */ }
683
741
  if (notifyConductor) {
684
742
  try {
685
743
  const condId = (0, config_1.conductorWorkflowId)(ensemble);
@@ -722,6 +780,9 @@ function createOutboxActivities(client, config, ingestTokens) {
722
780
  ...(reason !== undefined ? { reason } : {}),
723
781
  ...(requestedBy !== undefined ? { requestedBy } : {}),
724
782
  });
783
+ // T1.1 — ring so a stretched pump's next tickReset looks at
784
+ // ding-latency, not its ceiling. D14 semantics untouched (§2.1).
785
+ ringDoorbell(doorbells, ensemble, targetPlayerId);
725
786
  log(`Reset queued for "${targetPlayerId}"${reason ? ` (reason: ${reason})` : ''}`);
726
787
  return { success: true };
727
788
  }
@@ -971,5 +1032,5 @@ function createOutboxActivities(client, config, ingestTokens) {
971
1032
  async hardTerminateAttachment(input) {
972
1033
  return (0, hard_terminate_1.hardTerminateAttachment)(input);
973
1034
  },
974
- };
1035
+ });
975
1036
  }