agent-tempo 1.7.0 → 2.0.0-beta.2

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 (171) hide show
  1. package/CLAUDE.md +10 -13
  2. package/README.md +21 -15
  3. package/dashboard/dist/assets/index-BbR7zIdK.js.map +1 -1
  4. package/dashboard/package.json +1 -1
  5. package/dist/activities/hard-terminate.d.ts +2 -2
  6. package/dist/activities/hard-terminate.js +3 -9
  7. package/dist/activities/maestro.d.ts +8 -13
  8. package/dist/activities/maestro.js +3 -10
  9. package/dist/activities/outbox.js +16 -2
  10. package/dist/activities/resolve.d.ts +18 -7
  11. package/dist/activities/resolve.js +58 -46
  12. package/dist/adapters/base.js +3 -0
  13. package/dist/adapters/claude-code/adapter.js +7 -0
  14. package/dist/cli/command-center-command.js +15 -1
  15. package/dist/cli/commands.d.ts +8 -0
  16. package/dist/cli/commands.js +79 -11
  17. package/dist/cli/daemon-command.js +19 -33
  18. package/dist/cli/help-text.js +9 -7
  19. package/dist/cli/home-command.d.ts +26 -0
  20. package/dist/cli/home-command.js +156 -0
  21. package/dist/cli/removed-verbs.d.ts +0 -4
  22. package/dist/cli/removed-verbs.js +23 -12
  23. package/dist/cli/sa-preflight.d.ts +0 -14
  24. package/dist/cli/sa-preflight.js +1 -17
  25. package/dist/cli/startup.d.ts +1 -10
  26. package/dist/cli/startup.js +0 -40
  27. package/dist/cli.js +34 -80
  28. package/dist/client/core.js +13 -15
  29. package/dist/client/index.d.ts +2 -1
  30. package/dist/client/interface.d.ts +5 -14
  31. package/dist/client/with-spawn.d.ts +9 -4
  32. package/dist/client/with-spawn.js +7 -10
  33. package/dist/config.d.ts +14 -13
  34. package/dist/config.js +14 -0
  35. package/dist/constants.d.ts +50 -0
  36. package/dist/constants.js +60 -1
  37. package/dist/daemon.js +81 -0
  38. package/dist/http/auth.d.ts +3 -21
  39. package/dist/http/auth.js +10 -31
  40. package/dist/http/event-types.d.ts +38 -4
  41. package/dist/http/orphans.d.ts +3 -3
  42. package/dist/http/orphans.js +3 -3
  43. package/dist/http/server.d.ts +0 -5
  44. package/dist/http/server.js +79 -29
  45. package/dist/http/snapshot.js +3 -0
  46. package/dist/http/writes.d.ts +1 -1
  47. package/dist/http/writes.js +25 -2
  48. package/dist/observability/nondeterminism-alarm.d.ts +113 -0
  49. package/dist/observability/nondeterminism-alarm.js +162 -0
  50. package/dist/palette/index.d.ts +6 -7
  51. package/dist/palette/index.js +6 -7
  52. package/dist/pi/mission-control/actions.d.ts +67 -1
  53. package/dist/pi/mission-control/actions.js +61 -0
  54. package/dist/pi/mission-control/extension.d.ts +77 -0
  55. package/dist/pi/mission-control/extension.js +171 -0
  56. package/dist/pi/workflow-client.d.ts +14 -16
  57. package/dist/pi/workflow-client.js +18 -44
  58. package/dist/server-tools.js +30 -29
  59. package/dist/server.js +42 -0
  60. package/dist/tools/action-guard.d.ts +23 -0
  61. package/dist/tools/action-guard.js +33 -0
  62. package/dist/tools/coat-check.d.ts +20 -0
  63. package/dist/tools/coat-check.js +129 -0
  64. package/dist/tools/gate.d.ts +13 -0
  65. package/dist/tools/gate.js +88 -0
  66. package/dist/tools/schedule.d.ts +18 -0
  67. package/dist/tools/schedule.js +93 -1
  68. package/dist/tools/stage.d.ts +17 -0
  69. package/dist/tools/stage.js +85 -1
  70. package/dist/tools/state.d.ts +14 -0
  71. package/dist/tools/state.js +95 -0
  72. package/dist/types.d.ts +54 -7
  73. package/dist/upgrade/boot-guard.d.ts +95 -0
  74. package/dist/upgrade/boot-guard.js +88 -0
  75. package/dist/upgrade/from-upgrade.d.ts +95 -0
  76. package/dist/upgrade/from-upgrade.js +330 -0
  77. package/dist/upgrade/phase-engine.js +4 -1
  78. package/dist/utils/duration.d.ts +3 -6
  79. package/dist/utils/duration.js +3 -6
  80. package/dist/utils/format-hosts.d.ts +1 -1
  81. package/dist/utils/format-hosts.js +0 -2
  82. package/dist/utils/grpc-shutdown-guard.js +0 -4
  83. package/dist/utils/hosts.d.ts +4 -10
  84. package/dist/utils/hosts.js +12 -28
  85. package/dist/utils/orphan-guard.d.ts +37 -0
  86. package/dist/utils/orphan-guard.js +26 -0
  87. package/dist/utils/search-attributes.d.ts +28 -18
  88. package/dist/utils/search-attributes.js +48 -18
  89. package/dist/utils/visibility-deadline.d.ts +16 -0
  90. package/dist/utils/visibility-deadline.js +16 -0
  91. package/dist/workflows/maestro-signals.d.ts +0 -13
  92. package/dist/workflows/maestro-signals.js +6 -14
  93. package/dist/workflows/maestro.js +27 -32
  94. package/dist/workflows/scheduler.js +7 -1
  95. package/dist/workflows/session.js +237 -153
  96. package/dist/workflows/signals.d.ts +9 -1
  97. package/dist/workflows/signals.js +8 -6
  98. package/package.json +1 -6
  99. package/workflow-bundle.js +478 -225
  100. package/dist/cli/legacy-migration.d.ts +0 -35
  101. package/dist/cli/legacy-migration.js +0 -341
  102. package/dist/client/ensure-conductor-spawned.d.ts +0 -35
  103. package/dist/client/ensure-conductor-spawned.js +0 -48
  104. package/dist/tui/App.d.ts +0 -85
  105. package/dist/tui/App.js +0 -1791
  106. package/dist/tui/bootstrap-types.d.ts +0 -46
  107. package/dist/tui/bootstrap-types.js +0 -7
  108. package/dist/tui/commands.d.ts +0 -71
  109. package/dist/tui/commands.js +0 -1375
  110. package/dist/tui/components/ChatView.d.ts +0 -35
  111. package/dist/tui/components/ChatView.js +0 -54
  112. package/dist/tui/components/CommandPalette.d.ts +0 -21
  113. package/dist/tui/components/CommandPalette.js +0 -67
  114. package/dist/tui/components/ConversationStream.d.ts +0 -114
  115. package/dist/tui/components/ConversationStream.js +0 -307
  116. package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
  117. package/dist/tui/components/CreateEnsembleWizard.js +0 -223
  118. package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
  119. package/dist/tui/components/DestroyConfirmModal.js +0 -62
  120. package/dist/tui/components/ErrorView.d.ts +0 -31
  121. package/dist/tui/components/ErrorView.js +0 -129
  122. package/dist/tui/components/HomeView.d.ts +0 -54
  123. package/dist/tui/components/HomeView.js +0 -306
  124. package/dist/tui/components/LoadLineupModal.d.ts +0 -18
  125. package/dist/tui/components/LoadLineupModal.js +0 -79
  126. package/dist/tui/components/MainView.d.ts +0 -21
  127. package/dist/tui/components/MainView.js +0 -107
  128. package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
  129. package/dist/tui/components/NewEnsembleModal.js +0 -73
  130. package/dist/tui/components/Picker.d.ts +0 -23
  131. package/dist/tui/components/Picker.js +0 -70
  132. package/dist/tui/components/PlayerDetailView.d.ts +0 -26
  133. package/dist/tui/components/PlayerDetailView.js +0 -118
  134. package/dist/tui/components/PromptArea.d.ts +0 -50
  135. package/dist/tui/components/PromptArea.js +0 -303
  136. package/dist/tui/components/RecruitWizard.d.ts +0 -17
  137. package/dist/tui/components/RecruitWizard.js +0 -221
  138. package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
  139. package/dist/tui/components/RestoreConfirmModal.js +0 -71
  140. package/dist/tui/components/ScheduleWizard.d.ts +0 -19
  141. package/dist/tui/components/ScheduleWizard.js +0 -259
  142. package/dist/tui/components/Splash.d.ts +0 -23
  143. package/dist/tui/components/Splash.js +0 -221
  144. package/dist/tui/components/StatusBar.d.ts +0 -48
  145. package/dist/tui/components/StatusBar.js +0 -128
  146. package/dist/tui/components/StatusOverlay.d.ts +0 -15
  147. package/dist/tui/components/StatusOverlay.js +0 -76
  148. package/dist/tui/components/TitleBar.d.ts +0 -10
  149. package/dist/tui/components/TitleBar.js +0 -21
  150. package/dist/tui/index.d.ts +0 -15
  151. package/dist/tui/index.js +0 -162
  152. package/dist/tui/ink-context.d.ts +0 -18
  153. package/dist/tui/ink-context.js +0 -59
  154. package/dist/tui/ink-loader.d.ts +0 -26
  155. package/dist/tui/ink-loader.js +0 -42
  156. package/dist/tui/removed-commands.d.ts +0 -9
  157. package/dist/tui/removed-commands.js +0 -22
  158. package/dist/tui/sse-handler.d.ts +0 -52
  159. package/dist/tui/sse-handler.js +0 -157
  160. package/dist/tui/store.d.ts +0 -598
  161. package/dist/tui/store.js +0 -753
  162. package/dist/tui/utils/format.d.ts +0 -56
  163. package/dist/tui/utils/format.js +0 -155
  164. package/dist/tui/utils/fullscreen.d.ts +0 -23
  165. package/dist/tui/utils/fullscreen.js +0 -71
  166. package/dist/tui/utils/history.d.ts +0 -10
  167. package/dist/tui/utils/history.js +0 -85
  168. package/dist/tui/utils/platform.d.ts +0 -45
  169. package/dist/tui/utils/platform.js +0 -258
  170. package/dist/tui/utils/theme.d.ts +0 -21
  171. package/dist/tui/utils/theme.js +0 -24
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CONSUMED_SNAPSHOT_FILENAME = void 0;
4
+ exports.planRecreation = planRecreation;
5
+ exports.collectUndeliveredCues = collectUndeliveredCues;
6
+ exports.archiveSnapshot = archiveSnapshot;
7
+ exports.runFromUpgrade = runFromUpgrade;
8
+ /**
9
+ * #786 — `agent-tempo up --from-upgrade` (the 2.0 side of the cutover).
10
+ *
11
+ * After `upgrade-to-2` (#785) has snapshotted continuity and destroyed every
12
+ * 1.x run, a fresh 2.0 daemon boots cleanly (the boot guard sees zero
13
+ * un-stamped workflows). This module is what `up --from-upgrade` then runs: it
14
+ * reads `upgrade-snapshot-v1.json` and recreates a fresh protocol-2 workflow
15
+ * for every player in the snapshot ROSTER.
16
+ *
17
+ * Each recreated session carries the structural identity needed to exist as a
18
+ * protocol-2 workflow (`ensemble`, `playerId`, `workDir`, `agentType`,
19
+ * `isConductor`, plus the cheap structural memo: gitRoot/playerType) AND the
20
+ * continuity the snapshot captured. The workflow self-stamps the
21
+ * `AgentTempoProtocol` memo on start (and we seed it here too, so the row is
22
+ * stamped even before the first workflow task runs).
23
+ *
24
+ * **Continuity seeding (the #786 fast-follow — a non-droppable beta.1 gate):**
25
+ * - **#334 state slots** — seeded onto each session's `playerState` so the
26
+ * player's own curated context survives the cutover.
27
+ * - **`sessionId`** — seeded onto `metadata.sessionId` so the first re-attach
28
+ * can `--resume` into the original adapter conversation.
29
+ * - **Non-default recruit `model`** — seeded onto `metadata.model` so an
30
+ * ad-hoc claude-api / opencode / pi player is recreated on the SAME model.
31
+ * - **Durable schedules + scheduler** — for any ensemble with captured
32
+ * schedules, the per-ensemble `agentSchedulerWorkflow` is recreated and
33
+ * seeded with the schedule entries (the #786 MVP correctly skipped it since
34
+ * it is lazily created; we recreate it here only when there is intent to
35
+ * restore). Empty-schedule ensembles get NO scheduler — it would just
36
+ * self-terminate after its 30s empty grace.
37
+ *
38
+ * **Re-attach is a SEPARATE operator step — `up --from-upgrade` does NOT
39
+ * re-spawn adapters.** Every recreated session lands in the `booting` phase
40
+ * (no `currentAttachment`, stale-detection suppressed) — a durable skeleton
41
+ * with continuity seeded, awaiting an adapter. Auto-spawning N adapters across
42
+ * hosts during a migration is surprising and risky (terminals popping,
43
+ * cross-host spawns); the snapshot is about durable continuity, not live
44
+ * process resurrection. The operator brings each session live on their own
45
+ * schedule via `restart` / `restore` (which honour the seeded `sessionId` for
46
+ * `--resume`). This is flagged for the migration note in the docs.
47
+ *
48
+ * **Undelivered cues are surfaced for REVIEW only — NEVER redelivered**
49
+ * (replaying stale cues into fresh sessions is worse than operator triage,
50
+ * #785 §4). The caller prints them; this module just collects them.
51
+ *
52
+ * **Archive-on-success:** the snapshot is renamed to
53
+ * `upgrade-snapshot-v1.consumed.json` only when every workflow was recreated
54
+ * without error. On any partial failure the snapshot is left PRISTINE so the
55
+ * operator can fix the cause and re-run (recreation is idempotent via
56
+ * `USE_EXISTING`).
57
+ */
58
+ const fs_1 = require("fs");
59
+ const path_1 = require("path");
60
+ const os_1 = require("os");
61
+ const client_1 = require("@temporalio/client");
62
+ const config_1 = require("../config");
63
+ const search_attributes_1 = require("../utils/search-attributes");
64
+ const constants_1 = require("../constants");
65
+ const default_part_1 = require("../utils/default-part");
66
+ const snapshot_v1_1 = require("./snapshot-v1");
67
+ /** Filename the consumed snapshot is archived to on success. */
68
+ exports.CONSUMED_SNAPSHOT_FILENAME = 'upgrade-snapshot-v1.consumed.json';
69
+ /**
70
+ * Pure planner: turn a snapshot into the list of protocol-2 workflows to
71
+ * recreate. One `agentMaestroWorkflow` per ensemble (so the ensemble is
72
+ * coordinated + discoverable) plus one `agentSessionWorkflow` per roster player
73
+ * (conductor via {@link conductorWorkflowId}, others via
74
+ * {@link sessionWorkflowId}). Structural essentials only — see the module
75
+ * header. Exported so the recreation set is unit-testable without Temporal.
76
+ */
77
+ function planRecreation(snapshot, opts) {
78
+ const specs = [];
79
+ for (const ensemble of snapshot.ensembles) {
80
+ // Per-ensemble maestro hub — structural coordination workflow. Recreated so
81
+ // the ensemble is observable; idempotent via USE_EXISTING at start time.
82
+ specs.push({
83
+ workflowId: (0, config_1.maestroWorkflowId)(ensemble.name),
84
+ workflowType: 'agentMaestroWorkflow',
85
+ ensemble: ensemble.name,
86
+ args: [{ ensemble: ensemble.name }],
87
+ searchAttributes: { AgentTempoEnsemble: [ensemble.name] },
88
+ memo: { [search_attributes_1.MEMO_KEYS.protocol]: constants_1.PROTOCOL_VERSION },
89
+ });
90
+ for (const player of ensemble.players) {
91
+ const workflowId = player.isConductor
92
+ ? (0, config_1.conductorWorkflowId)(ensemble.name)
93
+ : (0, config_1.sessionWorkflowId)(ensemble.name, player.playerId);
94
+ const playerHostname = player.hostname || opts.hostname;
95
+ const autoSummary = (0, default_part_1.defaultPart)({
96
+ playerType: player.playerType,
97
+ isConductor: player.isConductor,
98
+ workDir: player.workDir,
99
+ adapterType: player.agentType,
100
+ });
101
+ const playerState = seedPlayerState(player, snapshot.createdAt);
102
+ const input = {
103
+ metadata: {
104
+ playerId: player.playerId,
105
+ ensemble: ensemble.name,
106
+ hostname: playerHostname,
107
+ workDir: player.workDir,
108
+ isConductor: player.isConductor,
109
+ agentType: player.agentType,
110
+ ...(player.gitRoot ? { gitRoot: player.gitRoot } : {}),
111
+ ...(player.playerType ? { playerType: player.playerType } : {}),
112
+ // Resume pointer — lets the first re-attach `--resume` into the
113
+ // original adapter conversation (#785 continuity).
114
+ ...(player.sessionId ? { sessionId: player.sessionId } : {}),
115
+ // Non-default recruit model — ad-hoc claude-api/opencode/pi players
116
+ // are recreated on the SAME model (#131 / #449 / #734).
117
+ ...(player.model ? { model: player.model } : {}),
118
+ },
119
+ // Carried so the stamp is in history from run 1; the workflow also
120
+ // upserts it authoritatively from the constant.
121
+ protocol: constants_1.PROTOCOL_VERSION,
122
+ autoSummary,
123
+ // #334 saveable-state slots — the player's own curated continuity.
124
+ // Omitted entirely for the no-state case (mirrors the workflow's
125
+ // populated-only CAN carry idiom).
126
+ ...(playerState ? { playerState } : {}),
127
+ // Recreated skeletons have no live adapter yet — suppress stale
128
+ // detection until one attaches (mirrors recruit / lineup pre-create).
129
+ disableStaleDetection: true,
130
+ temporalConfig: {
131
+ temporalAddress: opts.temporalAddress,
132
+ temporalNamespace: opts.temporalNamespace,
133
+ taskQueue: opts.taskQueue,
134
+ },
135
+ };
136
+ specs.push({
137
+ workflowId,
138
+ workflowType: 'agentSessionWorkflow',
139
+ ensemble: ensemble.name,
140
+ playerId: player.playerId,
141
+ args: [input],
142
+ searchAttributes: {
143
+ AgentTempoHostname: [playerHostname],
144
+ AgentTempoEnsemble: [ensemble.name],
145
+ AgentTempoPlayerId: [player.playerId],
146
+ },
147
+ memo: {
148
+ ...(player.gitRoot ? { [search_attributes_1.MEMO_KEYS.gitRoot]: player.gitRoot } : {}),
149
+ ...(player.playerType ? { [search_attributes_1.MEMO_KEYS.playerType]: player.playerType } : {}),
150
+ [search_attributes_1.MEMO_KEYS.isConductor]: player.isConductor,
151
+ [search_attributes_1.MEMO_KEYS.part]: autoSummary,
152
+ [search_attributes_1.MEMO_KEYS.workDir]: player.workDir,
153
+ [search_attributes_1.MEMO_KEYS.agentType]: player.agentType,
154
+ // Seed the protocol stamp on the START memo so the boot guard sees a
155
+ // stamped row even before the workflow's first task runs.
156
+ [search_attributes_1.MEMO_KEYS.protocol]: constants_1.PROTOCOL_VERSION,
157
+ },
158
+ });
159
+ }
160
+ // Per-ensemble scheduler — recreated ONLY when there is captured intent to
161
+ // restore. The scheduler is lazily created (it self-terminates after a 30s
162
+ // empty grace), so seeding an empty one is pointless; we recreate it solely
163
+ // to re-home the durable schedule entries (#785 §4). The scheduler workflow
164
+ // self-stamps the protocol memo on start; we also seed it on the START memo
165
+ // so the boot guard sees a stamped row immediately.
166
+ if (ensemble.schedules.length > 0) {
167
+ specs.push({
168
+ workflowId: (0, config_1.schedulerWorkflowId)(ensemble.name),
169
+ workflowType: 'agentSchedulerWorkflow',
170
+ ensemble: ensemble.name,
171
+ args: [{ ensemble: ensemble.name, entries: ensemble.schedules.map(toScheduleEntry) }],
172
+ searchAttributes: { AgentTempoEnsemble: [ensemble.name] },
173
+ memo: { [search_attributes_1.MEMO_KEYS.protocol]: constants_1.PROTOCOL_VERSION },
174
+ });
175
+ }
176
+ }
177
+ return specs;
178
+ }
179
+ /**
180
+ * Convert a player's captured #334 state slots (`key → content`) into the
181
+ * `playerState` map the session workflow seeds at boot. `save_state` is
182
+ * owner-only, so `savedBy` is the player itself; `savedAt` uses the snapshot's
183
+ * freeze time (the best available timestamp — the original per-slot write time
184
+ * is not captured in the v1 contract). Returns `undefined` for the common
185
+ * no-state case so the field is omitted from the seed input.
186
+ */
187
+ function seedPlayerState(player, frozenAt) {
188
+ const keys = Object.keys(player.stateSlots);
189
+ if (keys.length === 0)
190
+ return undefined;
191
+ const out = {};
192
+ for (const key of keys) {
193
+ out[key] = { content: player.stateSlots[key], savedAt: frozenAt, savedBy: player.playerId };
194
+ }
195
+ return out;
196
+ }
197
+ /**
198
+ * Convert a captured {@link SnapshotSchedule} (a frozen, decoupled copy) back
199
+ * into a live {@link ScheduleEntry} for the recreated scheduler. `firedCount`
200
+ * is not part of the durable-intent contract (it is a runtime accumulator), so
201
+ * it resets to 0 — the recreated schedule starts a fresh fire history.
202
+ */
203
+ function toScheduleEntry(s) {
204
+ return {
205
+ name: s.name,
206
+ message: s.message,
207
+ target: s.target,
208
+ createdBy: s.createdBy,
209
+ nextFireAt: s.nextFireAt,
210
+ type: s.type,
211
+ firedCount: 0,
212
+ ...(s.interval !== undefined ? { interval: s.interval } : {}),
213
+ ...(s.cronExpression ? { cronExpression: s.cronExpression } : {}),
214
+ ...(s.timezone ? { timezone: s.timezone } : {}),
215
+ ...(s.until ? { until: s.until } : {}),
216
+ ...(s.remainingCount !== undefined ? { remainingCount: s.remainingCount } : {}),
217
+ };
218
+ }
219
+ /** Collect every undelivered cue across the snapshot (review only). */
220
+ function collectUndeliveredCues(snapshot) {
221
+ const out = [];
222
+ for (const ensemble of snapshot.ensembles) {
223
+ for (const player of ensemble.players) {
224
+ for (const msg of player.undeliveredMessages) {
225
+ out.push({
226
+ ensemble: ensemble.name,
227
+ playerId: player.playerId,
228
+ from: msg.from,
229
+ text: msg.text,
230
+ timestamp: msg.timestamp,
231
+ });
232
+ }
233
+ }
234
+ }
235
+ return out;
236
+ }
237
+ /**
238
+ * Archive the consumed snapshot: `upgrade-snapshot-v1.json` →
239
+ * `upgrade-snapshot-v1.consumed.json`. Overwrites a prior `.consumed.json`
240
+ * from an earlier run (rename is atomic on the same volume). Returns the
241
+ * archive path.
242
+ */
243
+ function archiveSnapshot(home) {
244
+ const from = (0, snapshot_v1_1.snapshotPath)(home);
245
+ const to = (0, path_1.join)(home, exports.CONSUMED_SNAPSHOT_FILENAME);
246
+ (0, fs_1.renameSync)(from, to);
247
+ return to;
248
+ }
249
+ /**
250
+ * Recreate protocol-2 workflows from the on-disk upgrade snapshot.
251
+ *
252
+ * Returns a structured result; never throws on a per-workflow failure (those
253
+ * are collected in {@link FromUpgradeResult.failures}). A malformed snapshot
254
+ * DOES throw (via `readSnapshot` → `SnapshotValidationError`) — a corrupt
255
+ * continuity file is a loud failure, never a silent skip.
256
+ */
257
+ async function runFromUpgrade(client, opts) {
258
+ const log = opts.log ?? (() => { });
259
+ const snapshot = (0, snapshot_v1_1.readSnapshot)(opts.home); // throws (loud) on a malformed file
260
+ if (!snapshot) {
261
+ return {
262
+ ok: false,
263
+ snapshotFound: false,
264
+ ensemblesRecreated: 0,
265
+ workflowsCreated: 0,
266
+ failures: [],
267
+ undeliveredCues: [],
268
+ archived: false,
269
+ };
270
+ }
271
+ const specs = planRecreation(snapshot, {
272
+ hostname: opts.hostname ?? (0, os_1.hostname)(),
273
+ taskQueue: opts.taskQueue,
274
+ temporalAddress: opts.temporalAddress,
275
+ temporalNamespace: opts.temporalNamespace,
276
+ });
277
+ const failures = [];
278
+ let created = 0;
279
+ for (const spec of specs) {
280
+ try {
281
+ await client.workflow.start(spec.workflowType, {
282
+ workflowId: spec.workflowId,
283
+ taskQueue: opts.taskQueue,
284
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
285
+ args: spec.args,
286
+ // Idempotent re-run: an already-recreated workflow is left as-is.
287
+ workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
288
+ searchAttributes: spec.searchAttributes,
289
+ memo: spec.memo,
290
+ });
291
+ created++;
292
+ log(`from-upgrade: recreated ${spec.workflowType} ${spec.workflowId}`);
293
+ }
294
+ catch (err) {
295
+ const error = err instanceof Error ? err.message : String(err);
296
+ failures.push({ workflowId: spec.workflowId, error });
297
+ log(`from-upgrade: FAILED to recreate ${spec.workflowId}: ${error}`);
298
+ }
299
+ }
300
+ const undeliveredCues = collectUndeliveredCues(snapshot);
301
+ // Archive ONLY on full success — leave the snapshot pristine on partial fail
302
+ // so the operator can fix the cause and re-run.
303
+ let archived = false;
304
+ let archivePath;
305
+ if (failures.length === 0) {
306
+ try {
307
+ archivePath = archiveSnapshot(opts.home);
308
+ archived = true;
309
+ log(`from-upgrade: archived snapshot → ${exports.CONSUMED_SNAPSHOT_FILENAME}`);
310
+ }
311
+ catch (err) {
312
+ // Recreation succeeded but the archive rename failed (e.g. a lock on
313
+ // Windows). Don't fail the run — surface it so the operator can move the
314
+ // file manually (a re-run is a harmless idempotent no-op).
315
+ log(`from-upgrade: WARNING — recreation succeeded but archiving ${snapshot_v1_1.SNAPSHOT_FILENAME} ` +
316
+ `failed: ${err instanceof Error ? err.message : String(err)}. ` +
317
+ `Move it aside manually so a future boot doesn't re-read it.`);
318
+ }
319
+ }
320
+ return {
321
+ ok: failures.length === 0,
322
+ snapshotFound: true,
323
+ ensemblesRecreated: snapshot.ensembles.length,
324
+ workflowsCreated: created,
325
+ failures,
326
+ undeliveredCues,
327
+ archived,
328
+ ...(archivePath ? { archivePath } : {}),
329
+ };
330
+ }
@@ -245,7 +245,10 @@ async function enumerateEnsembleNames(deps) {
245
245
  }
