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
package/dashboard/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-tempo-dashboard",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "2.0.0-beta.
|
|
4
|
+
"version": "2.0.0-beta.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Web dashboard for agent-tempo. Bundled into the npm package; served by the daemon at /dashboard/*.",
|
|
7
7
|
"scripts": {
|
|
@@ -30,6 +30,9 @@ function createMaestroActivities(client, opts = {}) {
|
|
|
30
30
|
// tempo bucket can diff across refreshes.
|
|
31
31
|
activityCount: s.activityCount,
|
|
32
32
|
lastActivityAt: s.lastActivityAt,
|
|
33
|
+
// #886 slice 2 — carry the degraded flag through to the wire so the
|
|
34
|
+
// dashboard/board can render an uncertain row instead of dropping it.
|
|
35
|
+
...(s.degraded ? { degraded: true } : {}),
|
|
33
36
|
});
|
|
34
37
|
// #753 — attribute every Temporal call made by these activities (however
|
|
35
38
|
// deep, e.g. scanEnsembleSessions → queryHandleWithTimeout) to the maestro.
|
|
@@ -292,6 +292,18 @@ function createOutboxActivities(client, config, ingestTokens, doorbells) {
|
|
|
292
292
|
// session workflow and dispatch path can resolve the adapter descriptor
|
|
293
293
|
// from the registry without falling back to the legacy agentType field.
|
|
294
294
|
adapterId: adapters_1.registry.resolveFromAgentType(agent),
|
|
295
|
+
// #704 — resolve the adapter descriptor's dialog-blocking property at
|
|
296
|
+
// spawn time and thread it onto durable metadata so the session workflow
|
|
297
|
+
// can ARM/DISARM the booting watchdog structurally (no agentType
|
|
298
|
+
// hardcode). Headless adapters omit it ⇒ falsy ⇒ ARMED; interactive
|
|
299
|
+
// `claude-code` sets it true ⇒ disarmed until #890.
|
|
300
|
+
canBlockOnDialog: adapters_1.registry.get(adapters_1.registry.resolveFromAgentType(agent))?.canBlockOnDialog === true,
|
|
301
|
+
// #704 — optional env override for the booting deadline (workflows
|
|
302
|
+
// can't read env). Only set when a valid positive integer is provided.
|
|
303
|
+
...(() => {
|
|
304
|
+
const ms = Number(process.env[config_2.ENV.BOOTING_DEADLINE_MS]);
|
|
305
|
+
return Number.isFinite(ms) && ms > 0 ? { bootingDeadlineMs: ms } : {};
|
|
306
|
+
})(),
|
|
295
307
|
sessionId,
|
|
296
308
|
// #131 / #449 Phase C — persist the claude-api / opencode model
|
|
297
309
|
// on durable metadata so restart can recover the original choice
|
|
@@ -87,6 +87,17 @@ export interface EnsembleSessionInfo {
|
|
|
87
87
|
* session. Undefined when the session predates W2 or the query failed.
|
|
88
88
|
*/
|
|
89
89
|
lastActivityAt?: string;
|
|
90
|
+
/**
|
|
91
|
+
* #886 slice 2 — `true` when this row was produced from a DEGRADED scan:
|
|
92
|
+
* the visibility list returned the workflow, but extracting its observation
|
|
93
|
+
* fields threw, so the rich metadata (part/workDir/hostname/…) is a
|
|
94
|
+
* best-effort blank rather than authoritative. The row's IDENTITY
|
|
95
|
+
* (`workflowId` + best-effort `playerId`) is preserved so the player stays
|
|
96
|
+
* in the roster marked uncertain — instead of being silently dropped, which
|
|
97
|
+
* reads as "player absent" and causes roster flapping (contra #777). Absent
|
|
98
|
+
* / `false` on every healthy row.
|
|
99
|
+
*/
|
|
100
|
+
degraded?: boolean;
|
|
90
101
|
}
|
|
91
102
|
/**
|
|
92
103
|
* T0.1 (#748) — cloud-profile ensemble scan. Observation path ONLY (see the
|
|
@@ -96,13 +107,13 @@ export interface EnsembleSessionInfo {
|
|
|
96
107
|
* - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
|
|
97
108
|
* filter SA — no more cluster-wide list + per-session `getMetadata`
|
|
98
109
|
* pre-filtering (the unfiltered scan was the dominant idle-burn driver).
|
|
99
|
-
* -
|
|
100
|
-
* player
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
110
|
+
* - The entire player row is read from the list result (SAs + memo) —
|
|
111
|
+
* **zero** per-player queries except the BPM `getActivityState` query,
|
|
112
|
+
* which is intentionally kept per the architect's ruling (deriving BPM
|
|
113
|
+
* from phase transitions would change the metric's meaning). Every 2.0
|
|
114
|
+
* session seeds the observation memo at workflow start (T0.5 #747 /
|
|
115
|
+
* #757), so `AgentTempoWorkDir` is always present; the pre-v1.8 memo-
|
|
116
|
+
* absent per-player `getMetadata`/`getPart` fallback was removed in #874.
|
|
106
117
|
*
|
|
107
118
|
* Staleness: SA/memo reads are eventually consistent (tens of seconds worst
|
|
108
119
|
* case under backlog) — acceptable for the observation path per the design
|
|
@@ -160,13 +160,13 @@ async function resolveByDerivedId(client, ensemble, playerName) {
|
|
|
160
160
|
* - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
|
|
161
161
|
* filter SA — no more cluster-wide list + per-session `getMetadata`
|
|
162
162
|
* pre-filtering (the unfiltered scan was the dominant idle-burn driver).
|
|
163
|
-
* -
|
|
164
|
-
* player
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
163
|
+
* - The entire player row is read from the list result (SAs + memo) —
|
|
164
|
+
* **zero** per-player queries except the BPM `getActivityState` query,
|
|
165
|
+
* which is intentionally kept per the architect's ruling (deriving BPM
|
|
166
|
+
* from phase transitions would change the metric's meaning). Every 2.0
|
|
167
|
+
* session seeds the observation memo at workflow start (T0.5 #747 /
|
|
168
|
+
* #757), so `AgentTempoWorkDir` is always present; the pre-v1.8 memo-
|
|
169
|
+
* absent per-player `getMetadata`/`getPart` fallback was removed in #874.
|
|
170
170
|
*
|
|
171
171
|
* Staleness: SA/memo reads are eventually consistent (tens of seconds worst
|
|
172
172
|
* case under backlog) — acceptable for the observation path per the design
|
|
@@ -179,52 +179,64 @@ async function scanEnsembleSessionsCloud(client, ensemble, log = () => { }) {
|
|
|
179
179
|
try {
|
|
180
180
|
for await (const workflow of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.scanEnsembleSessions, 'scanEnsembleSessionsCloud')) {
|
|
181
181
|
try {
|
|
182
|
-
const handle = client.workflow.getHandle(workflow.workflowId);
|
|
183
182
|
const phase = (0, search_attributes_1.getAttachmentPhase)(workflow);
|
|
184
183
|
const playerId = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoPlayerId') ?? workflow.workflowId;
|
|
185
184
|
const hostname = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoHostname') ?? '';
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
else {
|
|
205
|
-
// Legacy run (pre-v1.8 memo) — per-player query fallback, bounded
|
|
206
|
-
// (#433). Same two queries the legacy scan used.
|
|
207
|
-
const metadata = await (0, query_timeout_1.queryHandleWithTimeout)(handle, 'getMetadata');
|
|
208
|
-
const part = await (0, query_timeout_1.queryHandleWithTimeout)(handle, 'getPart');
|
|
209
|
-
row = {
|
|
210
|
-
playerId: metadata.playerId,
|
|
211
|
-
part,
|
|
212
|
-
hostname: metadata.hostname,
|
|
213
|
-
workDir: metadata.workDir,
|
|
214
|
-
gitRoot: metadata.gitRoot,
|
|
215
|
-
gitBranch: metadata.gitBranch,
|
|
216
|
-
isConductor: metadata.isConductor,
|
|
217
|
-
agentType: metadata.agentType || 'claude',
|
|
218
|
-
playerType: metadata.playerType,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
185
|
+
// Every 2.0 session seeds the observation memo at workflow start
|
|
186
|
+
// (T0.5 #747 / #757), so the entire player row is read straight from
|
|
187
|
+
// the visibility list result — SAs + memo, zero per-player queries
|
|
188
|
+
// (the parallel BPM `getActivityState` query below aside). The
|
|
189
|
+
// pre-v1.8 memo-absent `getMetadata`/`getPart` fallback was removed
|
|
190
|
+
// in #874 (dead under the 2.0 clean cutover).
|
|
191
|
+
const row = {
|
|
192
|
+
playerId,
|
|
193
|
+
part: (0, search_attributes_1.getPart)(workflow) ?? '',
|
|
194
|
+
hostname,
|
|
195
|
+
workDir: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.workDir) ?? '',
|
|
196
|
+
gitRoot: (0, search_attributes_1.getWorkflowMetaString)(workflow, search_attributes_1.MEMO_KEYS.gitRoot),
|
|
197
|
+
gitBranch: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.gitBranch),
|
|
198
|
+
isConductor: (0, search_attributes_1.getIsConductor)(workflow)
|
|
199
|
+
?? (workflow.workflowId?.endsWith('-conductor') ?? false),
|
|
200
|
+
agentType: (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.agentType) || 'claude',
|
|
201
|
+
playerType: (0, search_attributes_1.getPlayerType)(workflow),
|
|
202
|
+
};
|
|
221
203
|
// BPM fields filled in below — kept out of the enumeration loop so
|
|
222
204
|
// per-player query latency can't eat the visibility deadline.
|
|
223
205
|
sessions.push({ workflowId: workflow.workflowId, ...row, phase });
|
|
224
206
|
}
|
|
225
|
-
catch {
|
|
226
|
-
//
|
|
227
|
-
//
|
|
207
|
+
catch (rowErr) {
|
|
208
|
+
// #886 slice 2 — observation extraction threw for this workflow. The
|
|
209
|
+
// OLD behaviour silently dropped the row, which makes a transient
|
|
210
|
+
// failure read as "player absent" → roster flapping (contra #777). The
|
|
211
|
+
// workflow IS in the visibility list, so the player exists; emit a
|
|
212
|
+
// DEGRADED row that preserves identity (workflowId + best-effort
|
|
213
|
+
// playerId) and flags `degraded: true`, instead of dropping it.
|
|
214
|
+
//
|
|
215
|
+
// playerId is salvaged in its own guard so a throwing getter can't
|
|
216
|
+
// escape to the outer (visibility-timeout-only) catch and fail the
|
|
217
|
+
// whole scan; it falls back to the workflowId. Identity preservation
|
|
218
|
+
// is the point — a row keyed on the real playerId avoids the
|
|
219
|
+
// remove+add churn that dropping (or re-keying on workflowId) causes.
|
|
220
|
+
let salvagedPlayerId = workflow.workflowId;
|
|
221
|
+
try {
|
|
222
|
+
salvagedPlayerId =
|
|
223
|
+
(0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoPlayerId') ?? workflow.workflowId;
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
/* keep workflowId */
|
|
227
|
+
}
|
|
228
|
+
sessions.push({
|
|
229
|
+
workflowId: workflow.workflowId,
|
|
230
|
+
playerId: salvagedPlayerId,
|
|
231
|
+
part: '',
|
|
232
|
+
hostname: '',
|
|
233
|
+
workDir: '',
|
|
234
|
+
isConductor: workflow.workflowId.endsWith('-conductor'),
|
|
235
|
+
agentType: 'claude',
|
|
236
|
+
degraded: true,
|
|
237
|
+
});
|
|
238
|
+
log(`scanEnsembleSessionsCloud: degraded row for ${workflow.workflowId} ` +
|
|
239
|
+
`(observation extraction failed: ${rowErr instanceof Error ? rowErr.message : String(rowErr)})`);
|
|
228
240
|
}
|
|
229
241
|
}
|
|
230
242
|
}
|
|
@@ -22,6 +22,13 @@ exports.claudeCodeDescriptor = {
|
|
|
22
22
|
// Interactive class — 60s cadence per design §4.3. The base class drives the
|
|
23
23
|
// heartbeat loop at this interval when the V2 lifecycle path is active.
|
|
24
24
|
heartbeatMs: 60_000,
|
|
25
|
+
// #704 — the interactive spawn passes `--dangerously-load-development-channels`,
|
|
26
|
+
// which shows a BLOCKING dev-channels dialog that requires a human click and
|
|
27
|
+
// can park the session pre-attach. So the booting attach-timeout watchdog must
|
|
28
|
+
// NOT arm for this adapter (an operator-away false-kill is worse than the hang).
|
|
29
|
+
// The 6 headless adapters omit this (⇒ falsy ⇒ armed). Flip to remove once #890
|
|
30
|
+
// dissolves the dialog. See docs/design/704-is-demo-companion-brief.md §2.
|
|
31
|
+
canBlockOnDialog: true,
|
|
25
32
|
};
|
|
26
33
|
// #749: one family of backoff curves across the codebase — this poller's
|
|
27
34
|
// error backoff shares the SDK idle-backoff constants (values unchanged:
|
|
@@ -55,6 +55,7 @@ const config_1 = require("../config");
|
|
|
55
55
|
const spawn_1 = require("../spawn");
|
|
56
56
|
const probe_1 = require("../pi/probe");
|
|
57
57
|
const install_1 = require("../pi/install");
|
|
58
|
+
const constants_1 = require("../constants");
|
|
58
59
|
const out = __importStar(require("./output"));
|
|
59
60
|
/**
|
|
60
61
|
* Public entry point — invoked by the CLI dispatcher. Launches the board in a
|
|
@@ -95,6 +96,14 @@ async function commandCenterCommand(args) {
|
|
|
95
96
|
process.exit(1);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
// #791 — surface the three access tiers up front so the subscription-friendly
|
|
100
|
+
// posture is discoverable: the board + operator controls need NO auth on a
|
|
101
|
+
// local daemon; only the LLM planner needs a Claude subscription (`/login`,
|
|
102
|
+
// zero API key) or an API key. Wording is the single source of truth in
|
|
103
|
+
// constants.ts (mirrored into docs/cli.md, drift-guarded).
|
|
104
|
+
out.log(out.dim(' Access tiers (the board works with no auth — only the LLM planner needs a key/login):'));
|
|
105
|
+
for (const line of (0, constants_1.commandCenterAccessTierLines)())
|
|
106
|
+
out.log(out.dim(line));
|
|
98
107
|
// Admin (T3) token — mission-control's operator write/gate surface reads it.
|
|
99
108
|
// #54: a LOCAL (loopback) daemon grants full trust tokenless, so a tokenless
|
|
100
109
|
// board is fully functional locally; only a REMOTE / 0.0.0.0 daemon requires the
|
|
@@ -105,8 +114,13 @@ async function commandCenterCommand(args) {
|
|
|
105
114
|
out.log(out.dim(` ${config_1.ENV.HTTP_ADMIN_TOKEN} not set — fine for a local (loopback) daemon (full trust). ` +
|
|
106
115
|
'Set it only if this board drives a remote / 0.0.0.0 daemon.'));
|
|
107
116
|
}
|
|
117
|
+
// #791 — NOT a warning: a missing API key is an expected, fully-supported
|
|
118
|
+
// state. The board + operator controls work regardless; point at the `/login`
|
|
119
|
+
// zero-API-key planner path (Claude Pro/Max OAuth) rather than implying a
|
|
120
|
+
// degraded fallback.
|
|
108
121
|
if (!process.env.ANTHROPIC_API_KEY) {
|
|
109
|
-
out.
|
|
122
|
+
out.log(out.dim(' ANTHROPIC_API_KEY not set — the board + operator controls still work. ' +
|
|
123
|
+
'Run /login inside the board to enable the LLM planner on your Claude subscription (zero API key).'));
|
|
110
124
|
}
|
|
111
125
|
const temporalEnvVars = {
|
|
112
126
|
[config_1.ENV.TEMPORAL_ADDRESS]: config.temporalAddress,
|
package/dist/cli/help-text.js
CHANGED
|
@@ -69,6 +69,7 @@ ${out.bold('Usage:')}
|
|
|
69
69
|
agent-tempo <command> [options]
|
|
70
70
|
|
|
71
71
|
${out.bold('Commands:')}
|
|
72
|
+
${out.cyan('home')} Bare landing (default): auto-provision, show status, print operator hints
|
|
72
73
|
${out.cyan('up')} Start infrastructure only — Temporal, daemon, MCP registration
|
|
73
74
|
${out.cyan('down')} Stop infrastructure; workflows stay parked for the next ${out.dim('up')}
|
|
74
75
|
${out.cyan('down --destroy [-y]')} Terminate every workflow across every ensemble, then stop infrastructure
|
|
@@ -93,6 +94,7 @@ ${out.bold('Commands:')}
|
|
|
93
94
|
${out.cyan('init')} Register MCP server globally (or --project for .mcp.json)
|
|
94
95
|
${out.cyan('install-pi')} Install the Pi extensions into Pi settings (or --project for .pi/settings.json)
|
|
95
96
|
${out.cyan('preflight')} Run preflight checks only
|
|
97
|
+
${out.cyan('version')} Print the installed version (alias: --version, -v)
|
|
96
98
|
${out.cyan('help')} Show this help message
|
|
97
99
|
|
|
98
100
|
${out.bold('Removed — use the command-center board')} ${out.dim('(agent-tempo command-center)')}:
|
package/dist/config.d.ts
CHANGED
|
@@ -18,6 +18,20 @@ export declare const ENV: {
|
|
|
18
18
|
readonly DEFAULT_AGENT: "AGENT_TEMPO_DEFAULT_AGENT";
|
|
19
19
|
readonly PLAYER_TYPE: "AGENT_TEMPO_PLAYER_TYPE";
|
|
20
20
|
readonly CLAUDE_BIN: "AGENT_TEMPO_CLAUDE_BIN";
|
|
21
|
+
/**
|
|
22
|
+
* #704 — override (ms) for the booting attach-timeout watchdog deadline. Read
|
|
23
|
+
* at spawn time (`startRecruitedSession`) and threaded onto durable metadata
|
|
24
|
+
* (`SessionMetadata.bootingDeadlineMs`) because workflows can't read env.
|
|
25
|
+
* Default 180s. For tests/tuning.
|
|
26
|
+
*/
|
|
27
|
+
readonly BOOTING_DEADLINE_MS: "AGENT_TEMPO_BOOTING_DEADLINE_MS";
|
|
28
|
+
/**
|
|
29
|
+
* #704 — override (ms) for how long a destroyed / boot-timed-out run's
|
|
30
|
+
* close-reason tombstone is honored by the bootstrap orphan-guard
|
|
31
|
+
* (`server.ts`). A late-launching orphan within this window self-exits.
|
|
32
|
+
* Default 6h (the observed orphan spawn delay was ~100min). For tuning.
|
|
33
|
+
*/
|
|
34
|
+
readonly ORPHAN_TOMBSTONE_TTL_MS: "AGENT_TEMPO_ORPHAN_TOMBSTONE_TTL_MS";
|
|
21
35
|
/**
|
|
22
36
|
* #753 — cadence (ms) of the periodic `[agent-tempo:action-counters]` log
|
|
23
37
|
* line each instrumented process emits. Default 5 minutes; `0` disables.
|
package/dist/config.js
CHANGED
|
@@ -47,6 +47,20 @@ exports.ENV = {
|
|
|
47
47
|
DEFAULT_AGENT: 'AGENT_TEMPO_DEFAULT_AGENT',
|
|
48
48
|
PLAYER_TYPE: 'AGENT_TEMPO_PLAYER_TYPE',
|
|
49
49
|
CLAUDE_BIN: 'AGENT_TEMPO_CLAUDE_BIN',
|
|
50
|
+
/**
|
|
51
|
+
* #704 — override (ms) for the booting attach-timeout watchdog deadline. Read
|
|
52
|
+
* at spawn time (`startRecruitedSession`) and threaded onto durable metadata
|
|
53
|
+
* (`SessionMetadata.bootingDeadlineMs`) because workflows can't read env.
|
|
54
|
+
* Default 180s. For tests/tuning.
|
|
55
|
+
*/
|
|
56
|
+
BOOTING_DEADLINE_MS: 'AGENT_TEMPO_BOOTING_DEADLINE_MS',
|
|
57
|
+
/**
|
|
58
|
+
* #704 — override (ms) for how long a destroyed / boot-timed-out run's
|
|
59
|
+
* close-reason tombstone is honored by the bootstrap orphan-guard
|
|
60
|
+
* (`server.ts`). A late-launching orphan within this window self-exits.
|
|
61
|
+
* Default 6h (the observed orphan spawn delay was ~100min). For tuning.
|
|
62
|
+
*/
|
|
63
|
+
ORPHAN_TOMBSTONE_TTL_MS: 'AGENT_TEMPO_ORPHAN_TOMBSTONE_TTL_MS',
|
|
50
64
|
/**
|
|
51
65
|
* #753 — cadence (ms) of the periodic `[agent-tempo:action-counters]` log
|
|
52
66
|
* line each instrumented process emits. Default 5 minutes; `0` disables.
|
package/dist/constants.d.ts
CHANGED
|
@@ -70,3 +70,31 @@ export declare function ensembleReadyBanner(name: string, playerCount: number):
|
|
|
70
70
|
* actually stops other players from acting while the conductor waits.
|
|
71
71
|
*/
|
|
72
72
|
export declare function ensembleReadyDirective(name: string, playerCount: number): string;
|
|
73
|
+
/** One command-center access tier — its short label + the source-of-truth wording. */
|
|
74
|
+
export interface CommandCenterAccessTier {
|
|
75
|
+
/** Short label shown at the head of the line (e.g. `No auth`, `/login`, `API key`). */
|
|
76
|
+
label: string;
|
|
77
|
+
/** Tier wording — mirrored VERBATIM into docs/cli.md (drift-guarded). */
|
|
78
|
+
description: string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* #791 — command-center access tiers. THE SINGLE SOURCE OF TRUTH for the three
|
|
82
|
+
* access levels the `command-center` board offers, shared between the launch
|
|
83
|
+
* banner (`src/cli/command-center-command.ts`) and `docs/cli.md`.
|
|
84
|
+
*
|
|
85
|
+
* The tiers are independent, NOT a strict ladder: tier 1 (board + operator
|
|
86
|
+
* controls) needs NO auth on a local loopback daemon; tiers 2/3 only add the
|
|
87
|
+
* LLM PLANNER (ask / handoff / recruit), which needs either a Claude
|
|
88
|
+
* subscription via in-session `/login` (zero API key) or an `ANTHROPIC_API_KEY`.
|
|
89
|
+
*
|
|
90
|
+
* `tests/cli/command-center-access-tiers.test.ts` asserts the banner renders
|
|
91
|
+
* every tier AND that `docs/cli.md` carries each tier's label + description
|
|
92
|
+
* verbatim — so the launch text and the docs can never drift.
|
|
93
|
+
*/
|
|
94
|
+
export declare const COMMAND_CENTER_ACCESS_TIERS: readonly CommandCenterAccessTier[];
|
|
95
|
+
/**
|
|
96
|
+
* Render {@link COMMAND_CENTER_ACCESS_TIERS} as plain indented ` • label — description`
|
|
97
|
+
* lines for the `command-center` launch banner. One string per tier; the caller
|
|
98
|
+
* styles them and prints a heading. Runtime-free, so it stays importable anywhere.
|
|
99
|
+
*/
|
|
100
|
+
export declare function commandCenterAccessTierLines(): string[];
|
package/dist/constants.js
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* code without dragging Node-only modules into the sandboxed bundle.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.PROTOCOL_VERSION = exports.ENSEMBLE_SENTINEL_FLAG = void 0;
|
|
9
|
+
exports.COMMAND_CENTER_ACCESS_TIERS = exports.PROTOCOL_VERSION = exports.ENSEMBLE_SENTINEL_FLAG = void 0;
|
|
10
10
|
exports.escapeNameForRegex = escapeNameForRegex;
|
|
11
11
|
exports.ensembleReadyBanner = ensembleReadyBanner;
|
|
12
12
|
exports.ensembleReadyDirective = ensembleReadyDirective;
|
|
13
|
+
exports.commandCenterAccessTierLines = commandCenterAccessTierLines;
|
|
13
14
|
/**
|
|
14
15
|
* CLI flag injected into every Claude Code spawn to carry the ensemble name as a
|
|
15
16
|
* sentinel in the process's CommandLine. `src/activities/hard-terminate.ts`
|
|
@@ -94,3 +95,39 @@ function ensembleReadyDirective(name, playerCount) {
|
|
|
94
95
|
'If the user has not spoken yet, wait silently.',
|
|
95
96
|
].join('\n');
|
|
96
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* #791 — command-center access tiers. THE SINGLE SOURCE OF TRUTH for the three
|
|
100
|
+
* access levels the `command-center` board offers, shared between the launch
|
|
101
|
+
* banner (`src/cli/command-center-command.ts`) and `docs/cli.md`.
|
|
102
|
+
*
|
|
103
|
+
* The tiers are independent, NOT a strict ladder: tier 1 (board + operator
|
|
104
|
+
* controls) needs NO auth on a local loopback daemon; tiers 2/3 only add the
|
|
105
|
+
* LLM PLANNER (ask / handoff / recruit), which needs either a Claude
|
|
106
|
+
* subscription via in-session `/login` (zero API key) or an `ANTHROPIC_API_KEY`.
|
|
107
|
+
*
|
|
108
|
+
* `tests/cli/command-center-access-tiers.test.ts` asserts the banner renders
|
|
109
|
+
* every tier AND that `docs/cli.md` carries each tier's label + description
|
|
110
|
+
* verbatim — so the launch text and the docs can never drift.
|
|
111
|
+
*/
|
|
112
|
+
exports.COMMAND_CENTER_ACCESS_TIERS = [
|
|
113
|
+
{
|
|
114
|
+
label: 'No auth',
|
|
115
|
+
description: 'board + operator controls (cue, pause, play, restart, destroy) — works out of the box on a local (loopback) daemon; no token or login required.',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
label: '/login',
|
|
119
|
+
description: 'LLM planner on your Claude Pro/Max subscription — run /login inside the board to enable the planner (ask, handoff, recruit) with zero API key.',
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'API key',
|
|
123
|
+
description: 'LLM planner on an API key — set ANTHROPIC_API_KEY before launch to run the planner against the Anthropic API.',
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
/**
|
|
127
|
+
* Render {@link COMMAND_CENTER_ACCESS_TIERS} as plain indented ` • label — description`
|
|
128
|
+
* lines for the `command-center` launch banner. One string per tier; the caller
|
|
129
|
+
* styles them and prints a heading. Runtime-free, so it stays importable anywhere.
|
|
130
|
+
*/
|
|
131
|
+
function commandCenterAccessTierLines() {
|
|
132
|
+
return exports.COMMAND_CENTER_ACCESS_TIERS.map((t) => ` • ${t.label} — ${t.description}`);
|
|
133
|
+
}
|
package/dist/daemon.js
CHANGED
|
@@ -75,6 +75,8 @@ const inner_loop_1 = require("./http/inner-loop");
|
|
|
75
75
|
const doorbell_1 = require("./http/doorbell");
|
|
76
76
|
const ingest_registry_1 = require("./http/ingest-registry");
|
|
77
77
|
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
78
|
+
const worker_2 = require("@temporalio/worker");
|
|
79
|
+
const nondeterminism_alarm_1 = require("./observability/nondeterminism-alarm");
|
|
78
80
|
const action_counters_1 = require("./utils/action-counters");
|
|
79
81
|
const daemon_1 = require("./cli/daemon");
|
|
80
82
|
const client_3 = require("./client");
|
|
@@ -847,6 +849,33 @@ async function main() {
|
|
|
847
849
|
// close race so a stray retry timer can't kill the long-lived daemon. See
|
|
848
850
|
// src/utils/grpc-shutdown-guard.ts.
|
|
849
851
|
(0, grpc_shutdown_guard_1.installGrpcShutdownGuard)();
|
|
852
|
+
// #886 slice 1 — install the nondeterminism alarm BEFORE any Worker.create.
|
|
853
|
+
// Wraps Temporal's Runtime logger so a nondeterminism / determinism-violation
|
|
854
|
+
// flap (the #801 incident: 57 workflow-task failures in 3min with ZERO
|
|
855
|
+
// operator signal) is NAMED instantly — a prominent `[agent-tempo:ALARM]`
|
|
856
|
+
// line with a running count — and surfaced on `GET /v1/health`
|
|
857
|
+
// (`HealthV1.nondeterminism`). Runtime.install must precede worker creation
|
|
858
|
+
// and can only run once per process; both hold here (daemon entry point).
|
|
859
|
+
{
|
|
860
|
+
const alarm = new nondeterminism_alarm_1.NondeterminismAlarm({
|
|
861
|
+
onHit: (count, sample) => {
|
|
862
|
+
// Prominent + greppable (`ALARM`), distinct from normal daemon chatter.
|
|
863
|
+
log(`[agent-tempo:ALARM] nondeterminism #${count} — ${sample.detail}`);
|
|
864
|
+
},
|
|
865
|
+
});
|
|
866
|
+
(0, nondeterminism_alarm_1.setGlobalNondeterminismAlarm)(alarm);
|
|
867
|
+
try {
|
|
868
|
+
// Match the SDK default logger level ('INFO') so verbosity is unchanged;
|
|
869
|
+
// the wrapper only ADDS alarm detection on WARN/ERROR records.
|
|
870
|
+
worker_2.Runtime.install({ logger: (0, nondeterminism_alarm_1.wrapLoggerWithAlarm)(new worker_2.DefaultLogger('INFO'), alarm) });
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
// Runtime.install throws if a Runtime already exists this process. Non-
|
|
874
|
+
// fatal: the alarm singleton still backs /v1/health; we just couldn't
|
|
875
|
+
// intercept Core logs. (Shouldn't happen — this is the daemon entry.)
|
|
876
|
+
log('nondeterminism alarm: Runtime.install skipped (runtime already initialized):', err instanceof Error ? err.message : err);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
850
879
|
// ADR 0014 §5.4 / gate 4 — dev daemon log self-identifies. Banner fires
|
|
851
880
|
// first so it lands at the top of `~/.agent-tempo-dev/daemon.log` for
|
|
852
881
|
// grep-friendly identification regardless of subsequent log volume.
|
|
@@ -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
|
package/dist/http/server.js
CHANGED
|
@@ -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';
|
|
@@ -706,6 +710,12 @@ function handleHealth(res, ctx) {
|
|
|
706
710
|
external: mem.external,
|
|
707
711
|
arrayBuffers: mem.arrayBuffers,
|
|
708
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
|
+
})(),
|
|
709
719
|
};
|
|
710
720
|
// Best-effort ensemble count — soft-fail to 0 rather than 500ing on a
|
|
711
721
|
// healthcheck. `/v1/health` MUST stay reachable even when Temporal is
|
package/dist/http/snapshot.js
CHANGED
|
@@ -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
|
/**
|