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
@@ -47,6 +47,29 @@ export interface HealthV1 {
47
47
  * semantics. Optional so legacy clients ignoring the field don't break.
48
48
  */
49
49
  memory?: MemoryUsageV1;
50
+ /**
51
+ * #886 slice 1 — daemon-side nondeterminism alarm state. Present when the
52
+ * daemon installed the alarm (always, in 2.0+); a non-zero `count` means a
53
+ * nondeterminism / determinism-violation flap was observed since boot (e.g.
54
+ * a 2.0 worker replaying a 1.x history, or a code/bundle skew). Lets external
55
+ * monitors and the dashboard poll the alarm without scraping `daemon.log`.
56
+ * Optional so pre-#886 clients (and non-daemon health responders) ignore it.
57
+ */
58
+ nondeterminism?: NondeterminismAlarmV1;
59
+ }
60
+ /** #886 — `/v1/health` shape of the nondeterminism alarm snapshot. */
61
+ export interface NondeterminismAlarmV1 {
62
+ /** Total nondeterminism records seen since daemon boot (0 = healthy). */
63
+ count: number;
64
+ /** ISO timestamp of the first hit; absent when `count === 0`. */
65
+ firstSeenAt?: string;
66
+ /** ISO timestamp of the most recent hit; absent when `count === 0`. */
67
+ lastSeenAt?: string;
68
+ /** Most-recent samples (capped, newest last) — `{ at, detail }`. */
69
+ recent: Array<{
70
+ at: string;
71
+ detail: string;
72
+ }>;
50
73
  }
51
74
  /** Snapshot of `process.memoryUsage()` at request time. All values in bytes. */
