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.
Files changed (118) hide show
  1. package/CLAUDE.md +29 -6
  2. package/README.md +2 -1
  3. package/dashboard/package.json +1 -1
  4. package/dist/activities/maestro.d.ts +46 -1
  5. package/dist/activities/maestro.js +69 -20
  6. package/dist/activities/outbox.d.ts +14 -1
  7. package/dist/activities/outbox.js +71 -10
  8. package/dist/activities/resolve.d.ts +118 -17
  9. package/dist/activities/resolve.js +256 -28
  10. package/dist/activities/schedule-fire.js +5 -2
  11. package/dist/adapters/base.d.ts +2 -0
  12. package/dist/adapters/base.js +9 -0
  13. package/dist/adapters/claude-api/adapter.js +26 -4
  14. package/dist/adapters/claude-code/adapter.js +15 -9
  15. package/dist/adapters/claude-code-headless/adapter.js +22 -4
  16. package/dist/adapters/copilot/adapter.js +58 -8
  17. package/dist/adapters/opencode/adapter.js +26 -6
  18. package/dist/adapters/sdk/base.d.ts +60 -0
  19. package/dist/adapters/sdk/base.js +83 -0
  20. package/dist/adapters/sdk/doorbell-client.d.ts +106 -0
  21. package/dist/adapters/sdk/doorbell-client.js +261 -0
  22. package/dist/adapters/sdk/idle-backoff.d.ts +71 -0
  23. package/dist/adapters/sdk/idle-backoff.js +136 -0
  24. package/dist/cli/command-center-command.js +25 -2
  25. package/dist/cli/commands.d.ts +28 -0
  26. package/dist/cli/commands.js +111 -20
  27. package/dist/cli/config-command.js +14 -0
  28. package/dist/cli/daemon-command.js +41 -5
  29. package/dist/cli/daemon.d.ts +245 -5
  30. package/dist/cli/daemon.js +505 -44
  31. package/dist/cli/help-text.js +3 -1
  32. package/dist/cli/mcp.d.ts +26 -2
  33. package/dist/cli/mcp.js +33 -4
  34. package/dist/cli/sa-preflight.d.ts +27 -1
  35. package/dist/cli/sa-preflight.js +29 -5
  36. package/dist/cli/startup.js +8 -2
  37. package/dist/cli/upgrade-to-2-command.d.ts +10 -0
  38. package/dist/cli/upgrade-to-2-command.js +159 -0
  39. package/dist/cli.js +37 -5
  40. package/dist/client/core.d.ts +6 -0
  41. package/dist/client/core.js +81 -34
  42. package/dist/client/subscribe.d.ts +10 -0
  43. package/dist/client/subscribe.js +2 -0
  44. package/dist/config.d.ts +53 -0
  45. package/dist/config.js +70 -1
  46. package/dist/daemon.d.ts +78 -7
  47. package/dist/daemon.js +313 -29
  48. package/dist/ensemble/saver.d.ts +10 -0
  49. package/dist/ensemble/saver.js +18 -6
  50. package/dist/http/aggregate.d.ts +88 -1
  51. package/dist/http/aggregate.js +157 -13
  52. package/dist/http/deliverability.d.ts +68 -0
  53. package/dist/http/deliverability.js +78 -0
  54. package/dist/http/doorbell-routes.d.ts +32 -0
  55. package/dist/http/doorbell-routes.js +72 -0
  56. package/dist/http/doorbell.d.ts +45 -0
  57. package/dist/http/doorbell.js +162 -0
  58. package/dist/http/inner-loop-routes.d.ts +9 -0
  59. package/dist/http/inner-loop-routes.js +4 -0
  60. package/dist/http/qa.js +8 -1
  61. package/dist/http/server.d.ts +9 -0
  62. package/dist/http/server.js +45 -1
  63. package/dist/http/snapshot.d.ts +28 -5
  64. package/dist/http/snapshot.js +16 -6
  65. package/dist/http/writes.js +24 -2
  66. package/dist/pi/cue-pump.d.ts +107 -7
  67. package/dist/pi/cue-pump.js +131 -24
  68. package/dist/pi/extension.js +11 -0
  69. package/dist/pi/headless.js +49 -13
  70. package/dist/pi/install.d.ts +15 -0
  71. package/dist/pi/install.js +35 -0
  72. package/dist/pi/mission-control/actions.d.ts +52 -3
  73. package/dist/pi/mission-control/actions.js +112 -3
  74. package/dist/pi/mission-control/board.d.ts +104 -0
  75. package/dist/pi/mission-control/board.js +103 -2
  76. package/dist/pi/mission-control/extension.d.ts +166 -1
  77. package/dist/pi/mission-control/extension.js +477 -15
  78. package/dist/pi/mission-control/render.d.ts +25 -0
  79. package/dist/pi/mission-control/render.js +142 -8
  80. package/dist/pi/workflow-client.d.ts +29 -0
  81. package/dist/pi/workflow-client.js +87 -3
  82. package/dist/reconcile/orphans.js +8 -9
  83. package/dist/server-tools.js +1 -1
  84. package/dist/server.js +11 -1
  85. package/dist/spawn.d.ts +78 -26
  86. package/dist/spawn.js +69 -37
  87. package/dist/tools/broadcast.js +15 -1
  88. package/dist/tools/cue.js +52 -11
  89. package/dist/tools/ensemble.js +43 -3
  90. package/dist/tools/recruit.js +11 -29
  91. package/dist/tools/who-am-i.d.ts +3 -2
  92. package/dist/tools/who-am-i.js +9 -2
  93. package/dist/types.d.ts +42 -0
  94. package/dist/upgrade/phase-engine.d.ts +160 -0
  95. package/dist/upgrade/phase-engine.js +555 -0
  96. package/dist/upgrade/snapshot-v1.d.ts +214 -0
  97. package/dist/upgrade/snapshot-v1.js +165 -0
  98. package/dist/utils/action-counters.d.ts +75 -0
  99. package/dist/utils/action-counters.js +279 -0
  100. package/dist/utils/format-hosts.js +7 -0
  101. package/dist/utils/hosts.js +5 -0
  102. package/dist/utils/sdk-probe.d.ts +12 -0
  103. package/dist/utils/sdk-probe.js +28 -0
  104. package/dist/utils/search-attributes.d.ts +76 -4
  105. package/dist/utils/search-attributes.js +89 -4
  106. package/dist/utils/suspension.d.ts +99 -0
  107. package/dist/utils/suspension.js +128 -0
  108. package/dist/utils/visibility-deadline.js +5 -0
  109. package/dist/worker.d.ts +16 -1
  110. package/dist/worker.js +25 -4
  111. package/dist/workflows/maestro.d.ts +10 -0
  112. package/dist/workflows/maestro.js +126 -21
  113. package/dist/workflows/scheduler.js +17 -2
  114. package/dist/workflows/session.js +251 -14
  115. package/dist/workflows/signals.d.ts +8 -0
  116. package/dist/workflows/signals.js +3 -2
  117. package/package.json +8 -4
  118. package/workflow-bundle.js +580 -40
