claude-tempo 0.22.0-beta.1 → 0.22.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/dist/activities/outbox.d.ts +6 -6
- package/dist/activities/outbox.js +10 -9
- package/dist/cli/commands.js +39 -9
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/copilot-bridge.d.ts +1 -0
- package/dist/copilot-bridge.js +63 -9
- package/dist/ensemble/saver.d.ts +7 -0
- package/dist/ensemble/saver.js +35 -0
- package/dist/spawn.d.ts +2 -0
- package/dist/spawn.js +1 -0
- package/dist/tui/App.js +17 -4
- package/dist/tui/client.js +5 -1
- package/dist/tui/commands.js +50 -16
- package/dist/tui/components/ConversationStream.js +3 -1
- package/dist/tui/components/CreateEnsembleWizard.js +66 -2
- package/dist/tui/store.d.ts +9 -0
- package/dist/types.d.ts +2 -2
- package/dist/workflows/session.js +5 -4
- package/dist/workflows/signals.d.ts +1 -1
- package/package.json +1 -1
- package/workflow-bundle.js +6 -5
|
@@ -48,8 +48,8 @@ export interface SpawnProcessInput {
|
|
|
48
48
|
nativeResolvable?: boolean;
|
|
49
49
|
/** When true, use --resume instead of -n (reconnect to existing session). */
|
|
50
50
|
resume?: boolean;
|
|
51
|
-
/**
|
|
52
|
-
|
|
51
|
+
/** Session UUID — used for Copilot SDK sessionId and Claude Code --resume/--session-id. */
|
|
52
|
+
sessionId?: string;
|
|
53
53
|
/** Tool restrictions from the agent definition frontmatter. */
|
|
54
54
|
allowedTools?: string[];
|
|
55
55
|
/** Custom claude binary path (from config.claudeBin). */
|
|
@@ -70,8 +70,8 @@ export interface EncoreResult {
|
|
|
70
70
|
agentDefinitionPath?: string;
|
|
71
71
|
nativeResolvable?: boolean;
|
|
72
72
|
allowedTools?: string[];
|
|
73
|
-
/**
|
|
74
|
-
|
|
73
|
+
/** Session UUID — used for Copilot SDK sessionId and Claude Code --resume/--session-id. */
|
|
74
|
+
sessionId?: string;
|
|
75
75
|
temporalAddress: string;
|
|
76
76
|
temporalNamespace: string;
|
|
77
77
|
/** Custom claude binary path (from config.claudeBin). */
|
|
@@ -82,8 +82,8 @@ export interface OutboxActivityResult {
|
|
|
82
82
|
error?: string;
|
|
83
83
|
}
|
|
84
84
|
export interface RecruitResult extends OutboxActivityResult {
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/** Session UUID assigned at recruit time. */
|
|
86
|
+
sessionId?: string;
|
|
87
87
|
}
|
|
88
88
|
export interface OutboxActivities {
|
|
89
89
|
deliverCue(input: DeliverCueInput): Promise<OutboxActivityResult>;
|
|
@@ -107,8 +107,8 @@ function createOutboxActivities(client, config) {
|
|
|
107
107
|
? (0, config_1.conductorWorkflowId)(ensemble)
|
|
108
108
|
: (0, config_1.sessionWorkflowId)(ensemble, targetName);
|
|
109
109
|
const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
|
|
110
|
-
// Generate a UUID for the
|
|
111
|
-
const
|
|
110
|
+
// Generate a UUID for the session — used for deterministic --resume on encore
|
|
111
|
+
const sessionId = crypto.randomUUID();
|
|
112
112
|
const sessionInput = {
|
|
113
113
|
metadata: {
|
|
114
114
|
playerId: targetName,
|
|
@@ -120,7 +120,7 @@ function createOutboxActivities(client, config) {
|
|
|
120
120
|
isConductor,
|
|
121
121
|
agentType: agent,
|
|
122
122
|
status: 'pending',
|
|
123
|
-
|
|
123
|
+
sessionId,
|
|
124
124
|
...(agentDefinition ? { playerType: agentDefinition } : {}),
|
|
125
125
|
...(agentDefinitionDescription ? { playerTypeDescription: agentDefinitionDescription } : {}),
|
|
126
126
|
recruitedBy: fromPlayerId,
|
|
@@ -149,15 +149,15 @@ function createOutboxActivities(client, config) {
|
|
|
149
149
|
ClaudeTempoPlayerId: [targetName],
|
|
150
150
|
},
|
|
151
151
|
});
|
|
152
|
-
log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${
|
|
153
|
-
return { success: true,
|
|
152
|
+
log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${sessionId})`);
|
|
153
|
+
return { success: true, sessionId };
|
|
154
154
|
}
|
|
155
155
|
catch (err) {
|
|
156
156
|
throw activity_1.ApplicationFailure.nonRetryable(`Failed to start recruited session "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
157
157
|
}
|
|
158
158
|
},
|
|
159
159
|
async spawnProcess(input) {
|
|
160
|
-
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume,
|
|
160
|
+
const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, sessionId, allowedTools, claudeBin } = input;
|
|
161
161
|
// Read secrets from the worker's config closure — never from workflow state
|
|
162
162
|
const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
|
|
163
163
|
try {
|
|
@@ -175,6 +175,7 @@ function createOutboxActivities(client, config) {
|
|
|
175
175
|
temporalTlsKeyPath,
|
|
176
176
|
isConductor,
|
|
177
177
|
workDir,
|
|
178
|
+
sessionId,
|
|
178
179
|
});
|
|
179
180
|
log(`Spawned copilot-bridge (pid ${pid}) in ${workDir} as "${targetName}"`);
|
|
180
181
|
}
|
|
@@ -194,8 +195,8 @@ function createOutboxActivities(client, config) {
|
|
|
194
195
|
// For encore: use UUID for deterministic --resume (no interactive picker).
|
|
195
196
|
// For new sessions: use --session-id to track the UUID for future encores.
|
|
196
197
|
const nameArgs = resume
|
|
197
|
-
? ['--resume',
|
|
198
|
-
: ['-n', targetName, ...(
|
|
198
|
+
? ['--resume', sessionId || targetName]
|
|
199
|
+
: ['-n', targetName, ...(sessionId ? ['--session-id', sessionId] : [])];
|
|
199
200
|
// Build --allowedTools flag from agent definition frontmatter
|
|
200
201
|
const allowedToolsFlags = allowedTools && allowedTools.length > 0
|
|
201
202
|
? ['--allowedTools', ...allowedTools]
|
|
@@ -296,7 +297,7 @@ function createOutboxActivities(client, config) {
|
|
|
296
297
|
agentDefinitionPath,
|
|
297
298
|
nativeResolvable,
|
|
298
299
|
allowedTools,
|
|
299
|
-
|
|
300
|
+
sessionId: metadata.sessionId || undefined,
|
|
300
301
|
temporalAddress: config.temporalAddress,
|
|
301
302
|
temporalNamespace: config.temporalNamespace,
|
|
302
303
|
claudeBin: config.claudeBin,
|
package/dist/cli/commands.js
CHANGED
|
@@ -438,10 +438,25 @@ const SEARCH_ATTRIBUTES = [
|
|
|
438
438
|
{ name: 'ClaudeTempoStatus', type: 'Keyword' },
|
|
439
439
|
{ name: 'ClaudeTempoPlayerType', type: 'Keyword' },
|
|
440
440
|
];
|
|
441
|
-
function isTemporalReachable(config) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
async function isTemporalReachable(config) {
|
|
442
|
+
try {
|
|
443
|
+
const conn = await (0, connection_1.createTemporalConnection)(config);
|
|
444
|
+
try {
|
|
445
|
+
// Verify namespace is ready — a gRPC connection alone doesn't guarantee the server can serve requests
|
|
446
|
+
const client = new client_1.Client({ connection: conn, namespace: config.temporalNamespace || 'default' });
|
|
447
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
448
|
+
for await (const _ of client.workflow.list({ query: 'WorkflowId = "__readiness_probe__"' })) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
finally {
|
|
453
|
+
await conn.close();
|
|
454
|
+
}
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
445
460
|
}
|
|
446
461
|
function temporalCliExists() {
|
|
447
462
|
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -777,7 +792,7 @@ async function up(opts) {
|
|
|
777
792
|
isConductor: true,
|
|
778
793
|
agentType: conductorAgent,
|
|
779
794
|
status: 'pending',
|
|
780
|
-
|
|
795
|
+
sessionId: conductorSessionId,
|
|
781
796
|
...(resolvedConductorType ? { playerType: resolvedConductorType.name, playerTypeDescription: resolvedConductorType.description || '' } : {}),
|
|
782
797
|
},
|
|
783
798
|
autoSummary: `Conductor session`,
|
|
@@ -858,7 +873,7 @@ async function up(opts) {
|
|
|
858
873
|
console.log();
|
|
859
874
|
out.log(`Recruiting ${lineup.players.length} player${lineup.players.length !== 1 ? 's' : ''} from lineup...`);
|
|
860
875
|
for (const player of lineup.players) {
|
|
861
|
-
const playerAgent = player.agent === 'copilot' ? 'copilot' : 'claude';
|
|
876
|
+
const playerAgent = player.agent === 'copilot' ? 'copilot' : (player.agent === 'claude' ? 'claude' : opts.agent);
|
|
862
877
|
const playerWorkDir = player.workDir || process.cwd();
|
|
863
878
|
const playerTypeName = player.type;
|
|
864
879
|
const resolvedPlayerType = playerTypeName ? (0, agent_types_1.resolveAgentType)(playerTypeName) : null;
|
|
@@ -877,7 +892,7 @@ async function up(opts) {
|
|
|
877
892
|
isConductor: false,
|
|
878
893
|
agentType: playerAgent,
|
|
879
894
|
status: 'pending',
|
|
880
|
-
|
|
895
|
+
sessionId: playerSessionId,
|
|
881
896
|
recruitedBy: sessionName,
|
|
882
897
|
...(resolvedPlayerType ? { playerType: resolvedPlayerType.name, playerTypeDescription: resolvedPlayerType.description || '' } : {}),
|
|
883
898
|
},
|
|
@@ -965,8 +980,23 @@ async function up(opts) {
|
|
|
965
980
|
try {
|
|
966
981
|
const entry = lineupScheduleToEntry(sched);
|
|
967
982
|
const schedulerWfId = (0, config_1.schedulerWorkflowId)(opts.ensemble);
|
|
968
|
-
|
|
969
|
-
|
|
983
|
+
// Try to signal existing scheduler; if not running, start it with this schedule as seed
|
|
984
|
+
try {
|
|
985
|
+
const handle = client.workflow.getHandle(schedulerWfId);
|
|
986
|
+
await handle.describe();
|
|
987
|
+
await handle.signal(scheduler_signals_1.addScheduleSignal, entry);
|
|
988
|
+
}
|
|
989
|
+
catch {
|
|
990
|
+
await client.workflow.start('claudeSchedulerWorkflow', {
|
|
991
|
+
workflowId: schedulerWfId,
|
|
992
|
+
taskQueue: config.taskQueue,
|
|
993
|
+
args: [{ ensemble: opts.ensemble, entries: [entry] }],
|
|
994
|
+
workflowIdConflictPolicy: client_1.WorkflowIdConflictPolicy.USE_EXISTING,
|
|
995
|
+
searchAttributes: {
|
|
996
|
+
ClaudeTempoEnsemble: [opts.ensemble],
|
|
997
|
+
},
|
|
998
|
+
});
|
|
999
|
+
}
|
|
970
1000
|
out.check(sched.name, true, `→ ${sched.target}`);
|
|
971
1001
|
}
|
|
972
1002
|
catch (err) {
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export declare const ENV: {
|
|
|
8
8
|
readonly BRIDGE_NAME: "COPILOT_BRIDGE_NAME";
|
|
9
9
|
readonly BRIDGE_MODE: "CLAUDE_TEMPO_BRIDGE_MODE";
|
|
10
10
|
readonly BRIDGE_MODEL: "COPILOT_BRIDGE_MODEL";
|
|
11
|
+
readonly BRIDGE_SESSION_ID: "COPILOT_BRIDGE_SESSION_ID";
|
|
11
12
|
readonly TEMPORAL_ADDRESS: "TEMPORAL_ADDRESS";
|
|
12
13
|
readonly TEMPORAL_NAMESPACE: "TEMPORAL_NAMESPACE";
|
|
13
14
|
readonly TEMPORAL_API_KEY: "TEMPORAL_API_KEY";
|
package/dist/config.js
CHANGED
|
@@ -29,6 +29,7 @@ exports.ENV = {
|
|
|
29
29
|
BRIDGE_NAME: 'COPILOT_BRIDGE_NAME',
|
|
30
30
|
BRIDGE_MODE: 'CLAUDE_TEMPO_BRIDGE_MODE',
|
|
31
31
|
BRIDGE_MODEL: 'COPILOT_BRIDGE_MODEL',
|
|
32
|
+
BRIDGE_SESSION_ID: 'COPILOT_BRIDGE_SESSION_ID',
|
|
32
33
|
TEMPORAL_ADDRESS: 'TEMPORAL_ADDRESS',
|
|
33
34
|
TEMPORAL_NAMESPACE: 'TEMPORAL_NAMESPACE',
|
|
34
35
|
TEMPORAL_API_KEY: 'TEMPORAL_API_KEY',
|
package/dist/copilot-bridge.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* CLAUDE_TEMPO_PLAYER_NAME — player ID for workflow registration (set by spawner for deterministic workflow IDs)
|
|
17
17
|
* COPILOT_BRIDGE_NAME — player name for set_name (optional)
|
|
18
18
|
* COPILOT_BRIDGE_MODEL — model to use (optional)
|
|
19
|
+
* COPILOT_BRIDGE_SESSION_ID — deterministic session ID for resumable sessions (optional)
|
|
19
20
|
* GITHUB_TOKEN — GitHub auth token (optional, uses logged-in user by default)
|
|
20
21
|
*/
|
|
21
22
|
export {};
|
package/dist/copilot-bridge.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* CLAUDE_TEMPO_PLAYER_NAME — player ID for workflow registration (set by spawner for deterministic workflow IDs)
|
|
18
18
|
* COPILOT_BRIDGE_NAME — player name for set_name (optional)
|
|
19
19
|
* COPILOT_BRIDGE_MODEL — model to use (optional)
|
|
20
|
+
* COPILOT_BRIDGE_SESSION_ID — deterministic session ID for resumable sessions (optional)
|
|
20
21
|
* GITHUB_TOKEN — GitHub auth token (optional, uses logged-in user by default)
|
|
21
22
|
*/
|
|
22
23
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
@@ -86,6 +87,8 @@ const MAX_CONSECUTIVE_FAILURES = 3;
|
|
|
86
87
|
const MAX_SESSION_RECREATIONS = 2;
|
|
87
88
|
/** Check workflow status every N polls (~30s at 2s interval). */
|
|
88
89
|
const WORKFLOW_STATUS_CHECK_INTERVAL = 15;
|
|
90
|
+
/** Proactively recreate the Copilot session after this idle period (ms). Default 60 min. */
|
|
91
|
+
const SESSION_MAX_IDLE_MS = 60 * 60 * 1000;
|
|
89
92
|
/** Wrap createSession with a timeout so auth/network hangs don't block forever. */
|
|
90
93
|
async function createSessionWithTimeout(copilotClient, sessionConfig, timeoutMs = CREATE_SESSION_TIMEOUT_MS) {
|
|
91
94
|
let timer;
|
|
@@ -106,6 +109,7 @@ async function main() {
|
|
|
106
109
|
const config = (0, config_1.getConfig)();
|
|
107
110
|
const playerName = process.env[config_1.ENV.BRIDGE_NAME];
|
|
108
111
|
const model = process.env[config_1.ENV.BRIDGE_MODEL];
|
|
112
|
+
const copilotSessionId = process.env[config_1.ENV.BRIDGE_SESSION_ID] || `tempo-${config.ensemble}-${playerName || 'unknown'}-${Date.now()}-${process.pid}`;
|
|
109
113
|
const workDir = process.cwd();
|
|
110
114
|
log(`Starting Copilot bridge in ${workDir} (ensemble: ${config.ensemble})`);
|
|
111
115
|
// Connect Temporal client (for polling only — the MCP server child process runs its own worker)
|
|
@@ -156,6 +160,7 @@ async function main() {
|
|
|
156
160
|
},
|
|
157
161
|
});
|
|
158
162
|
const sessionConfig = {
|
|
163
|
+
sessionId: copilotSessionId,
|
|
159
164
|
// approveAll is intentional: Copilot bridge sessions run headless with no
|
|
160
165
|
// interactive terminal, so there is no way to prompt for permission approval.
|
|
161
166
|
// All tool calls are auto-approved by design — the bridge operator accepts
|
|
@@ -290,6 +295,11 @@ async function main() {
|
|
|
290
295
|
process.exit(1);
|
|
291
296
|
}
|
|
292
297
|
log(`Workflow ready: ${expectedWorkflowId}`);
|
|
298
|
+
// Store sessionId in workflow metadata for future encore/resume
|
|
299
|
+
try {
|
|
300
|
+
await handle.signal('updateMetadata', { sessionId: copilotSessionId });
|
|
301
|
+
}
|
|
302
|
+
catch { /* workflow may not be ready yet */ }
|
|
293
303
|
// If a name was requested, send the set_name instruction
|
|
294
304
|
if (playerName) {
|
|
295
305
|
log(`Sending set_name instruction for "${playerName}"...`);
|
|
@@ -314,6 +324,8 @@ async function main() {
|
|
|
314
324
|
let pollCount = 0;
|
|
315
325
|
let consecutiveFailures = 0;
|
|
316
326
|
let sessionRecreations = 0;
|
|
327
|
+
let proactiveRecreations = 0;
|
|
328
|
+
let lastActivityTime = Date.now();
|
|
317
329
|
// interval declared here, assigned after poll is defined
|
|
318
330
|
let interval;
|
|
319
331
|
// Shared cleanup — disconnects session, removes PID file, stops client.
|
|
@@ -351,18 +363,33 @@ async function main() {
|
|
|
351
363
|
log(`ERROR: Exceeded max session recreations (${MAX_SESSION_RECREATIONS}). Giving up.`);
|
|
352
364
|
return false;
|
|
353
365
|
}
|
|
354
|
-
log(`Attempting session
|
|
366
|
+
log(`Attempting session recovery (${sessionRecreations}/${MAX_SESSION_RECREATIONS})...`);
|
|
355
367
|
try {
|
|
356
368
|
await session.disconnect().catch(() => { });
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
369
|
+
// Try resumeSession first to preserve conversation history
|
|
370
|
+
try {
|
|
371
|
+
const { sessionId: _discard, ...resumeConfig } = sessionConfig;
|
|
372
|
+
session = await copilotClient.resumeSession(copilotSessionId, resumeConfig);
|
|
373
|
+
attachEventLogger(session);
|
|
374
|
+
sessionAlive = true;
|
|
375
|
+
consecutiveFailures = 0;
|
|
376
|
+
lastActivityTime = Date.now();
|
|
377
|
+
log(`Session resumed successfully: ${session.sessionId}`);
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
catch (resumeErr) {
|
|
381
|
+
log(`resumeSession failed (${resumeErr?.message}), falling back to createSession`);
|
|
382
|
+
session = await createSessionWithTimeout(copilotClient, sessionConfig);
|
|
383
|
+
attachEventLogger(session);
|
|
384
|
+
sessionAlive = true;
|
|
385
|
+
consecutiveFailures = 0;
|
|
386
|
+
lastActivityTime = Date.now();
|
|
387
|
+
log(`Session recreated (fresh) successfully: ${session.sessionId}`);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
363
390
|
}
|
|
364
391
|
catch (err) {
|
|
365
|
-
log(`Session
|
|
392
|
+
log(`Session recovery failed: ${err?.message}`);
|
|
366
393
|
return false;
|
|
367
394
|
}
|
|
368
395
|
}
|
|
@@ -393,13 +420,37 @@ async function main() {
|
|
|
393
420
|
process.exit(0);
|
|
394
421
|
}
|
|
395
422
|
}
|
|
423
|
+
// Proactive stale-session detection — recreate before the SDK server GCs the session
|
|
424
|
+
const idleMs = Date.now() - lastActivityTime;
|
|
425
|
+
if (idleMs > SESSION_MAX_IDLE_MS && !processing) {
|
|
426
|
+
try {
|
|
427
|
+
processing = true; // guard against overlapping polls during async recreation
|
|
428
|
+
log(`Session idle for ${(idleMs / 1000 / 60).toFixed(0)}min — proactively recreating`);
|
|
429
|
+
proactiveRecreations++;
|
|
430
|
+
const recovered = await recreateSession();
|
|
431
|
+
if (recovered) {
|
|
432
|
+
// Proactive recreation is lifecycle management, not failure recovery — restore failure budget
|
|
433
|
+
// but don't reset to 0: use proactiveRecreations to cap total lifecycle recreations
|
|
434
|
+
sessionRecreations = Math.max(0, sessionRecreations - 1);
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
// Session is almost certainly dead server-side — force immediate recovery on next message
|
|
438
|
+
// Use MAX - 1 so the next poll error increments to the threshold and triggers recovery
|
|
439
|
+
consecutiveFailures = MAX_CONSECUTIVE_FAILURES - 1;
|
|
440
|
+
sessionAlive = false;
|
|
441
|
+
log('ERROR: Proactive session recreation failed — will force recovery on next message');
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
processing = false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
396
448
|
try {
|
|
397
449
|
const messages = await handle.query('pendingMessages');
|
|
398
450
|
if (messages.length === 0)
|
|
399
451
|
return;
|
|
400
452
|
processing = true;
|
|
401
453
|
const ids = messages.map((m) => m.id);
|
|
402
|
-
await handle.signal('markDelivered', ids);
|
|
403
454
|
// Format messages into a single prompt, appending ack instruction for Maestro messages
|
|
404
455
|
const prompt = messages
|
|
405
456
|
.map((m) => {
|
|
@@ -417,8 +468,11 @@ async function main() {
|
|
|
417
468
|
const elapsed = Date.now() - t0;
|
|
418
469
|
log(`sendAndWait completed in ${elapsed}ms`);
|
|
419
470
|
log(`Response: ${JSON.stringify(result)?.substring(0, 500)}`);
|
|
471
|
+
// Mark delivered only after successful send — failed messages stay in pending queue for retry
|
|
472
|
+
await handle.signal('markDelivered', ids);
|
|
420
473
|
// Success — reset failure tracking
|
|
421
474
|
consecutiveFailures = 0;
|
|
475
|
+
lastActivityTime = Date.now();
|
|
422
476
|
sessionAlive = true;
|
|
423
477
|
processing = false;
|
|
424
478
|
}
|
package/dist/ensemble/saver.d.ts
CHANGED
|
@@ -15,3 +15,10 @@ export declare function listLineups(): Array<{
|
|
|
15
15
|
* Read a saved lineup by name from ~/.claude-tempo/ensembles/.
|
|
16
16
|
*/
|
|
17
17
|
export declare function readSavedLineup(name: string): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* List all available lineups — saved (~/.claude-tempo/ensembles/) + shipped (examples/ensembles/).
|
|
20
|
+
*/
|
|
21
|
+
export declare function listAllLineups(): Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
source: 'saved' | 'shipped';
|
|
24
|
+
}>;
|
package/dist/ensemble/saver.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.saveLineup = saveLineup;
|
|
4
4
|
exports.listLineups = listLineups;
|
|
5
5
|
exports.readSavedLineup = readSavedLineup;
|
|
6
|
+
exports.listAllLineups = listAllLineups;
|
|
6
7
|
const fs_1 = require("fs");
|
|
7
8
|
const path_1 = require("path");
|
|
8
9
|
const yaml_1 = require("yaml");
|
|
@@ -129,6 +130,40 @@ function readSavedLineup(name) {
|
|
|
129
130
|
}
|
|
130
131
|
return null;
|
|
131
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* Find the package root by walking up from a directory.
|
|
135
|
+
*/
|
|
136
|
+
function findPackageRoot(dir) {
|
|
137
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(dir, 'package.json')))
|
|
138
|
+
return dir;
|
|
139
|
+
const parent = (0, path_1.resolve)(dir, '..');
|
|
140
|
+
return parent === dir ? dir : findPackageRoot(parent);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* List all available lineups — saved (~/.claude-tempo/ensembles/) + shipped (examples/ensembles/).
|
|
144
|
+
*/
|
|
145
|
+
function listAllLineups() {
|
|
146
|
+
const results = [];
|
|
147
|
+
// Saved lineups
|
|
148
|
+
for (const l of listLineups()) {
|
|
149
|
+
results.push({ name: l.name, source: 'saved' });
|
|
150
|
+
}
|
|
151
|
+
// Shipped examples
|
|
152
|
+
const pkgRoot = findPackageRoot((0, path_1.resolve)(__dirname));
|
|
153
|
+
const shippedDir = (0, path_1.join)(pkgRoot, 'examples', 'ensembles');
|
|
154
|
+
if ((0, fs_1.existsSync)(shippedDir)) {
|
|
155
|
+
for (const f of (0, fs_1.readdirSync)(shippedDir)) {
|
|
156
|
+
if (f.endsWith('.yaml') || f.endsWith('.yml')) {
|
|
157
|
+
const name = f.replace(/\.ya?ml$/, '');
|
|
158
|
+
// Skip if already in saved (saved takes precedence)
|
|
159
|
+
if (!results.some(r => r.name === name)) {
|
|
160
|
+
results.push({ name, source: 'shipped' });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return results;
|
|
166
|
+
}
|
|
132
167
|
function formatDurationMs(ms) {
|
|
133
168
|
if (ms >= 86_400_000 && ms % 86_400_000 === 0)
|
|
134
169
|
return `${ms / 86_400_000}d`;
|
package/dist/spawn.d.ts
CHANGED
|
@@ -64,6 +64,8 @@ export interface CopilotBridgeOpts {
|
|
|
64
64
|
workDir: string;
|
|
65
65
|
/** Directory for log and PID files. Defaults to `logs/` inside workDir. */
|
|
66
66
|
logDir?: string;
|
|
67
|
+
/** Copilot SDK session ID for resumable sessions. */
|
|
68
|
+
sessionId?: string;
|
|
67
69
|
}
|
|
68
70
|
export interface CopilotBridgeResult {
|
|
69
71
|
pid: number | undefined;
|
package/dist/spawn.js
CHANGED
|
@@ -427,6 +427,7 @@ function spawnCopilotBridge(opts) {
|
|
|
427
427
|
...(opts.temporalApiKey ? { [config_1.ENV.TEMPORAL_API_KEY]: opts.temporalApiKey } : {}),
|
|
428
428
|
...(opts.temporalTlsCertPath ? { [config_1.ENV.TEMPORAL_TLS_CERT_PATH]: opts.temporalTlsCertPath } : {}),
|
|
429
429
|
...(opts.temporalTlsKeyPath ? { [config_1.ENV.TEMPORAL_TLS_KEY_PATH]: opts.temporalTlsKeyPath } : {}),
|
|
430
|
+
...(opts.sessionId ? { [config_1.ENV.BRIDGE_SESSION_ID]: opts.sessionId } : {}),
|
|
430
431
|
},
|
|
431
432
|
});
|
|
432
433
|
child.unref();
|
package/dist/tui/App.js
CHANGED
|
@@ -222,6 +222,17 @@ function App({ api, ensemble, defaultAgent }) {
|
|
|
222
222
|
return;
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
+
// Gates/stages — Enter shows detail for selected item
|
|
226
|
+
if ((s.overlay.type === 'gates' || s.overlay.type === 'stages') && key.return) {
|
|
227
|
+
const selected = s.overlay.items[s.overlay.selectedIndex];
|
|
228
|
+
if (selected) {
|
|
229
|
+
const detail = selected.sublabel
|
|
230
|
+
? `\n ${selected.label}\n\n ${selected.sublabel.split(' ').join('\n ')}`
|
|
231
|
+
: `\n ${selected.label}\n\n No details available.`;
|
|
232
|
+
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: selected.label.slice(0, 40), content: detail });
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
225
236
|
return; // Swallow all other input while overlay is active
|
|
226
237
|
}
|
|
227
238
|
// Player detail view — Escape goes back, ↑↓ scrolls messages
|
|
@@ -703,7 +714,7 @@ function App({ api, ensemble, defaultAgent }) {
|
|
|
703
714
|
if (atMatch) {
|
|
704
715
|
// @player message → send directly to that player
|
|
705
716
|
const [, targetPlayer, message] = atMatch;
|
|
706
|
-
dispatch({ type: 'APPEND_SENT_MESSAGE', to: targetPlayer, text: message });
|
|
717
|
+
dispatch({ type: 'APPEND_SENT_MESSAGE', to: targetPlayer, text: `@${targetPlayer} ${message}` });
|
|
707
718
|
api.sendAsMaestro(s.activeEnsemble, targetPlayer, message).catch(err => dispatch({
|
|
708
719
|
type: 'COMMIT_STATIC',
|
|
709
720
|
item: { id: nextStaticId(), type: 'error', content: `\u2717 Failed to deliver to @${targetPlayer}: ${err}`, timestamp: Date.now() },
|
|
@@ -838,9 +849,11 @@ function App({ api, ensemble, defaultAgent }) {
|
|
|
838
849
|
}));
|
|
839
850
|
dispatch({ type: 'REFRESH_ENSEMBLE_DATA', players, messages: [], history: [], schedules });
|
|
840
851
|
// Skip redundant conversation dispatches when data hasn't changed
|
|
852
|
+
// Always dispatch on first poll (conversation === null) to exit "Loading messages..."
|
|
841
853
|
const lastChatMsg = chatResult.messages[chatResult.messages.length - 1];
|
|
842
854
|
const newLastTs = lastChatMsg?.timestamp ?? '';
|
|
843
|
-
|
|
855
|
+
const isFirstLoad = stateRef.current.conversation === null;
|
|
856
|
+
if (isFirstLoad || chatResult.total !== lastChatRef.current.total || newLastTs !== lastChatRef.current.lastTs) {
|
|
844
857
|
dispatch({ type: 'SET_CONVERSATION', conversation });
|
|
845
858
|
dispatch({ type: 'SET_ENSEMBLE_CHAT', chat: chatResult });
|
|
846
859
|
lastChatRef.current = { total: chatResult.total, lastTs: newLastTs };
|
|
@@ -1000,7 +1013,7 @@ function App({ api, ensemble, defaultAgent }) {
|
|
|
1000
1013
|
? ['up', name, '--lineup', lineup]
|
|
1001
1014
|
: ['up', name];
|
|
1002
1015
|
await new Promise((resolve, reject) => {
|
|
1003
|
-
execFile('claude-tempo', args, { cwd: workDir, timeout: 60000 }, (err, _stdout, stderr) => {
|
|
1016
|
+
execFile('claude-tempo', args, { cwd: workDir, timeout: 60000, shell: true }, (err, _stdout, stderr) => {
|
|
1004
1017
|
if (err)
|
|
1005
1018
|
reject(new Error(stderr?.trim() || err.message || 'Unknown error'));
|
|
1006
1019
|
else
|
|
@@ -1337,7 +1350,7 @@ function App({ api, ensemble, defaultAgent }) {
|
|
|
1337
1350
|
react_1.default.createElement(PromptArea_1.PromptArea, {
|
|
1338
1351
|
hints: promptHints,
|
|
1339
1352
|
onSubmit: handleSubmit,
|
|
1340
|
-
disabled: state.phase
|
|
1353
|
+
disabled: (state.phase !== 'main' && state.phase !== 'chat') || !!state.confirmingStop || !!state.confirmingDisband || !!state.confirmingLineup || state.pickerVisible || state.statusOverlay || !!state.overlay,
|
|
1341
1354
|
commandNames: commandNamesList,
|
|
1342
1355
|
playerNames: playerNamesList,
|
|
1343
1356
|
initialHistory: cmdHistory,
|
package/dist/tui/client.js
CHANGED
|
@@ -29,7 +29,7 @@ function createTempoClient(client) {
|
|
|
29
29
|
try {
|
|
30
30
|
const h = handle(globalMaestroId);
|
|
31
31
|
const byEnsemble = await h.query('maestroPlayersByEnsemble');
|
|
32
|
-
|
|
32
|
+
const results = Object.entries(byEnsemble).map(([name, players]) => {
|
|
33
33
|
const conductor = players.find(p => p.isConductor);
|
|
34
34
|
return {
|
|
35
35
|
name,
|
|
@@ -38,6 +38,10 @@ function createTempoClient(client) {
|
|
|
38
38
|
conductorStatus: conductor?.status,
|
|
39
39
|
};
|
|
40
40
|
});
|
|
41
|
+
// Only trust Maestro if it has discovered ensembles; fall through to
|
|
42
|
+
// Strategy 2 when empty — the Maestro may not have refreshed yet.
|
|
43
|
+
if (results.length > 0)
|
|
44
|
+
return results;
|
|
41
45
|
}
|
|
42
46
|
catch {
|
|
43
47
|
// Global Maestro not available — fall through
|
package/dist/tui/commands.js
CHANGED
|
@@ -14,6 +14,7 @@ exports.formatHelpSummary = formatHelpSummary;
|
|
|
14
14
|
*/
|
|
15
15
|
const child_process_1 = require("child_process");
|
|
16
16
|
const platform_1 = require("./utils/platform");
|
|
17
|
+
const saver_1 = require("../ensemble/saver");
|
|
17
18
|
// ── Parser ──
|
|
18
19
|
/**
|
|
19
20
|
* Parse raw input into a structured command.
|
|
@@ -358,7 +359,7 @@ async function handleGates(_args, dispatch, api, ctx) {
|
|
|
358
359
|
type: 'gates',
|
|
359
360
|
title: 'Quality Gates',
|
|
360
361
|
items: allItems,
|
|
361
|
-
hint: 'esc=close',
|
|
362
|
+
hint: '\u21B5=detail esc=close',
|
|
362
363
|
},
|
|
363
364
|
});
|
|
364
365
|
}
|
|
@@ -414,7 +415,7 @@ async function handleStages(_args, dispatch, api, ctx) {
|
|
|
414
415
|
type: 'stages',
|
|
415
416
|
title: 'Stages',
|
|
416
417
|
items: allItems,
|
|
417
|
-
hint: 'esc=close',
|
|
418
|
+
hint: '\u21B5=detail esc=close',
|
|
418
419
|
},
|
|
419
420
|
});
|
|
420
421
|
}
|
|
@@ -523,18 +524,26 @@ async function handleSearch(args, dispatch, api, ctx) {
|
|
|
523
524
|
? [{ name: ctx.activeEnsemble }]
|
|
524
525
|
: await api.discoverEnsembles();
|
|
525
526
|
const allResults = [];
|
|
527
|
+
const seen = new Set();
|
|
526
528
|
for (const ens of ensembles) {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
+
// Fetch from both Maestro event log (100) and ensemble chat cache (500)
|
|
530
|
+
const [messages, chatResult] = await Promise.all([
|
|
531
|
+
api.getMessages(ens.name, 100),
|
|
532
|
+
api.getEnsembleChat(ens.name, 0, 500).catch(() => ({ messages: [] })),
|
|
533
|
+
]);
|
|
534
|
+
// Merge both sources, dedup by from+to+timestamp prefix
|
|
535
|
+
const combined = [
|
|
536
|
+
...messages.map(m => ({ from: m.from, to: m.to, text: m.text, timestamp: m.timestamp })),
|
|
537
|
+
...chatResult.messages.map((m) => ({ from: m.from, to: m.to, text: m.text, timestamp: m.timestamp })),
|
|
538
|
+
];
|
|
539
|
+
for (const m of combined) {
|
|
540
|
+
const dedupKey = `${m.from}:${m.to}:${m.text.slice(0, 60)}:${m.timestamp.slice(0, 19)}`;
|
|
541
|
+
if (seen.has(dedupKey))
|
|
542
|
+
continue;
|
|
543
|
+
seen.add(dedupKey);
|
|
529
544
|
const haystack = `${m.from} ${m.to} ${m.text}`.toLowerCase();
|
|
530
545
|
if (haystack.includes(termLower)) {
|
|
531
|
-
allResults.push({
|
|
532
|
-
ensemble: ens.name,
|
|
533
|
-
from: m.from,
|
|
534
|
-
to: m.to,
|
|
535
|
-
text: m.text,
|
|
536
|
-
timestamp: m.timestamp,
|
|
537
|
-
});
|
|
546
|
+
allResults.push({ ensemble: ens.name, ...m });
|
|
538
547
|
}
|
|
539
548
|
}
|
|
540
549
|
}
|
|
@@ -543,14 +552,14 @@ async function handleSearch(args, dispatch, api, ctx) {
|
|
|
543
552
|
return;
|
|
544
553
|
}
|
|
545
554
|
const lines = [`\n ${allResults.length} result${allResults.length !== 1 ? 's' : ''} for "${term}":\n`];
|
|
546
|
-
for (const r of allResults.slice(-
|
|
555
|
+
for (const r of allResults.slice(-50)) {
|
|
547
556
|
const time = formatTimestamp(r.timestamp);
|
|
548
557
|
const text = r.text.replace(/\n/g, ' ');
|
|
549
558
|
const truncated = text.length > 70 ? text.slice(0, 67) + '...' : text;
|
|
550
559
|
lines.push(` ${time} ${r.from} \u2192 ${r.to}: ${truncated}`);
|
|
551
560
|
}
|
|
552
|
-
if (allResults.length >
|
|
553
|
-
lines.push(`\n ... and ${allResults.length -
|
|
561
|
+
if (allResults.length > 50) {
|
|
562
|
+
lines.push(`\n ... and ${allResults.length - 50} more. Narrow your search for fewer results.`);
|
|
554
563
|
}
|
|
555
564
|
dispatch({ type: 'SHOW_COMMAND_OVERLAY', title: `Search \u00B7 "${term}"`, content: lines.join('\n') });
|
|
556
565
|
}
|
|
@@ -567,7 +576,8 @@ async function handleRecruitConductor(_args, dispatch, api, ctx) {
|
|
|
567
576
|
}
|
|
568
577
|
commitStatic(dispatch, 'info', '\u2026 Recruiting conductor (tempo-conductor)...');
|
|
569
578
|
// Spawn the conductor directly via CLI — no conductor exists yet to receive commands
|
|
570
|
-
|
|
579
|
+
// shell: true resolves .cmd wrappers on Windows
|
|
580
|
+
(0, child_process_1.execFile)('claude-tempo', ['conduct', ensemble], { timeout: 30000, shell: true }, (execErr) => {
|
|
571
581
|
if (execErr) {
|
|
572
582
|
commitStatic(dispatch, 'error', `\u2717 Failed to recruit conductor: ${execErr.message || execErr}`);
|
|
573
583
|
}
|
|
@@ -585,7 +595,31 @@ async function handleLineup(args, dispatch, api, ctx) {
|
|
|
585
595
|
const subcommand = args[0].toLowerCase();
|
|
586
596
|
if (subcommand === 'load') {
|
|
587
597
|
if (args.length < 2) {
|
|
588
|
-
|
|
598
|
+
// No file arg — show available lineups
|
|
599
|
+
try {
|
|
600
|
+
const lineups = (0, saver_1.listAllLineups)();
|
|
601
|
+
if (lineups.length === 0) {
|
|
602
|
+
commitStatic(dispatch, 'info', 'No lineups available. Create one with /lineup save.');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const items = lineups.map(l => ({
|
|
606
|
+
id: l.name,
|
|
607
|
+
label: l.name,
|
|
608
|
+
sublabel: l.source === 'saved' ? 'saved' : 'shipped example',
|
|
609
|
+
}));
|
|
610
|
+
dispatch({
|
|
611
|
+
type: 'SHOW_OVERLAY',
|
|
612
|
+
overlay: {
|
|
613
|
+
type: 'lineups',
|
|
614
|
+
title: 'Available Lineups',
|
|
615
|
+
items,
|
|
616
|
+
hint: 'Usage: /lineup load <name> \u00B7 esc=close',
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
commitStatic(dispatch, 'error', 'Usage: /lineup load <name>');
|
|
622
|
+
}
|
|
589
623
|
return;
|
|
590
624
|
}
|
|
591
625
|
const filePath = args[1];
|
|
@@ -57,9 +57,11 @@ function ConversationStream({ conversation, sentMessages, contentHeight, overflo
|
|
|
57
57
|
const allConvoMsgs = [...conversation];
|
|
58
58
|
for (const m of sentMessages) {
|
|
59
59
|
const ts = new Date(m.timestamp).getTime();
|
|
60
|
+
// Strip @player prefix from sent text for comparison (server doesn't have it)
|
|
61
|
+
const sentBody = m.text.replace(/^@\S+\s+/, '');
|
|
60
62
|
const alreadyOnServer = conversation.some(c => c.direction === 'out' &&
|
|
61
63
|
Math.abs(new Date(c.timestamp).getTime() - ts) < 30000 &&
|
|
62
|
-
c.text.slice(0, 60) ===
|
|
64
|
+
c.text.slice(0, 60) === sentBody.slice(0, 60));
|
|
63
65
|
if (!alreadyOnServer) {
|
|
64
66
|
allConvoMsgs.push({ id: `local-${m.timestamp}`, from: 'you', to: m.to, text: m.text, timestamp: m.timestamp, direction: 'out' });
|
|
65
67
|
}
|