52
75
  export interface MemoryUsageV1 {
@@ -208,6 +231,16 @@ export interface PlayerSummaryV1 {
208
231
  contextTokens?: number;
209
232
  /** 3c Tier-1 coarse — context usage as a percentage of the model window. Additive. */
210
233
  contextPercent?: number;
234
+ /**
235
+ * #886 slice 2 — `true` when the daemon's observation scan could only produce
236
+ * a DEGRADED row for this player: the session workflow was listed but its
237
+ * metadata extraction failed, so the non-identity fields (part/workDir/phase/…)
238
+ * are best-effort blanks. Lets the dashboard/board render an "uncertain" badge
239
+ * instead of dropping the player, which would read as a departure and cause
240
+ * roster flapping (contra #777). Additive + optional per the §6 stability rule;
241
+ * absent on every healthy row.
242
+ */
243
+ degraded?: boolean;
211
244
  }
212
245
  /**
213
246
  * The eventId token used in the `id:` line of the SSE frame and as the
@@ -242,10 +275,11 @@ export type EventIdToken = string;
242
275
  * - `'stale'` — matches a host with `freshness === 'stale'`
243
276
  * - `'missing'` — `preferredHost` is null OR no matching host record
244
277
  *
245
- * `migrateCommand` is the TUI slash-command the operator pastes into their
246
- * own session on the migrate target. `--yes-steal=` (NOT
247
- * `--confirm-steal-from-host`) is the actual flag accepted by
248
- * `src/tui/commands.ts:handleMigrate`. When `preferredHost` is null the
278
+ * `migrateCommand` is the `/migrate` slash-command the operator pastes into
279
+ * their own session on the migrate target (the command-center board; the Ink
280
+ * TUI that originally handled it was removed in #789). `--yes-steal=` (NOT
281
+ * `--confirm-steal-from-host`) is the actual flag the `/migrate` handler
282
+ * accepts. When `preferredHost` is null the
249
283
  * command targets the local host and includes the steal guard pre-filled
250
284
  * with the last-known host (or a literal `'(unknown)'` when even that is
251
285
  * missing — the operator must edit it before submit).
@@ -30,9 +30,9 @@ import type { TempoClient } from '../client/interface';
30
30
  import type { HostInfo } from '../types';
31
31
  import type { OrphanCandidate } from '../reconcile/orphans';
32
32
  /**
33
- * Render the TUI `/migrate` slash command the operator pastes into their
34
- * own session to recover an orphan. Wording mirrors
35
- * `src/tui/commands.ts:handleMigrate` exactly:
33
+ * Render the `/migrate` slash command the operator pastes into their own
34
+ * session to recover an orphan (the command-center board; the Ink TUI that
35
+ * originally handled it was removed in #789). Wording:
36
36
  * - positional `<playerId> <host>`
37
37
  * - flag form `--yes-steal=<currentHost>` (NOT `--confirm-steal-from-host`)
38
38
  *
@@ -6,9 +6,9 @@ exports.buildOrphansResponse = buildOrphansResponse;
6
6
  exports.handleOrphans = handleOrphans;
7
7
  const responses_1 = require("./responses");
8
8
  /**
9
- * Render the TUI `/migrate` slash command the operator pastes into their
10
- * own session to recover an orphan. Wording mirrors
11
- * `src/tui/commands.ts:handleMigrate` exactly:
9
+ * Render the `/migrate` slash command the operator pastes into their own
10
+ * session to recover an orphan (the command-center board; the Ink TUI that
11
+ * originally handled it was removed in #789). Wording:
12
12
  * - positional `<playerId> <host>`
13
13
  * - flag form `--yes-steal=<currentHost>` (NOT `--confirm-steal-from-host`)
14
14
  *
@@ -61,11 +61,6 @@ export interface HttpServerOptions {
61
61
  * don't fight production daemons running on the same machine.
62
62
  */
63
63
  portFilePath?: string;
64
- /**
65
- * @deprecated 3e — back-compat alias for {@link readToken}. A single injected
66
- * bearer is treated as the READ token (T1). Prefer `readToken`/`adminToken`.
67
- */
68
- httpToken?: string;
69
64
  /** 3e — inject the read-tier (T1) token directly (tests). Overrides config/env. */
70
65
  readToken?: string;
71
66
  /** 3e — inject the admin (T1+T2+T3) token directly (tests). Overrides env. */
@@ -72,6 +72,10 @@ const action_counters_1 = require("../utils/action-counters");
72
72
  const snapshot_1 = require("./snapshot");
73
73
  const orphans_1 = require("./orphans");
74
74
  const sse_handler_1 = require("./sse-handler");
75
+ // #886 slice 1 — surface the daemon nondeterminism alarm on /v1/health. The
76
+ // alarm core is Temporal-VALUE-free, so this import doesn't pull the worker
77
+ // runtime into the HTTP layer.
78
+ const nondeterminism_alarm_1 = require("../observability/nondeterminism-alarm");
75
79
  const log = (...args) => console.error(`[agent-tempo:http ${new Date().toISOString()}]`, ...args);
76
80
  /** Default bind addr per SSE-PROTOCOL.md §1. */
77
81
  exports.DEFAULT_BIND_ADDR = '127.0.0.1';
@@ -103,42 +107,22 @@ async function startHttpServer(opts) {
103
107
  // generation now so the daemon doesn't crash mid-request when the first
104
108
  // bearer-required call shows up.
105
109
  const bindIsLoopback = (0, auth_1.isLoopbackBindAddr)(bindAddr);
106
- // 3e RBAC token resolution. Back-compat: a single `httpToken` option (or a
107
- // legacy config.json `httpToken`) is adopted as the READ token (T1); the ADMIN
108
- // token is env-var-only. Explicit `readToken`/`adminToken` options override
109
- // (used by tests). loopback bind ⇒ no bearer required ⇒ tokens may be null.
110
- let readToken;
111
- let legacyMigrated = false;
112
- if (opts.readToken !== undefined) {
113
- readToken = opts.readToken;
114
- }
115
- else if (opts.httpToken !== undefined) {
116
- // Back-compat: a single injected bearer is treated as the READ token (T1).
117
- readToken = opts.httpToken;
118
- }
119
- else {
120
- const loaded = (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
121
- readToken = loaded.token;
122
- legacyMigrated = loaded.legacy;
123
- }
110
+ // 3e RBAC token resolution. The READ token (T1) comes from the explicit
111
+ // `readToken` option (tests) or env/config/auto-gen; the ADMIN token is
112
+ // env-var-only. loopback bind no bearer required ⇒ tokens may be null.
113
+ const readToken = opts.readToken !== undefined
114
+ ? opts.readToken
115
+ : (0, auth_1.loadReadToken)({ bearerRequired: !bindIsLoopback });
124
116
  const adminToken = opts.adminToken ?? (0, auth_1.loadAdminToken)();
125
117
  if (!bindIsLoopback && !readToken) {
126
118
  throw new Error('Bearer token required for non-loopback bind but none configured. ' +
127
119
  'Set AGENT_TEMPO_HTTP_READ_TOKEN (or readToken in ~/.agent-tempo/config.json), ' +
128
120
  'or unset AGENT_TEMPO_HTTP_BIND.');
129
121
  }
130
- // 3e MD-E — one-time startup warnings (non-blocking).
122
+ // 3e MD-E — one-time startup warning (non-blocking).
131
123
  //
132
- // (1) Legacy migration: a pre-3e single `httpToken` was adopted as the READ
133
- // token (T1) and no admin token is configured, so writes / inner-tail
134
- // (all Tier ≥ 2) will 503 until an admin token is set.
135
- if (legacyMigrated && adminToken === null) {
136
- log('NOTICE: adopted legacy config.json `httpToken` as the read-tier token. ' +
137
- 'Writes and the inner-tail are admin-only and will return ' +
138
- '503 until you set AGENT_TEMPO_HTTP_ADMIN_TOKEN (env-var only).');
139
- }
140
- // (2) Plaintext-bearer exposure: binding to a non-loopback address serves the
141
- // bearer token over cleartext HTTP. Suppressible, never blocking.
124
+ // Plaintext-bearer exposure: binding to a non-loopback address serves the
125
+ // bearer token over cleartext HTTP. Suppressible, never blocking.
142
126
  if (!bindIsLoopback && process.env[config_1.ENV.TLS_ACKNOWLEDGED] !== '1') {
143
127
  log(`WARNING: binding to non-loopback ${bindAddr} serves the bearer token over ` +
144
128
  'plaintext HTTP. Terminate TLS at a reverse proxy, or tunnel via SSH/Tailscale. ' +
@@ -433,6 +417,37 @@ async function handle(req, res, ctx) {
433
417
  }
434
418
  return (0, coat_check_1.handleCoatCheckGet)(res, ctx.client, ensemble, ticket);
435
419
  }
420
+ // #742 — per-player attachment-info read (`/attachment-info` board command).
421
+ // 2-segment GET; MUST be matched BEFORE the generic writeMatch below, which
422
+ // would otherwise 404 `attachment-info` as an unknown write action. The
423
+ // player is a `?playerId=` query param (the path stays ensemble-scoped).
424
+ const attachmentInfoMatch = pathname.match(/^\/v1\/ensembles\/([^/]+)\/attachment-info$/);
425
+ if (attachmentInfoMatch) {
426
+ const ensemble = decodeURIComponent(attachmentInfoMatch[1]);
427
+ if (!gateTier(1))
428
+ return; // L3 — read (Tier 1).
429
+ if (method !== 'GET') {
430
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET, OPTIONS' });
431
+ }
432
+ const playerId = url.searchParams.get('playerId');
433
+ if (!playerId)
434
+ return (0, responses_1.errorResponse)(res, 400, { error: 'missing-field', field: 'playerId' });
435
+ return handleAttachmentInfo(res, ctx, ensemble, playerId);
436
+ }
437
+ // #742 — active-schedule listing (`/schedule list` board command). 2-segment
438
+ // GET; same ordering rationale as attachment-info (precede writeMatch). The
439
+ // schedule WRITE path is the `unschedule` write action below + the MCP
440
+ // `schedule` tool for create (no thin-shim create method on the client).
441
+ const schedulesMatch = pathname.match(/^\/v1\/ensembles\/([^/]+)\/schedules$/);
442
+ if (schedulesMatch) {
443
+ const ensemble = decodeURIComponent(schedulesMatch[1]);
444
+ if (!gateTier(1))
445
+ return; // L3 — read (Tier 1).
446
+ if (method !== 'GET') {
447
+ return (0, responses_1.errorResponse)(res, 405, { error: 'method-not-allowed' }, { Allow: 'GET, OPTIONS' });
448
+ }
449
+ return handleSchedules(res, ctx, ensemble);
450
+ }
436
451
  // Write surface (PR-7a of #340) — POST `/v1/ensembles/:ensemble/<action>`
437
452
  // Match BEFORE the GET-only method gate; everything else (POST to a
438
453
  // read endpoint, GET to a write endpoint) flows into the 405 fallback
@@ -695,6 +710,12 @@ function handleHealth(res, ctx) {
695
710
  external: mem.external,
696
711
  arrayBuffers: mem.arrayBuffers,
697
712
  },
713
+ // #886 slice 1 — nondeterminism alarm snapshot (present when the daemon
714
+ // installed the alarm; `count > 0` signals an observed flap since boot).
715
+ ...(() => {
716
+ const snap = (0, nondeterminism_alarm_1.getGlobalNondeterminismAlarm)()?.snapshot();
717
+ return snap ? { nondeterminism: snap } : {};
718
+ })(),
698
719
  };
699
720
  // Best-effort ensemble count — soft-fail to 0 rather than 500ing on a
700
721
  // healthcheck. `/v1/health` MUST stay reachable even when Temporal is
@@ -718,6 +739,35 @@ async function handleHosts(res, ctx) {
718
739
  const hosts = await ctx.client.listHosts();
719
740
  (0, responses_1.jsonResponse)(res, 200, hosts);
720
741
  }
742
+ /**
743
+ * #742 — a player's V2 attachment-lifecycle snapshot. Thin shim over
744
+ * `client.attachmentInfo`; the command-center `/attachment-info` board command
745
+ * renders the payload via the shared `formatAttachmentInfoForDisplay`. A 404 is
746
+ * surfaced when the player's session can't be resolved (it throws "No session
747
+ * found …", mirroring the recall not-found contract).
748
+ */
749
+ async function handleAttachmentInfo(res, ctx, ensemble, playerId) {
750
+ try {
751
+ const info = await ctx.client.attachmentInfo(ensemble, playerId);
752
+ (0, responses_1.jsonResponse)(res, 200, info);
753
+ }
754
+ catch (err) {
755
+ const message = err instanceof Error ? err.message : String(err);
756
+ if (/no session found|not found/i.test(message)) {
757
+ return (0, responses_1.errorResponse)(res, 404, { error: 'session-not-found', ensemble, playerId, detail: message });
758
+ }
759
+ throw err;
760
+ }
761
+ }
762
+ /**
763
+ * #742 — active-schedule listing. Thin shim over `client.getSchedules` (which
764
+ * already degrades to `[]` when no scheduler is running). The command-center
765
+ * `/schedule [list]` board command renders the entries.
766
+ */
767
+ async function handleSchedules(res, ctx, ensemble) {
768
+ const schedules = await ctx.client.getSchedules(ensemble);
769
+ (0, responses_1.jsonResponse)(res, 200, schedules);
770
+ }
721
771
  /**
722
772
  * #753 — daemon-process per-source Temporal action counters. Pure in-memory
723
773
  * read (zero Temporal calls); adapter and Pi processes self-report via their
@@ -108,6 +108,9 @@ function toPlayerSummaryV1(p, wireMeta = null) {
108
108
  ...(wireMeta?.coarse?.currentTool !== undefined ? { currentTool: wireMeta.coarse.currentTool } : {}),
109
109
  ...(wireMeta?.coarse?.contextTokens !== undefined ? { contextTokens: wireMeta.coarse.contextTokens } : {}),
110
110
  ...(wireMeta?.coarse?.contextPercent !== undefined ? { contextPercent: wireMeta.coarse.contextPercent } : {}),
111
+ // #886 slice 2 — carry the degraded/uncertain flag onto the wire so the
112
+ // dashboard/board can badge the player instead of dropping it (anti-flap).
113
+ ...(p.degraded ? { degraded: true } : {}),
111
114
  };
112
115
  }
113
116
  /**
@@ -42,7 +42,7 @@ export { WRITE_BODY_MAX };
42
42
  * mutations. Bodies are uniform `{ playerId, reason? }` (plus per-action
43
43
  * extras); the ensemble lives in the URL.
44
44
  */
45
- export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "reset", "destroy", "detach", "recall", "shutdown"];
45
+ export declare const WRITE_ACTIONS: readonly ["cue", "pause", "play", "release", "recruit", "restart", "reset", "destroy", "detach", "recall", "unschedule", "shutdown"];
46
46
  export type WriteAction = (typeof WRITE_ACTIONS)[number];
47
47
  /** Type guard — narrows an arbitrary string to a known `WriteAction`. */
48
48
  export declare function isWriteAction(s: string): s is WriteAction;
@@ -31,6 +31,7 @@ exports.WRITE_ACTIONS = [
31
31
  'destroy',
32
32
  'detach',
33
33
  'recall',
34
+ 'unschedule',
34
35
  'shutdown',
35
36
  ];
36
37
  /** Type guard — narrows an arbitrary string to a known `WriteAction`. */
@@ -69,6 +70,7 @@ async function handleWriteRoute(req, res, client, ensemble, action) {
69
70
  case 'destroy': return await handleDestroy(res, client, ensemble, body);
70
71
  case 'detach': return await handleDetach(res, client, ensemble, body);
71
72
  case 'recall': return await handleRecall(res, client, ensemble, body);
73
+ case 'unschedule': return await handleUnschedule(res, client, ensemble, body);
72
74
  case 'shutdown': return await handleShutdown(res, client, ensemble, body);
73
75
  }
74
76
  }
@@ -242,12 +244,33 @@ async function handleRecall(res, client, ensemble, body) {
242
244
  // The dashboard's `RecallResult` consumer expects a count, not the raw
243
245
  // arrays — projecting `received` + `sent` lengths here keeps the wire
244
246
  // payload tight and avoids leaking inbox + sent-history shape into the
245
- // browser-side type. Callers wanting the full timeline use the MCP
246
- // `recall` tool / TempoClient method directly.
247
+ // browser-side type. Callers wanting the full timeline pass `{ full: true }`
248
+ // (the command-center `/recall` board command, #742) or use the MCP `recall`
249
+ // tool / TempoClient method directly.
247
250
  const result = await client.recall(ensemble, playerId);
251
+ if (body.full === true) {
252
+ // Full timeline — the operator board feeds this through the shared
253
+ // `buildTimeline`/`formatRecall` helpers, same as the MCP tool. Kept behind
254
+ // an opt-in flag so the count-only default stays the dashboard's shape.
255
+ return (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, playerId, received: result.received, sent: result.sent });
256
+ }
248
257
  const messages = result.received.length + result.sent.length;
249
258
  (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, playerId, messages });