@@ -1,5 +1,16 @@
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;
11
+ /** Shared query for listing running session workflows. Exported for the
12
+ * ensemble-scoped variants in `client/core.ts` (#751). */
13
+ export declare const SESSION_LIST_QUERY = "WorkflowType = \"agentSessionWorkflow\" AND ExecutionStatus = \"Running\"";
3
14
  /**
4
15
  * Resolve a session by player name.
5
16
  * Lists all running session workflows and queries each for metadata.
@@ -8,15 +19,43 @@ import { AttachmentPhase } from '../types';
8
19
  *
9
20
  * Shared by activity files (outbox, schedule-fire) and the tools layer.
10
21
  *
11
- * **Deadline (#336/#529):** the visibility iterator is bounded by
12
- * `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On timeout,
13
- * throws `VisibilityIteratorTimeoutError` rather than returning `null`
14
- * silent `null` on a partially-scanned set would be indistinguishable
15
- * from "definitely not found," producing false "Player not found" errors
16
- * upstream. Every existing caller wraps this in a try/catch (outbox
17
- * activities, MCP tools' `defineTool` helper, CLI dev-verbs); the throw
18
- * propagates as a retryable / user-visible "lookup timed out" rather
19
- * than the misleading "player not found."
22
+ * DECISION-PATH FENCE (#748): this resolver feeds DECISION paths (outbox
23
+ * delivery addressing, schedule fires, tool targets). It must keep its
24
+ * direct per-session `getMetadata` queries do NOT migrate it to the
25
+ * eventually-consistent SA/memo read path. Observation-only scans belong
26
+ * in `scanEnsembleSessionsCloud`. Enforced by
27
+ * tests/conformance/decision-path-fence.test.ts.
28
+ *
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.
20
59
  */
