claude-tempo 0.15.0 → 0.16.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/README.md CHANGED
@@ -478,6 +478,8 @@ claude-tempo <command> [options]
478
478
  | `stop [ensemble]` | Stop sessions (`-n <name>` for one, `--all` for everything) |
479
479
  | `init` | Register claude-tempo MCP server globally (`--project` for per-directory) |
480
480
  | `preflight` | Run environment checks |
481
+ | `broadcast <msg>` | Send a message to all active players. Use `--type` to filter by player type, `--include-stale` to include stale sessions. |
482
+ | `encore <name>` | Revive a stale player session by name. Use `--host` to target a remote machine. |
481
483
  | `ensemble <sub>` | Manage saved lineups (`save`, `list`, `show`) |
482
484
  | `agent-types <sub>` | Manage player types (`list`, `show <name>`, `init`) |
483
485
  | `version` | Print the installed version |
@@ -46,6 +46,8 @@ export interface SpawnProcessInput {
46
46
  nativeResolvable?: boolean;
47
47
  /** When true, use --resume instead of -n (reconnect to existing session). */
48
48
  resume?: boolean;
49
+ /** Claude Code session UUID for --session-id (new sessions) or --resume (encore). */
50
+ claudeSessionId?: string;
49
51
  /** Tool restrictions from the agent definition frontmatter. */
50
52
  allowedTools?: string[];
51
53
  }
@@ -64,6 +66,8 @@ export interface EncoreResult {
64
66
  agentDefinitionPath?: string;
65
67
  nativeResolvable?: boolean;
66
68
  allowedTools?: string[];
69
+ /** Claude Code session UUID for deterministic --resume. */
70
+ claudeSessionId?: string;
67
71
  temporalAddress: string;
68
72
  temporalNamespace: string;
69
73
  }
@@ -71,11 +75,15 @@ export interface OutboxActivityResult {
71
75
  success: boolean;
72
76
  error?: string;
73
77
  }
78
+ export interface RecruitResult extends OutboxActivityResult {
79
+ /** Claude Code session UUID assigned at recruit time. */
80
+ claudeSessionId?: string;
81
+ }
74
82
  export interface OutboxActivities {
75
83
  deliverCue(input: DeliverCueInput): Promise<OutboxActivityResult>;
76
84
  deliverReport(input: DeliverReportInput): Promise<OutboxActivityResult>;
77
85
  terminateSession(input: TerminateSessionInput): Promise<OutboxActivityResult>;
78
- startRecruitedSession(input: StartRecruitedSessionInput): Promise<OutboxActivityResult>;
86
+ startRecruitedSession(input: StartRecruitedSessionInput): Promise<RecruitResult>;
79
87
  spawnProcess(input: SpawnProcessInput): Promise<OutboxActivityResult>;
80
88
  performEncore(input: PerformEncoreInput): Promise<EncoreResult>;
81
89
  }
@@ -98,6 +98,8 @@ function createOutboxActivities(client, config) {
98
98
  ? (0, config_1.conductorWorkflowId)(ensemble)
99
99
  : (0, config_1.sessionWorkflowId)(ensemble, targetName);
100
100
  const { gitRoot, gitBranch } = (0, git_info_1.getGitInfo)(workDir);
101
+ // Generate a UUID for the Claude Code session — used for deterministic --resume on encore
102
+ const claudeSessionId = crypto.randomUUID();
101
103
  const sessionInput = {
102
104
  metadata: {
103
105
  playerId: targetName,
@@ -109,6 +111,7 @@ function createOutboxActivities(client, config) {
109
111
  isConductor,
110
112
  agentType: agent,
111
113
  status: 'pending',
114
+ claudeSessionId,
112
115
  ...(agentDefinition ? { playerType: agentDefinition } : {}),
113
116
  ...(agentDefinitionDescription ? { playerTypeDescription: agentDefinitionDescription } : {}),
114
117
  recruitedBy: fromPlayerId,
@@ -137,15 +140,15 @@ function createOutboxActivities(client, config) {
137
140
  ClaudeTempoPlayerId: [targetName],
138
141
  },
139
142
  });
140
- log(`Pre-created workflow ${workflowId} for recruit "${targetName}"`);
141
- return { success: true };
143
+ log(`Pre-created workflow ${workflowId} for recruit "${targetName}" (sessionId=${claudeSessionId})`);
144
+ return { success: true, claudeSessionId };
142
145
  }
143
146
  catch (err) {
144
147
  throw activity_1.ApplicationFailure.nonRetryable(`Failed to start recruited session "${targetName}": ${err instanceof Error ? err.message : String(err)}`);
145
148
  }
146
149
  },