250
259
  }
260
+ /**
261
+ * Cancel a named schedule (#742 — the command-center `/unschedule` &
262
+ * `/schedule delete`). Thin shim over `client.cancelSchedule`. Body is
263
+ * `{ name }` (the schedule key), distinct from the per-player `{ playerId }`
264
+ * shape — schedules are ensemble-scoped, keyed by name. 200 (the cancel IS
265
+ * the operation, not a queued effect).
266
+ */
267
+ async function handleUnschedule(res, client, ensemble, body) {
268
+ const name = (0, body_1.stringField)(body, 'name');
269
+ if (!name)
270
+ return (0, responses_1.errorResponse)(res, 400, { error: 'missing-field', field: 'name' });
271
+ await client.cancelSchedule(ensemble, name);
272
+ (0, responses_1.jsonResponse)(res, 200, { ok: true, ensemble, name });
273
+ }
251
274
  /**
252
275
  * Ensemble teardown (#700 P1) — the HTTP verb the command-center `/ensemble-down`
253
276
  * needs (the CLI `down` + the `shutdown` MCP tool cover this, but neither was
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Nondeterminism alarm (#886 slice 1).
3
+ *
4
+ * The #801 incident produced **57 workflow-task failures in 3 minutes with
5
+ * ZERO operator signal** — a nondeterminism flap (a 2.0 worker replaying a
6
+ * 1.x-recorded history, or a code/bundle skew) surfaces only as Temporal's
7
+ * own `WARN`-level Core log line, buried in `daemon.log` among normal chatter.
8
+ * Nothing NAMED the flap, counted it, or raised its severity.
9
+ *
10
+ * This module gives the daemon a tiny, process-global counter that:
11
+ * 1. Intercepts Temporal Runtime log records (via {@link wrapLoggerWithAlarm},
12
+ * installed in the daemon's `Runtime.install({ logger })`).
13
+ * 2. Classifies nondeterminism / determinism-violation records
14
+ * ({@link isNondeterminismLog}) — wording-tolerant, like the SA-preflight
15
+ * marker set, so a Core/SDK phrasing change can't silently disarm it.
16
+ * 3. PROMOTES each hit to a prominent, greppable `[agent-tempo:ALARM]` line
17
+ * with a running count — so a flap is named the instant it starts.
18
+ * 4. Keeps a small rolling snapshot ({@link NondeterminismAlarm.snapshot})
19
+ * surfaced on `GET /v1/health` so external monitors / the dashboard can
20
+ * poll the alarm state without scraping logs.
21
+ *
22
+ * **Temporal-VALUE-free by design:** this module imports only the `Logger`
23
+ * TYPE from `@temporalio/common` (erased at compile). The worker `Runtime` /
24
+ * `DefaultLogger` VALUES are imported by the daemon, which wraps a real logger
25
+ * with {@link wrapLoggerWithAlarm} and installs it. That keeps the HTTP layer
26
+ * (`/v1/health`) able to read {@link getGlobalNondeterminismAlarm} without
27
+ * pulling `@temporalio/worker` into the HTTP module graph.
28
+ *
29
+ * NOTE: daemon-side observability code — NOT workflow code — so `Date`/clocks
30
+ * are fine here; `now` is injected purely for deterministic unit tests.
31
+ */
32
+ import type { Logger, LogMetadata } from '@temporalio/common';
33
+ /**
34
+ * Substrings (matched case-insensitively against the log message) that mark a
35
+ * nondeterminism / determinism-violation record. A SET rather than one phrase
36
+ * because the exact wording varies across Core (Rust) and SDK (JS) sources and
37
+ * versions — `DeterminismViolationError` surfaces "Replay failed with a
38
+ * nondeterminism error", Core forwards a `WARN` referencing nondeterminism, and
39
+ * Temporal surfaces the incident class under the `TMPRL1100` code. Matching a
40
+ * set keeps the alarm armed across phrasing drift (same rationale as
41
+ * `UNREGISTERED_SA_MARKERS` in `sa-preflight.ts`).
42
+ */
43
+ export declare const NONDETERMINISM_MARKERS: readonly string[];
44
+ /** A single recorded nondeterminism hit (most-recent-N kept by the alarm). */
45
+ export interface NondeterminismSample {
46
+ /** ISO timestamp of the hit. */
47
+ at: string;
48
+ /** Best-effort detail: workflow type / id from log meta + a message snippet. */
49
+ detail: string;
50
+ }
51
+ /** Pollable alarm state — embedded in `GET /v1/health` (`HealthV1.nondeterminism`). */
52
+ export interface NondeterminismAlarmSnapshot {
53
+ /** Total nondeterminism records seen since daemon boot. */
54
+ count: number;
55
+ /** ISO timestamp of the first hit, or `undefined` when count === 0. */
56
+ firstSeenAt?: string;
57
+ /** ISO timestamp of the most recent hit, or `undefined` when count === 0. */
58
+ lastSeenAt?: string;
59
+ /** Most-recent samples (capped), newest last. */
60
+ recent: NondeterminismSample[];
61
+ }
62
+ export interface NondeterminismAlarmOpts {
63
+ /** Injectable clock (epoch ms) — defaults to `Date.now`. Test seam only. */
64
+ now?: () => number;
65
+ /**
66
+ * Promotion sink — invoked on EVERY hit with the running count + sample, so
67
+ * the daemon can emit the prominent `[agent-tempo:ALARM]` line. Defaults to a
68
+ * no-op; the daemon passes a `console.error`-backed promoter. Kept injectable
69
+ * so unit tests can assert promotion without capturing stderr.
70
+ */
71
+ onHit?: (count: number, sample: NondeterminismSample) => void;
72
+ }
73
+ /**
74
+ * Process-global nondeterminism counter. One per daemon. Records hits, promotes
75
+ * each, and exposes a rolling snapshot.
76
+ */
77
+ export declare class NondeterminismAlarm {
78
+ private _count;
79
+ private _firstSeenAt?;
80
+ private _lastSeenAt?;
81
+ private readonly _recent;
82
+ private readonly now;
83
+ private readonly onHit;
84
+ constructor(opts?: NondeterminismAlarmOpts);
85
+ /** Record one nondeterminism hit from a log record. */
86
+ record(message: string, meta?: LogMetadata): void;
87
+ /** Current count (total hits since boot). */
88
+ get count(): number;
89
+ /** Immutable snapshot for `/v1/health`. */
90
+ snapshot(): NondeterminismAlarmSnapshot;
91
+ }
92
+ /**
93
+ * True when a log record (its message) signals a nondeterminism / determinism
94
+ * violation. Case-insensitive substring match over {@link NONDETERMINISM_MARKERS}.
95
+ */
96
+ export declare function isNondeterminismLog(message: string, _meta?: LogMetadata): boolean;
97
+ /**
98
+ * Wrap a base {@link Logger} so every record still flows to `base`, but any
99
+ * `WARN`/`ERROR` record matching {@link isNondeterminismLog} also feeds the
100
+ * alarm. Transparent: trace/debug/info pass straight through; warn/error/log are
101
+ * classified first, then forwarded unchanged.
102
+ *
103
+ * Only `WARN`/`ERROR` are inspected — a nondeterminism failure is always logged
104
+ * at one of those levels, and ignoring lower levels avoids a hot classifier on
105
+ * the (high-volume) debug/trace path.
106
+ */
107
+ export declare function wrapLoggerWithAlarm(base: Logger, alarm: NondeterminismAlarm): Logger;
108
+ /** Set the process-global alarm (daemon boot only). */
109
+ export declare function setGlobalNondeterminismAlarm(alarm: NondeterminismAlarm): void;
110
+ /** Read the process-global alarm, or `undefined` if not installed (e.g. tests, non-daemon). */
111
+ export declare function getGlobalNondeterminismAlarm(): NondeterminismAlarm | undefined;
112
+ /** Test seam — reset the singleton between unit tests. */
113
+ export declare function __resetGlobalNondeterminismAlarmForTests(): void;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NondeterminismAlarm = exports.NONDETERMINISM_MARKERS = void 0;
4
+ exports.isNondeterminismLog = isNondeterminismLog;
5
+ exports.wrapLoggerWithAlarm = wrapLoggerWithAlarm;
6
+ exports.setGlobalNondeterminismAlarm = setGlobalNondeterminismAlarm;
7
+ exports.getGlobalNondeterminismAlarm = getGlobalNondeterminismAlarm;
8
+ exports.__resetGlobalNondeterminismAlarmForTests = __resetGlobalNondeterminismAlarmForTests;
9
+ /**
10
+ * Substrings (matched case-insensitively against the log message) that mark a
11
+ * nondeterminism / determinism-violation record. A SET rather than one phrase
12
+ * because the exact wording varies across Core (Rust) and SDK (JS) sources and
13
+ * versions — `DeterminismViolationError` surfaces "Replay failed with a
14
+ * nondeterminism error", Core forwards a `WARN` referencing nondeterminism, and
15
+ * Temporal surfaces the incident class under the `TMPRL1100` code. Matching a
16
+ * set keeps the alarm armed across phrasing drift (same rationale as
17
+ * `UNREGISTERED_SA_MARKERS` in `sa-preflight.ts`).
18
+ */
19
+ exports.NONDETERMINISM_MARKERS = Object.freeze([
20
+ 'nondetermin',
21
+ 'non-determin',
22
+ 'determinismviolation',
23
+ 'tmprl1100',
24
+ ]);
25
+ /** Max recent samples retained for the `/v1/health` snapshot. */
26
+ const RECENT_CAP = 10;
27
+ /** Max message characters kept per sample (logs can be long stack-y blobs). */
28
+ const DETAIL_SNIPPET_MAX = 200;
29
+ /**
30
+ * Process-global nondeterminism counter. One per daemon. Records hits, promotes
31
+ * each, and exposes a rolling snapshot.
32
+ */
33
+ class NondeterminismAlarm {
34
+ _count = 0;
35
+ _firstSeenAt;
36
+ _lastSeenAt;
37
+ _recent = [];
38
+ now;
39
+ onHit;
40
+ constructor(opts = {}) {
41
+ this.now = opts.now ?? (() => Date.now());
42
+ this.onHit = opts.onHit ?? (() => { });
43
+ }
44
+ /** Record one nondeterminism hit from a log record. */
45
+ record(message, meta) {
46
+ const at = new Date(this.now()).toISOString();
47
+ this._count += 1;
48
+ if (this._firstSeenAt === undefined)
49
+ this._firstSeenAt = at;
50
+ this._lastSeenAt = at;
51
+ const sample = { at, detail: buildDetail(message, meta) };
52
+ this._recent.push(sample);
53
+ if (this._recent.length > RECENT_CAP)
54
+ this._recent.shift();
55
+ // Promote — fire the prominent, greppable alarm line (the whole point: the
56
+ // #801 flap produced zero operator signal). Never let a throwing sink eat
57
+ // the count.
58
+ try {
59
+ this.onHit(this._count, sample);
60
+ }
61
+ catch {
62
+ /* a broken promoter must not disarm the counter */
63
+ }
64
+ }
65
+ /** Current count (total hits since boot). */
66
+ get count() {
67
+ return this._count;
68
+ }
69
+ /** Immutable snapshot for `/v1/health`. */
70
+ snapshot() {
71
+ return {
72
+ count: this._count,
73
+ ...(this._firstSeenAt !== undefined ? { firstSeenAt: this._firstSeenAt } : {}),
74
+ ...(this._lastSeenAt !== undefined ? { lastSeenAt: this._lastSeenAt } : {}),
75
+ recent: this._recent.slice(),
76
+ };
77
+ }
78
+ }
79
+ exports.NondeterminismAlarm = NondeterminismAlarm;
80
+ /** Build the best-effort sample detail from log meta + a message snippet. */
81
+ function buildDetail(message, meta) {
82
+ const bits = [];
83
+ const wfType = meta?.['workflowType'];
84
+ const wfId = meta?.['workflowId'];
85
+ const runId = meta?.['runId'];
86
+ if (typeof wfType === 'string')
87
+ bits.push(`workflowType=${wfType}`);
88
+ if (typeof wfId === 'string')
89
+ bits.push(`workflowId=${wfId}`);
90
+ if (typeof runId === 'string')
91
+ bits.push(`runId=${runId}`);
92
+ const snippet = (message ?? '').slice(0, DETAIL_SNIPPET_MAX);
93
+ bits.push(snippet);
94
+ return bits.join(' · ');
95
+ }
96
+ /**
97
+ * True when a log record (its message) signals a nondeterminism / determinism
98
+ * violation. Case-insensitive substring match over {@link NONDETERMINISM_MARKERS}.
99
+ */
100
+ function isNondeterminismLog(message, _meta) {
101
+ if (!message)
102
+ return false;
103
+ const m = message.toLowerCase();
104
+ return exports.NONDETERMINISM_MARKERS.some((marker) => m.includes(marker));
105
+ }
106
+ /**
107
+ * Wrap a base {@link Logger} so every record still flows to `base`, but any
108
+ * `WARN`/`ERROR` record matching {@link isNondeterminismLog} also feeds the
109
+ * alarm. Transparent: trace/debug/info pass straight through; warn/error/log are
110
+ * classified first, then forwarded unchanged.
111
+ *
112
+ * Only `WARN`/`ERROR` are inspected — a nondeterminism failure is always logged
113
+ * at one of those levels, and ignoring lower levels avoids a hot classifier on
114
+ * the (high-volume) debug/trace path.
115
+ */
116
+ function wrapLoggerWithAlarm(base, alarm) {
117
+ const consider = (level, message, meta) => {
118
+ if ((level === 'WARN' || level === 'ERROR') && isNondeterminismLog(message, meta)) {
119
+ alarm.record(message, meta);
120
+ }
121
+ };
122
+ return {
123
+ log(level, message, meta) {
124
+ consider(level, message, meta);
125
+ base.log(level, message, meta);
126
+ },
127
+ trace(message, meta) {
128
+ base.trace(message, meta);
129
+ },
130
+ debug(message, meta) {
131
+ base.debug(message, meta);
132
+ },
133
+ info(message, meta) {
134
+ base.info(message, meta);
135
+ },
136
+ warn(message, meta) {
137
+ consider('WARN', message, meta);
138
+ base.warn(message, meta);
139
+ },
140
+ error(message, meta) {
141
+ consider('ERROR', message, meta);
142
+ base.error(message, meta);
143
+ },
144
+ };
145
+ }
146
+ // ── Process singleton ───────────────────────────────────────────────────────
147
+ // The daemon installs ONE alarm at boot; `/v1/health` reads it without
148
+ // threading a reference through the HTTP server constructor (and without
149
+ // importing `@temporalio/worker` into the HTTP layer).
150
+ let globalAlarm;
151
+ /** Set the process-global alarm (daemon boot only). */
152
+ function setGlobalNondeterminismAlarm(alarm) {
153
+ globalAlarm = alarm;
154
+ }
155
+ /** Read the process-global alarm, or `undefined` if not installed (e.g. tests, non-daemon). */
156
+ function getGlobalNondeterminismAlarm() {
157
+ return globalAlarm;
158
+ }
159
+ /** Test seam — reset the singleton between unit tests. */
160
+ function __resetGlobalNondeterminismAlarmForTests() {
161
+ globalAlarm = undefined;
162
+ }
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Pure palette-input classification + filtering helpers.
3
3
  *
