agent-tempo 2.0.0-beta.1 → 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 (40) hide show
  1. package/dashboard/package.json +1 -1
  2. package/dist/activities/maestro.js +3 -0
  3. package/dist/activities/outbox.js +12 -0
  4. package/dist/activities/resolve.d.ts +18 -7
  5. package/dist/activities/resolve.js +58 -46
  6. package/dist/adapters/claude-code/adapter.js +7 -0
  7. package/dist/cli/command-center-command.js +15 -1
  8. package/dist/cli/help-text.js +2 -0
  9. package/dist/config.d.ts +14 -0
  10. package/dist/config.js +14 -0
  11. package/dist/constants.d.ts +28 -0
  12. package/dist/constants.js +38 -1
  13. package/dist/daemon.js +29 -0
  14. package/dist/http/event-types.d.ts +33 -0
  15. package/dist/http/server.js +10 -0
  16. package/dist/http/snapshot.js +3 -0
  17. package/dist/observability/nondeterminism-alarm.d.ts +113 -0
  18. package/dist/observability/nondeterminism-alarm.js +162 -0
  19. package/dist/server-tools.js +30 -29
  20. package/dist/server.js +42 -0
  21. package/dist/tools/action-guard.d.ts +23 -0
  22. package/dist/tools/action-guard.js +33 -0
  23. package/dist/tools/coat-check.d.ts +20 -0
  24. package/dist/tools/coat-check.js +129 -0
  25. package/dist/tools/gate.d.ts +13 -0
  26. package/dist/tools/gate.js +88 -0
  27. package/dist/tools/schedule.d.ts +18 -0
  28. package/dist/tools/schedule.js +93 -1
  29. package/dist/tools/stage.d.ts +17 -0
  30. package/dist/tools/stage.js +85 -1
  31. package/dist/tools/state.d.ts +14 -0
  32. package/dist/tools/state.js +95 -0
  33. package/dist/types.d.ts +39 -1
  34. package/dist/utils/orphan-guard.d.ts +37 -0
  35. package/dist/utils/orphan-guard.js +26 -0
  36. package/dist/utils/search-attributes.d.ts +1 -0
  37. package/dist/utils/search-attributes.js +9 -0
  38. package/dist/workflows/session.js +193 -43
  39. package/package.json +1 -1
  40. package/workflow-bundle.js +241 -45
@@ -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
+ }
@@ -11,9 +11,8 @@ const listen_1 = require("./tools/listen");
11
11
  const recruit_1 = require("./tools/recruit");
12
12
  const report_1 = require("./tools/report");
13
13
  const set_name_1 = require("./tools/set-name");
14
+ // #793 — canonical `schedule` (action create|cancel|list) + unschedule/schedules aliases.
14
15
  const schedule_1 = require("./tools/schedule");
15
- const unschedule_1 = require("./tools/unschedule");
16
- const schedules_1 = require("./tools/schedules");
17
16
  const save_lineup_1 = require("./tools/save-lineup");
18
17
  const load_lineup_1 = require("./tools/load-lineup");
19
18
  const agent_types_1 = require("./tools/agent-types");
@@ -25,13 +24,13 @@ const pause_1 = require("./tools/pause");
25
24
  const play_1 = require("./tools/play");
26
25
  const shutdown_1 = require("./tools/shutdown");
27
26
  const restore_1 = require("./tools/restore");
28
- const quality_gate_1 = require("./tools/quality-gate");
27
+ // #793 canonical `gate` (action define|list) + quality_gate/gates aliases.
28
+ // `evaluate_gate` stays a SEPARATE tool (partial merge — runtime op, not CRUD).
29
+ const gate_1 = require("./tools/gate");
29
30
  const evaluate_gate_1 = require("./tools/evaluate-gate");
30
- const gates_1 = require("./tools/gates");
31
31
  const worktree_1 = require("./tools/worktree");
32
+ // #793 — canonical `stage` (action create|list|cancel) + stages/cancel_stage aliases.
32
33
  const stage_1 = require("./tools/stage");
33
- const stages_1 = require("./tools/stages");
34
- const cancel_stage_1 = require("./tools/cancel-stage");
35
34
  const restart_1 = require("./tools/restart");
36
35
  const destroy_1 = require("./tools/destroy");
37
36
  const reset_1 = require("./tools/reset");
@@ -39,15 +38,12 @@ const migrate_1 = require("./tools/migrate");
39
38
  const attachment_info_1 = require("./tools/attachment-info");
