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.
- package/dashboard/package.json +1 -1
- package/dist/activities/maestro.js +3 -0
- package/dist/activities/outbox.js +12 -0
- package/dist/activities/resolve.d.ts +18 -7
- package/dist/activities/resolve.js +58 -46
- package/dist/adapters/claude-code/adapter.js +7 -0
- package/dist/cli/command-center-command.js +15 -1
- package/dist/cli/help-text.js +2 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +14 -0
- package/dist/constants.d.ts +28 -0
- package/dist/constants.js +38 -1
- package/dist/daemon.js +29 -0
- package/dist/http/event-types.d.ts +33 -0
- package/dist/http/server.js +10 -0
- package/dist/http/snapshot.js +3 -0
- package/dist/observability/nondeterminism-alarm.d.ts +113 -0
- package/dist/observability/nondeterminism-alarm.js +162 -0
- package/dist/server-tools.js +30 -29
- package/dist/server.js +42 -0
- package/dist/tools/action-guard.d.ts +23 -0
- package/dist/tools/action-guard.js +33 -0
- package/dist/tools/coat-check.d.ts +20 -0
- package/dist/tools/coat-check.js +129 -0
- package/dist/tools/gate.d.ts +13 -0
- package/dist/tools/gate.js +88 -0
- package/dist/tools/schedule.d.ts +18 -0
- package/dist/tools/schedule.js +93 -1
- package/dist/tools/stage.d.ts +17 -0
- package/dist/tools/stage.js +85 -1
- package/dist/tools/state.d.ts +14 -0
- package/dist/tools/state.js +95 -0
- package/dist/types.d.ts +39 -1
- package/dist/utils/orphan-guard.d.ts +37 -0
- package/dist/utils/orphan-guard.js +26 -0
- package/dist/utils/search-attributes.d.ts +1 -0
- package/dist/utils/search-attributes.js +9 -0
- package/dist/workflows/session.js +193 -43
- package/package.json +1 -1
- 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
|
+
}
|
package/dist/server-tools.js
CHANGED
|
@@ -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
|
-
|
|
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 —
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
//
|
|
47
|
-
const
|
|
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,
|
|
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
|
|
94
|
-
|
|
95
|
-
(0,
|
|
96
|
-
(0,
|
|
97
|
-
// #318 —
|
|
98
|
-
//
|
|
99
|
-
// set at the tool layer via
|
|
100
|
-
(
|
|
101
|
-
(0,
|
|
102
|
-
(0,
|
|
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(
|
|
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
|
+
}
|