agent-tempo 1.7.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +0 -10
- package/dist/activities/outbox.js +4 -2
- package/dist/adapters/base.js +3 -0
- 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 +7 -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 +0 -13
- package/dist/constants.d.ts +22 -0
- package/dist/constants.js +23 -1
- package/dist/daemon.js +52 -0
- package/dist/http/auth.d.ts +3 -21
- package/dist/http/auth.js +10 -31
- package/dist/http/event-types.d.ts +5 -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 +69 -29
- package/dist/http/writes.d.ts +1 -1
- package/dist/http/writes.js +25 -2
- 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/types.d.ts +15 -6
- 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/search-attributes.d.ts +27 -18
- package/dist/utils/search-attributes.js +39 -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 +44 -110
- package/dist/workflows/signals.d.ts +9 -1
- package/dist/workflows/signals.js +8 -6
- package/package.json +1 -6
- package/workflow-bundle.js +239 -182
- 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
|
@@ -194,6 +194,83 @@ export declare class Controller {
|
|
|
194
194
|
/** First whitespace-delimited token + the remainder. */
|
|
195
195
|
private static splitFirst;
|
|
196
196
|
cmdPlayers(ctx: McExtensionContext): Promise<void>;
|
|
197
|
+
/**
|
|
198
|
+
* Connected-host listing — reuses the shared `formatHostList` formatter (the
|
|
199
|
+
* same text the CLI `hosts` and MCP `hosts` tool render). `includeStale`
|
|
200
|
+
* surfaces hosts past their liveness window (`/hosts --all`). Returns the
|
|
201
|
+
* text so a future planner tool can reuse it (same shape as
|
|
202
|
+
* {@link ensemblesText}). NOT ensemble-scoped — the host registry is global.
|
|
203
|
+
*/
|
|
204
|
+
hostsText(includeStale: boolean): Promise<{
|
|
205
|
+
ok: true;
|
|
206
|
+
text: string;
|
|
207
|
+
} | {
|
|
208
|
+
ok: false;
|
|
209
|
+
error: string;
|
|
210
|
+
}>;
|
|
211
|
+
cmdHosts(args: string, ctx: McExtensionContext): Promise<void>;
|
|
212
|
+
/**
|
|
213
|
+
* Full board detail — the human counterpart to the `observe_board` planner
|
|
214
|
+
* tool. Renders the live `BoardModel` (already kept current by the coarse SSE
|
|
215
|
+
* stream), so there's no extra fetch: the board IS the snapshot. Richer than
|
|
216
|
+
* the TUI `/status` overlay (phase glyphs + currentTool + context% + tail).
|
|
217
|
+
*/
|
|
218
|
+
cmdStatus(ctx: McExtensionContext): void;
|
|
219
|
+
/**
|
|
220
|
+
* `/go [player]` — free HELD players. Uses the dedicated `release` route so
|
|
221
|
+
* it frees holds WITHOUT clearing the PAUSE axis (that's `/play` / `/unpause`).
|
|
222
|
+
* No arg → release all held in the ensemble; `/go <player>` releases one.
|
|
223
|
+
*/
|
|
224
|
+
cmdGo(args: string, ctx: McExtensionContext): Promise<void>;
|
|
225
|
+
/**
|
|
226
|
+
* A player's attachment-lifecycle snapshot — reuses the shared
|
|
227
|
+
* `formatAttachmentInfoForDisplay` (the same text the CLI `attachment-info`
|
|
228
|
+
* and MCP `attachment_info` tool render). Returns the text so a future planner
|
|
229
|
+
* tool can reuse it (same shape as {@link hostsText}).
|
|
230
|
+
*/
|
|
231
|
+
attachmentInfoText(playerId: string): Promise<{
|
|
232
|
+
ok: true;
|
|
233
|
+
text: string;
|
|
234
|
+
} | {
|
|
235
|
+
ok: false;
|
|
236
|
+
error: string;
|
|
237
|
+
}>;
|
|
238
|
+
cmdAttachmentInfo(args: string, ctx: McExtensionContext): Promise<void>;
|
|
239
|
+
/**
|
|
240
|
+
* A player's message timeline — reuses the shared `buildTimeline`/`formatRecall`
|
|
241
|
+
* helpers (the same render the CLI `recall` and MCP `recall` tool produce).
|
|
242
|
+
* Includes sent messages (operator wants the full picture). Pulls the full
|
|
243
|
+
* timeline via the `{ full: true }` recall route, NOT the count-only default.
|
|
244
|
+
*/
|
|
245
|
+
recallText(playerId: string): Promise<{
|
|
246
|
+
ok: true;
|
|
247
|
+
text: string;
|
|
248
|
+
} | {
|
|
249
|
+
ok: false;
|
|
250
|
+
error: string;
|
|
251
|
+
}>;
|
|
252
|
+
cmdRecall(args: string, ctx: McExtensionContext): Promise<void>;
|
|
253
|
+
/**
|
|
254
|
+
* Active-schedule listing — renders each {@link ScheduleEntry} in the same
|
|
255
|
+
* shape the MCP `schedules` tool produces (name → target | recurrence | next).
|
|
256
|
+
*/
|
|
257
|
+
schedulesText(): Promise<{
|
|
258
|
+
ok: true;
|
|
259
|
+
text: string;
|
|
260
|
+
} | {
|
|
261
|
+
ok: false;
|
|
262
|
+
error: string;
|
|
263
|
+
}>;
|
|
264
|
+
/**
|
|
265
|
+
* `/schedule [list | delete <name>]` — list active schedules (default) or
|
|
266
|
+
* cancel one. CREATE is intentionally NOT here: there is no thin-shim create
|
|
267
|
+
* method on the client (the MCP `schedule` tool signals the scheduler workflow
|
|
268
|
+
* directly), so create stays on the MCP tool / CLI per the architect's
|
|
269
|
+
* thin-shim ruling.
|
|
270
|
+
*/
|
|
271
|
+
cmdSchedule(args: string, ctx: McExtensionContext): Promise<void>;
|
|
272
|
+
/** `/unschedule <name>` — alias for `/schedule delete <name>`. */
|
|
273
|
+
cmdUnschedule(args: string, ctx: McExtensionContext): Promise<void>;
|
|
197
274
|
/**
|
|
198
275
|
* One-line ensembles listing — `▶` marks the board's current binding. Shared
|
|
199
276
|
* by the `/ensembles` command and the `list_ensembles` planner tool. Returns
|
|
@@ -74,6 +74,9 @@ const inner_tail_1 = require("./inner-tail");
|
|
|
74
74
|
const ensure_infra_1 = require("../../cli/ensure-infra");
|
|
75
75
|
const zod_to_typebox_1 = require("../zod-to-typebox");
|
|
76
76
|
const validation_1 = require("../../utils/validation");
|
|
77
|
+
const format_hosts_1 = require("../../utils/format-hosts");
|
|
78
|
+
const attachment_format_1 = require("../../utils/attachment-format");
|
|
79
|
+
const recall_format_1 = require("../../utils/recall-format");
|
|
77
80
|
/** The durable conductor a `/handoff` targets by default (matches catalog's conductorName default). */
|
|
78
81
|
const DEFAULT_CONDUCTOR = 'conductor';
|
|
79
82
|
/**
|
|
@@ -96,6 +99,34 @@ const ASK_POLL_INTERVAL_MS = 1_000;
|
|
|
96
99
|
function mintQuestionId() {
|
|
97
100
|
return `q-${crypto.randomUUID()}`;
|
|
98
101
|
}
|
|
102
|
+
/** Compact recurrence label for a schedule (ms interval → "5m"/"1h"/…). */
|
|
103
|
+
function formatScheduleInterval(ms) {
|
|
104
|
+
if (ms >= 86_400_000)
|
|
105
|
+
return `${ms / 86_400_000}d`;
|
|
106
|
+
if (ms >= 3_600_000)
|
|
107
|
+
return `${ms / 3_600_000}h`;
|
|
108
|
+
if (ms >= 60_000)
|
|
109
|
+
return `${ms / 60_000}m`;
|
|
110
|
+
return `${ms / 1000}s`;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* #742 — render one {@link ScheduleEntry} for the board, mirroring the MCP
|
|
114
|
+
* `schedules` tool line shape (name → target | recurrence (bounds) | next + msg
|
|
115
|
+
* preview). Kept module-local + pure so the controller test can assert it.
|
|
116
|
+
*/
|
|
117
|
+
function formatScheduleLine(s) {
|
|
118
|
+
const recur = s.cronExpression
|
|
119
|
+
? `cron: ${s.cronExpression} (${s.timezone || 'UTC'})`
|
|
120
|
+
: s.interval ? `every ${formatScheduleInterval(s.interval)}` : 'one-shot';
|
|
121
|
+
const bounds = [];
|
|
122
|
+
if (s.until)
|
|
123
|
+
bounds.push(`until ${s.until}`);
|
|
124
|
+
if (s.remainingCount != null)
|
|
125
|
+
bounds.push(`${s.firedCount}/${s.firedCount + s.remainingCount} fired`);
|
|
126
|
+
const boundsStr = bounds.length ? ` (${bounds.join(', ')})` : '';
|
|
127
|
+
const msgPreview = s.message.length > 60 ? s.message.slice(0, 57) + '...' : s.message;
|
|
128
|
+
return `• ${s.name} → ${s.target} | ${recur}${boundsStr} | next: ${s.nextFireAt}\n msg: ${msgPreview}`;
|
|
129
|
+
}
|
|
99
130
|
/**
|
|
100
131
|
* #700 P2 — build the planner-wake injection from a resolved `answer` SSE event.
|
|
101
132
|
* Pure (testable). The planner has no inbox; the SSE `answer` event is turned
|
|
@@ -344,6 +375,139 @@ class Controller {
|
|
|
344
375
|
const ids = (0, board_1.sortedPlayerIds)(this.model);
|
|
345
376
|
this.notify(ctx, ids.length ? `Players (${ids.length}): ${ids.join(', ')}` : 'No players in the ensemble.');
|
|
346
377
|
}
|
|
378
|
+
// ── Hosts (#742 gap 5) ──
|
|
379
|
+
/**
|
|
380
|
+
* Connected-host listing — reuses the shared `formatHostList` formatter (the
|
|
381
|
+
* same text the CLI `hosts` and MCP `hosts` tool render). `includeStale`
|
|
382
|
+
* surfaces hosts past their liveness window (`/hosts --all`). Returns the
|
|
383
|
+
* text so a future planner tool can reuse it (same shape as
|
|
384
|
+
* {@link ensemblesText}). NOT ensemble-scoped — the host registry is global.
|
|
385
|
+
*/
|
|
386
|
+
async hostsText(includeStale) {
|
|
387
|
+
const r = await this.actions.listHosts();
|
|
388
|
+
if (!r.ok)
|
|
389
|
+
return r;
|
|
390
|
+
return { ok: true, text: (0, format_hosts_1.formatHostList)(r.hosts, { includeStale }) };
|
|
391
|
+
}
|
|
392
|
+
async cmdHosts(args, ctx) {
|
|
393
|
+
const includeStale = /(?:^|\s)(?:--all|-a)(?:\s|$)/.test(args);
|
|
394
|
+
const r = await this.hostsText(includeStale);
|
|
395
|
+
this.notify(ctx, r.ok ? r.text : `hosts failed: ${r.error}`);
|
|
396
|
+
}
|
|
397
|
+
// ── Status (#742 gap 9) ──
|
|
398
|
+
/**
|
|
399
|
+
* Full board detail — the human counterpart to the `observe_board` planner
|
|
400
|
+
* tool. Renders the live `BoardModel` (already kept current by the coarse SSE
|
|
401
|
+
* stream), so there's no extra fetch: the board IS the snapshot. Richer than
|
|
402
|
+
* the TUI `/status` overlay (phase glyphs + currentTool + context% + tail).
|
|
403
|
+
*/
|
|
404
|
+
cmdStatus(ctx) {
|
|
405
|
+
this.notify(ctx, (0, render_1.renderBoard)(this.model, this.localHost).join('\n'));
|
|
406
|
+
}
|
|
407
|
+
// ── Release / go (#742 gap 8) ──
|
|
408
|
+
/**
|
|
409
|
+
* `/go [player]` — free HELD players. Uses the dedicated `release` route so
|
|
410
|
+
* it frees holds WITHOUT clearing the PAUSE axis (that's `/play` / `/unpause`).
|
|
411
|
+
* No arg → release all held in the ensemble; `/go <player>` releases one.
|
|
412
|
+
*/
|
|
413
|
+
async cmdGo(args, ctx) {
|
|
414
|
+
const player = args.trim().split(/\s+/).filter(Boolean)[0];
|
|
415
|
+
const label = player ? `go (release ${player})` : 'go (release held)';
|
|
416
|
+
this.report(ctx, label, await this.actions.release(player || undefined));
|
|
417
|
+
}
|
|
418
|
+
// ── Attachment-info (#742 — /attachment-info) ──
|
|
419
|
+
/**
|
|
420
|
+
* A player's attachment-lifecycle snapshot — reuses the shared
|
|
421
|
+
* `formatAttachmentInfoForDisplay` (the same text the CLI `attachment-info`
|
|
422
|
+
* and MCP `attachment_info` tool render). Returns the text so a future planner
|
|
423
|
+
* tool can reuse it (same shape as {@link hostsText}).
|
|
424
|
+
*/
|
|
425
|
+
async attachmentInfoText(playerId) {
|
|
426
|
+
const r = await this.actions.attachmentInfo(playerId);
|
|
427
|
+
if (!r.ok)
|
|
428
|
+
return r;
|
|
429
|
+
return { ok: true, text: (0, attachment_format_1.formatAttachmentInfoForDisplay)(playerId, r.info).join('\n') };
|
|
430
|
+
}
|
|
431
|
+
async cmdAttachmentInfo(args, ctx) {
|
|
432
|
+
const player = args.trim().split(/\s+/).filter(Boolean)[0];
|
|
433
|
+
if (!player) {
|
|
434
|
+
this.notify(ctx, 'Usage: /attachment-info <player>');
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
const r = await this.attachmentInfoText(player);
|
|
438
|
+
this.notify(ctx, r.ok ? r.text : `attachment-info failed: ${r.error}`);
|
|
439
|
+
}
|
|
440
|
+
// ── Recall (#742 — /recall) ──
|
|
441
|
+
/**
|
|
442
|
+
* A player's message timeline — reuses the shared `buildTimeline`/`formatRecall`
|
|
443
|
+
* helpers (the same render the CLI `recall` and MCP `recall` tool produce).
|
|
444
|
+
* Includes sent messages (operator wants the full picture). Pulls the full
|
|
445
|
+
* timeline via the `{ full: true }` recall route, NOT the count-only default.
|
|
446
|
+
*/
|
|
447
|
+
async recallText(playerId) {
|
|
448
|
+
const r = await this.actions.recall(playerId);
|
|
449
|
+
if (!r.ok)
|
|
450
|
+
return r;
|
|
451
|
+
const timeline = (0, recall_format_1.buildTimeline)(r.received, r.sent, true);
|
|
452
|
+
return { ok: true, text: (0, recall_format_1.formatRecall)(timeline).text };
|
|
453
|
+
}
|
|
454
|
+
async cmdRecall(args, ctx) {
|
|
455
|
+
const player = args.trim().split(/\s+/).filter(Boolean)[0];
|
|
456
|
+
if (!player) {
|
|
457
|
+
this.notify(ctx, 'Usage: /recall <player>');
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const r = await this.recallText(player);
|
|
461
|
+
this.notify(ctx, r.ok ? r.text : `recall failed: ${r.error}`);
|
|
462
|
+
}
|
|
463
|
+
// ── Schedules (#742 — /schedule, /unschedule) ──
|
|
464
|
+
/**
|
|
465
|
+
* Active-schedule listing — renders each {@link ScheduleEntry} in the same
|
|
466
|
+
* shape the MCP `schedules` tool produces (name → target | recurrence | next).
|
|
467
|
+
*/
|
|
468
|
+
async schedulesText() {
|
|
469
|
+
const r = await this.actions.listSchedules();
|
|
470
|
+
if (!r.ok)
|
|
471
|
+
return r;
|
|
472
|
+
if (r.schedules.length === 0)
|
|
473
|
+
return { ok: true, text: 'No active schedules.' };
|
|
474
|
+
return { ok: true, text: r.schedules.map(formatScheduleLine).join('\n') };
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* `/schedule [list | delete <name>]` — list active schedules (default) or
|
|
478
|
+
* cancel one. CREATE is intentionally NOT here: there is no thin-shim create
|
|
479
|
+
* method on the client (the MCP `schedule` tool signals the scheduler workflow
|
|
480
|
+
* directly), so create stays on the MCP tool / CLI per the architect's
|
|
481
|
+
* thin-shim ruling.
|
|
482
|
+
*/
|
|
483
|
+
async cmdSchedule(args, ctx) {
|
|
484
|
+
const [sub, rest] = Controller.splitFirst(args);
|
|
485
|
+
if (sub === 'delete' || sub === 'rm') {
|
|
486
|
+
const name = rest.trim().split(/\s+/).filter(Boolean)[0];
|
|
487
|
+
if (!name) {
|
|
488
|
+
this.notify(ctx, 'Usage: /schedule delete <name>');
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
this.report(ctx, `unschedule ${name}`, await this.actions.unschedule(name));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (sub === 'create' || sub === 'new' || sub === 'add') {
|
|
495
|
+
this.notify(ctx, 'Schedule creation runs through the MCP `schedule` tool / `agent-tempo schedule` CLI — the board lists and deletes only.');
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// Default (no arg or `list`) → list.
|
|
499
|
+
const r = await this.schedulesText();
|
|
500
|
+
this.notify(ctx, r.ok ? r.text : `schedules failed: ${r.error}`);
|
|
501
|
+
}
|
|
502
|
+
/** `/unschedule <name>` — alias for `/schedule delete <name>`. */
|
|
503
|
+
async cmdUnschedule(args, ctx) {
|
|
504
|
+
const name = args.trim().split(/\s+/).filter(Boolean)[0];
|
|
505
|
+
if (!name) {
|
|
506
|
+
this.notify(ctx, 'Usage: /unschedule <name>');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
this.report(ctx, `unschedule ${name}`, await this.actions.unschedule(name));
|
|
510
|
+
}
|
|
347
511
|
// ── Multi-ensemble home (#790) ──
|
|
348
512
|
/**
|
|
349
513
|
* One-line ensembles listing — `▶` marks the board's current binding. Shared
|
|
@@ -1086,6 +1250,13 @@ function createMissionControlExtension(deps = {}) {
|
|
|
1086
1250
|
pi.on('session_shutdown', () => teardown());
|
|
1087
1251
|
// Operator commands (display-only widget → slash-commands drive everything).
|
|
1088
1252
|
pi.registerCommand('players', { description: 'List ensemble players', handler: (_a, ctx) => ctrl.cmdPlayers(ctx) });
|
|
1253
|
+
pi.registerCommand('hosts', { description: 'List connected hosts (/hosts [--all])', handler: (a, ctx) => ctrl.cmdHosts(a, ctx) });
|
|
1254
|
+
pi.registerCommand('status', { description: 'Show the full board detail', handler: (_a, ctx) => ctrl.cmdStatus(ctx) });
|
|
1255
|
+
pi.registerCommand('go', { description: 'Release HELD players (/go [player])', handler: (a, ctx) => ctrl.cmdGo(a, ctx) });
|
|
1256
|
+
pi.registerCommand('attachment-info', { description: 'Show a player\'s attachment lifecycle (/attachment-info <player>)', handler: (a, ctx) => ctrl.cmdAttachmentInfo(a, ctx) });
|
|
1257
|
+
pi.registerCommand('recall', { description: 'Show a player\'s message timeline (/recall <player>)', handler: (a, ctx) => ctrl.cmdRecall(a, ctx) });
|
|
1258
|
+
pi.registerCommand('schedule', { description: 'List or delete schedules (/schedule [list | delete <name>])', handler: (a, ctx) => ctrl.cmdSchedule(a, ctx) });
|
|
1259
|
+
pi.registerCommand('unschedule', { description: 'Cancel a schedule (/unschedule <name>)', handler: (a, ctx) => ctrl.cmdUnschedule(a, ctx) });
|
|
1089
1260
|
// #790 — multi-ensemble home: list + re-bind.
|
|
1090
1261
|
pi.registerCommand('ensembles', { description: 'List all ensembles (▶ marks the bound one)', handler: (_a, ctx) => ctrl.cmdEnsembles(ctx) });
|
|
1091
1262
|
pi.registerCommand('ensemble', { description: 'Switch the board to another ensemble (/ensemble <name> [--force])', handler: (a, ctx) => ctrl.cmdEnsemble(a, ctx) });
|
|
@@ -153,26 +153,24 @@ export declare class PiWorkflowClient {
|
|
|
153
153
|
fetchPending(): Promise<Message[]>;
|
|
154
154
|
/** Acknowledge delivered cue ids so the workflow stops re-serving them. */
|
|
155
155
|
ackDelivered(messageIds: string[]): Promise<void>;
|
|
156
|
-
/**
|
|
157
|
-
* `true` until a `pendingIntake` query is rejected as unregistered — i.e.
|
|
158
|
-
* the session workflow predates #750. Cached so the pump pays the failed
|
|
159
|
-
* probe ONCE, not every tick, then sticks to the legacy two-query path for
|
|
160
|
-
* the lifetime of this client. Known cost-only staleness: the Pi client
|
|
161
|
-
* handle follows continueAsNew transparently, so a pre-#750 workflow that
|
|
162
|
-
* CANs onto a #750+ worker bundle MID-SESSION keeps paying 2 queries/tick
|
|
163
|
-
* here until the extension process restarts (fresh client → fresh probe).
|
|
164
|
-
* Benign and self-healing — correctness is unaffected either way.
|
|
165
|
-
*/
|
|
166
|
-
private combinedIntakeSupported;
|
|
167
156
|
/**
|
|
168
157
|
* One-query intake: undelivered cues + the pending reset together —
|
|
169
|
-
* halves the pump's idle Temporal actions (2 → 1 query/tick).
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
158
|
+
* halves the pump's idle Temporal actions (2 → 1 query/tick).
|
|
159
|
+
*
|
|
160
|
+
* 2.0 (#788): the pre-#750 two-query fallback (`combinedIntakeSupported`
|
|
161
|
+
* probe + the legacy `pendingMessages` + `pendingReset` pair) is gone — the
|
|
162
|
+
* A2 clean cutover (guarded by #786) guarantees every 2.0 session workflow
|
|
163
|
+
* serves the `pendingIntake` handler, so the fallback was dead.
|
|
173
164
|
*/
|
|
174
165
|
fetchIntake(): Promise<PendingIntake>;
|
|
175
|
-
/**
|
|
166
|
+
/**
|
|
167
|
+
* Poll the workflow's single-slot pending reset (null = none).
|
|
168
|
+
*
|
|
169
|
+
* 2.0 (#788): the standalone `pendingReset` query was removed — derive the
|
|
170
|
+
* reset from the combined `pendingIntake` query. Only the cue-pump's
|
|
171
|
+
* no-`intakeSource` fallback path reaches this; the primary pump path reads
|
|
172
|
+
* `pendingReset` straight off `fetchIntake`'s result.
|
|
173
|
+
*/
|
|
176
174
|
fetchPendingReset(): Promise<PendingReset | null>;
|
|
177
175
|
/**
|
|
178
176
|
* Ack a performed reset by id — the workflow clears the slot ONLY if the id
|
|
@@ -23,6 +23,7 @@ const action_counters_1 = require("../utils/action-counters");
|
|
|
23
23
|
const search_attributes_1 = require("../utils/search-attributes");
|
|
24
24
|
const config_1 = require("../config");
|
|
25
25
|
const signals_1 = require("../workflows/signals");
|
|
26
|
+
const constants_1 = require("../constants");
|
|
26
27
|
/** Adapter identity advertised on claim. Pi interactive players are `interactive`-class. */
|
|
27
28
|
const PI_ADAPTER_ID = 'pi';
|
|
28
29
|
/**
|
|
@@ -160,6 +161,7 @@ class PiWorkflowClient {
|
|
|
160
161
|
adapterId: PI_ADAPTER_ID,
|
|
161
162
|
adapterClass: 'interactive',
|
|
162
163
|
leaseMs: this.leaseMs,
|
|
164
|
+
protocolVersion: constants_1.PROTOCOL_VERSION, // #786 — 2.0 wire handshake
|
|
163
165
|
...(this.expectedAttachmentId ? { expectedAttachmentId: this.expectedAttachmentId } : {}),
|
|
164
166
|
},
|
|
165
167
|
],
|
|
@@ -286,57 +288,29 @@ class PiWorkflowClient {
|
|
|
286
288
|
await handle.signal(signals_1.markDeliveredSignal, messageIds);
|
|
287
289
|
}
|
|
288
290
|
// ── Combined intake (T0.3 of #747, #750) ──
|
|
289
|
-
/**
|
|
290
|
-
* `true` until a `pendingIntake` query is rejected as unregistered — i.e.
|
|
291
|
-
* the session workflow predates #750. Cached so the pump pays the failed
|
|
292
|
-
* probe ONCE, not every tick, then sticks to the legacy two-query path for
|
|
293
|
-
* the lifetime of this client. Known cost-only staleness: the Pi client
|
|
294
|
-
* handle follows continueAsNew transparently, so a pre-#750 workflow that
|
|
295
|
-
* CANs onto a #750+ worker bundle MID-SESSION keeps paying 2 queries/tick
|
|
296
|
-
* here until the extension process restarts (fresh client → fresh probe).
|
|
297
|
-
* Benign and self-healing — correctness is unaffected either way.
|
|
298
|
-
*/
|
|
299
|
-
combinedIntakeSupported = true;
|
|
300
291
|
/**
|
|
301
292
|
* One-query intake: undelivered cues + the pending reset together —
|
|
302
|
-
* halves the pump's idle Temporal actions (2 → 1 query/tick).
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
293
|
+
* halves the pump's idle Temporal actions (2 → 1 query/tick).
|
|
294
|
+
*
|
|
295
|
+
* 2.0 (#788): the pre-#750 two-query fallback (`combinedIntakeSupported`
|
|
296
|
+
* probe + the legacy `pendingMessages` + `pendingReset` pair) is gone — the
|
|
297
|
+
* A2 clean cutover (guarded by #786) guarantees every 2.0 session workflow
|
|
298
|
+
* serves the `pendingIntake` handler, so the fallback was dead.
|
|
306
299
|
*/
|
|
307
300
|
async fetchIntake() {
|
|
308
|
-
|
|
309
|
-
const handle = this.requireHandle();
|
|
310
|
-
try {
|
|
311
|
-
const intake = await handle.query(signals_1.pendingIntakeQuery);
|
|
312
|
-
// Shape-guard: a real server either serves the query or rejects it
|
|
313
|
-
// as unregistered, but belt-and-braces against converter quirks /
|
|
314
|
-
// test doubles — a malformed result downgrades to the legacy pair
|
|
315
|
-
// rather than crashing the pump tick.
|
|
316
|
-
if (intake && Array.isArray(intake.messages))
|
|
317
|
-
return intake;
|
|
318
|
-
this.combinedIntakeSupported = false;
|
|
319
|
-
log('pendingIntake returned a malformed result — falling back to the legacy pair');
|
|
320
|
-
}
|
|
321
|
-
catch (err) {
|
|
322
|
-
if (!isQueryNotRegisteredError(err))
|
|
323
|
-
throw err;
|
|
324
|
-
this.combinedIntakeSupported = false;
|
|
325
|
-
log('pendingIntake query not registered on this workflow (pre-#750 build) — ' +
|
|
326
|
-
'falling back to the legacy pendingMessages + pendingReset pair');
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
const [messages, pendingReset] = await Promise.all([
|
|
330
|
-
this.fetchPending(),
|
|
331
|
-
this.fetchPendingReset(),
|
|
332
|
-
]);
|
|
333
|
-
return { messages, pendingReset };
|
|
301
|
+
return this.requireHandle().query(signals_1.pendingIntakeQuery);
|
|
334
302
|
}
|
|
335
303
|
// ── Reset intake (3d D14) ──
|
|
336
|
-
/**
|
|
304
|
+
/**
|
|
305
|
+
* Poll the workflow's single-slot pending reset (null = none).
|
|
306
|
+
*
|
|
307
|
+
* 2.0 (#788): the standalone `pendingReset` query was removed — derive the
|
|
308
|
+
* reset from the combined `pendingIntake` query. Only the cue-pump's
|
|
309
|
+
* no-`intakeSource` fallback path reaches this; the primary pump path reads
|
|
310
|
+
* `pendingReset` straight off `fetchIntake`'s result.
|
|
311
|
+
*/
|
|
337
312
|
async fetchPendingReset() {
|
|
338
|
-
|
|
339
|
-
return handle.query(signals_1.pendingResetQuery);
|
|
313
|
+
return (await this.requireHandle().query(signals_1.pendingIntakeQuery)).pendingReset;
|
|
340
314
|
}
|
|
341
315
|
/**
|
|
342
316
|
* Ack a performed reset by id — the workflow clears the slot ONLY if the id
|
package/dist/types.d.ts
CHANGED
|
@@ -290,7 +290,7 @@ export interface HostProfile {
|
|
|
290
290
|
*/
|
|
291
291
|
export interface InstanceInfo {
|
|
292
292
|
pid: number;
|
|
293
|
-
/** Parsed from
|
|
293
|
+
/** Parsed from the `agent-tempo:<hostname>:<pid>:<version>` worker identity. */
|
|
294
294
|
version: string;
|
|
295
295
|
/** Raw `poller.identity` as Temporal returned it. */
|
|
296
296
|
identity: string;
|
|
@@ -300,8 +300,6 @@ export interface InstanceInfo {
|
|
|
300
300
|
hasActivityWorker: boolean;
|
|
301
301
|
/** `true` when the per-host activity queue (`agent-tempo-<hostname>`) has a live poller. Controls `HostInfo.recruitReady`. */
|
|
302
302
|
hasHostQueueWorker: boolean;
|
|
303
|
-
/** Set only when identity was in the legacy `<pid>@<hostname>` form. */
|
|
304
|
-
legacy?: true;
|
|
305
303
|
}
|
|
306
304
|
/**
|
|
307
305
|
* Joined liveness + capability view returned by `src/utils/hosts.ts`.
|
|
@@ -346,6 +344,16 @@ export interface PlayerStateEntry {
|
|
|
346
344
|
}
|
|
347
345
|
export interface SessionInput {
|
|
348
346
|
metadata: SessionMetadata;
|
|
347
|
+
/**
|
|
348
|
+
* #786 — 2.0 cutover protocol stamp. Set to `PROTOCOL_VERSION` (2) at every
|
|
349
|
+
* 2.0 start site so it's in history from run 1 and survives `continueAsNew`
|
|
350
|
+
* (carried by the `...input` spread at the CAN site). The workflow ALSO upserts
|
|
351
|
+
* the `AgentTempoProtocol` memo authoritatively from the constant — a 2.0
|
|
352
|
+
* bundle always stamps 2 regardless of input — so a 1.x run (which never sets
|
|
353
|
+
* this) is detectable by the boot guard. Optional only for type back-compat
|
|
354
|
+
* across the cutover; 2.0 always sets it.
|
|
355
|
+
*/
|
|
356
|
+
protocol?: number;
|
|
349
357
|
/** Restored from continue-as-new */
|
|
350
358
|
part?: string;
|
|
351
359
|
/** Restored from continue-as-new (undelivered only) */
|
|
@@ -728,9 +736,10 @@ export interface ResetOutboxEntry extends OutboxEntryBase {
|
|
|
728
736
|
}
|
|
729
737
|
export type OutboxEntry = CueOutboxEntry | RecruitOutboxEntry | ReportOutboxEntry | StopOutboxEntry | ReleaseOutboxEntry | SpawnOutboxEntry | DetachOutboxEntry | DestroyOutboxEntry | RestartOutboxEntry | ResetOutboxEntry;
|
|
730
738
|
/**
|
|
731
|
-
* Pending reset flag set on a session workflow by `deliverReset`,
|
|
732
|
-
* Pi extension
|
|
733
|
-
* after the extension performs the wipe. Single-slot,
|
|
739
|
+
* Pending reset flag set on a session workflow by `deliverReset`, read by the
|
|
740
|
+
* Pi extension off the combined `pendingIntake` query and cleared via
|
|
741
|
+
* `ackResetSignal(resetId)` after the extension performs the wipe. Single-slot,
|
|
742
|
+
* latest-wins.
|
|
734
743
|
*/
|
|
735
744
|
export interface PendingReset {
|
|
736
745
|
/** Correlation id (the originating outbox entry id). Ack clears only on match. */
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #786 — the 2.0 cutover BOOT GUARD (the keystone's centerpiece).
|
|
3
|
+
*
|
|
4
|
+
* A 2.0 Temporal worker can never deterministically replay a 1.x-recorded
|
|
5
|
+
* workflow history — the bundle's command stream has diverged, so a replay
|
|
6
|
+
* faults (or, worse, silently corrupts) deep inside a workflow task. The
|
|
7
|
+
* `upgrade-to-2` cutover (#785) exists to destroy every 1.x run before a 2.0
|
|
8
|
+
* daemon ever polls. This guard is the enforcement: a 2.0 daemon REFUSES to
|
|
9
|
+
* register workers if visibility still shows ANY Running agent-tempo workflow
|
|
10
|
+
* that lacks the protocol-2 stamp.
|
|
11
|
+
*
|
|
12
|
+
* **Stamp source of truth:** every 2.0 workflow upserts the
|
|
13
|
+
* `AgentTempoProtocol` memo = {@link PROTOCOL_VERSION} early + unconditionally
|
|
14
|
+
* on every run (incl. continueAsNew successors) — see `src/workflows/*` and
|
|
15
|
+
* `src/constants.ts`. The memo rides the visibility list result directly
|
|
16
|
+
* ({@link getWorkflowProtocol} reads it off the row), so the scan needs NO
|
|
17
|
+
* per-workflow query. A 1.x run never executes that upsert, so its memo is
|
|
18
|
+
* `undefined` → un-stamped → the guard refuses.
|
|
19
|
+
*
|
|
20
|
+
* **FAIL-CLOSED (the #845 partial-scan lesson):** the scan is bounded by the
|
|
21
|
+
* `protocolGuardBoot` visibility budget. A timeout or any scan error is NOT a
|
|
22
|
+
* best-effort partial — it means "couldn't prove the cutover is clean," and an
|
|
23
|
+
* unverified scan must NEVER be read as clean. Both refuse with
|
|
24
|
+
* {@link ProtocolGuardResult.reason} = `'scan-incomplete'`; the daemon retries
|
|
25
|
+
* on the next boot. The cost of a false-refuse (one retry) is trivial next to
|
|
26
|
+
* the cost of a false-proceed (deterministic history corruption).
|
|
27
|
+
*
|
|
28
|
+
* This module is daemon-side (it holds a Temporal client), distinct from the
|
|
29
|
+
* deliberately SDK-free `snapshot-v1.ts` cross-release contract.
|
|
30
|
+
*/
|
|
31
|
+
import type { Client, WorkflowExecutionInfo } from '@temporalio/client';
|
|
32
|
+
/**
|
|
33
|
+
* The four agent-tempo workflow types. A Running workflow of any of these is
|
|
34
|
+
* an agent-tempo coordination workflow that a 2.0 worker would have to replay
|
|
35
|
+
* — so all four must be protocol-2 stamped before boot.
|
|
36
|
+
*/
|
|
37
|
+
export declare const AGENT_TEMPO_WORKFLOW_TYPES: readonly ["agentSessionWorkflow", "agentMaestroWorkflow", "agentSchedulerWorkflow", "agentGlobalMaestroWorkflow"];
|
|
38
|
+
/** A Running workflow that failed the protocol-2 stamp check. */
|
|
39
|
+
export interface ProtocolOffender {
|
|
40
|
+
workflowId: string;
|
|
41
|
+
/** Workflow type name from the visibility row (best-effort). */
|
|
42
|
+
workflowType?: string;
|
|
43
|
+
/** The stamp read from the row's memo — `undefined` for an un-stamped 1.x run. */
|
|
44
|
+
protocol: number | undefined;
|
|
45
|
+
}
|
|
46
|
+
/** Outcome of {@link checkProtocolGuard}. */
|
|
47
|
+
export interface ProtocolGuardResult {
|
|
48
|
+
/** `true` → every Running agent-tempo workflow is protocol-2 stamped (or none exist). */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
/**
|
|
51
|
+
* Why boot was refused. Only set when `ok` is `false`.
|
|
52
|
+
* - `'unstamped'` — at least one Running workflow lacks the protocol-2 stamp
|
|
53
|
+
* (a 1.x run survived the cutover). See {@link offenders}.
|
|
54
|
+
* - `'scan-incomplete'` — the visibility scan timed out or errored before
|
|
55
|
+
* completing, so cleanliness could not be PROVEN (fail-closed).
|
|
56
|
+
*/
|
|
57
|
+
reason?: 'unstamped' | 'scan-incomplete';
|
|
58
|
+
/** Number of Running agent-tempo workflows examined before the result. */
|
|
59
|
+
scanned: number;
|
|
60
|
+
/** Un-stamped offenders (only when `reason === 'unstamped'`). */
|
|
61
|
+
offenders: ProtocolOffender[];
|
|
62
|
+
/** Operator-facing explanation (always set on refusal). */
|
|
63
|
+
message?: string;
|
|
64
|
+
}
|
|
65
|
+
/** Injectable seams for {@link checkProtocolGuard} — production callers omit. */
|
|
66
|
+
export interface ProtocolGuardDeps {
|
|
67
|
+
/**
|
|
68
|
+
* Returns the visibility iterable for the guard query. Defaults to
|
|
69
|
+
* `client.workflow.list({ query })`. Tests inject a crafted async iterable
|
|
70
|
+
* of `WorkflowExecutionInfo`-shaped rows (only `workflowId`, `type`, `memo`
|
|
71
|
+
* are read) without a live Temporal connection.
|
|
72
|
+
*/
|
|
73
|
+
listWorkflows?: (query: string) => AsyncIterable<WorkflowExecutionInfo>;
|
|
74
|
+
/** Scan deadline. Default {@link VISIBILITY_DEADLINES_MS.protocolGuardBoot}. */
|
|
75
|
+
deadlineMs?: number;
|
|
76
|
+
/** Injectable clock — forwarded to {@link iterateWithDeadline}. Default `Date.now`. */
|
|
77
|
+
now?: () => number;
|
|
78
|
+
/** Log sink. Default no-op (the daemon caller logs the structured result). */
|
|
79
|
+
log?: (...args: unknown[]) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build the namespace-scoped visibility query for the guard. The client is
|
|
83
|
+
* already namespace-scoped, so no namespace clause is needed. Exposed for
|
|
84
|
+
* test introspection.
|
|
85
|
+
*/
|
|
86
|
+
export declare function buildProtocolGuardQuery(): string;
|
|
87
|
+
/**
|
|
88
|
+
* Scan every Running agent-tempo workflow and verify each carries the
|
|
89
|
+
* protocol-2 stamp. See the module header for the fail-closed contract.
|
|
90
|
+
*
|
|
91
|
+
* Never throws — every failure (un-stamped run, scan timeout, scan error) is
|
|
92
|
+
* returned as a structured `ok: false` result so the daemon caller decides how
|
|
93
|
+
* to surface it (it prints + `process.exit(1)`).
|
|
94
|
+
*/
|
|
95
|
+
export declare function checkProtocolGuard(client: Client, deps?: ProtocolGuardDeps): Promise<ProtocolGuardResult>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AGENT_TEMPO_WORKFLOW_TYPES = void 0;
|
|
4
|
+
exports.buildProtocolGuardQuery = buildProtocolGuardQuery;
|
|
5
|
+
exports.checkProtocolGuard = checkProtocolGuard;
|
|
6
|
+
const constants_1 = require("../constants");
|
|
7
|
+
const search_attributes_1 = require("../utils/search-attributes");
|
|
8
|
+
const visibility_deadline_1 = require("../utils/visibility-deadline");
|
|
9
|
+
/**
|
|
10
|
+
* The four agent-tempo workflow types. A Running workflow of any of these is
|
|
11
|
+
* an agent-tempo coordination workflow that a 2.0 worker would have to replay
|
|
12
|
+
* — so all four must be protocol-2 stamped before boot.
|
|
13
|
+
*/
|
|
14
|
+
exports.AGENT_TEMPO_WORKFLOW_TYPES = [
|
|
15
|
+
'agentSessionWorkflow',
|
|
16
|
+
'agentMaestroWorkflow',
|
|
17
|
+
'agentSchedulerWorkflow',
|
|
18
|
+
'agentGlobalMaestroWorkflow',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Build the namespace-scoped visibility query for the guard. The client is
|
|
22
|
+
* already namespace-scoped, so no namespace clause is needed. Exposed for
|
|
23
|
+
* test introspection.
|
|
24
|
+
*/
|
|
25
|
+
function buildProtocolGuardQuery() {
|
|
26
|
+
const types = exports.AGENT_TEMPO_WORKFLOW_TYPES.map((t) => `"${t}"`).join(', ');
|
|
27
|
+
return `WorkflowType IN (${types}) AND ExecutionStatus = "Running"`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Scan every Running agent-tempo workflow and verify each carries the
|
|
31
|
+
* protocol-2 stamp. See the module header for the fail-closed contract.
|
|
32
|
+
*
|
|
33
|
+
* Never throws — every failure (un-stamped run, scan timeout, scan error) is
|
|
34
|
+
* returned as a structured `ok: false` result so the daemon caller decides how
|
|
35
|
+
* to surface it (it prints + `process.exit(1)`).
|
|
36
|
+
*/
|
|
37
|
+
async function checkProtocolGuard(client, deps = {}) {
|
|
38
|
+
const log = deps.log ?? (() => { });
|
|
39
|
+
const deadlineMs = deps.deadlineMs ?? visibility_deadline_1.VISIBILITY_DEADLINES_MS.protocolGuardBoot;
|
|
40
|
+
const query = buildProtocolGuardQuery();
|
|
41
|
+
const listWorkflows = deps.listWorkflows ?? ((q) => client.workflow.list({ query: q }));
|
|
42
|
+
const offenders = [];
|
|
43
|
+
let scanned = 0;
|
|
44
|
+
try {
|
|
45
|
+
for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(listWorkflows(query), deadlineMs, 'protocolGuardBoot', deps.now)) {
|
|
46
|
+
scanned++;
|
|
47
|
+
const protocol = (0, search_attributes_1.getWorkflowProtocol)(wf);
|
|
48
|
+
if (protocol !== constants_1.PROTOCOL_VERSION) {
|
|
49
|
+
offenders.push({
|
|
50
|
+
workflowId: wf.workflowId,
|
|
51
|
+
// `WorkflowExecutionInfo.type` is the workflow type name string.
|
|
52
|
+
workflowType: wf.type,
|
|
53
|
+
protocol,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
// FAIL-CLOSED: a timeout OR any other scan error means we could not prove
|
|
60
|
+
// the namespace is clean. Refuse — never proceed on an unverified scan.
|
|
61
|
+
const kind = (0, visibility_deadline_1.isVisibilityTimeout)(err) ? 'timed out' : 'failed';
|
|
62
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
63
|
+
const message = `agent-tempo 2.0 boot guard could NOT verify a clean cutover — the ` +
|
|
64
|
+
`protocol scan ${kind} after ${scanned} workflow(s): ${detail}. ` +
|
|
65
|
+
`Refusing to boot (fail-closed): a 2.0 worker cannot safely replay a ` +
|
|
66
|
+
`1.x history. Retry once the Temporal namespace is responsive; if this ` +
|
|
67
|
+
`persists, run \`agent-tempo upgrade-to-2\` to complete the cutover.`;
|
|
68
|
+
log(`[boot-guard] scan-incomplete: ${message}`);
|
|
69
|
+
return { ok: false, reason: 'scan-incomplete', scanned, offenders, message };
|
|
70
|
+
}
|
|
71
|
+
if (offenders.length > 0) {
|
|
72
|
+
const sample = offenders
|
|
73
|
+
.slice(0, 5)
|
|
74
|
+
.map((o) => ` - ${o.workflowId} (protocol=${o.protocol ?? 'v1/unset'})`)
|
|
75
|
+
.join('\n');
|
|
76
|
+
const more = offenders.length > 5 ? `\n …and ${offenders.length - 5} more` : '';
|
|
77
|
+
const message = `agent-tempo 2.0 boot guard REFUSED to boot: ${offenders.length} of ` +
|
|
78
|
+
`${scanned} Running agent-tempo workflow(s) are not protocol-${constants_1.PROTOCOL_VERSION} ` +
|
|
79
|
+
`(2.0) stamped — these are pre-cutover 1.x runs that a 2.0 worker cannot ` +
|
|
80
|
+
`safely replay:\n${sample}${more}\n` +
|
|
81
|
+
`Complete the cutover with \`agent-tempo upgrade-to-2\` (it snapshots ` +
|
|
82
|
+
`continuity, then destroys the 1.x runs), then restart the daemon.`;
|
|
83
|
+
log(`[boot-guard] unstamped: refusing (${offenders.length}/${scanned} un-stamped)`);
|
|
84
|
+
return { ok: false, reason: 'unstamped', scanned, offenders, message };
|
|
85
|
+
}
|
|
86
|
+
log(`[boot-guard] clean: ${scanned} Running agent-tempo workflow(s), all protocol-${constants_1.PROTOCOL_VERSION} stamped`);
|
|
87
|
+
return { ok: true, scanned, offenders: [] };
|
|
88
|
+
}
|