40
39
  const hosts_1 = require("./tools/hosts");
41
40
  const set_ensemble_description_1 = require("./tools/set-ensemble-description");
42
- // #334 PR-1 — player saveable state (save_state / fetch_state / clear_state).
43
- const save_state_1 = require("./tools/save-state");
44
- const fetch_state_1 = require("./tools/fetch-state");
45
- const clear_state_1 = require("./tools/clear-state");
46
- // #318 — ensemble-shared coat-check (put / get / list / evict).
47
- const coat_check_put_1 = require("./tools/coat-check-put");
48
- const coat_check_get_1 = require("./tools/coat-check-get");
49
- const coat_check_list_1 = require("./tools/coat-check-list");
50
- const coat_check_evict_1 = require("./tools/coat-check-evict");
41
+ // #334 PR-1 / #793 canonical `state` (action save|fetch|clear) + save_state/
42
+ // fetch_state/clear_state aliases.
43
+ const state_1 = require("./tools/state");
44
+ // #318 / #793 — canonical `coat_check` (action put|get|list|evict) +
45
+ // coat_check_put/get/list/evict aliases.
46
+ const coat_check_1 = require("./tools/coat-check");
51
47
  const respond_1 = require("./tools/respond");
52
48
  /**
53
49
  * Register every tempo MCP tool onto `server`. Single source of truth — the
@@ -69,9 +65,10 @@ function buildAllTempoTools(opts) {
69
65
  (0, listen_1.buildListenTool)(handle),
70
66
  (0, recruit_1.buildRecruitTool)(client, config, getPlayerId, handle, ownAgentType, defaultAgentSource),
71
67
  (0, report_1.buildReportTool)(handle),
68
+ // #793 — canonical `schedule` (action defaults to "create") + legacy
69
+ // unschedule/schedules forwarding aliases.
72
70
  (0, schedule_1.buildScheduleTool)(client, config, getPlayerId),
73
- (0, unschedule_1.buildUnscheduleTool)(client, config),
74
- (0, schedules_1.buildSchedulesTool)(client, config),
71
+ ...(0, schedule_1.buildScheduleAliasTools)(client, config),
75
72
  (0, save_lineup_1.buildSaveLineupTool)(client, config, getPlayerId, isConductor),
76
73
  (0, load_lineup_1.buildLoadLineupTool)(client, config, getPlayerId, ownAgentType, handle, setPlayerId, isConductor),
77
74
  (0, agent_types_1.buildAgentTypesTool)(),
@@ -90,23 +87,27 @@ function buildAllTempoTools(opts) {
90
87
  (0, attachment_info_1.buildAttachmentInfoTool)(client, config),
91
88
  (0, hosts_1.buildHostsTool)(client, config),
92
89
  (0, set_ensemble_description_1.buildSetEnsembleDescriptionTool)(client, config),
93
- // #334 PR-1 — owner-write / peer-read player saveable state.
94
- (0, save_state_1.buildSaveStateTool)(handle, getPlayerId),
95
- (0, fetch_state_1.buildFetchStateTool)(client, config, handle, getPlayerId),
96
- (0, clear_state_1.buildClearStateTool)(handle),
97
- // #318 — ensemble-shared coat-check (put/get/list/evict). Any player can put;
98
- // any player can get/list; owner-or-conductor can evict. Audit identity is
99
- // set at the tool layer via getPlayerId() — no playerId arg on any schema.
100
- (0, coat_check_put_1.buildCoatCheckPutTool)(client, config, getPlayerId),
101
- (0, coat_check_get_1.buildCoatCheckGetTool)(client, config, getPlayerId),
102
- (0, coat_check_list_1.buildCoatCheckListTool)(client, config),
103
- (0, coat_check_evict_1.buildCoatCheckEvictTool)(client, config, getPlayerId),
90
+ // #334 PR-1 / #793 canonical `state` (owner-write / peer-read) + legacy
91
+ // save_state/fetch_state/clear_state forwarding aliases.
92
+ (0, state_1.buildStateTool)(client, config, handle, getPlayerId),
93
+ ...(0, state_1.buildStateAliasTools)(client, config, handle, getPlayerId),
94
+ // #318 / #793 canonical `coat_check` (put/get/list/evict) + legacy
95
+ // coat_check_* forwarding aliases. Any player can put/get/list;
96
+ // owner-or-conductor can evict. Audit identity is set at the tool layer via
97
+ // getPlayerId() no playerId arg on any schema.
98
+ (0, coat_check_1.buildCoatCheckTool)(client, config, getPlayerId),
99
+ ...(0, coat_check_1.buildCoatCheckAliasTools)(client, config, getPlayerId),
104
100
  // #700 P2 — answer a planner's correlated `[Q <id>]` question (writes the
105
101
  // maestro Q&A mailbox directly; from = getPlayerId(), no spoofable arg).
106
102
  (0, respond_1.buildRespondTool)(client, config, getPlayerId),
107
103
  ];
108
104
  if (isConductor) {
109
- tools.push((0, quality_gate_1.buildQualityGateTool)(handle, getPlayerId), (0, evaluate_gate_1.buildEvaluateGateTool)(handle, getPlayerId), (0, gates_1.buildGatesTool)(handle), (0, worktree_1.buildWorktreeTool)(client, config, handle, getPlayerId), (0, stage_1.buildStageTool)(handle, getPlayerId), (0, stages_1.buildStagesTool)(handle), (0, cancel_stage_1.buildCancelStageTool)(handle));
105
+ tools.push(
106
+ // #793 — canonical `gate` (define|list) + quality_gate/gates aliases.
107
+ // `evaluate_gate` stays a separate tool (partial-merge boundary).
108
+ (0, gate_1.buildGateTool)(handle, getPlayerId), (0, evaluate_gate_1.buildEvaluateGateTool)(handle, getPlayerId), ...(0, gate_1.buildGateAliasTools)(handle, getPlayerId), (0, worktree_1.buildWorktreeTool)(client, config, handle, getPlayerId),
109
+ // #793 — canonical `stage` (create|list|cancel) + stages/cancel_stage aliases.
110
+ (0, stage_1.buildStageTool)(handle, getPlayerId), ...(0, stage_1.buildStageAliasTools)(handle));
110
111
  }
111
112
  return tools;
112
113
  }
package/dist/server.js CHANGED
@@ -59,6 +59,7 @@ const parent_death_watchdog_1 = require("./utils/parent-death-watchdog");
59
59
  const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
60
60
  const action_counters_1 = require("./utils/action-counters");
61
61
  const search_attributes_1 = require("./utils/search-attributes");
62
+ const orphan_guard_1 = require("./utils/orphan-guard");
62
63
  const log = (...args) => console.error('[agent-tempo]', ...args);
63
64
  async function main() {
64
65
  (0, parent_death_watchdog_1.installParentDeathWatchdog)();
@@ -157,6 +158,12 @@ async function main() {
157
158
  isConductor,
158
159
  agentType: isBridgeMode ? 'copilot' : 'claude',
159
160
  adapterId,
161
+ // #704 — thread the adapter descriptor's dialog-blocking property onto
162
+ // durable metadata so the session workflow can ARM/DISARM the booting
163
+ // watchdog without importing the client-side registry. This self-bootstrap
164
+ // path is interactive `claude-code` (or the Copilot bridge), so it resolves
165
+ // to disarmed; recruited headless sessions arm via `startRecruitedSession`.
166
+ canBlockOnDialog: adapterDescriptor?.canBlockOnDialog === true,
160
167
  },
161
168
  // Issue #450 — self-bootstrap path has no resolved player type, so
162
169
  // this falls through to `'Conductor session'` for conductors and
@@ -170,6 +177,41 @@ async function main() {
170
177
  taskQueue: config.taskQueue,
171
178
  },
172
179
  };
180
+ // #704 Item 1b — late-orphan self-tombstone guard. A recruited process can
181
+ // launch LONG after its recruit was cancelled (slow / wedged cold start). If
182
+ // this derived id's latest run is CLOSED with a destroy / boot-timeout tombstone
183
+ // MEMO and NO newer RUNNING run exists, this process is that orphan — exit before
184
+ // `start(USE_EXISTING)` bootstraps a brand-new run that collides with whoever took
185
+ // over the worktree. The running-run × close-reason PAIR is the discriminator:
186
+ // every managed re-creation (recruit / restart / migrate / up) pre-creates a
187
+ // RUNNING run, so a legit reuse is seen as RUNNING and simply attaches — it never
188
+ // reaches this branch. Gated `!isConductor` (conductors are operator-driven via
189
+ // `up`, never a recruited orphan). A generous wall-clock TTL bounds a stale
190
+ // tombstone so a much-later legit manual reuse of the same name isn't blocked
191
+ // forever (observed orphan delay ~100min; default 6h, env-overridable).
192
+ if (!isConductor) {
193
+ const ttlMsRaw = Number(process.env[config_1.ENV.ORPHAN_TOMBSTONE_TTL_MS]);
194
+ const orphanTombstoneTtlMs = Number.isFinite(ttlMsRaw) && ttlMsRaw > 0 ? ttlMsRaw : 6 * 60 * 60 * 1000;
195
+ try {
196
+ const desc = await client.workflow.getHandle(workflowId).describe();
197
+ const closeReason = desc.memo?.[search_attributes_1.MEMO_KEYS.closeReason];
198
+ const orphan = (0, orphan_guard_1.shouldSelfExitAsOrphan)({
199
+ statusName: desc.status.name,
200
+ closeReason,
201
+ closeTimeMs: desc.closeTime ? desc.closeTime.getTime() : 0,
202
+ }, orphanTombstoneTtlMs, Date.now());
203
+ if (orphan) {
204
+ log(`recruit was cancelled (close-reason: ${closeReason}) — exiting to avoid ` +
205
+ `re-registering an orphan session for ${workflowId}`);
206
+ process.exit(0);
207
+ }
208
+ }
209
+ catch {
210
+ // No prior run (NotFound) → first-ever start for this id → proceed. Any other
211
+ // describe() failure is non-fatal: the guard is a best-effort backstop, not a
212
+ // correctness gate — fall through and let the start proceed.
213
+ }
214
+ }
173
215
  const startedHandle = await client.workflow.start('agentSessionWorkflow', {
174
216
  workflowId,
175
217
  taskQueue: config.taskQueue,
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Per-action runtime field guard for the canonical multi-action tools (#793).
3
+ *
4
+ * The tool-family merge (#793) collapses each family into one canonical tool
5
+ * with a flat `{ action, ...per-action optional fields }` param shape. Because
6
+ * the union makes every per-action field optional at the zod boundary (we do
7
+ * NOT use `z.discriminatedUnion` — see docs/design/793-tool-family-merge-brief.md
8
+ * §2), cross-field "this action requires that field" rules are enforced at
9
+ * RUNTIME inside the handler, before dispatch.
10
+ *
11
+ * {@link firstMissing} returns the first required field that is absent so the
12
+ * handler can return a friendly, actionable `fail(...)` message naming the
13
+ * exact tool, action, and field — instead of a cryptic downstream error.
14
+ */
15
+ /**
16
+ * Return the first field name in `fields` whose value in `args` is "missing"
17
+ * (`undefined`, `null`, or an empty string), or `null` when all are present.
18
+ *
19
+ * Empty-string counts as missing so a canonical handler rejects e.g.
20
+ * `coat_check{action:'get', ticket:''}` with the same friendly error a wholly
21
+ * omitted ticket would produce.
22
+ */
23
+ export declare function firstMissing(args: Record<string, unknown>, fields: readonly string[]): string | null;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ /**
3
+ * Per-action runtime field guard for the canonical multi-action tools (#793).
4
+ *
5
+ * The tool-family merge (#793) collapses each family into one canonical tool
6
+ * with a flat `{ action, ...per-action optional fields }` param shape. Because
7
+ * the union makes every per-action field optional at the zod boundary (we do
8
+ * NOT use `z.discriminatedUnion` — see docs/design/793-tool-family-merge-brief.md
9
+ * §2), cross-field "this action requires that field" rules are enforced at
10
+ * RUNTIME inside the handler, before dispatch.
11
+ *
12
+ * {@link firstMissing} returns the first required field that is absent so the
13
+ * handler can return a friendly, actionable `fail(...)` message naming the
14
+ * exact tool, action, and field — instead of a cryptic downstream error.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.firstMissing = firstMissing;
18
+ /**
19
+ * Return the first field name in `fields` whose value in `args` is "missing"
20
+ * (`undefined`, `null`, or an empty string), or `null` when all are present.
21
+ *
22
+ * Empty-string counts as missing so a canonical handler rejects e.g.
23
+ * `coat_check{action:'get', ticket:''}` with the same friendly error a wholly
24
+ * omitted ticket would produce.
25
+ */
26
+ function firstMissing(args, fields) {
27
+ for (const field of fields) {
28
+ const v = args[field];
29
+ if (v === undefined || v === null || v === '')
30
+ return field;
31
+ }
32
+ return null;
33
+ }
@@ -0,0 +1,20 @@
1
+ import { Client } from '@temporalio/client';
2
+ import { Config } from '../config';
3
+ import { type TempoToolDescriptor } from './descriptor';
4
+ /**
5
+ * Canonical `coat_check` tool. Dispatches on `action` to the same per-action
6
+ * logic the legacy tools used. Per-action required fields are runtime-guarded
7
+ * (the flat union makes them optional at the zod boundary).
8
+ */
9
+ export declare function buildCoatCheckTool(client: Client, config: Config, getPlayerId: () => string): TempoToolDescriptor;
10
+ /**
11
+ * Legacy forwarding aliases — `coat_check_put` / `coat_check_get` /
12
+ * `coat_check_list` / `coat_check_evict`. Each keeps its EXACT original param
13
+ * schema and handler (reused verbatim from the legacy descriptor); only the
14
+ * description gains a deprecation note pointing at the canonical tool.
15
+ *
16
+ * Authored as explicit object literals (not loop-generated) so the
17
+ * surface-drift name scrape and any future static analysis see each alias name
18
+ * adjacent to its description (docs/design/793-tool-family-merge-brief.md §6).
19
+ */
20
+ export declare function buildCoatCheckAliasTools(client: Client, config: Config, getPlayerId: () => string): TempoToolDescriptor[];
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildCoatCheckTool = buildCoatCheckTool;
4
+ exports.buildCoatCheckAliasTools = buildCoatCheckAliasTools;
5
+ /**
6
+ * `coat_check` — canonical multi-action coat-check tool (#793 tool-family merge).
7
+ *
8
+ * Merges the four legacy per-action tools (`coat_check_put` / `coat_check_get` /
9
+ * `coat_check_list` / `coat_check_evict`) into ONE canonical tool with a flat
10
+ * `{ action, ...per-action optional fields }` param shape (NOT a discriminated
11
+ * union — see docs/design/793-tool-family-merge-brief.md §2). The canonical name
12
+ * is **net-new**, so `action` is REQUIRED (no legacy caller to keep compatible).
13
+ *
14
+ * The legacy tools stay registered as thin forwarding aliases
15
+ * ({@link buildCoatCheckAliasTools}) so every existing caller keeps its exact
16
+ * original schema and behaviour — the alias-not-remove invariant (#793). Both the
17
+ * canonical handler and the aliases reuse the legacy descriptors' handler bodies
18
+ * verbatim, so there is a single implementation per action.
19
+ */
20
+ const zod_1 = require("zod");
21
+ const descriptor_1 = require("./descriptor");
22
+ const action_guard_1 = require("./action-guard");
23
+ const validation_1 = require("../utils/validation");
24
+ const coat_check_put_1 = require("./coat-check-put");
25
+ const coat_check_get_1 = require("./coat-check-get");
26
+ const coat_check_list_1 = require("./coat-check-list");
27
+ const coat_check_evict_1 = require("./coat-check-evict");
28
+ /**
29
+ * Canonical `coat_check` tool. Dispatches on `action` to the same per-action
30
+ * logic the legacy tools used. Per-action required fields are runtime-guarded
31
+ * (the flat union makes them optional at the zod boundary).
32
+ */
33
+ function buildCoatCheckTool(client, config, getPlayerId) {
34
+ const put = (0, coat_check_put_1.buildCoatCheckPutTool)(client, config, getPlayerId);
35
+ const get = (0, coat_check_get_1.buildCoatCheckGetTool)(client, config, getPlayerId);
36
+ const list = (0, coat_check_list_1.buildCoatCheckListTool)(client, config);
37
+ const evict = (0, coat_check_evict_1.buildCoatCheckEvictTool)(client, config, getPlayerId);
38
+ return {
39
+ name: 'coat_check',
40
+ description: 'Ensemble-shared transient store (stash large artifacts past the cue size cap). ' +
41
+ 'action="put" stashes content (summary+content[, contentType, ttlMs]) and returns a ticket; ' +
42
+ 'action="get" redeems a ticket (bumps fetch audit); ' +
43
+ 'action="list" surveys entry headers (putBy/prefix/unfetchedOnly filters); ' +
44
+ 'action="evict" removes an entry early (owner-or-conductor).',
45
+ params: {
46
+ action: zod_1.z.enum(['put', 'get', 'list', 'evict']).describe('Which coat-check operation to perform'),
47
+ // put:
48
+ summary: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_SUMMARY_MAX).optional().describe('put: short human-readable label for the entry'),
49
+ content: zod_1.z.string().min(1).max(validation_1.COAT_CHECK_CONTENT_MAX).optional().describe('put: the body to stash (≤32 KiB)'),
50
+ contentType: zod_1.z.string().max(validation_1.COAT_CHECK_CONTENT_TYPE_MAX).optional().describe('put: optional MIME/content-type hint'),
51
+ ttlMs: zod_1.z.number().int().min(validation_1.COAT_CHECK_TTL_MIN_MS).max(validation_1.COAT_CHECK_TTL_MAX_MS).optional().describe('put: optional time-to-live in ms (default 7d)'),
52
+ // get / evict:
53
+ ticket: zod_1.z.string().regex(validation_1.COAT_CHECK_TICKET_REGEX).max(validation_1.COAT_CHECK_TICKET_MAX).optional().describe('get/evict: the ticket id returned by put'),
54
+ // list:
55
+ putBy: zod_1.z.string().max(validation_1.PLAYER_NAME_MAX).optional().describe('list: filter to entries stashed by this player'),
56
+ prefix: zod_1.z.string().max(validation_1.COAT_CHECK_SUMMARY_MAX).optional().describe('list: filter to entries whose summary starts with this prefix'),
57
+ unfetchedOnly: zod_1.z.boolean().optional().describe('list: only entries that have never been fetched'),
58
+ },
59
+ handler: async (args) => {
60
+ const action = args.action;
61
+ switch (action) {
62
+ case 'put': {
63
+ const m = (0, action_guard_1.firstMissing)(args, ['summary', 'content']);
64
+ if (m)
65
+ return (0, descriptor_1.fail)(`coat_check action="put" requires "${m}".`);
66
+ return put.handler(args);
67
+ }
68
+ case 'get': {
69
+ const m = (0, action_guard_1.firstMissing)(args, ['ticket']);
70
+ if (m)
71
+ return (0, descriptor_1.fail)(`coat_check action="get" requires "${m}".`);
72
+ return get.handler(args);
73
+ }
74
+ case 'list':
75
+ return list.handler(args);
76
+ case 'evict': {
77
+ const m = (0, action_guard_1.firstMissing)(args, ['ticket']);
78
+ if (m)
79
+ return (0, descriptor_1.fail)(`coat_check action="evict" requires "${m}".`);
80
+ return evict.handler(args);
81
+ }
82
+ default:
83
+ return (0, descriptor_1.fail)(`Unknown coat_check action: ${String(action)}. Expected put | get | list | evict.`);
84
+ }
85
+ },
86
+ };
87
+ }
88
+ /**
89
+ * Legacy forwarding aliases — `coat_check_put` / `coat_check_get` /
90
+ * `coat_check_list` / `coat_check_evict`. Each keeps its EXACT original param
91
+ * schema and handler (reused verbatim from the legacy descriptor); only the
92
+ * description gains a deprecation note pointing at the canonical tool.
93
+ *
94
+ * Authored as explicit object literals (not loop-generated) so the
95
+ * surface-drift name scrape and any future static analysis see each alias name
96
+ * adjacent to its description (docs/design/793-tool-family-merge-brief.md §6).
97
+ */
98
+ function buildCoatCheckAliasTools(client, config, getPlayerId) {
99
+ const put = (0, coat_check_put_1.buildCoatCheckPutTool)(client, config, getPlayerId);
100
+ const get = (0, coat_check_get_1.buildCoatCheckGetTool)(client, config, getPlayerId);
101
+ const list = (0, coat_check_list_1.buildCoatCheckListTool)(client, config);
102
+ const evict = (0, coat_check_evict_1.buildCoatCheckEvictTool)(client, config, getPlayerId);
103
+ return [
104
+ {
105
+ name: 'coat_check_put',
106
+ description: 'DEPRECATED — use `coat_check` with action="put". ' + put.description,
107
+ params: put.params,
108
+ handler: put.handler,
109
+ },
110
+ {
111
+ name: 'coat_check_get',
112
+ description: 'DEPRECATED — use `coat_check` with action="get". ' + get.description,
113
+ params: get.params,
114
+ handler: get.handler,
115
+ },
116
+ {
117
+ name: 'coat_check_list',
118
+ description: 'DEPRECATED — use `coat_check` with action="list". ' + list.description,
119
+ params: list.params,
120
+ handler: list.handler,
121
+ },
122
+ {
123
+ name: 'coat_check_evict',
124
+ description: 'DEPRECATED — use `coat_check` with action="evict". ' + evict.description,
125
+ params: evict.params,
126
+ handler: evict.handler,
127
+ },
128
+ ];
129
+ }