21
60
  export declare function resolveSession(client: Client, ensemble: string, playerName: string): Promise<WorkflowHandle | null>;
22
61
  /** Info returned for each session by scanEnsembleSessions. */
@@ -50,15 +89,77 @@ export interface EnsembleSessionInfo {
50
89
  lastActivityAt?: string;
51
90
  }
52
91
  /**
53
- * Scan all running session workflows in an ensemble.
54
- * Returns metadata + part for each session. Shared by the ensemble MCP tool
55
- * and the Maestro refresh activity.
92
+ * T0.1 (#748) cloud-profile ensemble scan. Observation path ONLY (see the
93
+ * DECISION-PATH FENCE on {@link resolveSession}).
94
+ *
95
+ * Differences vs the legacy {@link scanEnsembleSessions}:
96
+ * - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
97
+ * filter SA — no more cluster-wide list + per-session `getMetadata`
98
+ * pre-filtering (the unfiltered scan was the dominant idle-burn driver).
99
+ * - For v1.8+ runs (memo-complete: `AgentTempoWorkDir` present), the entire
100
+ * player row is read from the list result (SAs + memo) — **zero**
101
+ * per-player queries except the BPM `getActivityState` query, which is
102
+ * intentionally kept per the architect's ruling (deriving BPM from phase
103
+ * transitions would change the metric's meaning).
104
+ * - Pre-v1.8 runs (no observation memo) fall back to the legacy per-player
105
+ * `getMetadata` + `getPart` queries — cost shrinks as old runs cycle out.
106
+ *
107
+ * Staleness: SA/memo reads are eventually consistent (tens of seconds worst
108
+ * case under backlog) — acceptable for the observation path per the design
109
+ * addendum §B; the aggregate's confirm-on-change hook re-validates phase
110
+ * transitions with a direct query before emitting SSE events.
111
+ */
112
+ export declare function scanEnsembleSessionsCloud(client: Client, ensemble: string, log?: (...args: unknown[]) => void): Promise<EnsembleSessionInfo[]>;
113
+ /**
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.
56
141
  *
57
142
  * **Deadline (#336/#529):** the iterator is bounded by
58
- * `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On
59
- * timeout, returns the partial result accumulated so far and emits a
60
- * warn log. This site is **partial-tolerant by design** the caller
61
- * (maestro refresh, ensemble MCP tool) treats the result as a
62
- * 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.
147
+ *
148
+ * T0.1 (#748): this legacy shape is the `costProfile: 'local'` path —
149
+ * byte-identical row data to pre-#748 behavior. The cloud profile uses
150
+ * {@link scanEnsembleSessionsCloud}.
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.
63
164
  */