246
246
  async function fetchHostProfiles(deps, queryTimeoutMs) {
247
247
  try {
248
- return (await (0, query_timeout_1.queryHandleWithTimeout)(deps.client.workflow.getHandle(config_1.GLOBAL_MAESTRO_WORKFLOW_ID), maestro_signals_1.hostProfilesQuery, { timeoutMs: queryTimeoutMs }));
248
+ // 2.0 (#788): legacy `hostProfilesQuery` removed use the combined
249
+ // existence+profiles query and take the profiles map.
250
+ const result = (await (0, query_timeout_1.queryHandleWithTimeout)(deps.client.workflow.getHandle(config_1.GLOBAL_MAESTRO_WORKFLOW_ID), maestro_signals_1.hostProfilesWithExistenceQuery, { timeoutMs: queryTimeoutMs }));
251
+ return result.profiles;
249
252
  }
250
253
  catch {
251
254
  return {};
@@ -20,11 +20,8 @@ export declare function formatDurationMs(ms: number): string;
20
20
  *
21
21
  * `now` is injectable for deterministic tests; defaults to `Date.now()`.
22
22
  *
23
- * **Why a second time helper exists** (the TUI has `formatRelativeTime`
24
- * in `src/tui/utils/format.ts` doing roughly the same thing):
25
- * `src/tui/` carries Ink/React transitive imports the headless tools layer
26
- * mustn't take on. This helper has zero dependencies and is freely
27
- * importable by any layer. Consolidation across the two homes is tracked
28
- * as future deduplication work, not in scope here.
23
+ * This helper has zero dependencies and is freely importable by any layer.
24
+ * (It once co-existed with the TUI's `formatRelativeTime`; the Ink TUI was
25
+ * deleted in #789, so this is now the single relative-time helper.)
29
26
  */
30
27
  export declare function formatTimeAgo(iso: string, now?: number): string;
@@ -45,12 +45,9 @@ function formatDurationMs(ms) {
45
45
  *
46
46
  * `now` is injectable for deterministic tests; defaults to `Date.now()`.
47
47
  *
48
- * **Why a second time helper exists** (the TUI has `formatRelativeTime`
49
- * in `src/tui/utils/format.ts` doing roughly the same thing):
50
- * `src/tui/` carries Ink/React transitive imports the headless tools layer
51
- * mustn't take on. This helper has zero dependencies and is freely
52
- * importable by any layer. Consolidation across the two homes is tracked
53
- * as future deduplication work, not in scope here.
48
+ * This helper has zero dependencies and is freely importable by any layer.
49
+ * (It once co-existed with the TUI's `formatRelativeTime`; the Ink TUI was
50
+ * deleted in #789, so this is now the single relative-time helper.)
54
51
  */
55
52
  function formatTimeAgo(iso, now = Date.now()) {
56
53
  const t = Date.parse(iso);
@@ -4,7 +4,7 @@
4
4
  * Single source of truth consumed by:
5
5
  * - `src/tools/hosts.ts` (MCP tool) — formatted text in `ok(...)` payload
6
6
  * - `src/cli/commands.ts` (CLI `hosts` command) — stdout table
7
- * - `src/tui/commands.ts` (TUI `/hosts` slash) — overlay content
7
+ * - `src/pi/mission-control/` (command-center `/hosts` board command) — board text
8
8
  *
9
9
  * Pure: no Temporal, no I/O, no ANSI colors (the CLI can wrap lines in
10
10
  * color downstream if desired; the formatter emits plain strings so
@@ -40,8 +40,6 @@ function formatSingleHost(host) {
40
40
  bits.push(`workers: ${workerBits.length > 0 ? workerBits.join('+') : 'none'}`);
41
41
  if (inst.lastAccessTime)
42
42
  bits.push(`last seen ${inst.lastAccessTime}`);
43
- if (inst.legacy)
44
- bits.push('legacy-identity');
45
43
  lines.push(` - ${bits.join(' · ')}`);
46
44
  }
47
45
  // Profile (capability).
@@ -54,10 +54,6 @@ const handler = (err) => {
54
54
  if (isBenignChannelShutdown(err)) {
55
55
  // A Temporal gRPC retry timer fired after we closed the connection. The
56
56
  // result we cared about was already captured (or degraded). Drop it.
57
- if (process.env.CLAUDE_TEMPO_DEBUG) {
58
- // eslint-disable-next-line no-console
59
- console.error('[agent-tempo] ignored post-shutdown gRPC channel error (benign Temporal retry-after-close race)');
60
- }
61
57
  return;
62
58
  }
63
59
  // Not ours — restore default crash behavior. Throwing from inside an
@@ -54,26 +54,20 @@ export interface ParsedIdentity {
54
54
  hostname: string;
55
55
  pid: number;
56
56
  version: string;
57
- /** `true` when the identity was in the legacy `<pid>@<hostname>` SDK-default shape. */
58
- legacy: boolean;
59
57
  }
60
58
  /**
61
59
  * Parse a Temporal poller identity back into the daemon that emitted it.
62
60
  *
63
61
  * Returns `null` for opaque / third-party identities (e.g. Temporal's own
64
- * system pollers or an unrelated worker sharing the namespace). Callers
65
- * skip those silently.
62
+ * system pollers, an unrelated worker sharing the namespace, or an SDK
63
+ * worker that left the default `<pid>@<hostname>` identity). Callers skip
64
+ * those silently.
66
65
  *
67
- * The v1.0 format (`agent-tempo:<hostname>:<pid>:<version>`) is
66
+ * The agent-tempo format (`agent-tempo:<hostname>:<pid>:<version>`) is
68
67
  * guaranteed to have exactly 4 colon-delimited segments because every
69
68
  * component has its own validation: hostname passes `PLAYER_NAME_REGEX`
70
69
  * (no colons possible), pid is numeric, and version is a semver-ish
71
70
  * string (no colons).
72
- *
73
- * Pre-v1.0 daemons emitted a `claude-tempo:` prefix. Under the v1.0 hard
74
- * break those daemons can't reach v1.x task queues anyway, but if one does
75
- * surface in a host listing we tag it `legacy: true` so operators can
76
- * distinguish a still-running old daemon from a fresh v1.x worker.
77
71
  */
78
72
  export declare function parseIdentity(identity: string): ParsedIdentity | null;
79
73
  interface RawPoller {
@@ -43,41 +43,26 @@ function __resetHostsCacheForTests() {
43
43
  * Parse a Temporal poller identity back into the daemon that emitted it.
44
44
  *
45
45
  * Returns `null` for opaque / third-party identities (e.g. Temporal's own
46
- * system pollers or an unrelated worker sharing the namespace). Callers
47
- * skip those silently.
46
+ * system pollers, an unrelated worker sharing the namespace, or an SDK
47
+ * worker that left the default `<pid>@<hostname>` identity). Callers skip
48
+ * those silently.
48
49
  *
49
- * The v1.0 format (`agent-tempo:<hostname>:<pid>:<version>`) is
50
+ * The agent-tempo format (`agent-tempo:<hostname>:<pid>:<version>`) is
50
51
  * guaranteed to have exactly 4 colon-delimited segments because every
51
52
  * component has its own validation: hostname passes `PLAYER_NAME_REGEX`
52
53
  * (no colons possible), pid is numeric, and version is a semver-ish
53
54
  * string (no colons).
54
- *
55
- * Pre-v1.0 daemons emitted a `claude-tempo:` prefix. Under the v1.0 hard
56
- * break those daemons can't reach v1.x task queues anyway, but if one does
57
- * surface in a host listing we tag it `legacy: true` so operators can
58
- * distinguish a still-running old daemon from a fresh v1.x worker.
59
55
  */
60
56
  function parseIdentity(identity) {
61
- if (identity.startsWith('agent-tempo:') || identity.startsWith('claude-tempo:')) {
62
- const legacy = identity.startsWith('claude-tempo:');
63
- const parts = identity.split(':');
64
- if (parts.length === 4) {
65
- const [, hostname, pidStr, version] = parts;
66
- const pid = Number(pidStr);
67
- if (hostname.length > 0 && Number.isFinite(pid) && pid > 0 && version.length > 0) {
68
- return { hostname, pid, version, legacy };
69
- }
70
- }
57
+ if (!identity.startsWith('agent-tempo:'))
71
58
  return null;
72
- }
73
- // Legacy SDK default format: `<pid>@<hostname>`
74
- const legacyMatch = identity.match(/^(\d+)@(.+)$/);
75
- if (legacyMatch) {
76
- const pid = Number(legacyMatch[1]);
77
- const hostname = legacyMatch[2];
78
- if (Number.isFinite(pid) && pid > 0 && hostname.length > 0) {
79
- return { hostname, pid, version: 'unknown', legacy: true };
80
- }
59
+ const parts = identity.split(':');
60
+ if (parts.length !== 4)
61
+ return null;
62
+ const [, hostname, pidStr, version] = parts;
63
+ const pid = Number(pidStr);
64
+ if (hostname.length > 0 && Number.isFinite(pid) && pid > 0 && version.length > 0) {
65
+ return { hostname, pid, version };
81
66
  }
82
67
  return null;
83
68
  }
@@ -192,7 +177,6 @@ async function listHosts(client, opts = {}) {
192
177
  hasWorkflowWorker: false,
193
178
  hasActivityWorker: false,
194
179
  hasHostQueueWorker: false,
195
- ...(ident.legacy ? { legacy: true } : {}),
196
180
  };
197
181
  byPid.set(pid, inst);
198
182
  }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * #704 Item 1b — late-orphan self-tombstone discriminator (pure).
3
+ *
4
+ * A recruited process can launch LONG after its recruit was cancelled (a slow /
5
+ * wedged cold start). When it finally boots and reaches the workflow-start site
6
+ * (`server.ts`), this predicate decides whether it is an ORPHAN of a cancelled
7
+ * recruit (→ self-exit, do not re-register) or a legitimate (re-)start (→ proceed).
8
+ *
9
+ * The discriminator is the **running-run × close-reason PAIR**:
10
+ * - If a RUNNING run exists for the derived id, this process is NOT an orphan —
11
+ * every managed re-creation (recruit / restart / migrate / up) pre-creates a
12
+ * RUNNING run before its process boots, so a legit reuse is seen as RUNNING and
13
+ * simply attaches via `USE_EXISTING`. Never self-exit.
14
+ * - Otherwise, the most-recent run is CLOSED. Self-exit ONLY when it closed with a
15
+ * typed tombstone reason (`destroyed` | `boot-timeout`) within a generous TTL.
16
+ * The TTL bounds a stale tombstone so a much-later legit manual reuse of the
17
+ * same name isn't blocked forever (the observed orphan spawn delay was ~100min).
18
+ *
19
+ * Pure + dependency-free so it unit-tests without a live Temporal env; `server.ts`
20
+ * supplies the `describe()`-derived inputs.
21
+ */
22
+ export interface OrphanTombstoneInput {
23
+ /** `describe().status.name` — e.g. `'RUNNING'`, `'COMPLETED'`, `'TERMINATED'`. */
24
+ statusName: string;
25
+ /** `describe().memo?.[MEMO_KEYS.closeReason]` — the typed close reason, if any. */
26
+ closeReason: unknown;
27
+ /** `describe().closeTime?.getTime() ?? 0` — ms epoch of run close, `0` if open/unknown. */
28
+ closeTimeMs: number;
29
+ }
30
+ /**
31
+ * Returns true iff the booting process should self-exit as a late orphan.
32
+ *
33
+ * @param input the `describe()`-derived run state for the derived workflow id
34
+ * @param ttlMs how long a tombstone is honored (ms); `> 0`
35
+ * @param nowMs current wall-clock ms (`Date.now()`)
36
+ */
37
+ export declare function shouldSelfExitAsOrphan(input: OrphanTombstoneInput, ttlMs: number, nowMs: number): boolean;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldSelfExitAsOrphan = shouldSelfExitAsOrphan;
4
+ /** Close reasons that tombstone a derived id against orphan re-registration. */
5
+ const TOMBSTONE_REASONS = new Set(['destroyed', 'boot-timeout']);
6
+ /**
7
+ * Returns true iff the booting process should self-exit as a late orphan.
8
+ *
9
+ * @param input the `describe()`-derived run state for the derived workflow id
10
+ * @param ttlMs how long a tombstone is honored (ms); `> 0`
11
+ * @param nowMs current wall-clock ms (`Date.now()`)
12
+ */
13
+ function shouldSelfExitAsOrphan(input, ttlMs, nowMs) {
14
+ // A running run means a legit (re-)creation already exists — attach, don't exit.
15
+ if (input.statusName === 'RUNNING')
16
+ return false;
17
+ // Only a typed tombstone close-reason qualifies.
18
+ if (typeof input.closeReason !== 'string' || !TOMBSTONE_REASONS.has(input.closeReason)) {
19
+ return false;
20
+ }
21
+ // Bound staleness: a tombstone older than the TTL is released so a legit
22
+ // much-later reuse of the same derived id isn't blocked.
23
+ if (!(input.closeTimeMs > 0))
24
+ return false;
25
+ return nowMs - input.closeTimeMs <= ttlMs;
26
+ }