agent-tempo 1.7.0-beta.9 → 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 +17 -1
- package/README.md +2 -1
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.js +10 -0
- package/dist/activities/resolve.d.ts +83 -18
- package/dist/activities/resolve.js +139 -26
- package/dist/cli/command-center-command.js +25 -2
- package/dist/cli/commands.d.ts +28 -0
- package/dist/cli/commands.js +88 -17
- package/dist/cli/mcp.d.ts +26 -2
- package/dist/cli/mcp.js +33 -4
- package/dist/cli/startup.js +8 -2
- package/dist/cli.js +17 -5
- package/dist/client/subscribe.d.ts +10 -0
- package/dist/client/subscribe.js +2 -0
- package/dist/daemon.d.ts +44 -7
- package/dist/daemon.js +54 -8
- package/dist/http/deliverability.d.ts +68 -0
- package/dist/http/deliverability.js +78 -0
- package/dist/http/qa.js +8 -1
- package/dist/http/writes.js +24 -2
- package/dist/pi/install.d.ts +15 -0
- package/dist/pi/install.js +35 -0
- package/dist/pi/mission-control/actions.d.ts +30 -2
- package/dist/pi/mission-control/actions.js +92 -3
- package/dist/pi/mission-control/board.d.ts +84 -0
- package/dist/pi/mission-control/board.js +62 -1
- package/dist/pi/mission-control/extension.d.ts +122 -1
- package/dist/pi/mission-control/extension.js +332 -13
- package/dist/pi/mission-control/render.d.ts +25 -0
- package/dist/pi/mission-control/render.js +131 -12
- package/dist/spawn.d.ts +43 -26
- package/dist/spawn.js +64 -37
- package/dist/tools/cue.js +12 -0
- package/dist/tools/ensemble.js +33 -2
- package/dist/tools/recruit.js +8 -26
- package/dist/utils/sdk-probe.d.ts +12 -0
- package/dist/utils/sdk-probe.js +28 -0
- package/package.json +8 -4
package/CLAUDE.md
CHANGED
|
@@ -174,6 +174,22 @@ npm run check:all # runs every CI gate locally (build, tests, drift checks, li
|
|
|
174
174
|
> surveys or migrations, always grep **both** `test/` and `tests/` or you will miss
|
|
175
175
|
> mocks and assertions that only live in one directory.
|
|
176
176
|
|
|
177
|
+
> **Windows `**`-glob false-negative (#707) — a zero-match can read as "clean".**
|
|
178
|
+
> On Windows, a ripgrep glob with a **leading directory segment before `**`**
|
|
179
|
+
> silently matches **zero** files: `Grep ... glob="src/**/*.ts"` returns *"No
|
|
180
|
+
> files found"* even when matches exist — a false negative that looks identical
|
|
181
|
+
> to "searched and found nothing." This bites call-site surveys, drift checks,
|
|
182
|
+
> and any "prove a symbol is absent" decision. **Reliable forms** (use these):
|
|
183
|
+
> path-scope instead — `Grep path="src" type="ts"` — or drop the leading dir —
|
|
184
|
+
> `glob="**/*.ts"`; or just `Read` the file when you know the path. A bare
|
|
185
|
+
> recursive `**/*.ext` works; `dir/**/*.ext` is the broken shape.
|
|
186
|
+
> The bug is in the search-tool layer, not this repo — **no repo check shells a
|
|
187
|
+
> `**` glob.** Repo lint/drift/conformance checks enumerate via `fs` recursion
|
|
188
|
+
> or `git ls-files` (never a shelled `**` glob) and now **assert a non-zero file
|
|
189
|
+
> count** (#707) so a future enumeration breakage fails loud instead of passing
|
|
190
|
+
> as "clean." Hold that bar for any new source-scanning check: *a check that
|
|
191
|
+
> scans nothing must never report success.*
|
|
192
|
+
|
|
177
193
|
> **Test-only hooks live with the module they reset and follow the
|
|
178
194
|
> `__<verb><Noun>ForTests` naming convention** — see
|
|
179
195
|
> [docs/adr/0006-test-hooks-naming.md](docs/adr/0006-test-hooks-naming.md). The
|
|
@@ -222,7 +238,7 @@ daemon worker notes, `npx ts-node` dev runner).
|
|
|
222
238
|
- **`upgrade-to-2` cutover** (#785, ratified migration protocol A2 — `docs/design/v2-scoping.md` §A.3): The 1.x → 2.0 migration verb. A 2.0 worker can never replay a 1.x-recorded run, so 2.0 ships behind a clean cutover: `agent-tempo upgrade-to-2` runs a six-phase protocol — **preflight** (enumerate ensembles; refuse if any connected daemon is below the 1.7.x version floor — `hostProfile.version` on the global maestro; covers same-host-stale-daemon, #801) → **pause** (brake the work *sources* — maestro + scheduler — leaving sessions live to drain) → **drain** (poll outboxes ≤60s; `--force-drain` records stragglers instead of stopping) → **snapshot** (freeze session dispatch, then capture continuity to `~/.agent-tempo/upgrade-snapshot-v1.json`) → **destroy** (idempotent teardown: peers → scheduler+maestro → conductor) → **done**. **Load-bearing invariant: SNAPSHOT strictly precedes DESTROY** (`destroyAndFinish` takes a persisted snapshot as a required arg); a crash leaves either everything intact or a durable snapshot + partial teardown, never destruction without capture. Resumable via the snapshot's phase stamp. The snapshot captures schedules (durable intent), #334 state slots (continuity), `sessionId` (resume pointer), the non-default recruit `model` (ad-hoc claude-api/opencode/pi continuity), and undelivered cues (operator review, NOT redelivered); coat-check is dropped (TTL-transient); gates/stages/worktrees are intentionally not captured (transient coordination state; worktree continuity rides `workDir`). **`src/upgrade/snapshot-v1.ts` is the cross-release interface the 2.0-side `up --from-upgrade` (#786) imports** — it is VERSIONED and deliberately Temporal-free; additive optional fields do NOT bump the version, only removed/renamed/retyped required fields do (with a 2.0 reader migration). CLI flags: `--yes` (skip confirm), `--dry-run` (print snapshot + destroy list, exit before pausing), `--force-drain`. The verb is crash-proof (`src/cli/upgrade-to-2-command.ts` dynamic-imports the Temporal-touching engine). See `src/upgrade/` and issue #785.
|
|
223
239
|
- **Coat-check** (#318, ADR 0008): Per-ensemble transient content store on Maestro state. Solves the 100 KB cue body cap — stash a large artifact with `coat_check_put` (returns a ticket id) and attach the ticket to a `cue` via `attachmentTicket`; the recipient calls `coat_check_get` to pull the full body. Four MCP tools: `coat_check_put` (any player; max 32 KiB per entry, 20 slots per ensemble, TTL 7d default), `coat_check_get` (any player; bumps fetch-audit counters), `coat_check_list` (read-only survey; headers only, content omitted), `coat_check_evict` (owner or conductor). Saturation rejects with `CoatCheckSlotsFull` (no LRU eviction). See `src/tools/coat-check-*.ts` and [docs/adr/0008-coat-check-pattern.md](docs/adr/0008-coat-check-pattern.md).
|
|
224
240
|
- **Lineup examples**: Six pre-built ensemble YAML files in `examples/ensembles/` — `tempo-big-band`, `tempo-dev-team`, `tempo-review-squad`, `tempo-jam-session`, `tempo-mock-jam` (dev-mode all-mock ensemble), `tempo-headless-jam` (#520 — all-`claude-code-headless` subscription-billed ensemble). Load with `agent-tempo up --lineup <name>` or the `load_lineup` tool.
|
|
225
|
-
- **GitHub App identity** (`agent-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `agent-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). On Windows,
|
|
241
|
+
- **GitHub App identity** (`agent-tempo[bot]`): When a player writes to GitHub — issue comments, PR creation/merge, commits, labels, check runs — **use `./scripts/ensemble-gh`** instead of `gh`. The wrapper mints a short-lived installation token so the action is attributed to `agent-tempo[bot]`, not to the human maintainer, making the AI authorship visible. Plain `gh` is still correct for read-only local dev (`gh pr view`, `gh repo clone`, `gh auth status`). On Windows, `./scripts/ensemble-gh` works directly from PowerShell via the `scripts/ensemble-gh.cmd` shim (#741); the `bash ./scripts/ensemble-gh` form also still works as an explicit fallback. Every bot-authored comment/PR body must include the AI attribution footer documented in [docs/github-app.md](docs/github-app.md).
|
|
226
242
|
|
|
227
243
|
See [docs/concepts.md](docs/concepts.md) for the full glossary (Adapter, Attachment phases, Restart, Detach/Destroy, Migrate, Broadcast, Recall, Schedule, Lineup, Quality Gate, Worktree, Stage, Hold/Release, Pause/Resume, Maestro, TempoClient, Command-center planner, and more).
|
|
228
244
|
|
package/README.md
CHANGED
|
@@ -148,12 +148,13 @@ agent-tempo # launch TUI (auto-provisions on first run)
|
|
|
148
148
|
agent-tempo up [ensemble] # provision infrastructure and launch conductor
|
|
149
149
|
agent-tempo down [--destroy] # tear down infrastructure (--destroy also terminates workflows)
|
|
150
150
|
agent-tempo status [ensemble] # list active sessions
|
|
151
|
-
agent-tempo destroy
|
|
151
|
+
agent-tempo destroy [ensemble] # terminate all sessions in an ensemble (defaults to "default")
|
|
152
152
|
agent-tempo restore <ensemble> # restore orphaned sessions on this host
|
|
153
153
|
agent-tempo hosts # list daemons polling this Temporal namespace (--all/--json)
|
|
154
154
|
agent-tempo recall <name> # read a player's message history (--limit/--offset/--preview/--json)
|
|
155
155
|
agent-tempo attachment-info <name> # inspect a session's phase, holder, lease, and heartbeat age
|
|
156
156
|
agent-tempo release [ensemble] # release held players (unlock + deliver tasks)
|
|
157
|
+
agent-tempo dashboard # open the web dashboard (--pair for QR-code cross-device access)
|
|
157
158
|
agent-tempo daemon <sub> # manage the worker daemon
|
|
158
159
|
agent-tempo upgrade # update to latest
|
|
159
160
|
```
|
package/dashboard/package.json
CHANGED
|
@@ -46,6 +46,7 @@ const git_info_1 = require("../git-info");
|
|
|
46
46
|
const spawn_1 = require("../spawn");
|
|
47
47
|
const config_2 = require("../config");
|
|
48
48
|
const resolve_1 = require("./resolve");
|
|
49
|
+
const visibility_deadline_1 = require("../utils/visibility-deadline");
|
|
49
50
|
const action_counters_1 = require("../utils/action-counters");
|
|
50
51
|
const search_attributes_1 = require("../utils/search-attributes");
|
|
51
52
|
const agent_types_1 = require("../ensemble/agent-types");
|
|
@@ -86,6 +87,15 @@ function isRetryableTemporalError(err) {
|
|
|
86
87
|
// to call unconditionally.
|
|
87
88
|
if (err instanceof activity_1.ApplicationFailure)
|
|
88
89
|
return false;
|
|
90
|
+
// #845 Mode A: a truncated visibility scan (resolveSession's deadline
|
|
91
|
+
// tripped mid-scan, #336/#529) is a LATENCY failure — the target may well
|
|
92
|
+
// exist; the scan just didn't finish. Treat it as transient so the
|
|
93
|
+
// activity retry policy re-runs resolveSession with a fresh 10s deadline
|
|
94
|
+
// (backoff-bounded) instead of collapsing it into a permanent
|
|
95
|
+
// "No active session found". This must NOT re-add an in-resolver retry
|
|
96
|
+
// loop — the bounding lives in Temporal's policy, not a hot-path scan.
|
|
97
|
+
if ((0, visibility_deadline_1.isVisibilityTimeout)(err))
|
|
98
|
+
return true;
|
|
89
99
|
const e = err;
|
|
90
100
|
const name = e?.name ?? '';
|
|
91
101
|
const msg = e?.message ?? '';
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Client, WorkflowHandle } from '@temporalio/client';
|
|
2
2
|
import { AttachmentPhase } from '../types';
|
|
3
|
+
/**
|
|
4
|
+
* Mode-B describe-by-id timeout (#845). The strongly-consistent
|
|
5
|
+
* `describe()` fallback on `resolveSession`'s not-found branch is a single
|
|
6
|
+
* O(1) RPC; 2s mirrors {@link DEFAULT_QUERY_TIMEOUT_MS} — two orders of
|
|
7
|
+
* magnitude over a healthy describe — so a wedged frontend can't re-hang
|
|
8
|
+
* the outbox loop the visibility deadline (#336/#529) was added to bound.
|
|
9
|
+
*/
|
|
10
|
+
export declare const RESOLVE_DESCRIBE_TIMEOUT_MS = 2000;
|
|
3
11
|
/** Shared query for listing running session workflows. Exported for the
|
|
4
12
|
* ensemble-scoped variants in `client/core.ts` (#751). */
|
|
5
13
|
export declare const SESSION_LIST_QUERY = "WorkflowType = \"agentSessionWorkflow\" AND ExecutionStatus = \"Running\"";
|
|
@@ -18,15 +26,36 @@ export declare const SESSION_LIST_QUERY = "WorkflowType = \"agentSessionWorkflow
|
|
|
18
26
|
* in `scanEnsembleSessionsCloud`. Enforced by
|
|
19
27
|
* tests/conformance/decision-path-fence.test.ts.
|
|
20
28
|
*
|
|
21
|
-
* **
|
|
22
|
-
* `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On
|
|
23
|
-
* throws `VisibilityIteratorTimeoutError` rather than returning
|
|
24
|
-
* — silent `null` on a partially-scanned set would be
|
|
25
|
-
* from "definitely not found
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
29
|
+
* **Mode A — deadline truncation (#336/#529):** the visibility iterator is
|
|
30
|
+
* bounded by `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On
|
|
31
|
+
* timeout it throws `VisibilityIteratorTimeoutError` rather than returning
|
|
32
|
+
* `null` — silent `null` on a partially-scanned set would be
|
|
33
|
+
* indistinguishable from "definitely not found." The throw is classified
|
|
34
|
+
* **retryable** by the outbox activity (`isRetryableTemporalError`), so
|
|
35
|
+
* Temporal's activity retry policy re-runs the lookup with a fresh
|
|
36
|
+
* deadline rather than collapsing it to a permanent "player not found."
|
|
37
|
+
* Synchronous tool/CLI callers surface it as a distinct "resolution
|
|
38
|
+
* incomplete — retry," never "not found."
|
|
39
|
+
*
|
|
40
|
+
* **Mode B — visibility-index lag (#845):** `list()` can complete normally
|
|
41
|
+
* (no throw) yet miss a freshly-started workflow because the visibility
|
|
42
|
+
* index trails the workflow store (observed live as a 3/8→8/8 roster
|
|
43
|
+
* during post-restart worker warmup). An early-exhausting scan is NOT
|
|
44
|
+
* proof of absence. So on the not-found branch we do **exactly one**
|
|
45
|
+
* strongly-consistent `describe()` against the *derived* workflow id —
|
|
46
|
+
* an O(1) read by primary key that bypasses the lagging index. This is a
|
|
47
|
+
* point lookup, NOT a re-scan: it cannot re-introduce the unbounded-scan
|
|
48
|
+
* hang the deadline guard was added to prevent.
|
|
49
|
+
*
|
|
50
|
+
* **Documented Mode-B limitation:** the derived id
|
|
51
|
+
* `agent-session-{ensemble}-{playerName}` is minted from a player's
|
|
52
|
+
* INITIAL name at spawn; `set_name` does not change the workflow id. So
|
|
53
|
+
* describe-by-derived-id false-negatives for a player that was both
|
|
54
|
+
* RENAMED and is currently index-lagged — it falls back to `null` (looks
|
|
55
|
+
* absent) for that narrow intersection. Accepted by design: it closes the
|
|
56
|
+
* gap for the cold-boot/warmup incident class (nobody renames mid-boot),
|
|
57
|
+
* and a second full re-scan to cover renamed∩lagged would put scan cost on
|
|
58
|
+
* every genuine typo'd-name lookup. See issue #845.
|
|
30
59
|
*/
|
|
31
60
|
export declare function resolveSession(client: Client, ensemble: string, playerName: string): Promise<WorkflowHandle | null>;
|
|
32
61
|
/** Info returned for each session by scanEnsembleSessions. */
|
|
@@ -82,19 +111,55 @@ export interface EnsembleSessionInfo {
|
|
|
82
111
|
*/
|
|
83
112
|
export declare function scanEnsembleSessionsCloud(client: Client, ensemble: string, log?: (...args: unknown[]) => void): Promise<EnsembleSessionInfo[]>;
|
|
84
113
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
114
|
+
* Result of {@link scanEnsembleSessionsWithStatus} — the session rows plus
|
|
115
|
+
* whether the visibility scan completed or was cut short (#845).
|
|
116
|
+
*
|
|
117
|
+
* `truncated` is the Mode-A signal: a `VisibilityIteratorTimeoutError`
|
|
118
|
+
* fired (the wall-clock deadline tripped mid-scan), so `sessions` is a
|
|
119
|
+
* partial snapshot, NOT the full roster. Callers that render a roster (the
|
|
120
|
+
* `ensemble` tool) MUST surface this so a partial set is never mistaken
|
|
121
|
+
* for a complete one. NOTE: this does NOT cover Mode B (visibility-index
|
|
122
|
+
* lag) — there the scan completes normally and `truncated` is `false` even
|
|
123
|
+
* though a freshly-started workflow may be missing; that's best-effort by
|
|
124
|
+
* design and self-heals on the next tick.
|
|
125
|
+
*
|
|
126
|
+
* `scanned` is the number of running workflows the iterator visited before
|
|
127
|
+
* completing or timing out — useful for warn logs ("partial: 3 of ≥N").
|
|
128
|
+
*/
|
|
129
|
+
export interface EnsembleScanResult {
|
|
130
|
+
sessions: EnsembleSessionInfo[];
|
|
131
|
+
truncated: boolean;
|
|
132
|
+
scanned: number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Scan all running session workflows in an ensemble, reporting whether the
|
|
136
|
+
* scan completed or was truncated by the visibility deadline (#845).
|
|
137
|
+
*
|
|
138
|
+
* This is the single source of truth for the local-profile ensemble scan;
|
|
139
|
+
* {@link scanEnsembleSessions} is a thin array-facade over it that drops
|
|
140
|
+
* the status fields for the many callers that don't need them.
|
|
88
141
|
*
|
|
89
142
|
* **Deadline (#336/#529):** the iterator is bounded by
|
|
90
|
-
* `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* best-effort snapshot that the next tick / re-invocation will fill in.
|
|
143
|
+
* `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On timeout
|
|
144
|
+
* the accumulated rows are returned with `truncated: true` and a warn log
|
|
145
|
+
* — the scan is **partial-tolerant by design**, but the truncation is now
|
|
146
|
+
* SIGNALLED rather than silent so a roster renderer can flag it.
|
|
95
147
|
*
|
|
96
148
|
* T0.1 (#748): this legacy shape is the `costProfile: 'local'` path —
|
|
97
|
-
* byte-identical to pre-#748 behavior. The cloud profile uses
|
|
149
|
+
* byte-identical row data to pre-#748 behavior. The cloud profile uses
|
|
98
150
|
* {@link scanEnsembleSessionsCloud}.
|
|
99
151
|
*/
|
|
152
|
+
export declare function scanEnsembleSessionsWithStatus(client: Client, ensemble: string, log?: (...args: unknown[]) => void): Promise<EnsembleScanResult>;
|
|
153
|
+
/**
|
|
154
|
+
* Scan all running session workflows in an ensemble — array facade over
|
|
155
|
+
* {@link scanEnsembleSessionsWithStatus}.
|
|
156
|
+
*
|
|
157
|
+
* Returns just the session rows; the truncation/scan-status fields are
|
|
158
|
+
* dropped. This is the byte-identical shape the maestro refresh activity,
|
|
159
|
+
* the #785 upgrade-snapshot, and the other roster consumers already depend
|
|
160
|
+
* on — keeping it a thin delegate means the truncation-signalling work
|
|
161
|
+
* (#845) does NOT ripple through those call sites. Callers that need to
|
|
162
|
+
* know whether the scan was complete (the `ensemble` tool) call the rich
|
|
163
|
+
* sibling directly.
|
|
164
|
+
*/
|
|
100
165
|
export declare function scanEnsembleSessions(client: Client, ensemble: string, log?: (...args: unknown[]) => void): Promise<EnsembleSessionInfo[]>;
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SESSION_LIST_QUERY = void 0;
|
|
3
|
+
exports.SESSION_LIST_QUERY = exports.RESOLVE_DESCRIBE_TIMEOUT_MS = void 0;
|
|
4
4
|
exports.resolveSession = resolveSession;
|
|
5
5
|
exports.scanEnsembleSessionsCloud = scanEnsembleSessionsCloud;
|
|
6
|
+
exports.scanEnsembleSessionsWithStatus = scanEnsembleSessionsWithStatus;
|
|
6
7
|
exports.scanEnsembleSessions = scanEnsembleSessions;
|
|
8
|
+
const config_1 = require("../config");
|
|
7
9
|
const search_attributes_1 = require("../utils/search-attributes");
|
|
8
10
|
const signals_1 = require("../workflows/signals");
|
|
9
11
|
const query_timeout_1 = require("../utils/query-timeout");
|
|
10
12
|
const visibility_deadline_1 = require("../utils/visibility-deadline");
|
|
13
|
+
/**
|
|
14
|
+
* Mode-B describe-by-id timeout (#845). The strongly-consistent
|
|
15
|
+
* `describe()` fallback on `resolveSession`'s not-found branch is a single
|
|
16
|
+
* O(1) RPC; 2s mirrors {@link DEFAULT_QUERY_TIMEOUT_MS} — two orders of
|
|
17
|
+
* magnitude over a healthy describe — so a wedged frontend can't re-hang
|
|
18
|
+
* the outbox loop the visibility deadline (#336/#529) was added to bound.
|
|
19
|
+
*/
|
|
20
|
+
exports.RESOLVE_DESCRIBE_TIMEOUT_MS = 2000;
|
|
11
21
|
/** Shared query for listing running session workflows. Exported for the
|
|
12
22
|
* ensemble-scoped variants in `client/core.ts` (#751). */
|
|
13
23
|
exports.SESSION_LIST_QUERY = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"`;
|
|
@@ -26,15 +36,36 @@ exports.SESSION_LIST_QUERY = `WorkflowType = "agentSessionWorkflow" AND Executio
|
|
|
26
36
|
* in `scanEnsembleSessionsCloud`. Enforced by
|
|
27
37
|
* tests/conformance/decision-path-fence.test.ts.
|
|
28
38
|
*
|
|
29
|
-
* **
|
|
30
|
-
* `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On
|
|
31
|
-
* throws `VisibilityIteratorTimeoutError` rather than returning
|
|
32
|
-
* — silent `null` on a partially-scanned set would be
|
|
33
|
-
* from "definitely not found
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* **Mode A — deadline truncation (#336/#529):** the visibility iterator is
|
|
40
|
+
* bounded by `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On
|
|
41
|
+
* timeout it throws `VisibilityIteratorTimeoutError` rather than returning
|
|
42
|
+
* `null` — silent `null` on a partially-scanned set would be
|
|
43
|
+
* indistinguishable from "definitely not found." The throw is classified
|
|
44
|
+
* **retryable** by the outbox activity (`isRetryableTemporalError`), so
|
|
45
|
+
* Temporal's activity retry policy re-runs the lookup with a fresh
|
|
46
|
+
* deadline rather than collapsing it to a permanent "player not found."
|
|
47
|
+
* Synchronous tool/CLI callers surface it as a distinct "resolution
|
|
48
|
+
* incomplete — retry," never "not found."
|
|
49
|
+
*
|
|
50
|
+
* **Mode B — visibility-index lag (#845):** `list()` can complete normally
|
|
51
|
+
* (no throw) yet miss a freshly-started workflow because the visibility
|
|
52
|
+
* index trails the workflow store (observed live as a 3/8→8/8 roster
|
|
53
|
+
* during post-restart worker warmup). An early-exhausting scan is NOT
|
|
54
|
+
* proof of absence. So on the not-found branch we do **exactly one**
|
|
55
|
+
* strongly-consistent `describe()` against the *derived* workflow id —
|
|
56
|
+
* an O(1) read by primary key that bypasses the lagging index. This is a
|
|
57
|
+
* point lookup, NOT a re-scan: it cannot re-introduce the unbounded-scan
|
|
58
|
+
* hang the deadline guard was added to prevent.
|
|
59
|
+
*
|
|
60
|
+
* **Documented Mode-B limitation:** the derived id
|
|
61
|
+
* `agent-session-{ensemble}-{playerName}` is minted from a player's
|
|
62
|
+
* INITIAL name at spawn; `set_name` does not change the workflow id. So
|
|
63
|
+
* describe-by-derived-id false-negatives for a player that was both
|
|
64
|
+
* RENAMED and is currently index-lagged — it falls back to `null` (looks
|
|
65
|
+
* absent) for that narrow intersection. Accepted by design: it closes the
|
|
66
|
+
* gap for the cold-boot/warmup incident class (nobody renames mid-boot),
|
|
67
|
+
* and a second full re-scan to cover renamed∩lagged would put scan cost on
|
|
68
|
+
* every genuine typo'd-name lookup. See issue #845.
|
|
38
69
|
*/
|
|
39
70
|
async function resolveSession(client, ensemble, playerName) {
|
|
40
71
|
for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query: exports.SESSION_LIST_QUERY }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.resolveSession, 'resolveSession')) {
|
|
@@ -49,16 +80,77 @@ async function resolveSession(client, ensemble, playerName) {
|
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
catch (err) {
|
|
52
|
-
// Re-throw deadline timeouts — callers that wrap us in
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// outbox logs / user-facing tool errors.
|
|
83
|
+
// Re-throw deadline timeouts (Mode A) — callers that wrap us in
|
|
84
|
+
// try/catch treat the typed throw as a soft "lookup timed out" path,
|
|
85
|
+
// distinct from the not-found `null` below.
|
|
56
86
|
if ((0, visibility_deadline_1.isVisibilityTimeout)(err))
|
|
57
87
|
throw err;
|
|
58
88
|
// Workflow may have just completed, or worker is wedged (#433) — skip
|
|
59
89
|
}
|
|
60
90
|
}
|
|
61
|
-
|
|
91
|
+
// Mode B (#845): the scan completed without a match, but the visibility
|
|
92
|
+
// index may simply be lagging a just-started workflow. One strongly-
|
|
93
|
+
// consistent describe-by-derived-id disambiguates "index lag" from
|
|
94
|
+
// "genuinely absent" without a second scan.
|
|
95
|
+
return resolveByDerivedId(client, ensemble, playerName);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Mode-B (#845) strongly-consistent fallback for {@link resolveSession}.
|
|
99
|
+
*
|
|
100
|
+
* Reads the session workflow by its *derived* id
|
|
101
|
+
* (`agent-session-{ensemble}-{playerName}`) via a single bounded
|
|
102
|
+
* `describe()` — a primary-key lookup that bypasses the eventually-
|
|
103
|
+
* consistent visibility index. Returns the handle whenever the execution
|
|
104
|
+
* is `RUNNING`; otherwise `null` (genuinely absent, terminated/completed,
|
|
105
|
+
* renamed-false-negative, or describe timed out).
|
|
106
|
+
*
|
|
107
|
+
* Deliberately RUNNING-only — NO attachment-phase filter (#845 JC2): a
|
|
108
|
+
* `gone` player has a LIVE workflow with a terminal adapter, which the
|
|
109
|
+
* #822/#834 deliverability contract handles as warn-but-queue, not
|
|
110
|
+
* "not found". Filtering it here would regress #834 for the lagged-gone
|
|
111
|
+
* window and diverge from the main scan loop (which has no phase filter).
|
|
112
|
+
*/
|
|
113
|
+
async function resolveByDerivedId(client, ensemble, playerName) {
|
|
114
|
+
let timer;
|
|
115
|
+
try {
|
|
116
|
+
// `getHandle` is a lazy, no-RPC handle construction in the real client;
|
|
117
|
+
// kept inside the try purely so a defensive throw can never escape the
|
|
118
|
+
// fallback (it must only ever upgrade a null to a handle, never error).
|
|
119
|
+
const handle = client.workflow.getHandle((0, config_1.sessionWorkflowId)(ensemble, playerName));
|
|
120
|
+
const timeout = new Promise((_, reject) => {
|
|
121
|
+
timer = setTimeout(() => reject(new Error('describe-by-id timed out')), exports.RESOLVE_DESCRIBE_TIMEOUT_MS);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
});
|
|
124
|
+
const desc = await Promise.race([handle.describe(), timeout]);
|
|
125
|
+
// Only a live (RUNNING) execution is a valid resolve target. A
|
|
126
|
+
// COMPLETED/TERMINATED latest run at this id means the player is gone,
|
|
127
|
+
// or the id was reused by a since-closed run → null. A RUNNING run
|
|
128
|
+
// under a reused id is legitimately the current player → return it.
|
|
129
|
+
//
|
|
130
|
+
// No attachment-phase filter (#845 JC2, architect ruling): the main
|
|
131
|
+
// scan loop returns the handle for ANY running session — phase=`gone`
|
|
132
|
+
// included — and #822/#834 treat `gone` as warn-but-QUEUE (the cue
|
|
133
|
+
// durably queues and auto-redelivers on re-attach), NOT "not found".
|
|
134
|
+
// Returning null for a lagged-`gone` player would bypass #822, re-
|
|
135
|
+
// introduce the false-not-found #834 fixed, and make resolution depend
|
|
136
|
+
// on visibility-index timing. The "don't deliver to a torn-down
|
|
137
|
+
// adapter" concern lives at the deliverability layer, not here.
|
|
138
|
+
if (desc.status.name !== 'RUNNING')
|
|
139
|
+
return null;
|
|
140
|
+
return handle;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// NotFound → genuinely absent (or the renamed∩lagged false-negative
|
|
144
|
+
// documented on resolveSession). Timeout/other → treat as absent; the
|
|
145
|
+
// caller's not-found path (or the activity retry policy for Mode A)
|
|
146
|
+
// handles it. We never throw from the fallback — it can only upgrade a
|
|
147
|
+
// null to a found handle, never turn a clean lookup into an error.
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
if (timer)
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
}
|
|
62
154
|
}
|
|
63
155
|
/**
|
|
64
156
|
* T0.1 (#748) — cloud-profile ensemble scan. Observation path ONLY (see the
|
|
@@ -162,25 +254,30 @@ async function scanEnsembleSessionsCloud(client, ensemble, log = () => { }) {
|
|
|
162
254
|
return sessions;
|
|
163
255
|
}
|
|
164
256
|
/**
|
|
165
|
-
* Scan all running session workflows in an ensemble
|
|
166
|
-
*
|
|
167
|
-
*
|
|
257
|
+
* Scan all running session workflows in an ensemble, reporting whether the
|
|
258
|
+
* scan completed or was truncated by the visibility deadline (#845).
|
|
259
|
+
*
|
|
260
|
+
* This is the single source of truth for the local-profile ensemble scan;
|
|
261
|
+
* {@link scanEnsembleSessions} is a thin array-facade over it that drops
|
|
262
|
+
* the status fields for the many callers that don't need them.
|
|
168
263
|
*
|
|
169
264
|
* **Deadline (#336/#529):** the iterator is bounded by
|
|
170
|
-
* `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
* best-effort snapshot that the next tick / re-invocation will fill in.
|
|
265
|
+
* `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On timeout
|
|
266
|
+
* the accumulated rows are returned with `truncated: true` and a warn log
|
|
267
|
+
* — the scan is **partial-tolerant by design**, but the truncation is now
|
|
268
|
+
* SIGNALLED rather than silent so a roster renderer can flag it.
|
|
175
269
|
*
|
|
176
270
|
* T0.1 (#748): this legacy shape is the `costProfile: 'local'` path —
|
|
177
|
-
* byte-identical to pre-#748 behavior. The cloud profile uses
|
|
271
|
+
* byte-identical row data to pre-#748 behavior. The cloud profile uses
|
|
178
272
|
* {@link scanEnsembleSessionsCloud}.
|
|
179
273
|
*/
|
|
180
|
-
async function
|
|
274
|
+
async function scanEnsembleSessionsWithStatus(client, ensemble, log = () => { }) {
|
|
181
275
|
const sessions = [];
|
|
276
|
+
let truncated = false;
|
|
277
|
+
let scanned = 0;
|
|
182
278
|
try {
|
|
183
279
|
for await (const workflow of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query: exports.SESSION_LIST_QUERY }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.scanEnsembleSessions, 'scanEnsembleSessions')) {
|
|
280
|
+
scanned++;
|
|
184
281
|
try {
|
|
185
282
|
const handle = client.workflow.getHandle(workflow.workflowId);
|
|
186
283
|
// Issue #433 — bound the metadata + part queries so a single wedged
|
|
@@ -234,11 +331,27 @@ async function scanEnsembleSessions(client, ensemble, log = () => { }) {
|
|
|
234
331
|
}
|
|
235
332
|
catch (err) {
|
|
236
333
|
if ((0, visibility_deadline_1.isVisibilityTimeout)(err)) {
|
|
334
|
+
truncated = true;
|
|
237
335
|
log(`scanEnsembleSessions: ${err.message} — returning partial (${sessions.length} sessions)`);
|
|
238
336
|
}
|
|
239
337
|
else {
|
|
240
338
|
throw err;
|
|
241
339
|
}
|
|
242
340
|
}
|
|
243
|
-
return sessions;
|
|
341
|
+
return { sessions, truncated, scanned };
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Scan all running session workflows in an ensemble — array facade over
|
|
345
|
+
* {@link scanEnsembleSessionsWithStatus}.
|
|
346
|
+
*
|
|
347
|
+
* Returns just the session rows; the truncation/scan-status fields are
|
|
348
|
+
* dropped. This is the byte-identical shape the maestro refresh activity,
|
|
349
|
+
* the #785 upgrade-snapshot, and the other roster consumers already depend
|
|
350
|
+
* on — keeping it a thin delegate means the truncation-signalling work
|
|
351
|
+
* (#845) does NOT ripple through those call sites. Callers that need to
|
|
352
|
+
* know whether the scan was complete (the `ensemble` tool) call the rich
|
|
353
|
+
* sibling directly.
|
|
354
|
+
*/
|
|
355
|
+
async function scanEnsembleSessions(client, ensemble, log = () => { }) {
|
|
356
|
+
return (await scanEnsembleSessionsWithStatus(client, ensemble, log)).sessions;
|
|
244
357
|
}
|
|
@@ -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.
|