64
165
  export declare function scanEnsembleSessions(client: Client, ensemble: string, log?: (...args: unknown[]) => void): Promise<EnsembleSessionInfo[]>;
@@ -1,13 +1,26 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SESSION_LIST_QUERY = exports.RESOLVE_DESCRIBE_TIMEOUT_MS = void 0;
3
4
  exports.resolveSession = resolveSession;
5
+ exports.scanEnsembleSessionsCloud = scanEnsembleSessionsCloud;
6
+ exports.scanEnsembleSessionsWithStatus = scanEnsembleSessionsWithStatus;
4
7
  exports.scanEnsembleSessions = scanEnsembleSessions;
8
+ const config_1 = require("../config");
5
9
  const search_attributes_1 = require("../utils/search-attributes");
6
10
  const signals_1 = require("../workflows/signals");
7
11
  const query_timeout_1 = require("../utils/query-timeout");
8
12
  const visibility_deadline_1 = require("../utils/visibility-deadline");
9
- /** Shared query for listing running session workflows. */
10
- const SESSION_LIST_QUERY = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"`;
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;
21
+ /** Shared query for listing running session workflows. Exported for the
22
+ * ensemble-scoped variants in `client/core.ts` (#751). */
23
+ exports.SESSION_LIST_QUERY = `WorkflowType = "agentSessionWorkflow" AND ExecutionStatus = "Running"`;
11
24
  /**
12
25
  * Resolve a session by player name.
13
26
  * Lists all running session workflows and queries each for metadata.
@@ -16,18 +29,46 @@ const SESSION_LIST_QUERY = `WorkflowType = "agentSessionWorkflow" AND ExecutionS
16
29
  *
17
30
  * Shared by activity files (outbox, schedule-fire) and the tools layer.
18
31
  *
19
- * **Deadline (#336/#529):** the visibility iterator is bounded by
20
- * `VISIBILITY_DEADLINES_MS.resolveSession` (default 10s). On timeout,
21
- * throws `VisibilityIteratorTimeoutError` rather than returning `null`
22
- * silent `null` on a partially-scanned set would be indistinguishable
23
- * from "definitely not found," producing false "Player not found" errors
24
- * upstream. Every existing caller wraps this in a try/catch (outbox
25
- * activities, MCP tools' `defineTool` helper, CLI dev-verbs); the throw
26
- * propagates as a retryable / user-visible "lookup timed out" rather
27
- * than the misleading "player not found."
32
+ * DECISION-PATH FENCE (#748): this resolver feeds DECISION paths (outbox
33
+ * delivery addressing, schedule fires, tool targets). It must keep its
34
+ * direct per-session `getMetadata` queries do NOT migrate it to the
35
+ * eventually-consistent SA/memo read path. Observation-only scans belong
36
+ * in `scanEnsembleSessionsCloud`. Enforced by
37
+ * tests/conformance/decision-path-fence.test.ts.
38
+ *
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.
28
69
  */
29
70
  async function resolveSession(client, ensemble, playerName) {
30
- for await (const wf of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query: SESSION_LIST_QUERY }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.resolveSession, 'resolveSession')) {
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')) {
31
72
  try {
32
73
  const handle = client.workflow.getHandle(wf.workflowId);
33
74
  // Issue #433 — bound the per-session metadata query so a wedged
@@ -39,33 +80,204 @@ async function resolveSession(client, ensemble, playerName) {
39
80
  }
40
81
  }
41
82
  catch (err) {
42
- // Re-throw deadline timeouts — callers that wrap us in try/catch
43
- // already treat unknown throws as a soft "lookup failed" path,
44
- // and the typed error name makes the failure mode legible in
45
- // 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.
46
86
  if ((0, visibility_deadline_1.isVisibilityTimeout)(err))
47
87
  throw err;
48
88
  // Workflow may have just completed, or worker is wedged (#433) — skip
49
89
  }
50
90
  }
51
- return null;
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
+ }
52
154
  }
