claude-tempo 0.16.2 → 0.16.3
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/cli/commands.js +30 -2
- package/dist/copilot-bridge.js +72 -36
- package/package.json +1 -1
package/dist/cli/commands.js
CHANGED
|
@@ -289,9 +289,12 @@ async function status(opts) {
|
|
|
289
289
|
const agent = s.agentType === 'copilot' ? out.dim(' [copilot]') : '';
|
|
290
290
|
const statusLabel = s.status === 'stale' ? out.yellow(' (stale)')
|
|
291
291
|
: s.status === 'pending' ? out.dim(' (pending)')
|
|
292
|
-
: ''
|
|
292
|
+
: s.status === 'blocked' ? out.yellow(' (blocked)')
|
|
293
|
+
: '';
|
|
294
|
+
// Show PID info for copilot bridge sessions
|
|
295
|
+
const pidInfo = s.agentType === 'copilot' ? getBridgePidInfo(s.name) : '';
|
|
293
296
|
const name = out.bold(s.name);
|
|
294
|
-
out.log(` ${name}${role}${statusLabel}${agent}`);
|
|
297
|
+
out.log(` ${name}${role}${statusLabel}${agent}${pidInfo}`);
|
|
295
298
|
if (s.part)
|
|
296
299
|
out.log(` ${out.dim(s.part)}`);
|
|
297
300
|
const details = [s.workDir, s.branch, s.host].filter(Boolean).join(' ');
|
|
@@ -1129,6 +1132,31 @@ async function stopByName(client, name, config, ensemble) {
|
|
|
1129
1132
|
process.exit(1);
|
|
1130
1133
|
}
|
|
1131
1134
|
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Read PID info for a copilot bridge session from its PID file.
|
|
1137
|
+
* Returns a formatted string like " (pid 12345)" or "" if no PID file found.
|
|
1138
|
+
*/
|
|
1139
|
+
function getBridgePidInfo(name) {
|
|
1140
|
+
const pidPath = (0, path_1.join)(process.cwd(), 'logs', `${name}.pid`);
|
|
1141
|
+
if (!(0, fs_1.existsSync)(pidPath))
|
|
1142
|
+
return '';
|
|
1143
|
+
try {
|
|
1144
|
+
const pid = parseInt((0, fs_1.readFileSync)(pidPath, 'utf8').trim(), 10);
|
|
1145
|
+
if (isNaN(pid))
|
|
1146
|
+
return '';
|
|
1147
|
+
// Check if process is still alive
|
|
1148
|
+
try {
|
|
1149
|
+
process.kill(pid, 0); // signal 0 = existence check, doesn't kill
|
|
1150
|
+
return out.dim(` (pid ${pid})`);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
return out.dim(` (pid ${pid}, dead)`);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
catch {
|
|
1157
|
+
return '';
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1132
1160
|
/**
|
|
1133
1161
|
* Kill a bridge process by reading its PID file from logs/.
|
|
1134
1162
|
* Cleans up the PID file after.
|
package/dist/copilot-bridge.js
CHANGED
|
@@ -84,6 +84,8 @@ const POLL_INTERVAL_MS = 2000;
|
|
|
84
84
|
const CREATE_SESSION_TIMEOUT_MS = 45_000;
|
|
85
85
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
86
86
|
const MAX_SESSION_RECREATIONS = 2;
|
|
87
|
+
/** Check workflow status every N polls (~30s at 2s interval). */
|
|
88
|
+
const WORKFLOW_STATUS_CHECK_INTERVAL = 15;
|
|
87
89
|
/** Wrap createSession with a timeout so auth/network hangs don't block forever. */
|
|
88
90
|
async function createSessionWithTimeout(copilotClient, sessionConfig, timeoutMs = CREATE_SESSION_TIMEOUT_MS) {
|
|
89
91
|
let timer;
|
|
@@ -252,6 +254,9 @@ async function main() {
|
|
|
252
254
|
catch (err) {
|
|
253
255
|
log(`Initial prompt error after ${Date.now()}ms:`, err?.message, err?.stack?.substring(0, 300));
|
|
254
256
|
}
|
|
257
|
+
// PID file paths — computed early so early-exit paths can clean up
|
|
258
|
+
const pidDir = path.join(workDir, 'logs');
|
|
259
|
+
const pidFile = path.join(pidDir, `${playerName || playerIdForWorkflow}.pid`);
|
|
255
260
|
// Wait for the MCP server's workflow to register in Temporal.
|
|
256
261
|
// We know the exact workflow ID because we pass CLAUDE_TEMPO_PLAYER_NAME to the
|
|
257
262
|
// MCP server — no need for a time-window heuristic that could misidentify workflows.
|
|
@@ -277,6 +282,11 @@ async function main() {
|
|
|
277
282
|
log(`ERROR: Workflow ${expectedWorkflowId} did not register within 30 seconds`);
|
|
278
283
|
await session.disconnect();
|
|
279
284
|
await copilotClient.stop();
|
|
285
|
+
// Clean up PID file to avoid stale entries in `claude-tempo status`
|
|
286
|
+
try {
|
|
287
|
+
fs.unlinkSync(pidFile);
|
|
288
|
+
}
|
|
289
|
+
catch { /* may not exist yet */ }
|
|
280
290
|
process.exit(1);
|
|
281
291
|
}
|
|
282
292
|
log(`Workflow ready: ${expectedWorkflowId}`);
|
|
@@ -288,6 +298,15 @@ async function main() {
|
|
|
288
298
|
log(`set_name completed in ${Date.now() - t0}ms`);
|
|
289
299
|
}
|
|
290
300
|
const MAESTRO_ACK = '\n\n[IMPORTANT: This message is from a human (Maestro). Immediately cue the sender back with a brief acknowledgment and your planned next step before doing the work.]';
|
|
301
|
+
// Write PID file so callers can find/kill orphaned bridge processes
|
|
302
|
+
try {
|
|
303
|
+
fs.mkdirSync(pidDir, { recursive: true });
|
|
304
|
+
fs.writeFileSync(pidFile, String(process.pid));
|
|
305
|
+
log(`PID file written: ${pidFile}`);
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
log(`Warning: could not write PID file: ${err?.message}`);
|
|
309
|
+
}
|
|
291
310
|
// Start message poller — inject messages into the Copilot session.
|
|
292
311
|
// Tracks consecutive failures and attempts session recreation before giving up.
|
|
293
312
|
let polling = true;
|
|
@@ -295,6 +314,36 @@ async function main() {
|
|
|
295
314
|
let pollCount = 0;
|
|
296
315
|
let consecutiveFailures = 0;
|
|
297
316
|
let sessionRecreations = 0;
|
|
317
|
+
// interval declared here, assigned after poll is defined
|
|
318
|
+
let interval;
|
|
319
|
+
// Shared cleanup — disconnects session, removes PID file, stops client.
|
|
320
|
+
// `signalTermination` controls whether we also signal the workflow to terminate
|
|
321
|
+
// (skip if the workflow is already gone).
|
|
322
|
+
let shuttingDown = false;
|
|
323
|
+
const cleanup = async (signalTermination) => {
|
|
324
|
+
if (shuttingDown)
|
|
325
|
+
return;
|
|
326
|
+
shuttingDown = true;
|
|
327
|
+
polling = false;
|
|
328
|
+
clearInterval(interval);
|
|
329
|
+
if (signalTermination) {
|
|
330
|
+
try {
|
|
331
|
+
await handle.signal('updateMetadata', { status: 'terminated', terminatedBy: 'system' });
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// workflow may already be gone
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
await session.disconnect();
|
|
339
|
+
}
|
|
340
|
+
catch { /* already disconnected */ }
|
|
341
|
+
try {
|
|
342
|
+
fs.unlinkSync(pidFile);
|
|
343
|
+
}
|
|
344
|
+
catch { /* already gone */ }
|
|
345
|
+
await copilotClient.stop();
|
|
346
|
+
};
|
|
298
347
|
/** Attempt to recreate the Copilot session after repeated failures. */
|
|
299
348
|
async function recreateSession() {
|
|
300
349
|
sessionRecreations++;
|
|
@@ -326,6 +375,24 @@ async function main() {
|
|
|
326
375
|
const silenceSec = ((Date.now() - lastEventTime) / 1000).toFixed(0);
|
|
327
376
|
log(`[health] poll #${pollCount}, sessionAlive=${sessionAlive}, lastEvent=${lastEventType} ${silenceSec}s ago`);
|
|
328
377
|
}
|
|
378
|
+
// Periodic workflow status check — detect external termination/completion
|
|
379
|
+
if (pollCount % WORKFLOW_STATUS_CHECK_INTERVAL === 0) {
|
|
380
|
+
try {
|
|
381
|
+
const desc = await handle.describe();
|
|
382
|
+
const wfStatus = desc.status.name;
|
|
383
|
+
if (wfStatus !== 'RUNNING') {
|
|
384
|
+
log(`Workflow status is ${wfStatus} — exiting cleanly`);
|
|
385
|
+
await cleanup(false); // workflow already gone, don't signal
|
|
386
|
+
process.exit(0);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
catch (err) {
|
|
390
|
+
// If we can't describe (e.g., workflow not found), it was likely terminated
|
|
391
|
+
log(`Workflow describe failed: ${err?.message} — treating as terminated`);
|
|
392
|
+
await cleanup(false);
|
|
393
|
+
process.exit(0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
329
396
|
try {
|
|
330
397
|
const messages = await handle.query('pendingMessages');
|
|
331
398
|
if (messages.length === 0)
|
|
@@ -365,49 +432,18 @@ async function main() {
|
|
|
365
432
|
const recovered = await recreateSession();
|
|
366
433
|
if (!recovered) {
|
|
367
434
|
log('ERROR: Session recovery failed. Shutting down bridge.');
|
|
368
|
-
|
|
369
|
-
clearInterval(interval);
|
|
435
|
+
await cleanup(true);
|
|
370
436
|
process.exit(2);
|
|
371
437
|
}
|
|
372
438
|
}
|
|
373
439
|
}
|
|
374
440
|
};
|
|
375
|
-
|
|
441
|
+
interval = setInterval(poll, POLL_INTERVAL_MS);
|
|
376
442
|
log('Message poller started. Bridge is running.');
|
|
377
|
-
//
|
|
378
|
-
const pidDir = path.join(workDir, 'logs');
|
|
379
|
-
const pidFile = path.join(pidDir, `${playerName || playerIdForWorkflow}.pid`);
|
|
380
|
-
try {
|
|
381
|
-
fs.mkdirSync(pidDir, { recursive: true });
|
|
382
|
-
fs.writeFileSync(pidFile, String(process.pid));
|
|
383
|
-
log(`PID file written: ${pidFile}`);
|
|
384
|
-
}
|
|
385
|
-
catch (err) {
|
|
386
|
-
log(`Warning: could not write PID file: ${err?.message}`);
|
|
387
|
-
}
|
|
388
|
-
// Graceful shutdown
|
|
443
|
+
// Graceful shutdown on SIGINT/SIGTERM — signal the workflow before exiting
|
|
389
444
|
const shutdown = async () => {
|
|
390
|
-
log('Shutting down...');
|
|
391
|
-
|
|
392
|
-
clearInterval(interval);
|
|
393
|
-
try {
|
|
394
|
-
await handle.signal('updateMetadata', { status: 'terminated', terminatedBy: 'system' });
|
|
395
|
-
}
|
|
396
|
-
catch {
|
|
397
|
-
// workflow may already be gone
|
|
398
|
-
}
|
|
399
|
-
try {
|
|
400
|
-
await session.disconnect();
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
// session may already be disconnected
|
|
404
|
-
}
|
|
405
|
-
// Clean up PID file
|
|
406
|
-
try {
|
|
407
|
-
fs.unlinkSync(pidFile);
|
|
408
|
-
}
|
|
409
|
-
catch { /* may already be gone */ }
|
|
410
|
-
await copilotClient.stop();
|
|
445
|
+
log('Shutting down (signal received)...');
|
|
446
|
+
await cleanup(true);
|
|
411
447
|
process.exit(0);
|
|
412
448
|
};
|
|
413
449
|
process.on('SIGINT', shutdown);
|