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.
- package/CLAUDE.md +10 -13
- package/README.md +21 -15
- package/dashboard/dist/assets/index-BbR7zIdK.js.map +1 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/hard-terminate.d.ts +2 -2
- package/dist/activities/hard-terminate.js +3 -9
- package/dist/activities/maestro.d.ts +8 -13
- package/dist/activities/maestro.js +3 -10
- package/dist/activities/outbox.js +16 -2
- package/dist/activities/resolve.d.ts +18 -7
- package/dist/activities/resolve.js +58 -46
- package/dist/adapters/base.js +3 -0
- package/dist/adapters/claude-code/adapter.js +7 -0
- package/dist/cli/command-center-command.js +15 -1
- package/dist/cli/commands.d.ts +8 -0
- package/dist/cli/commands.js +79 -11
- package/dist/cli/daemon-command.js +19 -33
- package/dist/cli/help-text.js +9 -7
- package/dist/cli/home-command.d.ts +26 -0
- package/dist/cli/home-command.js +156 -0
- package/dist/cli/removed-verbs.d.ts +0 -4
- package/dist/cli/removed-verbs.js +23 -12
- package/dist/cli/sa-preflight.d.ts +0 -14
- package/dist/cli/sa-preflight.js +1 -17
- package/dist/cli/startup.d.ts +1 -10
- package/dist/cli/startup.js +0 -40
- package/dist/cli.js +34 -80
- package/dist/client/core.js +13 -15
- package/dist/client/index.d.ts +2 -1
- package/dist/client/interface.d.ts +5 -14
- package/dist/client/with-spawn.d.ts +9 -4
- package/dist/client/with-spawn.js +7 -10
- package/dist/config.d.ts +14 -13
- package/dist/config.js +14 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.js +60 -1
- package/dist/daemon.js +81 -0
- package/dist/http/auth.d.ts +3 -21
- package/dist/http/auth.js +10 -31
- package/dist/http/event-types.d.ts +38 -4
- package/dist/http/orphans.d.ts +3 -3
- package/dist/http/orphans.js +3 -3
- package/dist/http/server.d.ts +0 -5
- package/dist/http/server.js +79 -29
- package/dist/http/snapshot.js +3 -0
- package/dist/http/writes.d.ts +1 -1
- package/dist/http/writes.js +25 -2
- package/dist/observability/nondeterminism-alarm.d.ts +113 -0
- package/dist/observability/nondeterminism-alarm.js +162 -0
- package/dist/palette/index.d.ts +6 -7
- package/dist/palette/index.js +6 -7
- package/dist/pi/mission-control/actions.d.ts +67 -1
- package/dist/pi/mission-control/actions.js +61 -0
- package/dist/pi/mission-control/extension.d.ts +77 -0
- package/dist/pi/mission-control/extension.js +171 -0
- package/dist/pi/workflow-client.d.ts +14 -16
- package/dist/pi/workflow-client.js +18 -44
- package/dist/server-tools.js +30 -29
- package/dist/server.js +42 -0
- package/dist/tools/action-guard.d.ts +23 -0
- package/dist/tools/action-guard.js +33 -0
- package/dist/tools/coat-check.d.ts +20 -0
- package/dist/tools/coat-check.js +129 -0
- package/dist/tools/gate.d.ts +13 -0
- package/dist/tools/gate.js +88 -0
- package/dist/tools/schedule.d.ts +18 -0
- package/dist/tools/schedule.js +93 -1
- package/dist/tools/stage.d.ts +17 -0
- package/dist/tools/stage.js +85 -1
- package/dist/tools/state.d.ts +14 -0
- package/dist/tools/state.js +95 -0
- package/dist/types.d.ts +54 -7
- package/dist/upgrade/boot-guard.d.ts +95 -0
- package/dist/upgrade/boot-guard.js +88 -0
- package/dist/upgrade/from-upgrade.d.ts +95 -0
- package/dist/upgrade/from-upgrade.js +330 -0
- package/dist/upgrade/phase-engine.js +4 -1
- package/dist/utils/duration.d.ts +3 -6
- package/dist/utils/duration.js +3 -6
- package/dist/utils/format-hosts.d.ts +1 -1
- package/dist/utils/format-hosts.js +0 -2
- package/dist/utils/grpc-shutdown-guard.js +0 -4
- package/dist/utils/hosts.d.ts +4 -10
- package/dist/utils/hosts.js +12 -28
- package/dist/utils/orphan-guard.d.ts +37 -0
- package/dist/utils/orphan-guard.js +26 -0
- package/dist/utils/search-attributes.d.ts +28 -18
- package/dist/utils/search-attributes.js +48 -18
- package/dist/utils/visibility-deadline.d.ts +16 -0
- package/dist/utils/visibility-deadline.js +16 -0
- package/dist/workflows/maestro-signals.d.ts +0 -13
- package/dist/workflows/maestro-signals.js +6 -14
- package/dist/workflows/maestro.js +27 -32
- package/dist/workflows/scheduler.js +7 -1
- package/dist/workflows/session.js +237 -153
- package/dist/workflows/signals.d.ts +9 -1
- package/dist/workflows/signals.js +8 -6
- package/package.json +1 -6
- package/workflow-bundle.js +478 -225
- package/dist/cli/legacy-migration.d.ts +0 -35
- package/dist/cli/legacy-migration.js +0 -341
- package/dist/client/ensure-conductor-spawned.d.ts +0 -35
- package/dist/client/ensure-conductor-spawned.js +0 -48
- package/dist/tui/App.d.ts +0 -85
- package/dist/tui/App.js +0 -1791
- package/dist/tui/bootstrap-types.d.ts +0 -46
- package/dist/tui/bootstrap-types.js +0 -7
- package/dist/tui/commands.d.ts +0 -71
- package/dist/tui/commands.js +0 -1375
- package/dist/tui/components/ChatView.d.ts +0 -35
- package/dist/tui/components/ChatView.js +0 -54
- package/dist/tui/components/CommandPalette.d.ts +0 -21
- package/dist/tui/components/CommandPalette.js +0 -67
- package/dist/tui/components/ConversationStream.d.ts +0 -114
- package/dist/tui/components/ConversationStream.js +0 -307
- package/dist/tui/components/CreateEnsembleWizard.d.ts +0 -19
- package/dist/tui/components/CreateEnsembleWizard.js +0 -223
- package/dist/tui/components/DestroyConfirmModal.d.ts +0 -17
- package/dist/tui/components/DestroyConfirmModal.js +0 -62
- package/dist/tui/components/ErrorView.d.ts +0 -31
- package/dist/tui/components/ErrorView.js +0 -129
- package/dist/tui/components/HomeView.d.ts +0 -54
- package/dist/tui/components/HomeView.js +0 -306
- package/dist/tui/components/LoadLineupModal.d.ts +0 -18
- package/dist/tui/components/LoadLineupModal.js +0 -79
- package/dist/tui/components/MainView.d.ts +0 -21
- package/dist/tui/components/MainView.js +0 -107
- package/dist/tui/components/NewEnsembleModal.d.ts +0 -9
- package/dist/tui/components/NewEnsembleModal.js +0 -73
- package/dist/tui/components/Picker.d.ts +0 -23
- package/dist/tui/components/Picker.js +0 -70
- package/dist/tui/components/PlayerDetailView.d.ts +0 -26
- package/dist/tui/components/PlayerDetailView.js +0 -118
- package/dist/tui/components/PromptArea.d.ts +0 -50
- package/dist/tui/components/PromptArea.js +0 -303
- package/dist/tui/components/RecruitWizard.d.ts +0 -17
- package/dist/tui/components/RecruitWizard.js +0 -221
- package/dist/tui/components/RestoreConfirmModal.d.ts +0 -18
- package/dist/tui/components/RestoreConfirmModal.js +0 -71
- package/dist/tui/components/ScheduleWizard.d.ts +0 -19
- package/dist/tui/components/ScheduleWizard.js +0 -259
- package/dist/tui/components/Splash.d.ts +0 -23
- package/dist/tui/components/Splash.js +0 -221
- package/dist/tui/components/StatusBar.d.ts +0 -48
- package/dist/tui/components/StatusBar.js +0 -128
- package/dist/tui/components/StatusOverlay.d.ts +0 -15
- package/dist/tui/components/StatusOverlay.js +0 -76
- package/dist/tui/components/TitleBar.d.ts +0 -10
- package/dist/tui/components/TitleBar.js +0 -21
- package/dist/tui/index.d.ts +0 -15
- package/dist/tui/index.js +0 -162
- package/dist/tui/ink-context.d.ts +0 -18
- package/dist/tui/ink-context.js +0 -59
- package/dist/tui/ink-loader.d.ts +0 -26
- package/dist/tui/ink-loader.js +0 -42
- package/dist/tui/removed-commands.d.ts +0 -9
- package/dist/tui/removed-commands.js +0 -22
- package/dist/tui/sse-handler.d.ts +0 -52
- package/dist/tui/sse-handler.js +0 -157
- package/dist/tui/store.d.ts +0 -598
- package/dist/tui/store.js +0 -753
- package/dist/tui/utils/format.d.ts +0 -56
- package/dist/tui/utils/format.js +0 -155
- package/dist/tui/utils/fullscreen.d.ts +0 -23
- package/dist/tui/utils/fullscreen.js +0 -71
- package/dist/tui/utils/history.d.ts +0 -10
- package/dist/tui/utils/history.js +0 -85
- package/dist/tui/utils/platform.d.ts +0 -45
- package/dist/tui/utils/platform.js +0 -258
- package/dist/tui/utils/theme.d.ts +0 -21
- 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
|
-
|
|
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 {};
|
package/dist/utils/duration.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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;
|
package/dist/utils/duration.js
CHANGED
|
@@ -45,12 +45,9 @@ function formatDurationMs(ms) {
|
|
|
45
45
|
*
|
|
46
46
|
* `now` is injectable for deterministic tests; defaults to `Date.now()`.
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
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/
|
|
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
|
package/dist/utils/hosts.d.ts
CHANGED
|
@@ -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
|
|
65
|
-
*
|
|
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
|
|
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 {
|
package/dist/utils/hosts.js
CHANGED
|
@@ -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
|
|
47
|
-
*
|
|
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
|
|
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:')
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|