53
155
  /**
54
- * Scan all running session workflows in an ensemble.
55
- * Returns metadata + part for each session. Shared by the ensemble MCP tool
56
- * and the Maestro refresh activity.
156
+ * T0.1 (#748) cloud-profile ensemble scan. Observation path ONLY (see the
157
+ * DECISION-PATH FENCE on {@link resolveSession}).
158
+ *
159
+ * Differences vs the legacy {@link scanEnsembleSessions}:
160
+ * - The visibility query is **ensemble-scoped** via the `AgentTempoEnsemble`
161
+ * filter SA — no more cluster-wide list + per-session `getMetadata`
162
+ * pre-filtering (the unfiltered scan was the dominant idle-burn driver).
163
+ * - For v1.8+ runs (memo-complete: `AgentTempoWorkDir` present), the entire
164
+ * player row is read from the list result (SAs + memo) — **zero**
165
+ * per-player queries except the BPM `getActivityState` query, which is
166
+ * intentionally kept per the architect's ruling (deriving BPM from phase
167
+ * transitions would change the metric's meaning).
168
+ * - Pre-v1.8 runs (no observation memo) fall back to the legacy per-player
169
+ * `getMetadata` + `getPart` queries — cost shrinks as old runs cycle out.
170
+ *
171
+ * Staleness: SA/memo reads are eventually consistent (tens of seconds worst
172
+ * case under backlog) — acceptable for the observation path per the design
173
+ * addendum §B; the aggregate's confirm-on-change hook re-validates phase
174
+ * transitions with a direct query before emitting SSE events.
175
+ */
176
+ async function scanEnsembleSessionsCloud(client, ensemble, log = () => { }) {
177
+ const sessions = [];
178
+ const query = `${exports.SESSION_LIST_QUERY} AND AgentTempoEnsemble = "${(0, search_attributes_1.sanitizeQueryValue)(ensemble)}"`;
179
+ try {
180
+ for await (const workflow of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.scanEnsembleSessions, 'scanEnsembleSessionsCloud')) {
181
+ try {
182
+ const handle = client.workflow.getHandle(workflow.workflowId);
183
+ const phase = (0, search_attributes_1.getAttachmentPhase)(workflow);
184
+ const playerId = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoPlayerId') ?? workflow.workflowId;
185
+ const hostname = (0, search_attributes_1.getSearchAttrString)(workflow, 'AgentTempoHostname') ?? '';
186
+ // v1.8-memo-observation-fields runs carry workDir on the memo — use
187
+ // its presence as the "memo-complete row" discriminator.
188
+ const workDir = (0, search_attributes_1.getMemoString)(workflow, search_attributes_1.MEMO_KEYS.workDir);
189
+ let row;
190
+ if (workDir !== undefined) {
191
+ row = {
192
+ playerId,
193
+ part: (0, search_attributes_1.getPart)(workflow) ?? '',
194
+ hostname,
195
+ 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
+ };
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
+ }
221
+ // BPM fields filled in below — kept out of the enumeration loop so
222
+ // per-player query latency can't eat the visibility deadline.
223
+ sessions.push({ workflowId: workflow.workflowId, ...row, phase });
224
+ }
225
+ catch {
226
+ // Workflow may have just completed, or a legacy-fallback query timed
227
+ // out (#433) — skip this row; the next tick fills it in.
228
+ }
229
+ }
230
+ }
231
+ catch (err) {
232
+ if ((0, visibility_deadline_1.isVisibilityTimeout)(err)) {
233
+ log(`scanEnsembleSessionsCloud: ${err.message} — returning partial (${sessions.length} sessions)`);
234
+ }
235
+ else {
236
+ throw err;
237
+ }
238
+ }
239
+ // BPM source — kept as a direct per-player query at the stretched cadence
240
+ // (architect's ruling; see design addendum §C(b)). Fired in PARALLEL after
241
+ // enumeration: same query count, but N bounded queries (≤2s each) overlap
242
+ // instead of stacking sequentially against the scan's wall clock.
243
+ // Best-effort per player — a wedged session contributes zero tempo.
244
+ await Promise.all(sessions.map(async (s) => {
245
+ try {
246
+ const activity = await (0, query_timeout_1.queryHandleWithTimeout)(client.workflow.getHandle(s.workflowId), signals_1.getActivityStateQuery);
247
+ s.activityCount = activity.activityCount;
248
+ s.lastActivityAt = activity.lastActivityAt;
249
+ }
250
+ catch {
251
+ // Session predates W2 or worker wedged — contributes zero tempo.
252
+ }
253
+ }));
254
+ return sessions;
255
+ }
256
+ /**
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.
57
263
  *
58
264
  * **Deadline (#336/#529):** the iterator is bounded by
59
- * `VISIBILITY_DEADLINES_MS.scanEnsembleSessions` (default 15s). On
60
- * timeout, returns the partial result accumulated so far and emits a
61
- * warn log. This site is **partial-tolerant by design** the caller
62
- * (maestro refresh, ensemble MCP tool) treats the result as a
63
- * 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.
269
+ *
270
+ * T0.1 (#748): this legacy shape is the `costProfile: 'local'` path —
271
+ * byte-identical row data to pre-#748 behavior. The cloud profile uses
272
+ * {@link scanEnsembleSessionsCloud}.
64
273
  */
