agent-tempo 1.5.1 → 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/package.json +1 -1
- package/dist/activities/outbox.d.ts +14 -1
- package/dist/activities/outbox.js +41 -0
- package/dist/cli/commands.js +25 -8
- package/dist/cli/config-command.d.ts +16 -0
- package/dist/cli/config-command.js +51 -5
- package/dist/cli/resolve-ensemble.d.ts +17 -0
- package/dist/cli/resolve-ensemble.js +20 -0
- package/dist/cli/sa-preflight.d.ts +8 -0
- package/dist/cli/sa-preflight.js +31 -0
- package/dist/cli.js +5 -1
- package/dist/config.d.ts +12 -3
- package/dist/config.js +15 -12
- package/dist/pi/cue-pump.d.ts +86 -13
- package/dist/pi/cue-pump.js +110 -15
- package/dist/pi/extension.d.ts +29 -15
- package/dist/pi/extension.js +96 -19
- package/dist/pi/index.d.ts +2 -2
- package/dist/pi/index.js +2 -1
- package/dist/pi/pi-types.d.ts +50 -0
- package/dist/pi/reset-pump.d.ts +55 -17
- package/dist/pi/reset-pump.js +70 -20
- package/dist/server-tools.d.ts +7 -1
- package/dist/server-tools.js +2 -2
- package/dist/server.js +5 -2
- package/dist/tools/recruit.d.ts +19 -2
- package/dist/tools/recruit.js +26 -2
- package/package.json +1 -1
package/dashboard/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Client } from '@temporalio/client';
|
|
2
2
|
import { Config } from '../config';
|
|
3
|
-
import { AgentType, MockMode, DetachReason } from '../types';
|
|
3
|
+
import { AgentType, AttachmentPhase, MockMode, DetachReason } from '../types';
|
|
4
4
|
import type { ClaudeCodeHeadlessPermissionMode } from '../adapters/claude-code-headless/types';
|
|
5
5
|
import type { IngestTokenRegistry } from '../http/ingest-registry';
|
|
6
6
|
import type { GateRegistry } from '../http/gate-registry';
|
|
@@ -180,6 +180,12 @@ export interface SpawnProcessInput {
|
|
|
180
180
|
export interface OutboxActivityResult {
|
|
181
181
|
success: boolean;
|
|
182
182
|
error?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Human-readable note for a non-failure outcome the caller may surface — e.g.
|
|
185
|
+
* #676 FIX-3's "skipped duplicate spawn" no-op. Floor today is the daemon log;
|
|
186
|
+
* structured here so a future workflow-side relay can surface it to the operator.
|
|
187
|
+
*/
|
|
188
|
+
note?: string;
|
|
183
189
|
}
|
|
184
190
|
export interface RecruitResult extends OutboxActivityResult {
|
|
185
191
|
/** Session UUID assigned at recruit time. */
|
|
@@ -214,4 +220,11 @@ export interface OutboxActivities {
|
|
|
214
220
|
* destroy path REVOKES it. Optional: undefined disables ingest-token minting
|
|
215
221
|
* (e.g. the dev test harness that constructs activities without the daemon).
|
|
216
222
|
*/
|
|
223
|
+
/**
|
|
224
|
+
* #676 FIX-3 — should spawnProcess SKIP as a duplicate dispatch? TRUE iff this is
|
|
225
|
+
* a FRESH recruit (no `attachmentId` handoff) AND a live adapter is already
|
|
226
|
+
* attached. A restart/migrate carries `attachmentId` (the handoff to its fresh
|
|
227
|
+
* claim — phase is legitimately live) → never skipped. Pure + exported for tests.
|
|
228
|
+
*/
|
|
229
|
+
export declare function shouldSkipDuplicateSpawn(attachmentId: string | undefined, phase: AttachmentPhase): boolean;
|
|
217
230
|
export declare function createOutboxActivities(client: Client, config: Config, ingestTokens?: IngestTokenRegistry, gate?: GateRegistry): OutboxActivities;
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.shouldSkipDuplicateSpawn = shouldSkipDuplicateSpawn;
|
|
36
37
|
exports.createOutboxActivities = createOutboxActivities;
|
|
37
38
|
const client_1 = require("@temporalio/client");
|
|
38
39
|
const activity_1 = require("@temporalio/activity");
|
|
@@ -144,6 +145,17 @@ function classifyAndRethrow(err, contextPrefix) {
|
|
|
144
145
|
* destroy path REVOKES it. Optional: undefined disables ingest-token minting
|
|
145
146
|
* (e.g. the dev test harness that constructs activities without the daemon).
|
|
146
147
|
*/
|
|
148
|
+
/**
|
|
149
|
+
* #676 FIX-3 — should spawnProcess SKIP as a duplicate dispatch? TRUE iff this is
|
|
150
|
+
* a FRESH recruit (no `attachmentId` handoff) AND a live adapter is already
|
|
151
|
+
* attached. A restart/migrate carries `attachmentId` (the handoff to its fresh
|
|
152
|
+
* claim — phase is legitimately live) → never skipped. Pure + exported for tests.
|
|
153
|
+
*/
|
|
154
|
+
function shouldSkipDuplicateSpawn(attachmentId, phase) {
|
|
155
|
+
if (attachmentId)
|
|
156
|
+
return false; // restart/migrate handoff — must spawn
|
|
157
|
+
return phase === 'attached' || phase === 'processing' || phase === 'awaiting';
|
|
158
|
+
}
|
|
147
159
|
function createOutboxActivities(client, config, ingestTokens, gate) {
|
|
148
160
|
return {
|
|
149
161
|
async deliverCue(input) {
|
|
@@ -315,6 +327,35 @@ function createOutboxActivities(client, config, ingestTokens, gate) {
|
|
|
315
327
|
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, sessionId, allowedTools, claudeBin, attachmentId, attachmentRunId, adapterId, mockMode, mockScenario, model, permissionMode, dangerouslySkipPermissions, toolAccess } = input;
|
|
316
328
|
// Read secrets from the worker's config closure — never from workflow state
|
|
317
329
|
const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
|
|
330
|
+
// #676 FIX-3 — double-dispatch backstop (ACTIVITY-level; no workflow/bundle
|
|
331
|
+
// touch). A FRESH recruit (NO attachmentId) of a name that ALREADY has a live
|
|
332
|
+
// adapter is a duplicate dispatch → skip the spawn so we don't race a second
|
|
333
|
+
// adapter for the lease. attachmentId PRESENT = a restart/migrate HANDOFF to a
|
|
334
|
+
// fresh claim (phase is legitimately {attached|processing|awaiting} from that
|
|
335
|
+
// claim) → MUST NOT skip, or restart attaches to a non-existent adapter.
|
|
336
|
+
// Guard ABOVE the agent switch so it covers every agent. TOCTOU best-effort —
|
|
337
|
+
// claimAttachment's expectedAttachmentId arbitrates the rare race.
|
|
338
|
+
if (!attachmentId) {
|
|
339
|
+
const checkWorkflowId = isConductor ? (0, config_1.conductorWorkflowId)(ensemble) : (0, config_1.sessionWorkflowId)(ensemble, targetName);
|
|
340
|
+
try {
|
|
341
|
+
const info = await client.workflow.getHandle(checkWorkflowId).query(signals_1.attachmentInfoQuery);
|
|
342
|
+
if (shouldSkipDuplicateSpawn(attachmentId, info.phase)) {
|
|
343
|
+
// Corrected message (architect): force does NOT bypass this skip under
|
|
344
|
+
// FIX-3(a), so do NOT tell the operator to pass force. Replace-a-live-
|
|
345
|
+
// adapter is restart/migrate's lane; a stale session self-heals in ~90s.
|
|
346
|
+
const note = `recruit skipped: player "${targetName}" is already attached (phase=${info.phase}) — ` +
|
|
347
|
+
`spawning now would create a duplicate adapter racing the live session. To replace it, ` +
|
|
348
|
+
`use \`restart\` (same host) or \`migrate\` (other host). If the session is actually stale, ` +
|
|
349
|
+
`its lease expires and it's reaped within ~90s, after which recruit spawns normally.`;
|
|
350
|
+
log(`[#676 FIX-3] ${note}`);
|
|
351
|
+
return { success: true, note };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
// Not-queryable-yet / transient → fall through to spawn (best-effort guard).
|
|
356
|
+
log(`FIX-3 attachment pre-check inconclusive for "${targetName}" — proceeding to spawn: ${err instanceof Error ? err.message : String(err)}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
318
359
|
try {
|
|
319
360
|
if (agent === 'mock') {
|
|
320
361
|
// ADR 0014 PR-2 — mock adapter spawns headless. No terminal,
|
package/dist/cli/commands.js
CHANGED
|
@@ -938,6 +938,7 @@ function temporalCliExists() {
|
|
|
938
938
|
}
|
|
939
939
|
function registerSearchAttributes(temporalAddress, namespace = 'default') {
|
|
940
940
|
let failed = 0;
|
|
941
|
+
let permissionBlocked = 0;
|
|
941
942
|
for (const attr of sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES) {
|
|
942
943
|
const r = (0, sa_preflight_1.registerSearchAttribute)(attr, temporalAddress, namespace);
|
|
943
944
|
switch (r.status) {
|
|
@@ -948,17 +949,33 @@ function registerSearchAttributes(temporalAddress, namespace = 'default') {
|
|
|
948
949
|
out.dim(` ${attr.name} (already registered)`);
|
|
949
950
|
break;
|
|
950
951
|
case 'failed':
|
|
951
|
-
//
|
|
952
|
-
//
|
|
953
|
-
//
|
|
954
|
-
//
|
|
955
|
-
// the
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
|
|
952
|
+
// A PERMISSION error (Temporal Cloud namespace API keys can't reach the
|
|
953
|
+
// operator service) means we can't tell whether the SA exists — NOT that
|
|
954
|
+
// it's missing. Don't print a scary per-attr "Failed to register" or count
|
|
955
|
+
// it as a failure; collapse to ONE soft line below and PROCEED. Reserve
|
|
956
|
+
// the per-attr warning + hard "will fail" conclusion for DEFINITIVE
|
|
957
|
+
// failures (e.g. the SQLite dev server's 10-Keyword-per-namespace cap).
|
|
958
|
+
if ((0, sa_preflight_1.isPermissionError)(r.detail)) {
|
|
959
|
+
permissionBlocked++;
|
|
960
|
+
}
|
|
961
|
+
else {
|
|
962
|
+
failed++;
|
|
963
|
+
out.warn(`Failed to register ${attr.name}: ${r.detail}`);
|
|
964
|
+
}
|
|
959
965
|
break;
|
|
960
966
|
}
|
|
961
967
|
}
|
|
968
|
+
// Permission-blocked (normal on Temporal Cloud): one accurate, non-alarming
|
|
969
|
+
// line — we couldn't manage the SAs, but that doesn't mean they're missing.
|
|
970
|
+
if (permissionBlocked > 0) {
|
|
971
|
+
const saList = sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES.map((a) => `${a.name}:${a.type}`).join(', ');
|
|
972
|
+
out.warn(`Couldn't verify search attributes — this credential lacks permission to manage them ` +
|
|
973
|
+
`(normal on Temporal Cloud, where search attributes are managed via the Cloud UI or tcld). ` +
|
|
974
|
+
`If workflow starts fail with "search attribute ... is not defined", create these ` +
|
|
975
|
+
`${sa_preflight_1.REQUIRED_SEARCH_ATTRIBUTES.length} via the Cloud UI / tcld: ${saList}. ` +
|
|
976
|
+
`Otherwise this is safe to ignore.`);
|
|
977
|
+
}
|
|
978
|
+
// DEFINITIVE failures genuinely block — keep the hard, actionable conclusion.
|
|
962
979
|
if (failed > 0) {
|
|
963
980
|
out.warn(`${failed} search attribute${failed === 1 ? '' : 's'} not registered — ` +
|
|
964
981
|
`workflow starts will fail. Resolve the errors above before continuing.`);
|
|
@@ -11,8 +11,24 @@ import type { AgentType } from '../types';
|
|
|
11
11
|
* are not offered here.
|
|
12
12
|
* Single source of truth for the interactive selector + `config set` validation
|
|
13
13
|
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
14
|
+
*
|
|
15
|
+
* DELIBERATE SUBSET of `AGENT_TYPES` (NOT derived from it): this is a CAPABILITY
|
|
16
|
+
* allowlist (conductor-capable production agents), distinct from `parseAgent`'s
|
|
17
|
+
* type-VALIDITY check, which accepts all of `AGENT_TYPES`. Keep the two separate —
|
|
18
|
+
* #683 was caused by a validity check (`config.ts`) that had been hardcoded to a
|
|
19
|
+
* stale subset; this one is intentionally narrow and must stay that way.
|
|
14
20
|
*/
|
|
15
21
|
export declare const VALID_DEFAULT_AGENTS: readonly AgentType[];
|
|
22
|
+
/** True when a config key holds a credential value that must be masked on display. */
|
|
23
|
+
export declare function isSecretKey(key: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Render a secret for display: a short non-sensitive prefix (when the value is
|
|
26
|
+
* long enough that the prefix reveals only a small fraction) + a masked tail +
|
|
27
|
+
* the char count. NEVER returns the full value. Empty/unset → "(not set)".
|
|
28
|
+
*
|
|
29
|
+
* Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function maskSecret(value: string | undefined | null): string;
|
|
16
32
|
/** Interactive config setup: `agent-tempo config` */
|
|
17
33
|
export declare function configInteractive(): Promise<void>;
|
|
18
34
|
/** Non-interactive: `agent-tempo config set <key> <value>` */
|
|
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.VALID_DEFAULT_AGENTS = void 0;
|
|
37
|
+
exports.isSecretKey = isSecretKey;
|
|
38
|
+
exports.maskSecret = maskSecret;
|
|
37
39
|
exports.configInteractive = configInteractive;
|
|
38
40
|
exports.configSet = configSet;
|
|
39
41
|
exports.configShow = configShow;
|
|
@@ -54,6 +56,12 @@ const out = __importStar(require("./output"));
|
|
|
54
56
|
* are not offered here.
|
|
55
57
|
* Single source of truth for the interactive selector + `config set` validation
|
|
56
58
|
* (#666 — adds `pi` so the new interactive Pi conductor can be the default).
|
|
59
|
+
*
|
|
60
|
+
* DELIBERATE SUBSET of `AGENT_TYPES` (NOT derived from it): this is a CAPABILITY
|
|
61
|
+
* allowlist (conductor-capable production agents), distinct from `parseAgent`'s
|
|
62
|
+
* type-VALIDITY check, which accepts all of `AGENT_TYPES`. Keep the two separate —
|
|
63
|
+
* #683 was caused by a validity check (`config.ts`) that had been hardcoded to a
|
|
64
|
+
* stale subset; this one is intentionally narrow and must stay that way.
|
|
57
65
|
*/
|
|
58
66
|
exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
59
67
|
// NOTE: `createTemporalConnection` is dynamic-imported inside `configInteractive`'s
|
|
@@ -61,7 +69,38 @@ exports.VALID_DEFAULT_AGENTS = ['claude', 'copilot', 'pi'];
|
|
|
61
69
|
// `@temporalio/client`, defeating the crash-proof property of `config show` /
|
|
62
70
|
// `config set` — both of which are pure fs operations and must remain operable
|
|
63
71
|
// under a broken Temporal SDK install.
|
|
64
|
-
|
|
72
|
+
// #684 — secret-masking. Any config field whose name looks like a credential is
|
|
73
|
+
// masked in EVERY display path (show / interactive default / set echo) so a key is
|
|
74
|
+
// never printed raw (terminal scrollback, screen-share, logs). Generalized on
|
|
75
|
+
// purpose: a future secret added to the config is masked BY DEFAULT, not leaked.
|
|
76
|
+
const SECRET_KEYS = new Set(['temporalApiKey', 'httpToken', 'readToken', 'adminToken']);
|
|
77
|
+
// Matches *_API_KEY / *ApiKey / *Token / *Secret / *Password but NOT path fields
|
|
78
|
+
// (e.g. temporalTlsKeyPath is a file path, not the key — it must stay visible).
|
|
79
|
+
const SECRET_KEY_PATTERN = /(api[_-]?key|token|secret|password)/i;
|
|
80
|
+
/** True when a config key holds a credential value that must be masked on display. */
|
|
81
|
+
function isSecretKey(key) {
|
|
82
|
+
if (/path$/i.test(key))
|
|
83
|
+
return false; // *Path fields are file locations, not secrets
|
|
84
|
+
return SECRET_KEYS.has(key) || SECRET_KEY_PATTERN.test(key);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Render a secret for display: a short non-sensitive prefix (when the value is
|
|
88
|
+
* long enough that the prefix reveals only a small fraction) + a masked tail +
|
|
89
|
+
* the char count. NEVER returns the full value. Empty/unset → "(not set)".
|
|
90
|
+
*
|
|
91
|
+
* Examples: `sk-ant-…•••• (set, 47 chars)` · short secret → `•••• (set, 6 chars)`.
|
|
92
|
+
*/
|
|
93
|
+
function maskSecret(value) {
|
|
94
|
+
if (value == null || value === '')
|
|
95
|
+
return '(not set)';
|
|
96
|
+
const len = value.length;
|
|
97
|
+
// Reveal a prefix only when it's a small fraction of the whole; never for short
|
|
98
|
+
// secrets (so the output can never contain the full input — see the unit test).
|
|
99
|
+
const prefixLen = len >= 12 ? 6 : len >= 8 ? 3 : 0;
|
|
100
|
+
const prefix = value.slice(0, prefixLen);
|
|
101
|
+
const masked = prefixLen > 0 ? `${prefix}…••••` : '••••';
|
|
102
|
+
return `${masked} (set, ${len} chars)`;
|
|
103
|
+
}
|
|
65
104
|
/** Read a line from stdin with a prompt and optional default value. */
|
|
66
105
|
function ask(prompt, defaultVal, mask = false) {
|
|
67
106
|
return new Promise((resolve) => {
|
|
@@ -69,7 +108,11 @@ function ask(prompt, defaultVal, mask = false) {
|
|
|
69
108
|
input: process.stdin,
|
|
70
109
|
output: process.stdout,
|
|
71
110
|
});
|
|
72
|
-
|
|
111
|
+
// #684 — for masked (secret) prompts NEVER echo the raw existing value as the
|
|
112
|
+
// shown default; render a masked hint instead. The real `defaultVal` is still
|
|
113
|
+
// returned on empty input, so an existing key is preserved without exposing it.
|
|
114
|
+
const shownDefault = mask ? maskSecret(defaultVal) : defaultVal;
|
|
115
|
+
const display = defaultVal ? `${prompt} (${shownDefault}): ` : `${prompt}: `;
|
|
73
116
|
if (mask) {
|
|
74
117
|
// For secret input: write prompt manually, mute output
|
|
75
118
|
process.stdout.write(`? ${display}`);
|
|
@@ -212,7 +255,9 @@ function configSet(key, value) {
|
|
|
212
255
|
}
|
|
213
256
|
config[configKey] = value;
|
|
214
257
|
(0, config_1.saveConfigFile)(config);
|
|
215
|
-
|
|
258
|
+
// #684 — echo through the same secret-masking path so `config set temporalApiKey …`
|
|
259
|
+
// never prints the value back raw (and a *Path field still shows its location).
|
|
260
|
+
out.success(`Set ${configKey} = ${isSecretKey(configKey) ? maskSecret(value) : value}`);
|
|
216
261
|
}
|
|
217
262
|
/** Show current config: `agent-tempo config show` */
|
|
218
263
|
function configShow() {
|
|
@@ -234,8 +279,9 @@ function configShow() {
|
|
|
234
279
|
for (const { key, configKey } of keys) {
|
|
235
280
|
const value = config[configKey];
|
|
236
281
|
const source = sources[configKey];
|
|
237
|
-
|
|
238
|
-
|
|
282
|
+
// #684 — secret-like fields go through maskSecret (prefix + masked tail + char
|
|
283
|
+
// count); everything else shows its value or "(not set)".
|
|
284
|
+
const display = isSecretKey(key) ? maskSecret(value) : (!value ? '(not set)' : value);
|
|
239
285
|
out.log(` ${key.padEnd(22)} ${display.padEnd(30)} ${out.dim(source)}`);
|
|
240
286
|
}
|
|
241
287
|
console.log();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the target ensemble with the precedence every CLI command uses (#685):
|
|
3
|
+
*
|
|
4
|
+
* `--ensemble` flag > positional arg > `AGENT_TEMPO_ENSEMBLE` env > `'default'`
|
|
5
|
+
*
|
|
6
|
+
* `up` previously passed a bare positional-derived value and IGNORED the
|
|
7
|
+
* `--ensemble` flag (so `agent-tempo up --ensemble pitest` silently launched in
|
|
8
|
+
* `default`). Centralizing the rule here makes it a single, unit-testable source
|
|
9
|
+
* of truth so it can't drift per-command again.
|
|
10
|
+
*
|
|
11
|
+
* Pure: `env` is injectable (defaults to the live `AGENT_TEMPO_ENSEMBLE`) so the
|
|
12
|
+
* precedence is testable without mutating `process.env`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveEnsemble(args: {
|
|
15
|
+
ensemble?: string;
|
|
16
|
+
positional: string[];
|
|
17
|
+
}, env?: string | undefined): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveEnsemble = resolveEnsemble;
|
|
4
|
+
const config_1 = require("../config");
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the target ensemble with the precedence every CLI command uses (#685):
|
|
7
|
+
*
|
|
8
|
+
* `--ensemble` flag > positional arg > `AGENT_TEMPO_ENSEMBLE` env > `'default'`
|
|
9
|
+
*
|
|
10
|
+
* `up` previously passed a bare positional-derived value and IGNORED the
|
|
11
|
+
* `--ensemble` flag (so `agent-tempo up --ensemble pitest` silently launched in
|
|
12
|
+
* `default`). Centralizing the rule here makes it a single, unit-testable source
|
|
13
|
+
* of truth so it can't drift per-command again.
|
|
14
|
+
*
|
|
15
|
+
* Pure: `env` is injectable (defaults to the live `AGENT_TEMPO_ENSEMBLE`) so the
|
|
16
|
+
* precedence is testable without mutating `process.env`.
|
|
17
|
+
*/
|
|
18
|
+
function resolveEnsemble(args, env = process.env[config_1.ENV.ENSEMBLE]) {
|
|
19
|
+
return args.ensemble || args.positional[1] || env || 'default';
|
|
20
|
+
}
|
|
@@ -88,6 +88,14 @@ export interface RegistrationResult {
|
|
|
88
88
|
/** stderr from the temporal CLI, populated when `status === 'failed'`. */
|
|
89
89
|
detail?: string;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* True when a registration error means "this credential can't manage search
|
|
93
|
+
* attributes" (a permission/authorization failure) rather than a definitive
|
|
94
|
+
* registration failure (e.g. the SQLite dev server's 10-Keyword cap). Used to
|
|
95
|
+
* avoid the false "not registered → starts will fail" conclusion on Temporal
|
|
96
|
+
* Cloud, where SA management lives behind the Cloud UI / `tcld`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function isPermissionError(detail: string | undefined): boolean;
|
|
91
99
|
/**
|
|
92
100
|
* Pure classifier — turn a temporal CLI exit into a {@link RegistrationStatus}.
|
|
93
101
|
* Extracted from {@link registerSearchAttribute} so the matching rules can
|
package/dist/cli/sa-preflight.js
CHANGED
|
@@ -39,6 +39,7 @@ exports.isTemporalCloud = isTemporalCloud;
|
|
|
39
39
|
exports.sdkProbeRegisteredAttributes = sdkProbeRegisteredAttributes;
|
|
40
40
|
exports.formatPreflightError = formatPreflightError;
|
|
41
41
|
exports.verifySearchAttributes = verifySearchAttributes;
|
|
42
|
+
exports.isPermissionError = isPermissionError;
|
|
42
43
|
exports.classifyRegistrationOutput = classifyRegistrationOutput;
|
|
43
44
|
exports.registerSearchAttribute = registerSearchAttribute;
|
|
44
45
|
exports.assertSearchAttributesOrExit = assertSearchAttributesOrExit;
|
|
@@ -284,6 +285,36 @@ async function verifySearchAttributes(opts) {
|
|
|
284
285
|
message: formatPreflightError(missing, opts.temporalNamespace, probeError, cloud),
|
|
285
286
|
};
|
|
286
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Substrings signalling the credential lacks PERMISSION to manage search
|
|
290
|
+
* attributes — distinct from a definitive registration failure. Temporal Cloud
|
|
291
|
+
* namespace API keys can't reach the operator service (Cloud manages SAs via its
|
|
292
|
+
* UI / `tcld`), so `temporal operator search-attribute create/list` returns
|
|
293
|
+
* "Request unauthorized" / PermissionDenied. That means we CANNOT determine
|
|
294
|
+
* whether the SAs are registered — NOT that they're missing. Concluding
|
|
295
|
+
* "not registered → workflow starts will fail" from a permission error is a
|
|
296
|
+
* false alarm: on Cloud the SAs are typically already present and starts succeed.
|
|
297
|
+
*/
|
|
298
|
+
const PERMISSION_ERROR_MARKERS = [
|
|
299
|
+
'request unauthorized',
|
|
300
|
+
'permission denied',
|
|
301
|
+
'permissiondenied',
|
|
302
|
+
'unauthorized',
|
|
303
|
+
'not authorized',
|
|
304
|
+
];
|
|
305
|
+
/**
|
|
306
|
+
* True when a registration error means "this credential can't manage search
|
|
307
|
+
* attributes" (a permission/authorization failure) rather than a definitive
|
|
308
|
+
* registration failure (e.g. the SQLite dev server's 10-Keyword cap). Used to
|
|
309
|
+
* avoid the false "not registered → starts will fail" conclusion on Temporal
|
|
310
|
+
* Cloud, where SA management lives behind the Cloud UI / `tcld`.
|
|
311
|
+
*/
|
|
312
|
+
function isPermissionError(detail) {
|
|
313
|
+
if (!detail)
|
|
314
|
+
return false;
|
|
315
|
+
const d = detail.toLowerCase();
|
|
316
|
+
return PERMISSION_ERROR_MARKERS.some((m) => d.includes(m));
|
|
317
|
+
}
|
|
287
318
|
/**
|
|
288
319
|
* Pure classifier — turn a temporal CLI exit into a {@link RegistrationStatus}.
|
|
289
320
|
* Extracted from {@link registerSearchAttribute} so the matching rules can
|
package/dist/cli.js
CHANGED
|
@@ -62,6 +62,7 @@ const types_1 = require("./types");
|
|
|
62
62
|
const config_1 = require("./config");
|
|
63
63
|
const legacy_migration_1 = require("./cli/legacy-migration");
|
|
64
64
|
const global_wrapper_1 = require("./cli/global-wrapper");
|
|
65
|
+
const resolve_ensemble_1 = require("./cli/resolve-ensemble");
|
|
65
66
|
const grpc_shutdown_guard_1 = require("./utils/grpc-shutdown-guard");
|
|
66
67
|
/** Package root — cli.js compiles to dist/cli.js, so one level up. Used by the inline `version` handler. */
|
|
67
68
|
const PACKAGE_ROOT = (0, path_1.resolve)(__dirname, '..');
|
|
@@ -473,7 +474,10 @@ async function main() {
|
|
|
473
474
|
break;
|
|
474
475
|
case 'up':
|
|
475
476
|
await up({
|
|
476
|
-
ensemble,
|
|
477
|
+
// #685 — honor `--ensemble` (flag > positional > env > 'default'), via the
|
|
478
|
+
// shared resolver. Previously `up` passed the bare positional-derived
|
|
479
|
+
// `ensemble`, so `--ensemble <name>` was silently ignored → launched in `default`.
|
|
480
|
+
ensemble: (0, resolve_ensemble_1.resolveEnsemble)(args),
|
|
477
481
|
name: args.name,
|
|
478
482
|
lineup: args.lineup,
|
|
479
483
|
noHold: args.noHold,
|
package/dist/config.d.ts
CHANGED
|
@@ -334,9 +334,18 @@ export declare function loadTemporalCliConfig(): PersistedConfig;
|
|
|
334
334
|
*/
|
|
335
335
|
export declare function parseTemporalYaml(content: string): PersistedConfig;
|
|
336
336
|
/**
|
|
337
|
-
* Parse an agent value against the {@link
|
|
338
|
-
*
|
|
339
|
-
*
|
|
337
|
+
* Parse an agent value against the canonical {@link AGENT_TYPES} union — the
|
|
338
|
+
* SINGLE SOURCE OF TRUTH for agent validity (shared with `cli.ts`'s `--agent`
|
|
339
|
+
* parser). Throws when `value` is present but not a known agent; returns
|
|
340
|
+
* `'claude'` for empty/unset values so callers can use it as a source-aware default.
|
|
341
|
+
*
|
|
342
|
+
* This is a pure type-VALIDITY check — it accepts EVERY `AgentType` (including
|
|
343
|
+
* `mock` and the headless adapters). Narrower CAPABILITY constraints are gated
|
|
344
|
+
* separately downstream: the recruit pre-flight rejects `mock` outside dev mode,
|
|
345
|
+
* and `config`'s `VALID_DEFAULT_AGENTS` restricts the persistent default to the
|
|
346
|
+
* conductor-capable subset. (#683: the former hardcoded `['claude','copilot']`
|
|
347
|
+
* list was stale — it rejected `defaultAgent=pi` at config LOAD, poisoning every
|
|
348
|
+
* command before the `--agent` flag was even read.)
|
|
340
349
|
*/
|
|
341
350
|
export declare function parseAgent(value: string | undefined, source: ConfigSource): AgentType;
|
|
342
351
|
/**
|
package/dist/config.js
CHANGED
|
@@ -23,14 +23,8 @@ const fs_1 = require("fs");
|
|
|
23
23
|
const path_1 = require("path");
|
|
24
24
|
const os_1 = require("os");
|
|
25
25
|
const zod_1 = require("zod");
|
|
26
|
+
const types_1 = require("./types");
|
|
26
27
|
const validation_1 = require("./utils/validation");
|
|
27
|
-
// `'mock'` is a valid `AgentType` value but intentionally NOT in the resolved
|
|
28
|
-
// `defaultAgent` set — recruit pre-flight rejects it outside dev mode anyway,
|
|
29
|
-
// and it's never a sensible *default* (each mock spawn is configured per call
|
|
30
|
-
// via the `agent: 'mock'` flag, not via the resolved chain). Listing it here
|
|
31
|
-
// would only enable users to set `defaultAgent=mock` in `~/.agent-tempo/config.json`,
|
|
32
|
-
// which the recruit gate would then turn around and reject in production.
|
|
33
|
-
const VALID_AGENTS = ['claude', 'copilot'];
|
|
34
28
|
/** Environment variable name constants — use these instead of string literals. */
|
|
35
29
|
exports.ENV = {
|
|
36
30
|
ENSEMBLE: 'AGENT_TEMPO_ENSEMBLE',
|
|
@@ -458,16 +452,25 @@ const AGENT_SOURCE_LABELS = {
|
|
|
458
452
|
none: 'none',
|
|
459
453
|
};
|
|
460
454
|
/**
|
|
461
|
-
* Parse an agent value against the {@link
|
|
462
|
-
*
|
|
463
|
-
*
|
|
455
|
+
* Parse an agent value against the canonical {@link AGENT_TYPES} union — the
|
|
456
|
+
* SINGLE SOURCE OF TRUTH for agent validity (shared with `cli.ts`'s `--agent`
|
|
457
|
+
* parser). Throws when `value` is present but not a known agent; returns
|
|
458
|
+
* `'claude'` for empty/unset values so callers can use it as a source-aware default.
|
|
459
|
+
*
|
|
460
|
+
* This is a pure type-VALIDITY check — it accepts EVERY `AgentType` (including
|
|
461
|
+
* `mock` and the headless adapters). Narrower CAPABILITY constraints are gated
|
|
462
|
+
* separately downstream: the recruit pre-flight rejects `mock` outside dev mode,
|
|
463
|
+
* and `config`'s `VALID_DEFAULT_AGENTS` restricts the persistent default to the
|
|
464
|
+
* conductor-capable subset. (#683: the former hardcoded `['claude','copilot']`
|
|
465
|
+
* list was stale — it rejected `defaultAgent=pi` at config LOAD, poisoning every
|
|
466
|
+
* command before the `--agent` flag was even read.)
|
|
464
467
|
*/
|
|
465
468
|
function parseAgent(value, source) {
|
|
466
469
|
if (value == null || value === '')
|
|
467
470
|
return 'claude';
|
|
468
|
-
if (!
|
|
471
|
+
if (!types_1.AGENT_TYPES.includes(value)) {
|
|
469
472
|
throw new Error(`Invalid agent "${value}" from ${AGENT_SOURCE_LABELS[source]}. ` +
|
|
470
|
-
`Valid values: ${
|
|
473
|
+
`Valid values: ${types_1.AGENT_TYPES.join(', ')}.`);
|
|
471
474
|
}
|
|
472
475
|
return value;
|
|
473
476
|
}
|
package/dist/pi/cue-pump.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cue pump — pulls cues queued on the session workflow and injects them into
|
|
3
|
-
*
|
|
2
|
+
* Cue pump — pulls cues queued on the session workflow and injects them into the
|
|
3
|
+
* LIVE Pi agent, then acks them.
|
|
4
4
|
*
|
|
5
5
|
* Pi has no reverse-RPC into a running session from Temporal, so (like the
|
|
6
6
|
* existing adapters) we poll `pendingMessages` and ack via `markDelivered`.
|
|
7
7
|
*
|
|
8
|
+
* ── Injection target: the STABLE `pi` handle, re-resolved per tick (#677) ──
|
|
9
|
+
* Pi 0.78.1's `SessionStartEvent` carries NO `session` field, so in INTERACTIVE
|
|
10
|
+
* mode `PiEventPayload.session` is null → the old `resolveSession` returned null
|
|
11
|
+
* every tick → the interactive Pi conductor NEVER received cues. The fix routes
|
|
12
|
+
* injection through the `pi` ExtensionAPI handle (`pi.sendMessage`), which is
|
|
13
|
+
* always live. Crucially the injector is RE-RESOLVED PER TICK from the surviving
|
|
14
|
+
* module-scope runtime — capturing it once silently dies after an interactive
|
|
15
|
+
* session switch (the runtime's `pi` is repointed on rebind). Headless still works
|
|
16
|
+
* (its `pi` is the real ExtensionAPI too); the legacy `session.sendCustomMessage`
|
|
17
|
+
* path is kept as a feature-detected fallback.
|
|
18
|
+
*
|
|
8
19
|
* Injection follows D10 cue-delivery semantics:
|
|
9
20
|
* - **deliverAs** — operator cue (`msg.isMaestro`, a human steering from the
|
|
10
21
|
* Maestro dashboard) → `'steer'` (interrupt the in-flight turn so the
|
|
@@ -16,44 +27,106 @@
|
|
|
16
27
|
* is a no-op when a turn is already running (the message just queues), so we
|
|
17
28
|
* don't need to race-check the idle state — set it unconditionally.
|
|
18
29
|
*
|
|
30
|
+
* ── Escalation (#677): turn-started → sendUserMessage ──
|
|
31
|
+
* `triggerTurn: true` on `sendMessage` SHOULD wake a cold-idle agent, but if it
|
|
32
|
+
* doesn't (e.g. a Pi regression, or a queued followUp that never drains), the
|
|
33
|
+
* cue sits unprocessed and silently. The pump therefore tracks the last cue it
|
|
34
|
+
* injected via the escalation-eligible `pi.sendMessage` route; on the NEXT tick,
|
|
35
|
+
* if NO turn started since (the runtime's `lastTurnStartAt` is still older than
|
|
36
|
+
* the inject), it re-injects the SAME cue via `pi.sendUserMessage` — a user-role
|
|
37
|
+
* message ALWAYS starts a turn. Escalation fires at most once per cue (it can't
|
|
38
|
+
* loop). The primary route stays `pi.sendMessage` so the `cue` customType +
|
|
39
|
+
* operator-vs-peer steer/followUp semantics are preserved; `sendUserMessage`
|
|
40
|
+
* loses both, so it is fallback-only.
|
|
41
|
+
*
|
|
19
42
|
* Adapted from Pi's `examples/extensions/file-trigger.ts`.
|
|
20
43
|
*/
|
|
21
44
|
import type { Message } from '../types';
|
|
22
|
-
import type { PiAgentSession } from './pi-types';
|
|
45
|
+
import type { ExtensionAPI, PiAgentSession, PiOutboundMessage, PiCustomMessageOptions } from './pi-types';
|
|
23
46
|
/** Source of pending cues + ack — satisfied by `PiWorkflowClient`. */
|
|
24
47
|
export interface CueSource {
|
|
25
48
|
fetchPending(): Promise<Message[]>;
|
|
26
49
|
ackDelivered(messageIds: string[]): Promise<void>;
|
|
27
50
|
}
|
|
28
51
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
52
|
+
* The live cue-injection capability, RE-RESOLVED each tick from the surviving
|
|
53
|
+
* runtime so a session switch never injects through a stale handle. Two routes:
|
|
54
|
+
* - PRIMARY (`pi.sendMessage`): preserves the `cue` customType + steer/followUp
|
|
55
|
+
* operator-vs-peer semantics. Escalation-eligible.
|
|
56
|
+
* - FALLBACK (`session.sendCustomMessage`): legacy path; NOT escalation-eligible.
|
|
57
|
+
*/
|
|
58
|
+
export interface MessageInjector {
|
|
59
|
+
/** Inject one cue (D10 — `cue` customType, steer/followUp + triggerTurn). */
|
|
60
|
+
inject(msg: PiOutboundMessage, opts: PiCustomMessageOptions): void | Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Re-inject the SAME cue as a user-role message (always wakes a turn). Present
|
|
63
|
+
* ONLY on the escalation-eligible `pi.sendMessage` route — its presence IS the
|
|
64
|
+
* "this route can escalate" signal (the legacy session fallback omits it).
|
|
65
|
+
*/
|
|
66
|
+
escalate?(text: string): void | Promise<void>;
|
|
67
|
+
/** Epoch-ms of the last observed `turn_start` (null = none yet) — drives escalation. */
|
|
68
|
+
lastTurnStartAt(): number | null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolves the CURRENT injection capability at tick time. Re-acquired every tick
|
|
72
|
+
* rather than captured once, so a Pi instance rebuild (D11) never injects through
|
|
73
|
+
* a stale `pi`/session. Returns `null` when nothing is attached yet.
|
|
32
74
|
*/
|
|
33
|
-
export type
|
|
75
|
+
export type InjectorResolver = () => MessageInjector | null;
|
|
76
|
+
/** The runtime slice {@link buildPiInjector} reads — satisfied by `PiPlayerRuntime`. */
|
|
77
|
+
export interface InjectorRuntime {
|
|
78
|
+
pi: ExtensionAPI | null;
|
|
79
|
+
session: PiAgentSession | null;
|
|
80
|
+
lastTurnStartAt: number | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Build the per-tick {@link MessageInjector} from the live runtime, PREFERRING the
|
|
84
|
+
* stable `pi.sendMessage` handle (interactive root-cause fix, #677) and falling
|
|
85
|
+
* back to `session.sendCustomMessage` only when `pi.sendMessage` is unavailable.
|
|
86
|
+
* Pure + feature-detected (`typeof`) so it's safe whatever Pi build is loaded and
|
|
87
|
+
* unit-testable without a real Pi.
|
|
88
|
+
*/
|
|
89
|
+
export declare function buildPiInjector(rt: InjectorRuntime | null | undefined): MessageInjector | null;
|
|
34
90
|
export interface CuePumpOptions {
|
|
35
91
|
source: CueSource;
|
|
36
|
-
|
|
92
|
+
resolveInjector: InjectorResolver;
|
|
37
93
|
/** Poll interval (ms). */
|
|
38
94
|
intervalMs?: number;
|
|
95
|
+
/** Injected clock (tests). Defaults to `Date.now`. */
|
|
96
|
+
now?: () => number;
|
|
39
97
|
}
|
|
40
98
|
export declare class CuePump {
|
|
41
99
|
private readonly source;
|
|
42
|
-
private readonly
|
|
100
|
+
private readonly resolveInjector;
|
|
43
101
|
private readonly intervalMs;
|
|
102
|
+
private readonly now;
|
|
44
103
|
private timer;
|
|
45
104
|
private draining;
|
|
105
|
+
/**
|
|
106
|
+
* The last cue injected via the escalation-eligible `pi.sendMessage` route,
|
|
107
|
+
* pending a turn-start check on the next tick. Cleared once a turn starts or
|
|
108
|
+
* once escalated (escalate-once invariant).
|
|
109
|
+
*/
|
|
110
|
+
private lastInject;
|
|
46
111
|
constructor(opts: CuePumpOptions);
|
|
47
112
|
start(): void;
|
|
48
113
|
stop(): void;
|
|
49
114
|
/**
|
|
50
|
-
* One poll cycle:
|
|
51
|
-
*
|
|
52
|
-
* overlaps the
|
|
115
|
+
* One poll cycle: (1) escalate a previously-injected cue that never woke a turn,
|
|
116
|
+
* then (2) fetch pending cues, inject each into the live agent, ack the ones
|
|
117
|
+
* successfully injected. Re-entrancy guarded so a slow tick never overlaps the
|
|
118
|
+
* next interval.
|
|
53
119
|
*/
|
|
54
120
|
tick(): Promise<void>;
|
|
55
121
|
/**
|
|
56
|
-
*
|
|
122
|
+
* If a previously sendMessage-injected cue has not been followed by a turn, the
|
|
123
|
+
* `triggerTurn` wake didn't take — re-inject the SAME cue as a user-role message
|
|
124
|
+
* (always starts a turn). Escalates at most once per cue; clears the tracker
|
|
125
|
+
* once a turn is observed.
|
|
126
|
+
*/
|
|
127
|
+
private maybeEscalate;
|
|
128
|
+
/**
|
|
129
|
+
* Inject one cue into the live agent (D10 — see file header). Operator cues
|
|
57
130
|
* `steer` (same-turn priority); peer cues `followUp` (queue). `triggerTurn` is
|
|
58
131
|
* always set: a no-op mid-turn, the required cold-idle wake otherwise.
|
|
59
132
|
*/
|