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 +2 -0
- package/dist/activities/outbox.d.ts +9 -1
- package/dist/activities/outbox.js +13 -5
- package/dist/types.d.ts +3 -1
- package/dist/utils/validation.d.ts +3 -1
- package/dist/utils/validation.js +5 -3
- package/dist/workflows/session.js +27 -1
- package/dist/workflows/signals.d.ts +1 -0
- package/package.json +1 -1
- package/workflow-bundle.js +28 -2
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<
|
|
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
|
-
|
|
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
|
|
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. */
|
package/dist/utils/validation.js
CHANGED
|
@@ -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
|
|
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>;
|