65
- async function scanEnsembleSessions(client, ensemble, log = () => { }) {
274
+ async function scanEnsembleSessionsWithStatus(client, ensemble, log = () => { }) {
66
275
  const sessions = [];
276
+ let truncated = false;
277
+ let scanned = 0;
67
278
  try {
68
- for await (const workflow of (0, visibility_deadline_1.iterateWithDeadline)(client.workflow.list({ query: SESSION_LIST_QUERY }), visibility_deadline_1.VISIBILITY_DEADLINES_MS.scanEnsembleSessions, 'scanEnsembleSessions')) {
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++;
69
281
  try {
70
282
  const handle = client.workflow.getHandle(workflow.workflowId);
71
283
  // Issue #433 — bound the metadata + part queries so a single wedged
@@ -119,11 +331,27 @@ async function scanEnsembleSessions(client, ensemble, log = () => { }) {
119
331
  }
120
332
  catch (err) {
121
333
  if ((0, visibility_deadline_1.isVisibilityTimeout)(err)) {
334
+ truncated = true;
122
335
  log(`scanEnsembleSessions: ${err.message} — returning partial (${sessions.length} sessions)`);
123
336
  }
124
337
  else {
125
338
  throw err;
126
339
  }
127
340
  }
128
- 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;
129
357
  }
@@ -4,13 +4,16 @@ exports.createScheduleActivities = createScheduleActivities;
4
4
  const croner_1 = require("croner");
5
5
  const config_1 = require("../config");
6
6
  const resolve_1 = require("./resolve");
7
+ const action_counters_1 = require("../utils/action-counters");
7
8
  const visibility_deadline_1 = require("../utils/visibility-deadline");
8
9
  /**
9
10
  * Create the schedule-fire activity bound to a Temporal client.
10
11
  * The returned object is registered with the worker as activities.
11
12
  */
12
13
  function createScheduleActivities(client) {
13
- return {
14
+ // #753 — attribute every Temporal call made by these activities (however
15
+ // deep, e.g. resolveSession → queryHandleWithTimeout) to the scheduler.
16
+ return (0, action_counters_1.tagActionSource)('schedule', {
14
17
  async computeNextCronFire(input) {
15
18
  const job = new croner_1.Cron(input.cronExpression, { timezone: input.timezone || 'UTC' });
16
19
  const next = job.nextRun();
@@ -104,7 +107,7 @@ function createScheduleActivities(client) {
104
107
  return { success: false, error: errorMsg };
105
108
  }
106
109
  },
107
- };
110
+ });
108
111
  }
109
112
  /**
110
113
  * Notify the schedule creator (or conductor as fallback) that delivery failed.
@@ -217,6 +217,7 @@ export declare abstract class BaseAttachment {
217
217
  * whatever comes next.
218
218
  */
219
219
  private tickHeartbeat;
220
+ private tickHeartbeatInner;
220
221
  private schedulePhaseWatcher;
221
222
  /**
222
223
  * Single tick of the phase-watcher loop. Same orphan-resistance scaffolding as
@@ -227,6 +228,7 @@ export declare abstract class BaseAttachment {
227
228
  * restart.
228
229
  */
229
230
  private tickPhaseWatcher;
231
+ private tickPhaseWatcherInner;
230
232
  /**
231
233
  * Shared error-classification path for the heartbeat + phase-watcher ticks (#226).
232
234
  *
@@ -7,6 +7,7 @@ exports._resetProcessLifecycleTelemetryForTest = _resetProcessLifecycleTelemetry
7
7
  exports._liveAdaptersForTest = _liveAdaptersForTest;
8
8
  const signals_1 = require("../workflows/signals");
9
9
  const terminal_error_1 = require("./terminal-error");
10
+ const action_counters_1 = require("../utils/action-counters");
10
11
  const log = (...args) => console.error('[agent-tempo:adapter]', ...args);
11
12
  // ── Hypothesis A telemetry (#258 follow-up) ─────────────────────────────
12
13
  //
@@ -491,6 +492,10 @@ class BaseAttachment {
491
492
  * whatever comes next.
492
493
  */
493
494
  async tickHeartbeat() {
495
+ // #753 — liveness-lease signals are metered as their own source.
496
+ return (0, action_counters_1.withActionSource)('heartbeat', () => this.tickHeartbeatInner());
497
+ }
498
+ async tickHeartbeatInner() {
494
499
  try {
495
500
  if (this.stopped || this.terminalFired) {
496
501
  this.logGuardTrip('heartbeat');
@@ -551,6 +556,10 @@ class BaseAttachment {
551
556
  * restart.
552
557
  */
553
558
  async tickPhaseWatcher() {
559
+ // #753 — relaxed-poll attachmentInfo queries are metered as their own source.
560
+ return (0, action_counters_1.withActionSource)('phase-watcher', () => this.tickPhaseWatcherInner());
561
+ }
562
+ async tickPhaseWatcherInner() {
554
563
  try {
555
564
  if (this.stopped || this.terminalFired) {
556
565
  this.logGuardTrip('phase-watcher');
@@ -75,6 +75,7 @@ const client_1 = require("@temporalio/client");
75
75
  const base_1 = require("../sdk/base");
76
76
  const config_1 = require("../../config");
77
77
  const connection_1 = require("../../connection");
78
+ const action_counters_1 = require("../../utils/action-counters");
78
79
  const signals_1 = require("../../workflows/signals");
79
80
  const server_tools_1 = require("../../server-tools");
80
81
  const mcp_bridge_1 = require("./mcp-bridge");
@@ -135,7 +136,11 @@ function truncateErr(err, max = 240) {
135
136
  }
136
137
  /** Default model id — verification addendum §1 (no date suffix on direct API). */
137
138
  const DEFAULT_MODEL = 'claude-opus-4-7';
138
- /** Idle poll cadence (ms). Matches Copilot bridge — short enough for snappy cue delivery, loose enough not to hammer Temporal. */
139
+ /**
140
+ * Busy-wait cadence (ms) while an SDK turn is in flight. The IDLE cadence is
141
+ * owned by the inherited `pollBackoff` (#749, T0.2): 2s base stretching to a
142
+ * 30s cap on empty polls, snapping back to base on any delivered message.
143
+ */
139
144
  const POLL_INTERVAL_MS = 2000;
140
145
  /** Workflow-register poll loop bounds. */
141
146
  const WORKFLOW_REGISTER_ATTEMPTS = 30;
@@ -223,6 +228,7 @@ class DirectApiAttachment extends base_1.SdkAttachment {
223
228
  const client = new client_1.Client({
224
229
  connection,
225
230
  namespace: config.temporalNamespace,
231
+ interceptors: (0, action_counters_1.actionCountingInterceptors)(),
226
232
  });
227
233
  // Hand the client + host to BaseAttachment so startV2Lifecycle can
228
234
  // issue claimAttachment + heartbeat against it.
@@ -331,6 +337,10 @@ class DirectApiAttachment extends base_1.SdkAttachment {
331
337
  await this.mcp?.close().catch(() => { });
332
338
  process.exit(1);
333
339
  }
340
+ // T1.1 PR-2 — cue doorbell: ding ⇒ immediate poll + backoff reset;
341
+ // connected ⇒ 60s idle ceiling. No token / daemon down ⇒ silent no-op
342
+ // (pure #761 polling).
343
+ this.startDoorbell(config.ensemble, playerIdForWorkflow);
334
344
  // PID file so callers can find / kill orphaned adapter processes.
335
345
  // #690 — write/unlink the EXACT path the spawner computed (ENV.PID_FILE) so the
336
346
  // adapter pid can't diverge from the spawner's; helper fallback for a manual launch.
@@ -356,7 +366,9 @@ class DirectApiAttachment extends base_1.SdkAttachment {
356
366
  process.on('SIGINT', shutdown);
357
367
  process.on('SIGTERM', shutdown);
358
368
  // Drive the poll loop until cleanup is requested.
359
- await this.pollLoop(handle);
369
+ // #753 — meter the poll loop's Temporal calls under 'sdk-poller'.
370
+ await (0, action_counters_1.withActionSource)('sdk-poller', () => this.pollLoop(handle));
371
+ this.stopDoorbell();
360
372
  try {
361
373
  fs.unlinkSync(pidFile);
362
374
  }
@@ -381,6 +393,9 @@ class DirectApiAttachment extends base_1.SdkAttachment {
381
393
  // wedge the player indefinitely.
382
394
  let consecutiveFailures = 0;
383
395
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
396
+ // #749: fresh poll loop (initial start OR reconnect) begins at the fast
397
+ // cadence — never inherit a stale 30s interval across a restart.
398
+ this.pollBackoff.reset();
384
399
  while (polling && !this.shouldStop()) {
385
400
  pollCount++;
386
401
  // Periodic workflow status check — detect external destroy / completion.
@@ -418,13 +433,20 @@ class DirectApiAttachment extends base_1.SdkAttachment {
418
433
  }
419
434
  catch (err) {
420
435
  log(`pendingMessages query failed: ${err?.message ?? err}`);
421
- await sleep(POLL_INTERVAL_MS);
436
+ // #749: grow on errors too — mirrors the interactive poller's shape
437
+ // (a wedged worker shouldn't be hammered at 2s).
438
+ // T1.1: pollSleep is doorbell-interruptible — a ding ends it early.
439
+ await this.pollSleep(this.pollBackoff.next(false));
422
440
  continue;
423
441
  }
424
442
  if (messages.length === 0) {
425
- await sleep(POLL_INTERVAL_MS);
443
+ // #749: idle backoff — 2s → 30s while nothing is pending (60s
444
+ // ceiling while the T1.1 doorbell is connected; a ding wakes it).
445
+ await this.pollSleep(this.pollBackoff.next(false));
426
446
  continue;
427
447
  }
448
+ // #749: live conversation — snap back to the fast cadence.
449
+ this.pollBackoff.reset();
428
450
  processing = true;
429
451
  try {
430
452
  // Representative message for processingStart/End pairing; ack the