agent-tempo 1.7.0-beta.8 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +29 -6
- package/README.md +2 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/maestro.d.ts +46 -1
- package/dist/activities/maestro.js +69 -20
- package/dist/activities/outbox.d.ts +14 -1
- package/dist/activities/outbox.js +71 -10
- package/dist/activities/resolve.d.ts +118 -17
- package/dist/activities/resolve.js +256 -28
- package/dist/activities/schedule-fire.js +5 -2
- package/dist/adapters/base.d.ts +2 -0
- package/dist/adapters/base.js +9 -0
- package/dist/adapters/claude-api/adapter.js +26 -4
- package/dist/adapters/claude-code/adapter.js +15 -9
- package/dist/adapters/claude-code-headless/adapter.js +22 -4
- package/dist/adapters/copilot/adapter.js +58 -8
- package/dist/adapters/opencode/adapter.js +26 -6
- package/dist/adapters/sdk/base.d.ts +60 -0
- package/dist/adapters/sdk/base.js +83 -0
- package/dist/adapters/sdk/doorbell-client.d.ts +106 -0
- package/dist/adapters/sdk/doorbell-client.js +261 -0
- package/dist/adapters/sdk/idle-backoff.d.ts +71 -0
- package/dist/adapters/sdk/idle-backoff.js +136 -0
- package/dist/cli/command-center-command.js +25 -2
- package/dist/cli/commands.d.ts +28 -0
- package/dist/cli/commands.js +111 -20
- package/dist/cli/config-command.js +14 -0
- package/dist/cli/daemon-command.js +41 -5
- package/dist/cli/daemon.d.ts +245 -5
- package/dist/cli/daemon.js +505 -44
- package/dist/cli/help-text.js +3 -1
- package/dist/cli/mcp.d.ts +26 -2
- package/dist/cli/mcp.js +33 -4
- package/dist/cli/sa-preflight.d.ts +27 -1
- package/dist/cli/sa-preflight.js +29 -5
- package/dist/cli/startup.js +8 -2
- package/dist/cli/upgrade-to-2-command.d.ts +10 -0
- package/dist/cli/upgrade-to-2-command.js +159 -0
- package/dist/cli.js +37 -5
- package/dist/client/core.d.ts +6 -0
- package/dist/client/core.js +81 -34
- package/dist/client/subscribe.d.ts +10 -0
- package/dist/client/subscribe.js +2 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.js +70 -1
- package/dist/daemon.d.ts +78 -7
- package/dist/daemon.js +313 -29
- package/dist/ensemble/saver.d.ts +10 -0
- package/dist/ensemble/saver.js +18 -6
- package/dist/http/aggregate.d.ts +88 -1
- package/dist/http/aggregate.js +157 -13
- package/dist/http/deliverability.d.ts +68 -0
- package/dist/http/deliverability.js +78 -0
- package/dist/http/doorbell-routes.d.ts +32 -0
- package/dist/http/doorbell-routes.js +72 -0
- package/dist/http/doorbell.d.ts +45 -0
- package/dist/http/doorbell.js +162 -0
- package/dist/http/inner-loop-routes.d.ts +9 -0
- package/dist/http/inner-loop-routes.js +4 -0
- package/dist/http/qa.js +8 -1
- package/dist/http/server.d.ts +9 -0
- package/dist/http/server.js +45 -1
- package/dist/http/snapshot.d.ts +28 -5
- package/dist/http/snapshot.js +16 -6
- package/dist/http/writes.js +24 -2
- package/dist/pi/cue-pump.d.ts +107 -7
- package/dist/pi/cue-pump.js +131 -24
- package/dist/pi/extension.js +11 -0
- package/dist/pi/headless.js +49 -13
- package/dist/pi/install.d.ts +15 -0
- package/dist/pi/install.js +35 -0
- package/dist/pi/mission-control/actions.d.ts +52 -3
- package/dist/pi/mission-control/actions.js +112 -3
- package/dist/pi/mission-control/board.d.ts +104 -0
- package/dist/pi/mission-control/board.js +103 -2
- package/dist/pi/mission-control/extension.d.ts +166 -1
- package/dist/pi/mission-control/extension.js +477 -15
- package/dist/pi/mission-control/render.d.ts +25 -0
- package/dist/pi/mission-control/render.js +142 -8
- package/dist/pi/workflow-client.d.ts +29 -0
- package/dist/pi/workflow-client.js +87 -3
- package/dist/reconcile/orphans.js +8 -9
- package/dist/server-tools.js +1 -1
- package/dist/server.js +11 -1
- package/dist/spawn.d.ts +78 -26
- package/dist/spawn.js +69 -37
- package/dist/tools/broadcast.js +15 -1
- package/dist/tools/cue.js +52 -11
- package/dist/tools/ensemble.js +43 -3
- package/dist/tools/recruit.js +11 -29
- package/dist/tools/who-am-i.d.ts +3 -2
- package/dist/tools/who-am-i.js +9 -2
- package/dist/types.d.ts +42 -0
- package/dist/upgrade/phase-engine.d.ts +160 -0
- package/dist/upgrade/phase-engine.js +555 -0
- package/dist/upgrade/snapshot-v1.d.ts +214 -0
- package/dist/upgrade/snapshot-v1.js +165 -0
- package/dist/utils/action-counters.d.ts +75 -0
- package/dist/utils/action-counters.js +279 -0
- package/dist/utils/format-hosts.js +7 -0
- package/dist/utils/hosts.js +5 -0
- package/dist/utils/sdk-probe.d.ts +12 -0
- package/dist/utils/sdk-probe.js +28 -0
- package/dist/utils/search-attributes.d.ts +76 -4
- package/dist/utils/search-attributes.js +89 -4
- package/dist/utils/suspension.d.ts +99 -0
- package/dist/utils/suspension.js +128 -0
- package/dist/utils/visibility-deadline.js +5 -0
- package/dist/worker.d.ts +16 -1
- package/dist/worker.js +25 -4
- package/dist/workflows/maestro.d.ts +10 -0
- package/dist/workflows/maestro.js +126 -21
- package/dist/workflows/scheduler.js +17 -2
- package/dist/workflows/session.js +251 -14
- package/dist/workflows/signals.d.ts +8 -0
- package/dist/workflows/signals.js +3 -2
- package/package.json +8 -4
- package/workflow-bundle.js +580 -40
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/** Reconnect backoff: 1s doubling to a 30s cap (same family as the daemon's bind retry, #768). */
|
|
2
|
+
export declare function doorbellReconnectDelayMs(retry: number, baseMs?: number, capMs?: number): number;
|
|
3
|
+
/**
|
|
4
|
+
* Minimal streaming-fetch shape this client needs — injectable for tests.
|
|
5
|
+
* `body` must be an async-iterable of byte chunks (Node 18+ global `fetch`
|
|
6
|
+
* response bodies are).
|
|
7
|
+
*/
|
|
8
|
+
export type DoorbellFetch = (url: string, init: {
|
|
9
|
+
headers: Record<string, string>;
|
|
10
|
+
signal: AbortSignal;
|
|
11
|
+
}) => Promise<{
|
|
12
|
+
status: number;
|
|
13
|
+
body: AsyncIterable<Uint8Array> | null;
|
|
14
|
+
}>;
|
|
15
|
+
export interface DoorbellClientOptions {
|
|
16
|
+
/** The player's ensemble (URL path segment). */
|
|
17
|
+
ensemble: string;
|
|
18
|
+
/** The player's id (URL path segment). */
|
|
19
|
+
playerId: string;
|
|
20
|
+
/** Fired once per `ding` event (already coalesced daemon-side). */
|
|
21
|
+
onDing: () => void;
|
|
22
|
+
/**
|
|
23
|
+
* Fired on connected/disconnected transitions. Drives the idle-poll
|
|
24
|
+
* ceiling (connected ⇒ 60s, disconnected ⇒ the T0.2 30s floor). Never
|
|
25
|
+
* fired redundantly (true→true / false→false).
|
|
26
|
+
*/
|
|
27
|
+
onConnectionChange?: (connected: boolean) => void;
|
|
28
|
+
/** Ingest token. Defaults to `process.env[ENV.INGEST_TOKEN]`. */
|
|
29
|
+
ingestToken?: string;
|
|
30
|
+
/** Daemon port discovery. Defaults to {@link readPortFile}. */
|
|
31
|
+
readPort?: () => number | null;
|
|
32
|
+
/** HTTP transport. Defaults to global `fetch` (client disabled if absent). */
|
|
33
|
+
fetchFn?: DoorbellFetch;
|
|
34
|
+
/** Test seam — defaults to a real setTimeout sleep. */
|
|
35
|
+
sleep?: (ms: number) => Promise<void>;
|
|
36
|
+
log?: (...args: unknown[]) => void;
|
|
37
|
+
/** Reconnect backoff knobs (tests). */
|
|
38
|
+
baseDelayMs?: number;
|
|
39
|
+
maxDelayMs?: number;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Incremental SSE block parser for the doorbell stream. Feed raw text
|
|
43
|
+
* chunks; returns the number of `ding` events completed by this chunk.
|
|
44
|
+
* Keepalive comments (`:ka`) and the `:closed` marker carry no event name
|
|
45
|
+
* and are ignored. Exported @internal for unit tests.
|
|
46
|
+
*/
|
|
47
|
+
export declare class DingParser {
|
|
48
|
+
private buffer;
|
|
49
|
+
feed(chunk: string): number;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Wakeable sleep — the ding→immediate-poll primitive, shared by
|
|
53
|
+
* `SdkAttachment` (PR-2) and the Pi cue pump (PR-3, which is NOT an
|
|
54
|
+
* SdkAttachment). Semantics:
|
|
55
|
+
*
|
|
56
|
+
* - `sleep(ms)` parks until the timer fires OR `wake()` pops it early.
|
|
57
|
+
* - `wake()` with no parked sleep sets a level-triggered pending bit so
|
|
58
|
+
* the NEXT `sleep` returns immediately — a ding that lands while the
|
|
59
|
+
* consumer is mid-tick is consumed, not lost (mirrors the daemon
|
|
60
|
+
* registry's coalescing; extra wakes coalesce into one).
|
|
61
|
+
* - One parked sleep at a time (one poll loop per consumer). A STALE
|
|
62
|
+
* timer from an abandoned sleep (e.g. a `Promise.race` loser) is
|
|
63
|
+
* identity-guarded so it can never clobber a newer sleep's waker.
|
|
64
|
+
*/
|
|
65
|
+
export declare class WakeableSleep {
|
|
66
|
+
private wakeFn;
|
|
67
|
+
private pending;
|
|
68
|
+
sleep(ms: number): Promise<void>;
|
|
69
|
+
wake(): void;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Reconnecting SSE consumer for the daemon doorbell. `start()` launches the
|
|
73
|
+
* detached connect loop; `stop()` aborts the live stream and ends it. All
|
|
74
|
+
* callbacks fire on the client's own async path — keep them cheap (the
|
|
75
|
+
* adapters' `onDing` just wakes a sleeping poll loop).
|
|
76
|
+
*/
|
|
77
|
+
export declare class DoorbellClient {
|
|
78
|
+
private readonly opts;
|
|
79
|
+
private readonly token;
|
|
80
|
+
private readonly readPort;
|
|
81
|
+
private readonly fetchFn;
|
|
82
|
+
private readonly sleep;
|
|
83
|
+
private readonly log;
|
|
84
|
+
private stopped;
|
|
85
|
+
private connected;
|
|
86
|
+
private abort;
|
|
87
|
+
private loop;
|
|
88
|
+
constructor(opts: DoorbellClientOptions);
|
|
89
|
+
/** Whether a doorbell stream is currently live (drives the poll ceiling). */
|
|
90
|
+
get isConnected(): boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Launch the connect loop. No token / no fetch ⇒ logs one breadcrumb and
|
|
93
|
+
* never subscribes (§5: pure T0.2 behavior). Idempotent.
|
|
94
|
+
*/
|
|
95
|
+
start(): void;
|
|
96
|
+
/** End the loop and abort any live stream. Idempotent. */
|
|
97
|
+
stop(): void;
|
|
98
|
+
private setConnected;
|
|
99
|
+
private runLoop;
|
|
100
|
+
/**
|
|
101
|
+
* One subscribe attempt. Returns `true` when the stream OPENED (HTTP 200)
|
|
102
|
+
* regardless of how it ended. Every failure path is silent-by-design
|
|
103
|
+
* below the transition breadcrumb.
|
|
104
|
+
*/
|
|
105
|
+
private connectOnce;
|
|
106
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DoorbellClient = exports.WakeableSleep = exports.DingParser = void 0;
|
|
4
|
+
exports.doorbellReconnectDelayMs = doorbellReconnectDelayMs;
|
|
5
|
+
/**
|
|
6
|
+
* DoorbellClient — adapter-side consumer of the daemon's cue doorbell
|
|
7
|
+
* (T1.1 PR-2, design: docs/design/t11-cue-doorbell.md §2.3/§2.4).
|
|
8
|
+
*
|
|
9
|
+
* Subscribes to `GET /doorbell/:ensemble/:playerId` (SSE; loopback +
|
|
10
|
+
* `X-Ingest-Token`) and fires `onDing` per coalesced ring. The §1 invariant
|
|
11
|
+
* governs everything here: **doorbell loss must be indistinguishable from
|
|
12
|
+
* doorbell-never-sent** —
|
|
13
|
+
*
|
|
14
|
+
* - A ding is a HINT, not a guarantee: the daemon's pending bit is
|
|
15
|
+
* level-triggered and coalesced, so a ding can arrive for a message the
|
|
16
|
+
* poll loop already fetched (one wasted query, §5 row 2) and a ring can
|
|
17
|
+
* be lost entirely (fallback poll delivers within the ceiling, §5 row 1).
|
|
18
|
+
* - EVERY failure (no token, port file missing, route 404/403/500, daemon
|
|
19
|
+
* down, stream drop) degrades to silent disconnected state + reconnect
|
|
20
|
+
* with capped backoff. NO error escalation, ever — a player must behave
|
|
21
|
+
* identically whether the doorbell never existed or just died (§5).
|
|
22
|
+
* - Connection-state transitions emit exactly one `[agent-tempo:doorbell]`
|
|
23
|
+
* breadcrumb each (the #249 observability lesson) — that is the only
|
|
24
|
+
* above-debug logging in this file.
|
|
25
|
+
*
|
|
26
|
+
* Discovery/auth follow the locked inner-loop ingress contract
|
|
27
|
+
* (`src/pi/inner-loop-client.ts`): port via {@link readPortFile} (`null` ⇒
|
|
28
|
+
* daemon HTTP not up), loopback base `http://127.0.0.1:{port}`, token from
|
|
29
|
+
* `AGENT_TEMPO_INGEST_TOKEN` (threaded at spawn; absent ⇒ never subscribes ⇒
|
|
30
|
+
* pure T0.2 behavior).
|
|
31
|
+
*
|
|
32
|
+
* **This file runs in the Node.js adapter process, NOT the Temporal workflow
|
|
33
|
+
* sandbox.**
|
|
34
|
+
*/
|
|
35
|
+
const config_1 = require("../../config");
|
|
36
|
+
const port_file_1 = require("../../http/port-file");
|
|
37
|
+
const defaultLog = (...args) => console.error('[agent-tempo:doorbell]', ...args);
|
|
38
|
+
/** Reconnect backoff: 1s doubling to a 30s cap (same family as the daemon's bind retry, #768). */
|
|
39
|
+
function doorbellReconnectDelayMs(retry, baseMs = 1_000, capMs = 30_000) {
|
|
40
|
+
return Math.min(capMs, baseMs * 2 ** Math.min(retry, 31));
|
|
41
|
+
}
|
|
42
|
+
/** Default transport — global `fetch` adapted to {@link DoorbellFetch}, or `null`. */
|
|
43
|
+
function resolveFetch() {
|
|
44
|
+
const g = globalThis.fetch;
|
|
45
|
+
if (typeof g !== 'function')
|
|
46
|
+
return null;
|
|
47
|
+
return g;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Cap on the partial-block buffer. Real doorbell blocks are tens of bytes;
|
|
51
|
+
* the daemon is trusted + loopback, so this is a cheap backstop against a
|
|
52
|
+
* pathological stream without `\n\n` terminators growing memory unboundedly
|
|
53
|
+
* — overflow just drops the buffer (a lost ding ≡ never-sent, §1).
|
|
54
|
+
*/
|
|
55
|
+
const MAX_PARSER_BUFFER = 8 * 1024;
|
|
56
|
+
/**
|
|
57
|
+
* Incremental SSE block parser for the doorbell stream. Feed raw text
|
|
58
|
+
* chunks; returns the number of `ding` events completed by this chunk.
|
|
59
|
+
* Keepalive comments (`:ka`) and the `:closed` marker carry no event name
|
|
60
|
+
* and are ignored. Exported @internal for unit tests.
|
|
61
|
+
*/
|
|
62
|
+
class DingParser {
|
|
63
|
+
buffer = '';
|
|
64
|
+
feed(chunk) {
|
|
65
|
+
this.buffer += chunk;
|
|
66
|
+
let dings = 0;
|
|
67
|
+
// SSE events are separated by a blank line. Process every complete
|
|
68
|
+
// block; keep the trailing partial in the buffer.
|
|
69
|
+
let sep = this.buffer.indexOf('\n\n');
|
|
70
|
+
while (sep !== -1) {
|
|
71
|
+
const block = this.buffer.slice(0, sep);
|
|
72
|
+
this.buffer = this.buffer.slice(sep + 2);
|
|
73
|
+
if (/^event:\s*ding\s*$/m.test(block))
|
|
74
|
+
dings++;
|
|
75
|
+
sep = this.buffer.indexOf('\n\n');
|
|
76
|
+
}
|
|
77
|
+
if (this.buffer.length > MAX_PARSER_BUFFER)
|
|
78
|
+
this.buffer = '';
|
|
79
|
+
return dings;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.DingParser = DingParser;
|
|
83
|
+
/**
|
|
84
|
+
* Wakeable sleep — the ding→immediate-poll primitive, shared by
|
|
85
|
+
* `SdkAttachment` (PR-2) and the Pi cue pump (PR-3, which is NOT an
|
|
86
|
+
* SdkAttachment). Semantics:
|
|
87
|
+
*
|
|
88
|
+
* - `sleep(ms)` parks until the timer fires OR `wake()` pops it early.
|
|
89
|
+
* - `wake()` with no parked sleep sets a level-triggered pending bit so
|
|
90
|
+
* the NEXT `sleep` returns immediately — a ding that lands while the
|
|
91
|
+
* consumer is mid-tick is consumed, not lost (mirrors the daemon
|
|
92
|
+
* registry's coalescing; extra wakes coalesce into one).
|
|
93
|
+
* - One parked sleep at a time (one poll loop per consumer). A STALE
|
|
94
|
+
* timer from an abandoned sleep (e.g. a `Promise.race` loser) is
|
|
95
|
+
* identity-guarded so it can never clobber a newer sleep's waker.
|
|
96
|
+
*/
|
|
97
|
+
class WakeableSleep {
|
|
98
|
+
wakeFn = null;
|
|
99
|
+
pending = false;
|
|
100
|
+
sleep(ms) {
|
|
101
|
+
if (this.pending) {
|
|
102
|
+
this.pending = false;
|
|
103
|
+
return Promise.resolve();
|
|
104
|
+
}
|
|
105
|
+
return new Promise((resolve) => {
|
|
106
|
+
const wake = () => {
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
if (this.wakeFn === wake)
|
|
109
|
+
this.wakeFn = null;
|
|
110
|
+
resolve();
|
|
111
|
+
};
|
|
112
|
+
const timer = setTimeout(() => {
|
|
113
|
+
if (this.wakeFn === wake)
|
|
114
|
+
this.wakeFn = null;
|
|
115
|
+
resolve();
|
|
116
|
+
}, ms);
|
|
117
|
+
timer.unref?.();
|
|
118
|
+
this.wakeFn = wake;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
wake() {
|
|
122
|
+
if (this.wakeFn) {
|
|
123
|
+
this.wakeFn();
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
this.pending = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.WakeableSleep = WakeableSleep;
|
|
131
|
+
/**
|
|
132
|
+
* Reconnecting SSE consumer for the daemon doorbell. `start()` launches the
|
|
133
|
+
* detached connect loop; `stop()` aborts the live stream and ends it. All
|
|
134
|
+
* callbacks fire on the client's own async path — keep them cheap (the
|
|
135
|
+
* adapters' `onDing` just wakes a sleeping poll loop).
|
|
136
|
+
*/
|
|
137
|
+
class DoorbellClient {
|
|
138
|
+
opts;
|
|
139
|
+
token;
|
|
140
|
+
readPort;
|
|
141
|
+
fetchFn;
|
|
142
|
+
sleep;
|
|
143
|
+
log;
|
|
144
|
+
stopped = false;
|
|
145
|
+
connected = false;
|
|
146
|
+
abort = null;
|
|
147
|
+
loop = null;
|
|
148
|
+
constructor(opts) {
|
|
149
|
+
this.opts = opts;
|
|
150
|
+
this.token = opts.ingestToken ?? process.env[config_1.ENV.INGEST_TOKEN];
|
|
151
|
+
this.readPort = opts.readPort ?? (() => (0, port_file_1.readPortFile)());
|
|
152
|
+
this.fetchFn = opts.fetchFn ?? resolveFetch();
|
|
153
|
+
this.sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms).unref?.()));
|
|
154
|
+
this.log = opts.log ?? defaultLog;
|
|
155
|
+
}
|
|
156
|
+
/** Whether a doorbell stream is currently live (drives the poll ceiling). */
|
|
157
|
+
get isConnected() {
|
|
158
|
+
return this.connected;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Launch the connect loop. No token / no fetch ⇒ logs one breadcrumb and
|
|
162
|
+
* never subscribes (§5: pure T0.2 behavior). Idempotent.
|
|
163
|
+
*/
|
|
164
|
+
start() {
|
|
165
|
+
if (this.loop)
|
|
166
|
+
return;
|
|
167
|
+
if (!this.token || !this.fetchFn) {
|
|
168
|
+
this.log(`doorbell disabled for ${this.opts.ensemble}/${this.opts.playerId} ` +
|
|
169
|
+
`(${!this.token ? 'no ingest token' : 'no fetch transport'}) — polling only`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.loop = this.runLoop().catch((err) => {
|
|
173
|
+
// The loop swallows everything itself — this is a last-resort guard so
|
|
174
|
+
// an unexpected throw can never escalate out of the doorbell (§5).
|
|
175
|
+
this.log('doorbell loop ended unexpectedly (polling covers):', err?.message ?? err);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
/** End the loop and abort any live stream. Idempotent. */
|
|
179
|
+
stop() {
|
|
180
|
+
if (this.stopped)
|
|
181
|
+
return;
|
|
182
|
+
this.stopped = true;
|
|
183
|
+
try {
|
|
184
|
+
this.abort?.abort();
|
|
185
|
+
}
|
|
186
|
+
catch { /* already settled */ }
|
|
187
|
+
this.setConnected(false);
|
|
188
|
+
}
|
|
189
|
+
setConnected(connected) {
|
|
190
|
+
if (this.connected === connected)
|
|
191
|
+
return;
|
|
192
|
+
this.connected = connected;
|
|
193
|
+
this.log(`doorbell ${connected ? 'connected' : 'disconnected'} ` +
|
|
194
|
+
`(${this.opts.ensemble}/${this.opts.playerId})`);
|
|
195
|
+
try {
|
|
196
|
+
this.opts.onConnectionChange?.(connected);
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
this.log('onConnectionChange threw (ignored):', err?.message ?? err);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async runLoop() {
|
|
203
|
+
for (let retry = 0; !this.stopped; retry++) {
|
|
204
|
+
const streamed = await this.connectOnce();
|
|
205
|
+
if (this.stopped)
|
|
206
|
+
return;
|
|
207
|
+
// A stream that delivered ANY bytes proves the daemon was reachable —
|
|
208
|
+
// reset the reconnect curve so a healthy-but-bounced daemon comes back
|
|
209
|
+
// at 1s, not wherever a prior outage left the counter.
|
|
210
|
+
if (streamed)
|
|
211
|
+
retry = 0;
|
|
212
|
+
await this.sleep(doorbellReconnectDelayMs(retry, this.opts.baseDelayMs, this.opts.maxDelayMs));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* One subscribe attempt. Returns `true` when the stream OPENED (HTTP 200)
|
|
217
|
+
* regardless of how it ended. Every failure path is silent-by-design
|
|
218
|
+
* below the transition breadcrumb.
|
|
219
|
+
*/
|
|
220
|
+
async connectOnce() {
|
|
221
|
+
const port = this.readPort();
|
|
222
|
+
if (port === null)
|
|
223
|
+
return false; // daemon HTTP not up — §5 "daemon down" row
|
|
224
|
+
const url = `http://127.0.0.1:${port}/doorbell/${encodeURIComponent(this.opts.ensemble)}/${encodeURIComponent(this.opts.playerId)}`;
|
|
225
|
+
this.abort = new AbortController();
|
|
226
|
+
try {
|
|
227
|
+
const res = await this.fetchFn(url, {
|
|
228
|
+
headers: { 'x-ingest-token': this.token },
|
|
229
|
+
signal: this.abort.signal,
|
|
230
|
+
});
|
|
231
|
+
if (res.status !== 200 || !res.body)
|
|
232
|
+
return false; // 403/404/5xx — §5 "route" rows
|
|
233
|
+
this.setConnected(true);
|
|
234
|
+
const parser = new DingParser();
|
|
235
|
+
const decoder = new TextDecoder();
|
|
236
|
+
for await (const chunk of res.body) {
|
|
237
|
+
if (this.stopped)
|
|
238
|
+
break;
|
|
239
|
+
const dings = parser.feed(decoder.decode(chunk, { stream: true }));
|
|
240
|
+
for (let i = 0; i < dings; i++) {
|
|
241
|
+
try {
|
|
242
|
+
this.opts.onDing();
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
this.log('onDing threw (ignored):', err?.message ?? err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Connect refused / aborted / read error — disconnected state covers it.
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
this.abort = null;
|
|
257
|
+
this.setConnected(false);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
exports.DoorbellClient = DoorbellClient;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Fast-poll cadence while a conversation is active (legacy behavior). */
|
|
2
|
+
export declare const SDK_POLL_BASE_MS = 2000;
|
|
3
|
+
/** Growth factor per consecutive empty poll. Matches the interactive
|
|
4
|
+
* claude-code poller's error-backoff factor (adapter.ts) — one family of
|
|
5
|
+
* curves across the codebase. */
|
|
6
|
+
export declare const SDK_POLL_BACKOFF_FACTOR = 1.5;
|
|
7
|
+
/** Idle ceiling. 15× the base — the steady-state idle cadence. */
|
|
8
|
+
export declare const SDK_POLL_MAX_MS = 30000;
|
|
9
|
+
/**
|
|
10
|
+
* T1.1 PR-2 — idle ceiling while the cue doorbell is CONNECTED. Latency is
|
|
11
|
+
* the doorbell's job; the 60s poll is reconciliation AND the only bound on
|
|
12
|
+
* worst-case delivery after a lost ding (§2.4 of the design doc — at-most-
|
|
13
|
+
* once, no replay), so it stays operator-tolerable rather than stretching
|
|
14
|
+
* to minutes. Disconnected ⇒ the T0.2 30s ceiling above (today's shipped
|
|
15
|
+
* behavior is the floor).
|
|
16
|
+
*/
|
|
17
|
+
export declare const SDK_POLL_DOORBELL_MAX_MS = 60000;
|
|
18
|
+
export interface IdleBackoffConfig {
|
|
19
|
+
baseMs: number;
|
|
20
|
+
factor: number;
|
|
21
|
+
maxMs: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the poll-backoff config, honoring the `AGENT_TEMPO_SDK_POLL_BASE_MS`
|
|
25
|
+
* / `AGENT_TEMPO_SDK_POLL_MAX_MS` env overrides (read at call time, not module
|
|
26
|
+
* load, so tests and spawners can set them per-process). Setting
|
|
27
|
+
* `..._MAX_MS` equal to `..._BASE_MS` pins the legacy fixed cadence —
|
|
28
|
+
* the dev/test escape hatch.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveIdleBackoffConfig(): IdleBackoffConfig;
|
|
31
|
+
/**
|
|
32
|
+
* T1.1 PR-2 — the ceiling to apply while the doorbell is connected
|
|
33
|
+
* (`AGENT_TEMPO_SDK_POLL_DOORBELL_MAX_MS` override; clamped ≥ the resolved
|
|
34
|
+
* base so a misconfigured override can never invert base/max).
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveDoorbellCeilingMs(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Pure delay computer for an idle-backoff poll loop.
|
|
39
|
+
*
|
|
40
|
+
* Usage per tick:
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* const delay = backoff.next(messages.length > 0);
|
|
44
|
+
* await sleep(delay);
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* - `next(true)` → conversation is live: reset to `baseMs` and return it.
|
|
48
|
+
* - `next(false)` → idle tick: grow the delay by `factor` (capped at `maxMs`)
|
|
49
|
+
* and return the grown value. Matches the reference
|
|
50
|
+
* poller's grow-then-sleep shape.
|
|
51
|
+
* - `reset()` → external "activity happened" signal (e.g. an SDK turn
|
|
52
|
+
* just finished and follow-ups are likely).
|
|
53
|
+
*/
|
|
54
|
+
export declare class IdleBackoff {
|
|
55
|
+
private readonly cfg;
|
|
56
|
+
private currentMs;
|
|
57
|
+
constructor(cfg?: IdleBackoffConfig);
|
|
58
|
+
/**
|
|
59
|
+
* T1.1 PR-2 — swap the idle ceiling (doorbell connected ⇒ 60s,
|
|
60
|
+
* disconnected ⇒ the 30s T0.2 floor). Base/factor/reset semantics are
|
|
61
|
+
* untouched; a `currentMs` above a LOWERED ceiling self-heals on the next
|
|
62
|
+
* `next()` (Math.min clamps it). Clamped ≥ base so the curve can't invert.
|
|
63
|
+
*/
|
|
64
|
+
setCeiling(maxMs: number): void;
|
|
65
|
+
/** The delay the next idle sleep would use (read-only; for logs/tests). */
|
|
66
|
+
get current(): number;
|
|
67
|
+
/** Compute the next poll delay from this tick's outcome. */
|
|
68
|
+
next(hadActivity: boolean): number;
|
|
69
|
+
/** Snap back to the fast cadence. */
|
|
70
|
+
reset(): void;
|
|
71
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IdleBackoff = exports.SDK_POLL_DOORBELL_MAX_MS = exports.SDK_POLL_MAX_MS = exports.SDK_POLL_BACKOFF_FACTOR = exports.SDK_POLL_BASE_MS = void 0;
|
|
4
|
+
exports.resolveIdleBackoffConfig = resolveIdleBackoffConfig;
|
|
5
|
+
exports.resolveDoorbellCeilingMs = resolveDoorbellCeilingMs;
|
|
6
|
+
/**
|
|
7
|
+
* Idle poll backoff (#749, T0.2 of the #747 Tier-0 cost epic).
|
|
8
|
+
*
|
|
9
|
+
* Every SDK-class adapter (copilot, claude-api, opencode,
|
|
10
|
+
* claude-code-headless, mock) drives a `pendingMessages` poll loop against
|
|
11
|
+
* its session workflow. Pre-#749 all five polled at a fixed 2s cadence —
|
|
12
|
+
* 43,200 billable queries/day/player even when the player sat idle
|
|
13
|
+
* overnight. Temporal Cloud bills queries as actions (1 each), so idle
|
|
14
|
+
* polling was the dominant per-player line item
|
|
15
|
+
* (docs/design/temporal-cost-rearchitecture.md, driver #3).
|
|
16
|
+
*
|
|
17
|
+
* This helper implements true IDLE backoff: each empty poll stretches the
|
|
18
|
+
* next delay by {@link SDK_POLL_BACKOFF_FACTOR} up to {@link SDK_POLL_MAX_MS};
|
|
19
|
+
* any delivered message snaps back to {@link SDK_POLL_BASE_MS} so an active
|
|
20
|
+
* conversation keeps the legacy 2s responsiveness. Steady-state idle lands
|
|
21
|
+
* at one query per 30s = 2,880/day — a 15× reduction (43,200 ÷ 2,880;
|
|
22
|
+
* ratio locked by a unit test).
|
|
23
|
+
*
|
|
24
|
+
* Trade-off (accepted in #749): the FIRST cue to a long-idle player waits up
|
|
25
|
+
* to 30s before the adapter notices. Delivery itself is unchanged — the
|
|
26
|
+
* workflow inbox is durable and the next poll drains the full batch. The
|
|
27
|
+
* T1.1 doorbell (push delivery) later removes even that latency.
|
|
28
|
+
*
|
|
29
|
+
* Pure and timer-free — callers own the `setTimeout`/`sleep`; this class
|
|
30
|
+
* only computes delays. That keeps it unit-testable without fake timers.
|
|
31
|
+
*
|
|
32
|
+
* **This file runs in the Node.js adapter process, NOT the Temporal workflow
|
|
33
|
+
* sandbox.**
|
|
34
|
+
*/
|
|
35
|
+
const config_1 = require("../../config");
|
|
36
|
+
/** Fast-poll cadence while a conversation is active (legacy behavior). */
|
|
37
|
+
exports.SDK_POLL_BASE_MS = 2_000;
|
|
38
|
+
/** Growth factor per consecutive empty poll. Matches the interactive
|
|
39
|
+
* claude-code poller's error-backoff factor (adapter.ts) — one family of
|
|
40
|
+
* curves across the codebase. */
|
|
41
|
+
exports.SDK_POLL_BACKOFF_FACTOR = 1.5;
|
|
42
|
+
/** Idle ceiling. 15× the base — the steady-state idle cadence. */
|
|
43
|
+
exports.SDK_POLL_MAX_MS = 30_000;
|
|
44
|
+
/**
|
|
45
|
+
* T1.1 PR-2 — idle ceiling while the cue doorbell is CONNECTED. Latency is
|
|
46
|
+
* the doorbell's job; the 60s poll is reconciliation AND the only bound on
|
|
47
|
+
* worst-case delivery after a lost ding (§2.4 of the design doc — at-most-
|
|
48
|
+
* once, no replay), so it stays operator-tolerable rather than stretching
|
|
49
|
+
* to minutes. Disconnected ⇒ the T0.2 30s ceiling above (today's shipped
|
|
50
|
+
* behavior is the floor).
|
|
51
|
+
*/
|
|
52
|
+
exports.SDK_POLL_DOORBELL_MAX_MS = 60_000;
|
|
53
|
+
/** Parse a positive-integer env override; fall back on absent/garbage. */
|
|
54
|
+
function envInt(name, fallback) {
|
|
55
|
+
const raw = process.env[name];
|
|
56
|
+
if (raw === undefined || raw === '')
|
|
57
|
+
return fallback;
|
|
58
|
+
const n = Number.parseInt(raw, 10);
|
|
59
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Resolve the poll-backoff config, honoring the `AGENT_TEMPO_SDK_POLL_BASE_MS`
|
|
63
|
+
* / `AGENT_TEMPO_SDK_POLL_MAX_MS` env overrides (read at call time, not module
|
|
64
|
+
* load, so tests and spawners can set them per-process). Setting
|
|
65
|
+
* `..._MAX_MS` equal to `..._BASE_MS` pins the legacy fixed cadence —
|
|
66
|
+
* the dev/test escape hatch.
|
|
67
|
+
*/
|
|
68
|
+
function resolveIdleBackoffConfig() {
|
|
69
|
+
const baseMs = envInt(config_1.ENV.SDK_POLL_BASE_MS, exports.SDK_POLL_BASE_MS);
|
|
70
|
+
const maxMs = Math.max(baseMs, envInt(config_1.ENV.SDK_POLL_MAX_MS, exports.SDK_POLL_MAX_MS));
|
|
71
|
+
return { baseMs, factor: exports.SDK_POLL_BACKOFF_FACTOR, maxMs };
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* T1.1 PR-2 — the ceiling to apply while the doorbell is connected
|
|
75
|
+
* (`AGENT_TEMPO_SDK_POLL_DOORBELL_MAX_MS` override; clamped ≥ the resolved
|
|
76
|
+
* base so a misconfigured override can never invert base/max).
|
|
77
|
+
*/
|
|
78
|
+
function resolveDoorbellCeilingMs() {
|
|
79
|
+
const baseMs = envInt(config_1.ENV.SDK_POLL_BASE_MS, exports.SDK_POLL_BASE_MS);
|
|
80
|
+
return Math.max(baseMs, envInt(config_1.ENV.SDK_POLL_DOORBELL_MAX_MS, exports.SDK_POLL_DOORBELL_MAX_MS));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Pure delay computer for an idle-backoff poll loop.
|
|
84
|
+
*
|
|
85
|
+
* Usage per tick:
|
|
86
|
+
*
|
|
87
|
+
* ```ts
|
|
88
|
+
* const delay = backoff.next(messages.length > 0);
|
|
89
|
+
* await sleep(delay);
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* - `next(true)` → conversation is live: reset to `baseMs` and return it.
|
|
93
|
+
* - `next(false)` → idle tick: grow the delay by `factor` (capped at `maxMs`)
|
|
94
|
+
* and return the grown value. Matches the reference
|
|
95
|
+
* poller's grow-then-sleep shape.
|
|
96
|
+
* - `reset()` → external "activity happened" signal (e.g. an SDK turn
|
|
97
|
+
* just finished and follow-ups are likely).
|
|
98
|
+
*/
|
|
99
|
+
class IdleBackoff {
|
|
100
|
+
cfg;
|
|
101
|
+
currentMs;
|
|
102
|
+
constructor(cfg = resolveIdleBackoffConfig()) {
|
|
103
|
+
// Copy — `setCeiling` mutates maxMs and must never write through to a
|
|
104
|
+
// shared config object a caller handed in.
|
|
105
|
+
this.cfg = { ...cfg };
|
|
106
|
+
this.currentMs = cfg.baseMs;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* T1.1 PR-2 — swap the idle ceiling (doorbell connected ⇒ 60s,
|
|
110
|
+
* disconnected ⇒ the 30s T0.2 floor). Base/factor/reset semantics are
|
|
111
|
+
* untouched; a `currentMs` above a LOWERED ceiling self-heals on the next
|
|
112
|
+
* `next()` (Math.min clamps it). Clamped ≥ base so the curve can't invert.
|
|
113
|
+
*/
|
|
114
|
+
setCeiling(maxMs) {
|
|
115
|
+
this.cfg.maxMs = Math.max(this.cfg.baseMs, maxMs);
|
|
116
|
+
}
|
|
117
|
+
/** The delay the next idle sleep would use (read-only; for logs/tests). */
|
|
118
|
+
get current() {
|
|
119
|
+
return this.currentMs;
|
|
120
|
+
}
|
|
121
|
+
/** Compute the next poll delay from this tick's outcome. */
|
|
122
|
+
next(hadActivity) {
|
|
123
|
+
if (hadActivity) {
|
|
124
|
+
this.currentMs = this.cfg.baseMs;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.currentMs = Math.min(this.currentMs * this.cfg.factor, this.cfg.maxMs);
|
|
128
|
+
}
|
|
129
|
+
return this.currentMs;
|
|
130
|
+
}
|
|
131
|
+
/** Snap back to the fast cadence. */
|
|
132
|
+
reset() {
|
|
133
|
+
this.currentMs = this.cfg.baseMs;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.IdleBackoff = IdleBackoff;
|
|
@@ -54,6 +54,7 @@ exports.commandCenterCommand = commandCenterCommand;
|
|
|
54
54
|
const config_1 = require("../config");
|
|
55
55
|
const spawn_1 = require("../spawn");
|
|
56
56
|
const probe_1 = require("../pi/probe");
|
|
57
|
+
const install_1 = require("../pi/install");
|
|
57
58
|
const out = __importStar(require("./output"));
|
|
58
59
|
/**
|
|
59
60
|
* Public entry point — invoked by the CLI dispatcher. Launches the board in a
|
|
@@ -62,7 +63,12 @@ const out = __importStar(require("./output"));
|
|
|
62
63
|
*/
|
|
63
64
|
async function commandCenterCommand(args) {
|
|
64
65
|
const config = (0, config_1.getConfig)(args);
|
|
65
|
-
|
|
66
|
+
// #820 (Bug 3) — honor the positional / `--ensemble` flag. `args.ensemble` is
|
|
67
|
+
// resolved by the CLI dispatch via the canonical `resolveEnsemble` (flag >
|
|
68
|
+
// positional > env > 'default'), mirroring `up` post-#685. `config.ensemble`
|
|
69
|
+
// IGNORES `overrides.ensemble` (config.ts hard-codes it from env/default), so
|
|
70
|
+
// reading it dropped the positional and the board silently watched 'default'.
|
|
71
|
+
const ensemble = args.ensemble ?? config.ensemble;
|
|
66
72
|
// Preflight — fail BEFORE launching a terminal that would die. checkPiNodeFloor
|
|
67
73
|
// is a best-effort Node-version proxy; buildPiCommandCenterSpawn's default
|
|
68
74
|
// resolver throws fail-clean when the Pi CLI is missing.
|
|
@@ -71,6 +77,24 @@ async function commandCenterCommand(args) {
|
|
|
71
77
|
out.error(`Cannot start command-center — ${nodeFloor.reason}`);
|
|
72
78
|
process.exit(1);
|
|
73
79
|
}
|
|
80
|
+
// #820 (Bug 2) — extension-registration guard. command-center relies on the
|
|
81
|
+
// GLOBAL Pi extension registration (it deliberately passes NO inline `-e`, unlike
|
|
82
|
+
// `up --agent pi`). On a fresh box that never ran `install-pi`, the extensions are
|
|
83
|
+
// absent from `~/.pi/agent/settings.json`, so a plain `pi` launches with NO board
|
|
84
|
+
// and NO error. Auto-install (idempotent — matches `up --agent pi`'s out-of-box
|
|
85
|
+
// behavior) before spawning; fail loudly with the exact command if the write fails.
|
|
86
|
+
if (!(0, install_1.arePiExtensionsRegistered)()) {
|
|
87
|
+
try {
|
|
88
|
+
const result = (0, install_1.installPiExtensions)();
|
|
89
|
+
out.log(out.dim(` Registered the Pi extensions in ${result.settingsPath} (first-run install-pi).`));
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
out.error('Cannot start command-center — the Pi extensions are not registered and auto-install failed: ' +
|
|
93
|
+
`${err instanceof Error ? err.message : String(err)}. ` +
|
|
94
|
+
'Run `agent-tempo install-pi` manually, then retry.');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
74
98
|
// Admin (T3) token — mission-control's operator write/gate surface reads it.
|
|
75
99
|
// #54: a LOCAL (loopback) daemon grants full trust tokenless, so a tokenless
|
|
76
100
|
// board is fully functional locally; only a REMOTE / 0.0.0.0 daemon requires the
|
|
@@ -112,5 +136,4 @@ async function commandCenterCommand(args) {
|
|
|
112
136
|
const { pid } = (0, spawn_1.launchInTerminal)(spawn.cmd, spawn.args, process.cwd(), spawn.env);
|
|
113
137
|
out.success(`Command-center launched for ensemble ${out.cyan(ensemble)} (pid ${pid ?? 'unknown'})`);
|
|
114
138
|
out.log(` ${out.dim('Observer-only Pi board — the player extension stays dormant in this session.')}`);
|
|
115
|
-
out.log(` ${out.dim('Requires `agent-tempo install-pi` to have registered the extensions.')}`);
|
|
116
139
|
}
|
package/dist/cli/commands.d.ts
CHANGED
|
@@ -8,8 +8,23 @@ export declare function status(opts: StatusOpts): Promise<void>;
|
|
|
8
8
|
interface InitOpts {
|
|
9
9
|
dir: string;
|
|
10
10
|
project?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* #818 — suppress the manual-setup "Next steps" block (start Temporal / start
|
|
13
|
+
* conductor). Set by the automated `up` flow, which has ALREADY started Temporal
|
|
14
|
+
* and launched the conductor — printing those steps there is stale and misleading.
|
|
15
|
+
* The manual `agent-tempo init` verb leaves it unset so the guidance still shows.
|
|
16
|
+
*/
|
|
17
|
+
suppressNextSteps?: boolean;
|
|
11
18
|
}
|
|
12
19
|
export declare function init(opts: InitOpts): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Per-project .mcp.json install (legacy, used with --project flag).
|
|
22
|
+
*
|
|
23
|
+
* Exported for #818 regression coverage (the `suppressNextSteps` gate) — it only
|
|
24
|
+
* touches the filesystem + stdout, so it is unit-testable without shelling out to
|
|
25
|
+
* `claude`. Not part of the public CLI surface.
|
|
26
|
+
*/
|
|
27
|
+
export declare function initProject(dir: string, suppressNextSteps?: boolean): void;
|
|
13
28
|
interface ServerOpts extends CliOverrides {
|
|
14
29
|
background: boolean;
|
|
15
30
|
}
|
|
@@ -36,6 +51,19 @@ interface UpOpts extends CliOverrides {
|
|
|
36
51
|
*/
|
|
37
52
|
scenario?: string;
|
|
38
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* #832 — the operator-facing "ensemble ready" lines for `up` / `conduct`.
|
|
56
|
+
*
|
|
57
|
+
* Keyed on the RESOLVED ensemble name (what `command-center` / `status` take —
|
|
58
|
+
* `--ensemble` flag > positional > env > `default`), NOT the lineup's `name:`
|
|
59
|
+
* field. The two diverge whenever any of those overrides the lineup name, so the
|
|
60
|
+
* old `Ensemble <lineup-name> is ready` line named a thing the operator can't
|
|
61
|
+
* actually `connect` to. Surfaces the connect command so the next step is one
|
|
62
|
+
* copy-paste away. Pure + exported for unit testing (the surrounding `up` does
|
|
63
|
+
* I/O and isn't unit-testable). The shared markdown {@link ensembleReadyBanner}
|
|
64
|
+
* stays lineup-keyed for the conductor's seeded directive (a different surface).
|
|
65
|
+
*/
|
|
66
|
+
export declare function formatEnsembleReadyLines(ensemble: string, lineupName: string, playerCount: number): [readyLine: string, connectLine: string];
|
|
39
67
|
export declare function up(opts: UpOpts): Promise<void>;
|
|
40
68
|
/**
|
|
41
69
|
* Format a `ScheduleEntry` recurrence for the `status` display.
|