147
150
  async spawnProcess(input) {
148
- const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, allowedTools } = input;
151
+ const { targetName, workDir, isConductor, agent, systemPrompt, ensemble, temporalAddress, temporalNamespace, agentDefinition, agentDefinitionPath, nativeResolvable, resume, claudeSessionId, allowedTools } = input;
149
152
  // Read secrets from the worker's config closure — never from workflow state
150
153
  const { temporalApiKey, temporalTlsCertPath, temporalTlsKeyPath } = config;
151
154
  try {
@@ -178,8 +181,12 @@ function createOutboxActivities(client, config) {
178
181
  else if (systemPrompt) {
179
182
  agentFlags = ['--system-prompt', systemPrompt];
180
183
  }
181
- // Use --resume for encore (reconnect to existing session) or -n for new sessions
182
- const nameArgs = resume ? ['--resume', targetName] : ['-n', targetName];
184
+ // Use --resume for encore (reconnect to existing session) or -n for new sessions.
185
+ // For encore: use UUID for deterministic --resume (no interactive picker).
186
+ // For new sessions: use --session-id to track the UUID for future encores.
187
+ const nameArgs = resume
188
+ ? ['--resume', claudeSessionId || targetName]
189
+ : ['-n', targetName, ...(claudeSessionId ? ['--session-id', claudeSessionId] : [])];
183
190
  // Build --allowedTools flag from agent definition frontmatter
184
191
  const allowedToolsFlags = allowedTools && allowedTools.length > 0
185
192
  ? ['--allowedTools', ...allowedTools]
@@ -280,6 +287,7 @@ function createOutboxActivities(client, config) {
280
287
  agentDefinitionPath,
281
288
  nativeResolvable,
282
289
  allowedTools,
290
+ claudeSessionId: metadata.claudeSessionId || undefined,
283
291
  temporalAddress: config.temporalAddress,
284
292
  temporalNamespace: config.temporalNamespace,
285
293
  };
package/dist/types.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type AgentType = 'claude' | 'copilot';
2
- export type SessionStatus = 'active' | 'stale' | 'pending' | 'terminated';
2
+ export type SessionStatus = 'active' | 'stale' | 'pending' | 'terminated' | 'blocked';
3
3
  export interface SessionMetadata {
4
4
  playerId: string;
5
5
  ensemble: string;
@@ -18,6 +18,8 @@ export interface SessionMetadata {
18
18
  recruitedBy?: string;
19
19
  /** Worktree path if this session was spawned in an isolated worktree. */
20
20
  worktreePath?: string;
21
+ /** Claude Code session UUID — used for deterministic --resume on encore. */
22
+ claudeSessionId?: string;
21
23
  }
22
24
  export interface AgentTypeInfo {
23
25
  name: string;
@@ -32,13 +32,15 @@ export declare const GATE_CRITERION_TEXT_MAX = 512;
32
32
  export declare const GATE_NOTES_MAX = 1024;
33
33
  /** Timeout for npm install in worktrees (60s). */
34
34
  export declare const WORKTREE_INSTALL_TIMEOUT = 60000;
35
+ /** Window for blocked session detection (5 minutes). */
36
+ export declare const BLOCKED_WINDOW_MS: number;
35
37
  /** Default number of recent messages to include as context in an encore. */
36
38
  export declare const ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
37
39
  /** Maximum length for message preview truncation. */
38
40
  export declare const PREVIEW_MAX_LENGTH = 200;
39
41
  /**
40
42
  * Whether a session should be included in a broadcast based on its status.
41
- * Always excludes pending and terminated. Excludes stale unless includeStale is true.
43
+ * Always excludes pending, terminated, and blocked. Excludes stale unless includeStale is true.
42
44
  */
43
45
  export declare function shouldIncludeInBroadcast(status: string | undefined, includeStale: boolean): boolean;
44
46
  /** Validate a player name string. Returns an error message or null if valid. */
@@ -4,7 +4,7 @@
4
4
  * Used by MCP tool Zod schemas and config validation.
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
- exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
7
+ exports.PREVIEW_MAX_LENGTH = exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = exports.BLOCKED_WINDOW_MS = exports.WORKTREE_INSTALL_TIMEOUT = exports.GATE_NOTES_MAX = exports.GATE_CRITERION_TEXT_MAX = exports.GATE_CRITERIA_MAX = exports.GATE_TASK_MAX = exports.CRON_EXPRESSION_MAX = exports.SCHEDULE_MESSAGE_MAX = exports.SCHEDULE_NAME_MAX = exports.PATH_MAX = exports.PART_MAX = exports.MESSAGE_MAX = exports.ENSEMBLE_NAME_REGEX = exports.PLAYER_NAME_MAX = exports.PLAYER_NAME_REGEX = void 0;
8
8
  exports.shouldIncludeInBroadcast = shouldIncludeInBroadcast;
9
9
  exports.validatePlayerName = validatePlayerName;
10
10
  exports.validateEnsembleName = validateEnsembleName;
@@ -38,17 +38,19 @@ exports.GATE_CRITERION_TEXT_MAX = 512;
38
38
  exports.GATE_NOTES_MAX = 1024;
39
39
  /** Timeout for npm install in worktrees (60s). */
40
40
  exports.WORKTREE_INSTALL_TIMEOUT = 60000;
41
+ /** Window for blocked session detection (5 minutes). */
42
+ exports.BLOCKED_WINDOW_MS = 5 * 60 * 1000;
41
43
  /** Default number of recent messages to include as context in an encore. */
42
44
  exports.ENCORE_DEFAULT_CONTEXT_MESSAGES = 10;
43
45
  /** Maximum length for message preview truncation. */
44
46
  exports.PREVIEW_MAX_LENGTH = 200;
45
47
  /**
46
48
  * Whether a session should be included in a broadcast based on its status.
47
- * Always excludes pending and terminated. Excludes stale unless includeStale is true.
49
+ * Always excludes pending, terminated, and blocked. Excludes stale unless includeStale is true.
48
50
  */
49
51
  function shouldIncludeInBroadcast(status, includeStale) {
50
52
  const s = status || 'active';
51
- if (s === 'pending' || s === 'terminated')
53
+ if (s === 'pending' || s === 'terminated' || s === 'blocked')
52
54
  return false;
53
55
  if (s === 'stale' && !includeStale)
54
56
  return false;
@@ -26,6 +26,7 @@ async function claudeSessionWorkflow(input) {
26
26
  (0, workflow_1.patched)('v0.11-check-and-set-status');
27
27
  (0, workflow_1.patched)('v0.13-quality-gates');
28
28
  (0, workflow_1.patched)('v0.14-worktrees');
29
+ (0, workflow_1.patched)('v0.15-blocked-detection');
29
30
  // Ensure search attributes are always current — critical when reconnecting
30
31
  // via WorkflowIdConflictPolicy.USE_EXISTING, which skips the attributes
31
32
  // passed to client.workflow.start().
@@ -43,6 +44,7 @@ async function claudeSessionWorkflow(input) {
43
44
  const sentMessages = input.sentMessages ?? [];
44
45
  const outbox = input.outbox ?? [];
45
46
  let lastActivityTime = Date.now();
47
+ let lastOutboundTime = Date.now();
46
48
  // ── Outbox Update + Query Handlers ──
47
49
  (0, workflow_1.setHandler)(signals_1.submitOutboxUpdate, (entryInput) => {
48
50
  const entry = {
@@ -66,6 +68,12 @@ async function claudeSessionWorkflow(input) {
66
68
  sentMessages.push({ id: entry.id, to: entry.targetPlayerId, text: '[encore requested]', timestamp: entry.createdAt });
67
69
  }
68
70
  lastActivityTime = Date.now();
71
+ lastOutboundTime = Date.now();
72
+ // Auto-recover from blocked when player sends outbound
73
+ if (input.metadata.status === 'blocked') {
74
+ input.metadata.status = 'active';
75
+ (0, workflow_1.upsertSearchAttributes)({ ClaudeTempoStatus: ['active'] });
76
+ }
69
77
  return entry.id;
70
78
  }, {
71
79
  validator: (entry) => {
@@ -89,6 +97,7 @@ async function claudeSessionWorkflow(input) {
89
97
  (0, workflow_1.setHandler)(signals_1.setPartSignal, (newPart) => {
90
98
  part = newPart;
91
99
  lastActivityTime = Date.now();
100
+ lastOutboundTime = Date.now();
92
101
  });
93
102
  (0, workflow_1.setHandler)(signals_1.setNameSignal, (newName) => {
94
103
  input.metadata.playerId = newName;
@@ -117,6 +126,8 @@ async function claudeSessionWorkflow(input) {
117
126
  input.metadata.playerTypeDescription = update.playerTypeDescription;
118
127
  if (update.worktreePath != null)
119
128
  input.metadata.worktreePath = update.worktreePath;
129
+ if (update.claudeSessionId != null)
130
+ input.metadata.claudeSessionId = update.claudeSessionId;
120
131
  if (update.status != null) {
121
132
  input.metadata.status = update.status;
122
133
  // Re-enable stale detection only when explicitly requested (server.ts sets this)
@@ -186,6 +197,9 @@ async function claudeSessionWorkflow(input) {
186
197
  timestamp: new Date().toISOString(),
187
198
  delivered: false,
188
199
  });
200
+ // Command processing counts as implicit outbound for blocked detection
201
+ lastActivityTime = Date.now();
202
+ lastOutboundTime = Date.now();
189
203
  });
190
204
  (0, workflow_1.setHandler)(signals_1.playerReportSignal, (report) => {
191
205
  reportHistory.push({
@@ -313,7 +327,7 @@ async function claudeSessionWorkflow(input) {
313
327
  break;
314
328
  case 'recruit': {
315
329
  const tc = input.temporalConfig;
316
- await startRecruitedSession({
330
+ const recruitResult = await startRecruitedSession({
317
331
  ensemble: input.metadata.ensemble,
318
332
  targetName: entry.targetName,
319
333
  workDir: entry.workDir,
@@ -341,6 +355,7 @@ async function claudeSessionWorkflow(input) {
341
355
  agentDefinition: entry.agentDefinition,
342
356
  agentDefinitionPath: entry.agentDefinitionPath,
343
357
  nativeResolvable: entry.nativeResolvable,
358
+ claudeSessionId: recruitResult.claudeSessionId,
344
359
  allowedTools: entry.allowedTools,
345
360
  });
346
361
  break;
@@ -367,6 +382,7 @@ async function claudeSessionWorkflow(input) {
367
382
  agentDefinitionPath: encoreResult.agentDefinitionPath,
368
383
  nativeResolvable: encoreResult.nativeResolvable,
369
384
  allowedTools: encoreResult.allowedTools,
385
+ claudeSessionId: encoreResult.claudeSessionId,
370
386
  resume: true,
371
387
  });
372
388
  }
@@ -411,6 +427,16 @@ async function claudeSessionWorkflow(input) {
411
427
  input.metadata.status = 'stale';
412
428
  (0, workflow_1.upsertSearchAttributes)({ ClaudeTempoStatus: ['stale'] });
413
429
  }
430
+ // Detect blocked session: active session with messages delivered but no outbound
431
+ // activity for 5+ minutes. The session is alive but may be stuck or spinning.
432
+ const BLOCKED_WINDOW_MS = 5 * 60 * 1000;
433
+ const hasDeliveredMessages = messages.some((m) => m.delivered);
434
+ if (input.metadata.status === 'active' &&
435
+ hasDeliveredMessages &&
436
+ now - lastOutboundTime > BLOCKED_WINDOW_MS) {
437
+ input.metadata.status = 'blocked';
438
+ (0, workflow_1.upsertSearchAttributes)({ ClaudeTempoStatus: ['blocked'] });
439
+ }
414
440
  // Heartbeat: if no activity for 1 hour, inject a probe message.
415
441
  // If the session is alive, it will consume and deliver it.
416
442
  // If dead, stale detection will mark it on the next loop iteration.
@@ -22,6 +22,7 @@ export declare const updateMetadataSignal: import("@temporalio/workflow").Signal
22
22
  playerType?: string;
23
23
  playerTypeDescription?: string;
24
24
  worktreePath?: string;
25
+ claudeSessionId?: string;
25
26
  }], string>;
26
27
  export declare const getPartQuery: import("@temporalio/workflow").QueryDefinition<string, [], string>;
27
28
  export declare const getMetadataQuery: import("@temporalio/workflow").QueryDefinition<SessionMetadata, [], string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-tempo",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "MCP server for multi-session Claude Code coordination via Temporal",
5
5
  "keywords": [
6
6
  "mcp",