praxis-agent 0.2.0 → 0.4.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/README.md +19 -4
- package/dist/application/session-service.d.ts +55 -0
- package/dist/application/session-service.d.ts.map +1 -1
- package/dist/application/session-service.js +939 -39
- package/dist/application/session-service.js.map +1 -1
- package/dist/cli/interactive.d.ts +72 -8
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +2809 -192
- package/dist/cli/interactive.js.map +1 -1
- package/dist/cli/protocol.d.ts +1 -0
- package/dist/cli/protocol.d.ts.map +1 -1
- package/dist/cli/protocol.js.map +1 -1
- package/dist/cli/tui/claude-style.d.ts +157 -3
- package/dist/cli/tui/claude-style.d.ts.map +1 -1
- package/dist/cli/tui/claude-style.js +403 -16
- package/dist/cli/tui/claude-style.js.map +1 -1
- package/dist/cli/tui/clipboard.d.ts +27 -0
- package/dist/cli/tui/clipboard.d.ts.map +1 -0
- package/dist/cli/tui/clipboard.js +165 -0
- package/dist/cli/tui/clipboard.js.map +1 -0
- package/dist/cli/tui/composer-editor.d.ts +19 -0
- package/dist/cli/tui/composer-editor.d.ts.map +1 -0
- package/dist/cli/tui/composer-editor.js +99 -0
- package/dist/cli/tui/composer-editor.js.map +1 -0
- package/dist/cli/tui/composer-images.d.ts +7 -0
- package/dist/cli/tui/composer-images.d.ts.map +1 -0
- package/dist/cli/tui/composer-images.js +48 -0
- package/dist/cli/tui/composer-images.js.map +1 -0
- package/dist/cli/tui/conversation-export.d.ts +6 -0
- package/dist/cli/tui/conversation-export.d.ts.map +1 -0
- package/dist/cli/tui/conversation-export.js +76 -0
- package/dist/cli/tui/conversation-export.js.map +1 -0
- package/dist/cli/tui/external-editor.d.ts +21 -0
- package/dist/cli/tui/external-editor.d.ts.map +1 -0
- package/dist/cli/tui/external-editor.js +144 -0
- package/dist/cli/tui/external-editor.js.map +1 -0
- package/dist/cli/tui/file-picker.d.ts +33 -0
- package/dist/cli/tui/file-picker.d.ts.map +1 -0
- package/dist/cli/tui/file-picker.js +117 -0
- package/dist/cli/tui/file-picker.js.map +1 -0
- package/dist/cli/tui/git-diff.d.ts +14 -0
- package/dist/cli/tui/git-diff.d.ts.map +1 -0
- package/dist/cli/tui/git-diff.js +55 -0
- package/dist/cli/tui/git-diff.js.map +1 -0
- package/dist/cli/tui/keybindings.d.ts +14 -0
- package/dist/cli/tui/keybindings.d.ts.map +1 -0
- package/dist/cli/tui/keybindings.js +430 -0
- package/dist/cli/tui/keybindings.js.map +1 -0
- package/dist/cli/tui/permission-settings.d.ts +18 -0
- package/dist/cli/tui/permission-settings.d.ts.map +1 -0
- package/dist/cli/tui/permission-settings.js +109 -0
- package/dist/cli/tui/permission-settings.js.map +1 -0
- package/dist/cli/tui/slash-commands.d.ts +11 -0
- package/dist/cli/tui/slash-commands.d.ts.map +1 -0
- package/dist/cli/tui/slash-commands.js +188 -0
- package/dist/cli/tui/slash-commands.js.map +1 -0
- package/dist/cli/tui/terminal-suspend.d.ts +5 -0
- package/dist/cli/tui/terminal-suspend.d.ts.map +1 -0
- package/dist/cli/tui/terminal-suspend.js +9 -0
- package/dist/cli/tui/terminal-suspend.js.map +1 -0
- package/dist/cli/tui/workspace-directories.d.ts +3 -0
- package/dist/cli/tui/workspace-directories.d.ts.map +1 -0
- package/dist/cli/tui/workspace-directories.js +55 -0
- package/dist/cli/tui/workspace-directories.js.map +1 -0
- package/dist/cli.d.ts +26 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +48 -5
- package/dist/cli.js.map +1 -1
- package/dist/compatibility/claude/compaction.d.ts +8 -0
- package/dist/compatibility/claude/compaction.d.ts.map +1 -1
- package/dist/compatibility/claude/compaction.js +45 -8
- package/dist/compatibility/claude/compaction.js.map +1 -1
- package/dist/compatibility/claude/fork.d.ts.map +1 -1
- package/dist/compatibility/claude/fork.js +24 -6
- package/dist/compatibility/claude/fork.js.map +1 -1
- package/dist/compatibility/claude/history.d.ts.map +1 -1
- package/dist/compatibility/claude/history.js +41 -1
- package/dist/compatibility/claude/history.js.map +1 -1
- package/dist/compatibility/claude/projection.d.ts +29 -1
- package/dist/compatibility/claude/projection.d.ts.map +1 -1
- package/dist/compatibility/claude/projection.js +132 -2
- package/dist/compatibility/claude/projection.js.map +1 -1
- package/dist/compatibility/claude/schema.d.ts.map +1 -1
- package/dist/compatibility/claude/schema.js +45 -8
- package/dist/compatibility/claude/schema.js.map +1 -1
- package/dist/compatibility/claude/translation.d.ts +7 -0
- package/dist/compatibility/claude/translation.d.ts.map +1 -1
- package/dist/compatibility/claude/translation.js +20 -0
- package/dist/compatibility/claude/translation.js.map +1 -1
- package/dist/core/runtime.d.ts +19 -0
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +30 -11
- package/dist/core/runtime.js.map +1 -1
- package/dist/extensions/claude-extensions.d.ts +12 -0
- package/dist/extensions/claude-extensions.d.ts.map +1 -1
- package/dist/extensions/claude-extensions.js +49 -0
- package/dist/extensions/claude-extensions.js.map +1 -1
- package/dist/hooks/claude-hook-tools.js +2 -2
- package/dist/hooks/claude-hook-tools.js.map +1 -1
- package/dist/persistence/claude-transcript-store.d.ts.map +1 -1
- package/dist/persistence/claude-transcript-store.js +12 -5
- package/dist/persistence/claude-transcript-store.js.map +1 -1
- package/dist/tools/claude-interactive-tools.d.ts +1 -0
- package/dist/tools/claude-interactive-tools.d.ts.map +1 -1
- package/dist/tools/claude-interactive-tools.js +12 -0
- package/dist/tools/claude-interactive-tools.js.map +1 -1
- package/dist/tools/local-tools.d.ts.map +1 -1
- package/dist/tools/local-tools.js +11 -0
- package/dist/tools/local-tools.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { lstat, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { appendFile, copyFile, link, lstat, mkdir, readdir, realpath, stat, unlink, } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
3
4
|
import { basename, extname, isAbsolute, join, relative } from 'node:path';
|
|
4
5
|
import { createClaudeCompactEntries, formatClaudeCompactSummary, getCumulativeDroppedTokens, } from '../compatibility/claude/compaction.js';
|
|
5
6
|
import { isClaudeSessionId, resolveClaudePaths, resolveClaudeScheduledTaskFile, } from '../compatibility/claude/paths.js';
|
|
6
7
|
import { downloadClaudeFileResources, } from '../compatibility/claude/file-resources.js';
|
|
7
8
|
import { createClaudeNativeFork } from '../compatibility/claude/fork.js';
|
|
8
|
-
import { selectClaudeTranscriptAtMessage } from '../compatibility/claude/history.js';
|
|
9
|
+
import { selectClaudeActiveTranscript, selectClaudeTranscriptAtMessage, } from '../compatibility/claude/history.js';
|
|
9
10
|
import { ClaudeFileHistory } from '../compatibility/claude/file-history.js';
|
|
10
11
|
import { getClaudePrLink } from '../compatibility/claude/pr-links.js';
|
|
11
|
-
import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
|
|
12
|
+
import { getClaudeAgentSetting, getClaudeLastPrompt, projectClaudeDisplayTranscript, projectClaudeModelMessages, } from '../compatibility/claude/projection.js';
|
|
12
13
|
import { selectClaudeSchemaAdapter, } from '../compatibility/claude/schema.js';
|
|
13
14
|
import { findUnresolvedClaudeToolCalls } from '../compatibility/claude/tool-links.js';
|
|
14
15
|
import { createClaudeAgentSettingEntry, createClaudeHookAttachmentEntries, createClaudeLastPromptEntry, createClaudeRuleAttachmentEntry, translateProviderEvents, } from '../compatibility/claude/translation.js';
|
|
15
16
|
import { AgentRunCancelledError, AgentRuntime, } from '../core/runtime.js';
|
|
16
17
|
import { usageCostUsd } from '../core/usage.js';
|
|
17
|
-
import { ContextBudget } from '../core/context-budget.js';
|
|
18
|
+
import { ContextBudget, estimateModelRequestTokens, } from '../core/context-budget.js';
|
|
18
19
|
import { injectFirstUserMessageContext, } from '../core/context.js';
|
|
19
20
|
import { ClaudeHookToolCoordinator } from '../hooks/claude-hook-tools.js';
|
|
20
21
|
import { ClaudeTranscriptStore, } from '../persistence/claude-transcript-store.js';
|
|
@@ -62,6 +63,8 @@ Your job is to predict what THEY would type - not what you think they should do.
|
|
|
62
63
|
|
|
63
64
|
Reply with ONLY the suggestion, no quotes or explanation.
|
|
64
65
|
Use 2-12 words. Do not ask a question, evaluate the prior response, introduce a new idea, or use a Claude voice. If the topic is unsafe or sensitive, reply with an empty string.`;
|
|
66
|
+
const SESSION_NAME_INSTRUCTION = `Generate a concise name for this coding session based on the conversation.
|
|
67
|
+
Use 2-5 short words in kebab-case. Reply with ONLY the name, without quotes, punctuation, or explanation.`;
|
|
65
68
|
function validPromptSuggestion(value) {
|
|
66
69
|
const suggestion = value.trim();
|
|
67
70
|
if (!suggestion)
|
|
@@ -73,6 +76,12 @@ function validPromptSuggestion(value) {
|
|
|
73
76
|
return null;
|
|
74
77
|
return suggestion;
|
|
75
78
|
}
|
|
79
|
+
function validSessionName(value) {
|
|
80
|
+
const name = value.trim().split(/\r?\n/u)[0]?.trim() ?? '';
|
|
81
|
+
return /^[\p{L}\p{N}]+(?:-[\p{L}\p{N}]+){1,4}$/u.test(name)
|
|
82
|
+
? name.toLocaleLowerCase()
|
|
83
|
+
: null;
|
|
84
|
+
}
|
|
76
85
|
export class ClaudeSessionService {
|
|
77
86
|
options;
|
|
78
87
|
schema;
|
|
@@ -82,9 +91,13 @@ export class ClaudeSessionService {
|
|
|
82
91
|
worktreeManager;
|
|
83
92
|
sessionCwds = new Map();
|
|
84
93
|
hostedSubagents = new Set();
|
|
94
|
+
hostedSubagentsByRegistry = new WeakMap();
|
|
95
|
+
backgroundNotificationWrites = new Map();
|
|
85
96
|
downloadedFileResourceSessions = new Set();
|
|
97
|
+
runtimeCwd;
|
|
86
98
|
constructor(options) {
|
|
87
99
|
this.options = options;
|
|
100
|
+
this.runtimeCwd = options.workspace?.cwd() ?? options.cwd;
|
|
88
101
|
this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
|
|
89
102
|
this.scheduledPrompts =
|
|
90
103
|
options.tools && (options.scheduledToolNames?.length ?? 0) > 0
|
|
@@ -116,6 +129,8 @@ export class ClaudeSessionService {
|
|
|
116
129
|
async close() {
|
|
117
130
|
this.scheduledPrompts?.close();
|
|
118
131
|
await Promise.all([...this.hostedSubagents].map((executor) => executor.close()));
|
|
132
|
+
await Promise.resolve();
|
|
133
|
+
await Promise.all([...this.backgroundNotificationWrites.values()]);
|
|
119
134
|
this.hostedSubagents.clear();
|
|
120
135
|
await this.workflowManager?.close();
|
|
121
136
|
}
|
|
@@ -282,7 +297,7 @@ export class ClaudeSessionService {
|
|
|
282
297
|
'Monitor',
|
|
283
298
|
'PushNotification',
|
|
284
299
|
];
|
|
285
|
-
|
|
300
|
+
const hostedRegistry = {
|
|
286
301
|
definitions: () => {
|
|
287
302
|
const definitions = interactiveRegistry.definitions();
|
|
288
303
|
return [...definitions].sort((left, right) => {
|
|
@@ -295,15 +310,140 @@ export class ClaudeSessionService {
|
|
|
295
310
|
prepare: (call, context) => interactiveRegistry.prepare(call, context),
|
|
296
311
|
execute: (call, context) => interactiveRegistry.execute(call, context),
|
|
297
312
|
};
|
|
313
|
+
if (subagentExecutor) {
|
|
314
|
+
this.hostedSubagentsByRegistry.set(hostedRegistry, subagentExecutor);
|
|
315
|
+
}
|
|
316
|
+
return hostedRegistry;
|
|
298
317
|
}
|
|
299
318
|
async run(prompt, signal, sessionId = randomUUID(), name, images, documents) {
|
|
300
319
|
this.worktreeManager?.bindSession(sessionId);
|
|
301
320
|
return this.executeTurn(sessionId, prompt, false, signal, name, images, documents);
|
|
302
321
|
}
|
|
322
|
+
async runShell(command, signal, sessionId = randomUUID(), name) {
|
|
323
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
324
|
+
return this.executeTurn(sessionId, `! ${command}`, false, signal, name, [], [], undefined, command);
|
|
325
|
+
}
|
|
303
326
|
async resume(sessionId, prompt, signal, name, images, documents, resumeSessionAt) {
|
|
304
327
|
this.worktreeManager?.bindSession(sessionId);
|
|
305
328
|
return this.executeTurn(sessionId, prompt, true, signal, name, images, documents, resumeSessionAt);
|
|
306
329
|
}
|
|
330
|
+
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
331
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
332
|
+
return this.executeTurn(sessionId, `! ${command}`, true, signal, name, [], [], resumeSessionAt, command);
|
|
333
|
+
}
|
|
334
|
+
async answerSideQuestion(sessionId, question, signal, onDelta, permissionMode = 'default') {
|
|
335
|
+
const prompt = question.trim();
|
|
336
|
+
if (!prompt)
|
|
337
|
+
throw new Error('Side question must not be empty');
|
|
338
|
+
const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
|
|
339
|
+
await this.appendInputHistory(`/btw ${prompt}`, activeSessionId);
|
|
340
|
+
const provider = this.provider();
|
|
341
|
+
let entries = [];
|
|
342
|
+
if (sessionId) {
|
|
343
|
+
const loaded = this.options.sessionPersistence === false
|
|
344
|
+
? await this.turnStore(activeSessionId).withLease((lease) => lease.load())
|
|
345
|
+
: {
|
|
346
|
+
status: 'completed',
|
|
347
|
+
value: await this.store(activeSessionId).loadReadOnly(),
|
|
348
|
+
};
|
|
349
|
+
if (loaded.status === 'conflict') {
|
|
350
|
+
throw new Error(`Claude side question conflict: ${loaded.reason}`);
|
|
351
|
+
}
|
|
352
|
+
if (loaded.value.entries.length === 0) {
|
|
353
|
+
throw new Error(`Claude session not found: ${activeSessionId}`);
|
|
354
|
+
}
|
|
355
|
+
entries = loaded.value.entries;
|
|
356
|
+
this.restoreWorktree(entries);
|
|
357
|
+
}
|
|
358
|
+
const assembledContext = await this.options.contextAssembler?.assemble({
|
|
359
|
+
cwd: this.activeCwd(),
|
|
360
|
+
});
|
|
361
|
+
const messages = [
|
|
362
|
+
...(assembledContext?.systemMessages ?? []),
|
|
363
|
+
...injectFirstUserMessageContext([
|
|
364
|
+
...projectClaudeModelMessages(entries),
|
|
365
|
+
{ role: 'user', content: prompt },
|
|
366
|
+
], assembledContext?.firstUserMessageContext),
|
|
367
|
+
];
|
|
368
|
+
const budget = this.contextBudget(provider);
|
|
369
|
+
if (budget)
|
|
370
|
+
budget.assertFits(budget.evaluate(messages));
|
|
371
|
+
let text = '';
|
|
372
|
+
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
373
|
+
for await (const event of provider.complete({
|
|
374
|
+
messages,
|
|
375
|
+
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
376
|
+
...(signal ? { signal } : {}),
|
|
377
|
+
})) {
|
|
378
|
+
if (event.type === 'text-delta') {
|
|
379
|
+
text += event.delta;
|
|
380
|
+
onDelta?.(event.delta);
|
|
381
|
+
}
|
|
382
|
+
else if (event.type === 'usage') {
|
|
383
|
+
usage = event.usage;
|
|
384
|
+
}
|
|
385
|
+
else if (event.type === 'tool-call') {
|
|
386
|
+
throw new Error('Side questions cannot call tools; press f to fork');
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const pricing = this.options.pricing?.resolve(provider.model ?? 'praxis/provider');
|
|
390
|
+
return {
|
|
391
|
+
sessionId: activeSessionId,
|
|
392
|
+
text,
|
|
393
|
+
usage,
|
|
394
|
+
...(pricing ? { costUsd: usageCostUsd(usage, pricing) } : {}),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async forkSideQuestion(sessionId, question, signal) {
|
|
398
|
+
const prompt = question.trim();
|
|
399
|
+
if (!prompt)
|
|
400
|
+
throw new Error('Side question must not be empty');
|
|
401
|
+
const name = prompt
|
|
402
|
+
.toLowerCase()
|
|
403
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
404
|
+
.replace(/^-+|-+$/gu, '')
|
|
405
|
+
.split('-')
|
|
406
|
+
.filter(Boolean)
|
|
407
|
+
.slice(0, 3)
|
|
408
|
+
.join('-')
|
|
409
|
+
.slice(0, 64) || 'side-question';
|
|
410
|
+
const registry = this.createHostedToolRegistry(sessionId);
|
|
411
|
+
const call = {
|
|
412
|
+
id: randomUUID(),
|
|
413
|
+
name: 'Agent',
|
|
414
|
+
input: {
|
|
415
|
+
description: prompt,
|
|
416
|
+
prompt,
|
|
417
|
+
subagent_type: 'general-purpose',
|
|
418
|
+
run_in_background: true,
|
|
419
|
+
name,
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
const context = {
|
|
423
|
+
cwd: this.activeCwd(),
|
|
424
|
+
...(signal ? { signal } : {}),
|
|
425
|
+
};
|
|
426
|
+
const prepared = await registry.prepare(call, context);
|
|
427
|
+
const result = await registry.execute(prepared, context);
|
|
428
|
+
const agentId = result.nativeToolUseResult?.agentId;
|
|
429
|
+
if (result.isError || typeof agentId !== 'string') {
|
|
430
|
+
throw new Error(result.content || 'Could not fork side question');
|
|
431
|
+
}
|
|
432
|
+
await this.appendSystemLocalCommand(sessionId, 'btw', prompt, `⑂ forked ${name} (${agentId.slice(-4)})`);
|
|
433
|
+
const executor = this.hostedSubagentsByRegistry.get(registry);
|
|
434
|
+
if (executor) {
|
|
435
|
+
void executor
|
|
436
|
+
.notifications(true)
|
|
437
|
+
.then(({ messages }) => this.enqueueBackgroundNotifications(sessionId, messages))
|
|
438
|
+
.catch((error) => this.options.eventSink?.({
|
|
439
|
+
type: 'warning',
|
|
440
|
+
message: error instanceof Error
|
|
441
|
+
? error.message
|
|
442
|
+
: `Could not persist background notification: ${String(error)}`,
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
return { agentId, name };
|
|
446
|
+
}
|
|
307
447
|
async promptSuggestion(sessionId, signal) {
|
|
308
448
|
const provider = this.provider();
|
|
309
449
|
const loaded = this.options.sessionPersistence === false
|
|
@@ -355,6 +495,28 @@ export class ClaudeSessionService {
|
|
|
355
495
|
}
|
|
356
496
|
return validPromptSuggestion(suggestion);
|
|
357
497
|
}
|
|
498
|
+
async sessionNameSuggestion(sessionId, signal) {
|
|
499
|
+
const provider = this.provider();
|
|
500
|
+
const loaded = await this.turnStore(sessionId).withLease((lease) => lease.load());
|
|
501
|
+
if (loaded.status === 'conflict' || loaded.value.entries.length === 0) {
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
let suggestion = '';
|
|
505
|
+
for await (const event of provider.complete({
|
|
506
|
+
messages: [
|
|
507
|
+
...projectClaudeModelMessages(loaded.value.entries),
|
|
508
|
+
{ role: 'user', content: SESSION_NAME_INSTRUCTION },
|
|
509
|
+
],
|
|
510
|
+
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
511
|
+
...(signal ? { signal } : {}),
|
|
512
|
+
})) {
|
|
513
|
+
if (event.type === 'text-delta')
|
|
514
|
+
suggestion += event.delta;
|
|
515
|
+
if (event.type === 'tool-call')
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
return validSessionName(suggestion);
|
|
519
|
+
}
|
|
358
520
|
async sessions() {
|
|
359
521
|
const paths = this.paths(randomUUID());
|
|
360
522
|
let names;
|
|
@@ -456,6 +618,344 @@ export class ClaudeSessionService {
|
|
|
456
618
|
throw error;
|
|
457
619
|
}
|
|
458
620
|
}
|
|
621
|
+
async transcript(sessionId, resumeSessionAt) {
|
|
622
|
+
try {
|
|
623
|
+
const recovery = await this.store(sessionId).loadReadOnly();
|
|
624
|
+
if (recovery.entries.length === 0) {
|
|
625
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
626
|
+
}
|
|
627
|
+
const entries = resumeSessionAt === undefined
|
|
628
|
+
? recovery.entries
|
|
629
|
+
: selectClaudeTranscriptAtMessage(recovery.entries, resumeSessionAt);
|
|
630
|
+
return projectClaudeDisplayTranscript(entries);
|
|
631
|
+
}
|
|
632
|
+
catch (error) {
|
|
633
|
+
if (error.code === 'ENOENT') {
|
|
634
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
635
|
+
}
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
async rename(sessionId, name) {
|
|
640
|
+
this.assertWritable();
|
|
641
|
+
const normalized = name.trim();
|
|
642
|
+
if (!normalized)
|
|
643
|
+
throw new Error('Session name must not be empty');
|
|
644
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
645
|
+
const snapshot = await lease.load();
|
|
646
|
+
if (snapshot.entries.length === 0) {
|
|
647
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
648
|
+
}
|
|
649
|
+
if (this.hasSessionName(snapshot.entries, normalized))
|
|
650
|
+
return;
|
|
651
|
+
const appendResult = await lease.appendMany(snapshot.tail, this.sessionNameEntries(sessionId, normalized));
|
|
652
|
+
if (appendResult.status === 'conflict') {
|
|
653
|
+
throw new Error(`Claude transcript rename conflict: ${appendResult.reason}`);
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
if (result.status === 'conflict') {
|
|
657
|
+
throw new Error(`Claude transcript rename conflict: ${result.reason}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async changeCwd(sessionId, requestedCwd) {
|
|
661
|
+
this.assertWritable();
|
|
662
|
+
const previousCwd = this.activeCwd();
|
|
663
|
+
const expandedCwd = requestedCwd === '~'
|
|
664
|
+
? homedir()
|
|
665
|
+
: requestedCwd.startsWith('~/')
|
|
666
|
+
? join(homedir(), requestedCwd.slice(2))
|
|
667
|
+
: requestedCwd;
|
|
668
|
+
const cwd = await realpath(isAbsolute(expandedCwd) ? expandedCwd : join(previousCwd, expandedCwd));
|
|
669
|
+
if (!(await stat(cwd)).isDirectory()) {
|
|
670
|
+
throw new Error(`Not a directory: ${requestedCwd}`);
|
|
671
|
+
}
|
|
672
|
+
if (sessionId && this.options.sessionPersistence !== false) {
|
|
673
|
+
const sourcePaths = resolveClaudePaths({
|
|
674
|
+
configDir: this.options.configRoot,
|
|
675
|
+
cwd: this.sessionCwds.get(sessionId) ?? previousCwd,
|
|
676
|
+
sessionId,
|
|
677
|
+
});
|
|
678
|
+
const targetPaths = resolveClaudePaths({
|
|
679
|
+
configDir: this.options.configRoot,
|
|
680
|
+
cwd,
|
|
681
|
+
sessionId,
|
|
682
|
+
});
|
|
683
|
+
if (sourcePaths.sessionFile !== targetPaths.sessionFile) {
|
|
684
|
+
try {
|
|
685
|
+
await lstat(targetPaths.sessionFile);
|
|
686
|
+
throw new Error(`Claude transcript already exists at relocation target: ${targetPaths.sessionFile}`);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
if (error.code !== 'ENOENT')
|
|
690
|
+
throw error;
|
|
691
|
+
}
|
|
692
|
+
const sourceStore = this.store(sessionId);
|
|
693
|
+
const relocated = await sourceStore.withLease(async (lease) => {
|
|
694
|
+
const snapshot = await lease.load();
|
|
695
|
+
if (snapshot.entries.length === 0) {
|
|
696
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
697
|
+
}
|
|
698
|
+
await mkdir(targetPaths.projectRoot, { recursive: true });
|
|
699
|
+
const relocationId = randomUUID();
|
|
700
|
+
const stagingFile = join(targetPaths.projectRoot, `.${sessionId}.${relocationId}.relocating`);
|
|
701
|
+
const stagingStore = new ClaudeTranscriptStore({
|
|
702
|
+
sessionFile: stagingFile,
|
|
703
|
+
lockFile: join(targetPaths.praxisRoot, 'locks', `${sessionId}.${relocationId}.lock`),
|
|
704
|
+
schema: this.schema,
|
|
705
|
+
});
|
|
706
|
+
let publishedIdentity;
|
|
707
|
+
try {
|
|
708
|
+
await copyFile(sourcePaths.sessionFile, stagingFile);
|
|
709
|
+
const staged = await stagingStore.withLease((stagingLease) => stagingLease.appendMany(snapshot.tail, [
|
|
710
|
+
{
|
|
711
|
+
type: 'relocated',
|
|
712
|
+
sessionId,
|
|
713
|
+
relocatedCwd: cwd,
|
|
714
|
+
},
|
|
715
|
+
...this.cdCommandEntries(sessionId, cwd, this.logicalTailUuid(snapshot.tail)),
|
|
716
|
+
]));
|
|
717
|
+
if (staged.status === 'conflict') {
|
|
718
|
+
throw new Error(`Claude transcript relocation conflict: ${staged.reason}`);
|
|
719
|
+
}
|
|
720
|
+
if (staged.value.status === 'conflict') {
|
|
721
|
+
throw new Error(`Claude transcript relocation conflict: ${staged.value.reason}`);
|
|
722
|
+
}
|
|
723
|
+
const stagingMetadata = await lstat(stagingFile);
|
|
724
|
+
await link(stagingFile, targetPaths.sessionFile);
|
|
725
|
+
publishedIdentity = {
|
|
726
|
+
dev: stagingMetadata.dev,
|
|
727
|
+
ino: stagingMetadata.ino,
|
|
728
|
+
};
|
|
729
|
+
await unlink(stagingFile);
|
|
730
|
+
await unlink(sourcePaths.sessionFile);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
await unlink(stagingFile).catch(() => undefined);
|
|
734
|
+
if (publishedIdentity) {
|
|
735
|
+
const targetMetadata = await lstat(targetPaths.sessionFile).catch(() => undefined);
|
|
736
|
+
if (targetMetadata?.dev === publishedIdentity.dev &&
|
|
737
|
+
targetMetadata.ino === publishedIdentity.ino) {
|
|
738
|
+
await unlink(targetPaths.sessionFile).catch(() => undefined);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
throw error;
|
|
742
|
+
}
|
|
743
|
+
});
|
|
744
|
+
if (relocated.status === 'conflict') {
|
|
745
|
+
throw new Error(`Claude transcript relocation conflict: ${relocated.reason}`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
await this.appendCdCommand(sessionId, cwd);
|
|
750
|
+
}
|
|
751
|
+
this.sessionCwds.set(sessionId, cwd);
|
|
752
|
+
this.runtimeCwd = cwd;
|
|
753
|
+
this.options.workspace?.setCwd(cwd);
|
|
754
|
+
return cwd;
|
|
755
|
+
}
|
|
756
|
+
this.runtimeCwd = cwd;
|
|
757
|
+
this.options.workspace?.setCwd(cwd);
|
|
758
|
+
if (sessionId)
|
|
759
|
+
this.sessionCwds.set(sessionId, cwd);
|
|
760
|
+
return cwd;
|
|
761
|
+
}
|
|
762
|
+
async recordCdUsage(sessionId) {
|
|
763
|
+
this.assertWritable();
|
|
764
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
765
|
+
const snapshot = await lease.load();
|
|
766
|
+
if (snapshot.entries.length === 0) {
|
|
767
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
768
|
+
}
|
|
769
|
+
const promptId = randomUUID();
|
|
770
|
+
const firstUuid = randomUUID();
|
|
771
|
+
const timestamp = new Date().toISOString();
|
|
772
|
+
const common = {
|
|
773
|
+
isSidechain: false,
|
|
774
|
+
promptId,
|
|
775
|
+
timestamp,
|
|
776
|
+
userType: 'external',
|
|
777
|
+
entrypoint: 'cli',
|
|
778
|
+
cwd: this.activeCwd(),
|
|
779
|
+
sessionId,
|
|
780
|
+
version: this.options.claudeVersion,
|
|
781
|
+
gitBranch: null,
|
|
782
|
+
};
|
|
783
|
+
const appendResult = await lease.appendMany(snapshot.tail, [
|
|
784
|
+
{
|
|
785
|
+
...common,
|
|
786
|
+
parentUuid: this.logicalTailUuid(snapshot.tail),
|
|
787
|
+
type: 'user',
|
|
788
|
+
message: {
|
|
789
|
+
role: 'user',
|
|
790
|
+
content: '<command-name>/cd</command-name>\n <command-message>cd</command-message>\n <command-args></command-args>',
|
|
791
|
+
},
|
|
792
|
+
uuid: firstUuid,
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
...common,
|
|
796
|
+
parentUuid: firstUuid,
|
|
797
|
+
type: 'user',
|
|
798
|
+
message: {
|
|
799
|
+
role: 'user',
|
|
800
|
+
content: '<local-command-stdout>Usage: /cd <path></local-command-stdout>',
|
|
801
|
+
},
|
|
802
|
+
uuid: randomUUID(),
|
|
803
|
+
},
|
|
804
|
+
]);
|
|
805
|
+
if (appendResult.status === 'conflict') {
|
|
806
|
+
throw new Error(`Claude cd usage append conflict: ${appendResult.reason}`);
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
if (result.status === 'conflict') {
|
|
810
|
+
throw new Error(`Claude cd usage conflict: ${result.reason}`);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
async recordBtwUsage(sessionId, permissionMode = 'default') {
|
|
814
|
+
const activeSessionId = await this.ensureLocalSession(sessionId, permissionMode);
|
|
815
|
+
await this.appendInputHistory('/btw', activeSessionId);
|
|
816
|
+
while (true) {
|
|
817
|
+
const result = await this.turnStore(activeSessionId).withLease(async (lease) => {
|
|
818
|
+
const snapshot = await lease.load();
|
|
819
|
+
if (snapshot.entries.length === 0) {
|
|
820
|
+
throw new Error(`Claude session not found: ${activeSessionId}`);
|
|
821
|
+
}
|
|
822
|
+
const entries = this.localCommandEntries(activeSessionId, this.activeCwd(), this.logicalTailUuid(snapshot.tail), 'btw', '', 'Usage: /btw <your question>');
|
|
823
|
+
const leafUuid = entries.at(-1)?.uuid;
|
|
824
|
+
if (typeof leafUuid !== 'string') {
|
|
825
|
+
throw new Error('Claude btw local command pair is incomplete');
|
|
826
|
+
}
|
|
827
|
+
const appended = await lease.appendMany(snapshot.tail, [
|
|
828
|
+
...entries,
|
|
829
|
+
{ type: 'last-prompt', leafUuid, sessionId: activeSessionId },
|
|
830
|
+
]);
|
|
831
|
+
if (appended.status === 'conflict') {
|
|
832
|
+
throw new Error(`Claude local command append conflict: ${appended.reason}`);
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
if (result.status === 'completed')
|
|
836
|
+
return activeSessionId;
|
|
837
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async compact(sessionId, signal, selection) {
|
|
841
|
+
this.assertWritable();
|
|
842
|
+
const provider = this.provider();
|
|
843
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
844
|
+
const snapshot = await lease.load();
|
|
845
|
+
if (snapshot.entries.length === 0) {
|
|
846
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
847
|
+
}
|
|
848
|
+
const activeEntries = selectClaudeActiveTranscript(snapshot.entries);
|
|
849
|
+
const allMessages = projectClaudeModelMessages(activeEntries);
|
|
850
|
+
let selectedEntries = activeEntries;
|
|
851
|
+
let logicalParentUuid = this.lastMessageUuid(activeEntries);
|
|
852
|
+
let preservedEntries = [];
|
|
853
|
+
if (selection) {
|
|
854
|
+
const targetIndex = activeEntries.findIndex((entry) => entry.uuid === selection.messageId);
|
|
855
|
+
if (targetIndex < 0) {
|
|
856
|
+
throw new Error(`No rewind point found with message.uuid of: ${selection.messageId}`);
|
|
857
|
+
}
|
|
858
|
+
if (selection.direction === 'from') {
|
|
859
|
+
selectedEntries = activeEntries.slice(targetIndex);
|
|
860
|
+
preservedEntries = activeEntries.slice(0, targetIndex);
|
|
861
|
+
logicalParentUuid = this.lastMessageUuid(preservedEntries);
|
|
862
|
+
if (!logicalParentUuid)
|
|
863
|
+
logicalParentUuid = selection.messageId;
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
selectedEntries = activeEntries.slice(0, targetIndex);
|
|
867
|
+
preservedEntries = activeEntries.slice(targetIndex);
|
|
868
|
+
const target = activeEntries[targetIndex];
|
|
869
|
+
logicalParentUuid =
|
|
870
|
+
typeof target?.parentUuid === 'string'
|
|
871
|
+
? target.parentUuid
|
|
872
|
+
: this.lastMessageUuid(selectedEntries);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
const selectedMessages = projectClaudeModelMessages(selectedEntries);
|
|
876
|
+
const messages = [
|
|
877
|
+
...selectedMessages,
|
|
878
|
+
...(selection?.context
|
|
879
|
+
? [
|
|
880
|
+
{
|
|
881
|
+
role: 'user',
|
|
882
|
+
content: `Additional summarization context: ${selection.context}`,
|
|
883
|
+
},
|
|
884
|
+
]
|
|
885
|
+
: []),
|
|
886
|
+
];
|
|
887
|
+
if (!logicalParentUuid || selectedMessages.length === 0) {
|
|
888
|
+
throw new Error('Cannot compact an empty Claude transcript');
|
|
889
|
+
}
|
|
890
|
+
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
891
|
+
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
892
|
+
}
|
|
893
|
+
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
894
|
+
const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
|
|
895
|
+
provider.capabilities.contextWindowTokens ??
|
|
896
|
+
200_000;
|
|
897
|
+
const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
|
|
898
|
+
messages,
|
|
899
|
+
targetTokens: Math.min(8192, Math.max(1, Math.floor(contextWindowTokens / 4))),
|
|
900
|
+
contextWindowTokens,
|
|
901
|
+
...(signal ? { signal } : {}),
|
|
902
|
+
});
|
|
903
|
+
if (signal?.aborted)
|
|
904
|
+
throw new AgentRunCancelledError();
|
|
905
|
+
const preTokens = estimateModelRequestTokens(allMessages);
|
|
906
|
+
const summary = compacted.summary;
|
|
907
|
+
const preservedMessages = projectClaudeModelMessages(preservedEntries);
|
|
908
|
+
const boundaryUuid = randomUUID();
|
|
909
|
+
const summaryUuid = randomUUID();
|
|
910
|
+
const uuids = [boundaryUuid, summaryUuid];
|
|
911
|
+
const entries = createClaudeCompactEntries({
|
|
912
|
+
sessionId,
|
|
913
|
+
logicalParentUuid,
|
|
914
|
+
summary,
|
|
915
|
+
preTokens,
|
|
916
|
+
postTokens: estimateModelRequestTokens([
|
|
917
|
+
{ role: 'user', content: formatClaudeCompactSummary(summary) },
|
|
918
|
+
...preservedMessages,
|
|
919
|
+
]),
|
|
920
|
+
previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
|
|
921
|
+
durationMs: compacted.durationMs,
|
|
922
|
+
cwd: this.activeCwd(),
|
|
923
|
+
claudeVersion: this.options.claudeVersion,
|
|
924
|
+
gitBranch: null,
|
|
925
|
+
trigger: 'manual',
|
|
926
|
+
...(selection
|
|
927
|
+
? {
|
|
928
|
+
summarizeMetadata: {
|
|
929
|
+
messagesSummarized: selectedMessages.length,
|
|
930
|
+
direction: selection.direction === 'to' ? 'up_to' : selection.direction,
|
|
931
|
+
},
|
|
932
|
+
preservedUuids: preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
933
|
+
}
|
|
934
|
+
: {}),
|
|
935
|
+
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
936
|
+
});
|
|
937
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
938
|
+
if (appendResult.status === 'conflict') {
|
|
939
|
+
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
940
|
+
}
|
|
941
|
+
this.options.eventSink?.({
|
|
942
|
+
type: 'compact-boundary',
|
|
943
|
+
trigger: 'manual',
|
|
944
|
+
preTokens,
|
|
945
|
+
uuid: boundaryUuid,
|
|
946
|
+
});
|
|
947
|
+
return {
|
|
948
|
+
summary,
|
|
949
|
+
usage: compacted.usage,
|
|
950
|
+
preTokens,
|
|
951
|
+
...(selection ? { messagesSummarized: selectedMessages.length } : {}),
|
|
952
|
+
};
|
|
953
|
+
});
|
|
954
|
+
if (result.status === 'conflict') {
|
|
955
|
+
throw new Error(`Claude transcript compact conflict: ${result.reason}`);
|
|
956
|
+
}
|
|
957
|
+
return result.value;
|
|
958
|
+
}
|
|
459
959
|
async fork(parentSessionId, sessionId = randomUUID(), resumeSessionAt) {
|
|
460
960
|
this.assertSessionPersistence();
|
|
461
961
|
this.assertWritable();
|
|
@@ -496,13 +996,100 @@ export class ClaudeSessionService {
|
|
|
496
996
|
throw new Error(`Claude file rewind conflict: ${result.reason}`);
|
|
497
997
|
}
|
|
498
998
|
}
|
|
499
|
-
async
|
|
999
|
+
async rewindPoints(sessionId) {
|
|
1000
|
+
const result = await this.turnStore(sessionId).withLease((lease) => lease.load());
|
|
1001
|
+
if (result.status === 'conflict') {
|
|
1002
|
+
throw new Error(`Claude transcript rewind conflict: ${result.reason}`);
|
|
1003
|
+
}
|
|
1004
|
+
if (result.value.entries.length === 0) {
|
|
1005
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1006
|
+
}
|
|
1007
|
+
const active = selectClaudeActiveTranscript(result.value.entries);
|
|
1008
|
+
const snapshots = new Set(active
|
|
1009
|
+
.filter((entry) => entry.type === 'file-history-snapshot' &&
|
|
1010
|
+
typeof entry.messageId === 'string')
|
|
1011
|
+
.map((entry) => entry.messageId));
|
|
1012
|
+
const changes = new Map();
|
|
1013
|
+
for (const entry of active) {
|
|
1014
|
+
if (entry.type !== 'file-history-delta' ||
|
|
1015
|
+
typeof entry.snapshotMessageId !== 'string' ||
|
|
1016
|
+
typeof entry.trackingPath !== 'string') {
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
const paths = changes.get(entry.snapshotMessageId) ?? new Set();
|
|
1020
|
+
paths.add(entry.trackingPath);
|
|
1021
|
+
changes.set(entry.snapshotMessageId, paths);
|
|
1022
|
+
}
|
|
1023
|
+
const points = [];
|
|
1024
|
+
let branchMessageId;
|
|
1025
|
+
for (const entry of active) {
|
|
1026
|
+
if ((entry.type === 'user' || entry.type === 'assistant') &&
|
|
1027
|
+
typeof entry.uuid === 'string') {
|
|
1028
|
+
const message = entry.message;
|
|
1029
|
+
if (typeof message === 'object' &&
|
|
1030
|
+
message !== null &&
|
|
1031
|
+
!Array.isArray(message)) {
|
|
1032
|
+
const role = message.role;
|
|
1033
|
+
const content = message.content;
|
|
1034
|
+
if (entry.type === 'user' &&
|
|
1035
|
+
role === 'user' &&
|
|
1036
|
+
typeof content === 'string' &&
|
|
1037
|
+
entry.isCompactSummary !== true &&
|
|
1038
|
+
entry.isMeta !== true &&
|
|
1039
|
+
typeof entry.sourceToolAssistantUUID !== 'string' &&
|
|
1040
|
+
!content.startsWith('<bash-input>') &&
|
|
1041
|
+
!content.startsWith('<local-command-') &&
|
|
1042
|
+
!content.startsWith('<command-name>')) {
|
|
1043
|
+
points.push({
|
|
1044
|
+
messageId: entry.uuid,
|
|
1045
|
+
prompt: content,
|
|
1046
|
+
...(typeof entry.timestamp === 'string'
|
|
1047
|
+
? { timestamp: entry.timestamp }
|
|
1048
|
+
: {}),
|
|
1049
|
+
...(branchMessageId === undefined ? {} : { branchMessageId }),
|
|
1050
|
+
fileChanges: [...(changes.get(entry.uuid) ?? [])],
|
|
1051
|
+
fileRestoreAvailable: snapshots.has(entry.uuid),
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (role === 'user' || role === 'assistant') {
|
|
1055
|
+
branchMessageId = entry.uuid;
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return points;
|
|
1061
|
+
}
|
|
1062
|
+
async setPermissionMode(sessionId, permissionMode) {
|
|
1063
|
+
this.assertSessionPersistence();
|
|
1064
|
+
this.assertWritable();
|
|
1065
|
+
const result = await this.store(sessionId).withLease(async (lease) => {
|
|
1066
|
+
const snapshot = await lease.load();
|
|
1067
|
+
if (snapshot.entries.length === 0) {
|
|
1068
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
1069
|
+
}
|
|
1070
|
+
this.options.interactiveTools?.restore(sessionId, snapshot.entries);
|
|
1071
|
+
const entry = {
|
|
1072
|
+
type: 'permission-mode',
|
|
1073
|
+
permissionMode,
|
|
1074
|
+
sessionId,
|
|
1075
|
+
};
|
|
1076
|
+
await this.append(lease, snapshot.tail, entry);
|
|
1077
|
+
await this.options.interactiveTools?.setMode(sessionId, permissionMode);
|
|
1078
|
+
});
|
|
1079
|
+
if (result.status === 'conflict') {
|
|
1080
|
+
throw new Error(`Claude permission mode update conflict: ${result.reason}`);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
async executeTurn(sessionId, prompt, requireExisting, signal, name, images = [], documents = [], resumeSessionAt, shellCommand) {
|
|
500
1084
|
this.assertWritable();
|
|
501
1085
|
if (prompt.length === 0 && images.length === 0 && documents.length === 0)
|
|
502
1086
|
throw new Error('Prompt must not be empty');
|
|
503
1087
|
if (name !== undefined && name.length === 0) {
|
|
504
1088
|
throw new Error('Session name must not be empty');
|
|
505
1089
|
}
|
|
1090
|
+
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
1091
|
+
throw new Error('Shell command must not be empty');
|
|
1092
|
+
}
|
|
506
1093
|
await this.ensureFileResources(sessionId, signal);
|
|
507
1094
|
this.worktreeManager?.bindSession(sessionId);
|
|
508
1095
|
if (this.options.initialWorktree) {
|
|
@@ -575,7 +1162,8 @@ export class ClaudeSessionService {
|
|
|
575
1162
|
if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
|
|
576
1163
|
throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
|
|
577
1164
|
}
|
|
578
|
-
|
|
1165
|
+
const shellInputUuid = shellCommand === undefined ? null : randomUUID();
|
|
1166
|
+
let currentPromptId = shellInputUuid;
|
|
579
1167
|
let lastAssistantUuid = null;
|
|
580
1168
|
const fileHistory = this.options.fileCheckpointing &&
|
|
581
1169
|
this.options.sessionPersistence !== false
|
|
@@ -1047,9 +1635,13 @@ export class ClaudeSessionService {
|
|
|
1047
1635
|
]
|
|
1048
1636
|
: []),
|
|
1049
1637
|
];
|
|
1050
|
-
const expansion =
|
|
1051
|
-
?
|
|
1052
|
-
|
|
1638
|
+
const expansion = shellCommand === undefined
|
|
1639
|
+
? this.options.extensions
|
|
1640
|
+
? await this.options.extensions.expandPromptAsync(prompt, signal, toolResultDirectory)
|
|
1641
|
+
: { userMessages: [prompt] }
|
|
1642
|
+
: {
|
|
1643
|
+
userMessages: [`<bash-input>${shellCommand}</bash-input>`],
|
|
1644
|
+
};
|
|
1053
1645
|
const expandedMessages = expansion.messages ?? expansion.userMessages.map((text) => ({ text }));
|
|
1054
1646
|
const attachmentIndex = expansion.messages
|
|
1055
1647
|
? expandedMessages.length - 1
|
|
@@ -1086,7 +1678,37 @@ export class ClaudeSessionService {
|
|
|
1086
1678
|
? { documents }
|
|
1087
1679
|
: {}),
|
|
1088
1680
|
}));
|
|
1681
|
+
const agentMentionMessages = shellCommand === undefined
|
|
1682
|
+
? (this.options.extensions?.agentMentionMessages(prompt) ?? [])
|
|
1683
|
+
: [];
|
|
1684
|
+
const injectAgentMentionContext = (messages) => {
|
|
1685
|
+
if (agentMentionMessages.length === 0)
|
|
1686
|
+
return [...messages];
|
|
1687
|
+
let insertionIndex = messages.length;
|
|
1688
|
+
let foundPrompt = false;
|
|
1689
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1690
|
+
const message = messages[index];
|
|
1691
|
+
if (message?.role === 'user' &&
|
|
1692
|
+
typeof message.content === 'string' &&
|
|
1693
|
+
message.content.endsWith(prompt)) {
|
|
1694
|
+
insertionIndex = index;
|
|
1695
|
+
foundPrompt = true;
|
|
1696
|
+
break;
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (!foundPrompt)
|
|
1700
|
+
return [...messages];
|
|
1701
|
+
return [
|
|
1702
|
+
...messages.slice(0, insertionIndex),
|
|
1703
|
+
...agentMentionMessages.map((content) => ({
|
|
1704
|
+
role: 'user',
|
|
1705
|
+
content,
|
|
1706
|
+
})),
|
|
1707
|
+
...messages.slice(insertionIndex),
|
|
1708
|
+
];
|
|
1709
|
+
};
|
|
1089
1710
|
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
|
|
1711
|
+
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
1090
1712
|
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
1091
1713
|
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = []) => {
|
|
1092
1714
|
if (!budget)
|
|
@@ -1094,13 +1716,13 @@ export class ClaudeSessionService {
|
|
|
1094
1716
|
const historyMessages = projectClaudeModelMessages(snapshot.entries);
|
|
1095
1717
|
const predicted = budget.evaluate([
|
|
1096
1718
|
...contextMessages,
|
|
1097
|
-
...
|
|
1719
|
+
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
1098
1720
|
], definitions);
|
|
1099
1721
|
if (!predicted.shouldCompact)
|
|
1100
1722
|
return;
|
|
1101
1723
|
const irreducibleMessages = [
|
|
1102
1724
|
...contextMessages,
|
|
1103
|
-
...
|
|
1725
|
+
...injectTurnContext([
|
|
1104
1726
|
...pendingMessages,
|
|
1105
1727
|
...preservedUserMessages.map((content) => ({
|
|
1106
1728
|
role: 'user',
|
|
@@ -1146,7 +1768,7 @@ export class ClaudeSessionService {
|
|
|
1146
1768
|
const boundaryUuid = randomUUID();
|
|
1147
1769
|
const summaryUuid = randomUUID();
|
|
1148
1770
|
const timestamp = new Date().toISOString();
|
|
1149
|
-
const preTokens = budget.evaluate([...contextMessages, ...
|
|
1771
|
+
const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
|
|
1150
1772
|
const compactEntries = (postTokens) => {
|
|
1151
1773
|
const uuids = [boundaryUuid, summaryUuid];
|
|
1152
1774
|
return createClaudeCompactEntries({
|
|
@@ -1187,13 +1809,10 @@ export class ClaudeSessionService {
|
|
|
1187
1809
|
...provisionalEntries,
|
|
1188
1810
|
...replayEntries,
|
|
1189
1811
|
]);
|
|
1190
|
-
const afterHistory = budget.evaluate([...contextMessages, ...
|
|
1812
|
+
const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
|
|
1191
1813
|
const afterPending = budget.evaluate([
|
|
1192
1814
|
...contextMessages,
|
|
1193
|
-
...
|
|
1194
|
-
...compactedHistory,
|
|
1195
|
-
...pendingMessages,
|
|
1196
|
-
]),
|
|
1815
|
+
...injectTurnContext([...compactedHistory, ...pendingMessages]),
|
|
1197
1816
|
], definitions);
|
|
1198
1817
|
budget.assertFits(afterPending);
|
|
1199
1818
|
const entries = [
|
|
@@ -1224,25 +1843,26 @@ export class ClaudeSessionService {
|
|
|
1224
1843
|
};
|
|
1225
1844
|
await compactIfNeeded(pendingUserMessages);
|
|
1226
1845
|
for (const [index, message] of expandedMessages.entries()) {
|
|
1846
|
+
if (shellCommand !== undefined)
|
|
1847
|
+
break;
|
|
1227
1848
|
const messageImages = [
|
|
1228
1849
|
...(index === attachmentIndex ? images : []),
|
|
1229
1850
|
...(message.images ?? []),
|
|
1230
1851
|
];
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
:
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
], this.translationContext(sessionId, snapshot));
|
|
1852
|
+
const persistenceEvent = messageImages.length > 0 ||
|
|
1853
|
+
(index === attachmentIndex && documents.length > 0)
|
|
1854
|
+
? {
|
|
1855
|
+
type: 'user-message',
|
|
1856
|
+
text: message.text,
|
|
1857
|
+
images: messageImages,
|
|
1858
|
+
...(index === attachmentIndex && documents.length > 0
|
|
1859
|
+
? { documents }
|
|
1860
|
+
: {}),
|
|
1861
|
+
}
|
|
1862
|
+
: index === 0
|
|
1863
|
+
? { type: 'user-text', text: message.text }
|
|
1864
|
+
: { type: 'user-text-block', text: message.text };
|
|
1865
|
+
const [userEntry] = translateProviderEvents([persistenceEvent], this.translationContext(sessionId, snapshot));
|
|
1246
1866
|
if (!userEntry)
|
|
1247
1867
|
throw new Error('Could not translate user prompt');
|
|
1248
1868
|
if (currentPromptId === null && typeof userEntry.uuid === 'string') {
|
|
@@ -1275,18 +1895,90 @@ export class ClaudeSessionService {
|
|
|
1275
1895
|
throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
|
|
1276
1896
|
}
|
|
1277
1897
|
}
|
|
1898
|
+
let shellUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1899
|
+
if (shellCommand !== undefined) {
|
|
1900
|
+
const call = {
|
|
1901
|
+
id: `shell_${randomUUID().replaceAll('-', '')}`,
|
|
1902
|
+
name: 'Bash',
|
|
1903
|
+
input: { command: shellCommand },
|
|
1904
|
+
};
|
|
1905
|
+
this.options.eventSink?.({
|
|
1906
|
+
type: 'shell-command',
|
|
1907
|
+
callId: call.id,
|
|
1908
|
+
command: shellCommand,
|
|
1909
|
+
});
|
|
1910
|
+
let shellResult;
|
|
1911
|
+
try {
|
|
1912
|
+
shellResult = await runtime.executeDirectToolCall(call, {
|
|
1913
|
+
cwd: this.activeCwd(),
|
|
1914
|
+
toolResultDirectory,
|
|
1915
|
+
messages: projectClaudeModelMessages(snapshot.entries),
|
|
1916
|
+
observer: {
|
|
1917
|
+
assistantCompleted: async () => undefined,
|
|
1918
|
+
toolCompleted: async () => undefined,
|
|
1919
|
+
followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
|
|
1920
|
+
},
|
|
1921
|
+
...(this.options.approveTool
|
|
1922
|
+
? { approveTool: this.options.approveTool }
|
|
1923
|
+
: {}),
|
|
1924
|
+
...(signal ? { signal } : {}),
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
1927
|
+
catch (error) {
|
|
1928
|
+
this.options.eventSink?.({
|
|
1929
|
+
type: 'shell-cancelled',
|
|
1930
|
+
callId: call.id,
|
|
1931
|
+
});
|
|
1932
|
+
throw error;
|
|
1933
|
+
}
|
|
1934
|
+
shellUsage = shellResult.usage ?? shellUsage;
|
|
1935
|
+
const stdout = shellResult.processOutput?.stdout ??
|
|
1936
|
+
(shellResult.isError ? '' : shellResult.content);
|
|
1937
|
+
const stderr = shellResult.processOutput?.stderr ??
|
|
1938
|
+
(shellResult.isError ? shellResult.content : '');
|
|
1939
|
+
const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
|
|
1940
|
+
const [inputEntry, outputEntry] = translateProviderEvents([
|
|
1941
|
+
{ type: 'bash-input', command: shellCommand },
|
|
1942
|
+
{ type: 'bash-output', stdout, stderr },
|
|
1943
|
+
], {
|
|
1944
|
+
...this.translationContext(sessionId, snapshot),
|
|
1945
|
+
createUuid: () => shellUuids.shift() ?? randomUUID(),
|
|
1946
|
+
});
|
|
1947
|
+
if (!inputEntry || !outputEntry) {
|
|
1948
|
+
throw new Error('Could not translate shell command result');
|
|
1949
|
+
}
|
|
1950
|
+
const shellAppend = await lease.appendMany(snapshot.tail, [
|
|
1951
|
+
inputEntry,
|
|
1952
|
+
outputEntry,
|
|
1953
|
+
]);
|
|
1954
|
+
if (shellAppend.status === 'conflict') {
|
|
1955
|
+
throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
|
|
1956
|
+
}
|
|
1957
|
+
snapshot = {
|
|
1958
|
+
entries: [...snapshot.entries, inputEntry, outputEntry],
|
|
1959
|
+
tail: shellAppend.tail,
|
|
1960
|
+
};
|
|
1961
|
+
currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
|
|
1962
|
+
this.options.eventSink?.({
|
|
1963
|
+
type: 'shell-result',
|
|
1964
|
+
callId: call.id,
|
|
1965
|
+
stdout,
|
|
1966
|
+
stderr,
|
|
1967
|
+
isError: shellResult.isError,
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1278
1970
|
if (budget) {
|
|
1279
1971
|
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
1280
1972
|
budget.assertFits(budget.evaluate([
|
|
1281
1973
|
...contextMessages,
|
|
1282
|
-
...
|
|
1974
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1283
1975
|
], definitions));
|
|
1284
1976
|
}
|
|
1285
1977
|
let stopHookActive = false;
|
|
1286
1978
|
const runtimeRequest = {
|
|
1287
1979
|
messages: [
|
|
1288
1980
|
...contextMessages,
|
|
1289
|
-
...
|
|
1981
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1290
1982
|
],
|
|
1291
1983
|
cwd: this.activeCwd(),
|
|
1292
1984
|
toolResultDirectory,
|
|
@@ -1301,7 +1993,7 @@ export class ClaudeSessionService {
|
|
|
1301
1993
|
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
1302
1994
|
return [
|
|
1303
1995
|
...contextMessages,
|
|
1304
|
-
...
|
|
1996
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1305
1997
|
];
|
|
1306
1998
|
},
|
|
1307
1999
|
...(this.options.hooks ||
|
|
@@ -1373,7 +2065,7 @@ export class ClaudeSessionService {
|
|
|
1373
2065
|
lastPrompt: prompt,
|
|
1374
2066
|
leafUuid: finalLeafUuid,
|
|
1375
2067
|
}));
|
|
1376
|
-
const totalUsage = mergeUsage(mergeUsage(recoveryUsage, compactionUsage), result.usage);
|
|
2068
|
+
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
1377
2069
|
return {
|
|
1378
2070
|
sessionId,
|
|
1379
2071
|
text: structuredCapture && structuredCapture.calls === 1
|
|
@@ -1604,7 +2296,7 @@ export class ClaudeSessionService {
|
|
|
1604
2296
|
: pathFromCwd;
|
|
1605
2297
|
}
|
|
1606
2298
|
activeCwd() {
|
|
1607
|
-
return this.options.workspace?.cwd() ?? this.
|
|
2299
|
+
return this.options.workspace?.cwd() ?? this.runtimeCwd;
|
|
1608
2300
|
}
|
|
1609
2301
|
restoreWorktree(entries) {
|
|
1610
2302
|
const latest = [...entries]
|
|
@@ -1620,10 +2312,218 @@ export class ClaudeSessionService {
|
|
|
1620
2312
|
paths(sessionId) {
|
|
1621
2313
|
return resolveClaudePaths({
|
|
1622
2314
|
configDir: this.options.configRoot,
|
|
1623
|
-
cwd: this.sessionCwds.get(sessionId) ?? this.
|
|
2315
|
+
cwd: this.sessionCwds.get(sessionId) ?? this.activeCwd(),
|
|
1624
2316
|
sessionId,
|
|
1625
2317
|
});
|
|
1626
2318
|
}
|
|
2319
|
+
async appendCdCommand(sessionId, cwd) {
|
|
2320
|
+
const result = await this.store(sessionId).withLease(async (lease) => {
|
|
2321
|
+
const snapshot = await lease.load();
|
|
2322
|
+
if (snapshot.entries.length === 0) {
|
|
2323
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2324
|
+
}
|
|
2325
|
+
const appendResult = await lease.appendMany(snapshot.tail, this.cdCommandEntries(sessionId, cwd, this.logicalTailUuid(snapshot.tail)));
|
|
2326
|
+
if (appendResult.status === 'conflict') {
|
|
2327
|
+
throw new Error(`Claude local command append conflict: ${appendResult.reason}`);
|
|
2328
|
+
}
|
|
2329
|
+
});
|
|
2330
|
+
if (result.status === 'conflict') {
|
|
2331
|
+
throw new Error(`Claude local command conflict: ${result.reason}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
async appendSystemLocalCommand(sessionId, command, args, output) {
|
|
2335
|
+
while (true) {
|
|
2336
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
2337
|
+
const snapshot = await lease.load();
|
|
2338
|
+
if (snapshot.entries.length === 0) {
|
|
2339
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2340
|
+
}
|
|
2341
|
+
const appendResult = await lease.appendMany(snapshot.tail, this.localCommandEntries(sessionId, this.activeCwd(), this.logicalTailUuid(snapshot.tail), command, args, output));
|
|
2342
|
+
if (appendResult.status === 'conflict') {
|
|
2343
|
+
throw new Error(`Claude local command append conflict: ${appendResult.reason}`);
|
|
2344
|
+
}
|
|
2345
|
+
});
|
|
2346
|
+
if (result.status === 'completed')
|
|
2347
|
+
return;
|
|
2348
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
async ensureLocalSession(sessionId, permissionMode) {
|
|
2352
|
+
if (sessionId)
|
|
2353
|
+
return sessionId;
|
|
2354
|
+
this.assertWritable();
|
|
2355
|
+
const createdSessionId = randomUUID();
|
|
2356
|
+
const store = this.turnStore(createdSessionId);
|
|
2357
|
+
const created = await store.create([
|
|
2358
|
+
{ type: 'mode', mode: 'normal', sessionId: createdSessionId },
|
|
2359
|
+
{
|
|
2360
|
+
type: 'permission-mode',
|
|
2361
|
+
permissionMode,
|
|
2362
|
+
sessionId: createdSessionId,
|
|
2363
|
+
},
|
|
2364
|
+
]);
|
|
2365
|
+
if (created.status === 'conflict') {
|
|
2366
|
+
throw new Error(`Session ID ${createdSessionId} is already in use`);
|
|
2367
|
+
}
|
|
2368
|
+
this.sessionCwds.set(createdSessionId, this.activeCwd());
|
|
2369
|
+
this.options.interactiveTools?.restore(createdSessionId, [
|
|
2370
|
+
{ type: 'mode', mode: 'normal', sessionId: createdSessionId },
|
|
2371
|
+
{
|
|
2372
|
+
type: 'permission-mode',
|
|
2373
|
+
permissionMode,
|
|
2374
|
+
sessionId: createdSessionId,
|
|
2375
|
+
},
|
|
2376
|
+
]);
|
|
2377
|
+
return createdSessionId;
|
|
2378
|
+
}
|
|
2379
|
+
async appendInputHistory(display, sessionId) {
|
|
2380
|
+
await mkdir(this.options.configRoot, { recursive: true });
|
|
2381
|
+
await appendFile(join(this.options.configRoot, 'history.jsonl'), `${JSON.stringify({
|
|
2382
|
+
display,
|
|
2383
|
+
pastedContents: {},
|
|
2384
|
+
timestamp: Date.now(),
|
|
2385
|
+
project: this.activeCwd(),
|
|
2386
|
+
sessionId,
|
|
2387
|
+
})}\n`, 'utf8');
|
|
2388
|
+
}
|
|
2389
|
+
async appendBackgroundNotification(sessionId, content) {
|
|
2390
|
+
while (true) {
|
|
2391
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
2392
|
+
const snapshot = await lease.load();
|
|
2393
|
+
if (snapshot.entries.length === 0) {
|
|
2394
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2395
|
+
}
|
|
2396
|
+
const timestamp = new Date().toISOString();
|
|
2397
|
+
const entries = [
|
|
2398
|
+
{
|
|
2399
|
+
type: 'queue-operation',
|
|
2400
|
+
operation: 'enqueue',
|
|
2401
|
+
timestamp,
|
|
2402
|
+
sessionId,
|
|
2403
|
+
content,
|
|
2404
|
+
},
|
|
2405
|
+
{
|
|
2406
|
+
type: 'queue-operation',
|
|
2407
|
+
operation: 'dequeue',
|
|
2408
|
+
timestamp,
|
|
2409
|
+
sessionId,
|
|
2410
|
+
},
|
|
2411
|
+
{
|
|
2412
|
+
parentUuid: this.logicalTailUuid(snapshot.tail),
|
|
2413
|
+
isSidechain: false,
|
|
2414
|
+
promptId: randomUUID(),
|
|
2415
|
+
type: 'user',
|
|
2416
|
+
message: { role: 'user', content },
|
|
2417
|
+
uuid: randomUUID(),
|
|
2418
|
+
timestamp,
|
|
2419
|
+
permissionMode: 'default',
|
|
2420
|
+
origin: { kind: 'task-notification' },
|
|
2421
|
+
promptSource: 'system',
|
|
2422
|
+
userType: 'external',
|
|
2423
|
+
entrypoint: 'cli',
|
|
2424
|
+
cwd: this.activeCwd(),
|
|
2425
|
+
sessionId,
|
|
2426
|
+
version: this.options.claudeVersion,
|
|
2427
|
+
gitBranch: null,
|
|
2428
|
+
},
|
|
2429
|
+
];
|
|
2430
|
+
const appended = await lease.appendMany(snapshot.tail, entries);
|
|
2431
|
+
if (appended.status === 'conflict') {
|
|
2432
|
+
throw new Error(`Claude background notification append conflict: ${appended.reason}`);
|
|
2433
|
+
}
|
|
2434
|
+
});
|
|
2435
|
+
if (result.status === 'completed')
|
|
2436
|
+
return;
|
|
2437
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
2438
|
+
}
|
|
2439
|
+
}
|
|
2440
|
+
enqueueBackgroundNotifications(sessionId, messages) {
|
|
2441
|
+
const previous = this.backgroundNotificationWrites.get(sessionId);
|
|
2442
|
+
const queued = (previous ?? Promise.resolve()).then(async () => {
|
|
2443
|
+
for (const message of messages) {
|
|
2444
|
+
await this.appendBackgroundNotification(sessionId, message);
|
|
2445
|
+
}
|
|
2446
|
+
});
|
|
2447
|
+
this.backgroundNotificationWrites.set(sessionId, queued);
|
|
2448
|
+
const cleanup = () => {
|
|
2449
|
+
if (this.backgroundNotificationWrites.get(sessionId) === queued)
|
|
2450
|
+
this.backgroundNotificationWrites.delete(sessionId);
|
|
2451
|
+
};
|
|
2452
|
+
void queued.then(cleanup, cleanup);
|
|
2453
|
+
return queued;
|
|
2454
|
+
}
|
|
2455
|
+
cdCommandEntries(sessionId, cwd, parentUuid) {
|
|
2456
|
+
const timestamp = new Date().toISOString();
|
|
2457
|
+
const common = {
|
|
2458
|
+
isSidechain: false,
|
|
2459
|
+
timestamp,
|
|
2460
|
+
userType: 'external',
|
|
2461
|
+
entrypoint: 'cli',
|
|
2462
|
+
cwd,
|
|
2463
|
+
sessionId,
|
|
2464
|
+
version: this.options.claudeVersion,
|
|
2465
|
+
gitBranch: null,
|
|
2466
|
+
};
|
|
2467
|
+
const localCommands = this.localCommandEntries(sessionId, cwd, parentUuid, 'cd', cwd, `Moved to ${cwd}`, timestamp);
|
|
2468
|
+
const secondUuid = localCommands[1]?.uuid;
|
|
2469
|
+
if (typeof secondUuid !== 'string') {
|
|
2470
|
+
throw new Error('Claude cd local command pair is incomplete');
|
|
2471
|
+
}
|
|
2472
|
+
return [
|
|
2473
|
+
...localCommands,
|
|
2474
|
+
{
|
|
2475
|
+
...common,
|
|
2476
|
+
parentUuid: secondUuid,
|
|
2477
|
+
promptId: randomUUID(),
|
|
2478
|
+
type: 'user',
|
|
2479
|
+
message: {
|
|
2480
|
+
role: 'user',
|
|
2481
|
+
content: `<system-reminder>\nThe session's working directory has changed to ${cwd} (via /cd). The environment block at the start of this conversation still names the previous directory — that information is stale. All tool calls and relative paths now resolve from ${cwd}.\n</system-reminder>`,
|
|
2482
|
+
},
|
|
2483
|
+
isMeta: true,
|
|
2484
|
+
uuid: randomUUID(),
|
|
2485
|
+
},
|
|
2486
|
+
];
|
|
2487
|
+
}
|
|
2488
|
+
localCommandEntries(sessionId, cwd, parentUuid, command, args, output, timestamp = new Date().toISOString()) {
|
|
2489
|
+
const firstUuid = randomUUID();
|
|
2490
|
+
return [
|
|
2491
|
+
{
|
|
2492
|
+
parentUuid,
|
|
2493
|
+
isSidechain: false,
|
|
2494
|
+
type: 'system',
|
|
2495
|
+
subtype: 'local_command',
|
|
2496
|
+
content: `<command-name>/${command}</command-name>\n <command-message>${command}</command-message>\n <command-args>${args}</command-args>`,
|
|
2497
|
+
level: 'info',
|
|
2498
|
+
timestamp,
|
|
2499
|
+
uuid: firstUuid,
|
|
2500
|
+
isMeta: false,
|
|
2501
|
+
userType: 'external',
|
|
2502
|
+
entrypoint: 'cli',
|
|
2503
|
+
cwd,
|
|
2504
|
+
sessionId,
|
|
2505
|
+
version: this.options.claudeVersion,
|
|
2506
|
+
gitBranch: null,
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
parentUuid: firstUuid,
|
|
2510
|
+
isSidechain: false,
|
|
2511
|
+
type: 'system',
|
|
2512
|
+
subtype: 'local_command',
|
|
2513
|
+
content: `<local-command-stdout>${output}</local-command-stdout>`,
|
|
2514
|
+
level: 'info',
|
|
2515
|
+
timestamp,
|
|
2516
|
+
uuid: randomUUID(),
|
|
2517
|
+
isMeta: false,
|
|
2518
|
+
userType: 'external',
|
|
2519
|
+
entrypoint: 'cli',
|
|
2520
|
+
cwd,
|
|
2521
|
+
sessionId,
|
|
2522
|
+
version: this.options.claudeVersion,
|
|
2523
|
+
gitBranch: null,
|
|
2524
|
+
},
|
|
2525
|
+
];
|
|
2526
|
+
}
|
|
1627
2527
|
store(sessionId) {
|
|
1628
2528
|
const paths = this.paths(sessionId);
|
|
1629
2529
|
return new ClaudeTranscriptStore({
|