4
- * Shared between the TUI (`src/tui/commands.ts`) and the dashboard
5
- * (`dashboard/src/components/chat/`) so the two surfaces' chat-input
6
- * autocomplete machinery stays in lockstep — same prefix detection,
7
- * same filtering semantics, same quote-aware tokenizer.
4
+ * Used by the dashboard (`dashboard/src/components/chat/`) chat-input
5
+ * autocomplete machinery prefix detection, filtering semantics, and the
6
+ * quote-aware tokenizer.
8
7
  *
9
- * Extracted from `src/tui/commands.ts` in #471/#472 (dashboard chat-input
10
- * TUI parity bundle). The TUI re-exports every symbol from here so existing
11
- * callers see no behaviour change. The dashboard imports directly from
8
+ * Originally extracted from the former `src/tui/commands.ts` in #471/#472
9
+ * (dashboard ↔ TUI parity bundle); the Ink TUI was deleted in #789, so the
10
+ * dashboard is now the sole consumer. The dashboard imports directly from
12
11
  * `agent-tempo/palette` (Vite alias `agent-tempo` → `../src`).
13
12
  *
14
13
  * **No imports allowed.** Keep this module dependency-free so it
@@ -2,14 +2,13 @@
2
2
  /**
3
3
  * Pure palette-input classification + filtering helpers.
4
4
  *
5
- * Shared between the TUI (`src/tui/commands.ts`) and the dashboard
6
- * (`dashboard/src/components/chat/`) so the two surfaces' chat-input
7
- * autocomplete machinery stays in lockstep — same prefix detection,
8
- * same filtering semantics, same quote-aware tokenizer.
5
+ * Used by the dashboard (`dashboard/src/components/chat/`) chat-input
6
+ * autocomplete machinery prefix detection, filtering semantics, and the
7
+ * quote-aware tokenizer.
9
8
  *
10
- * Extracted from `src/tui/commands.ts` in #471/#472 (dashboard chat-input
11
- * TUI parity bundle). The TUI re-exports every symbol from here so existing
12
- * callers see no behaviour change. The dashboard imports directly from
9
+ * Originally extracted from the former `src/tui/commands.ts` in #471/#472
10
+ * (dashboard ↔ TUI parity bundle); the Ink TUI was deleted in #789, so the
11
+ * dashboard is now the sole consumer. The dashboard imports directly from
13
12
  * `agent-tempo/palette` (Vite alias `agent-tempo` → `../src`).
14
13
  *
15
14
  * **No imports allowed.** Keep this module dependency-free so it