praxis-agent 0.2.0 → 0.3.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 +18 -4
- package/dist/application/session-service.d.ts +34 -0
- package/dist/application/session-service.d.ts.map +1 -1
- package/dist/application/session-service.js +646 -38
- package/dist/application/session-service.js.map +1 -1
- package/dist/cli/interactive.d.ts +68 -8
- package/dist/cli/interactive.d.ts.map +1 -1
- package/dist/cli/interactive.js +2617 -206
- 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 +142 -3
- package/dist/cli/tui/claude-style.d.ts.map +1 -1
- package/dist/cli/tui/claude-style.js +377 -16
- package/dist/cli/tui/claude-style.js.map +1 -1
- package/dist/cli/tui/clipboard.d.ts +24 -0
- package/dist/cli/tui/clipboard.d.ts.map +1 -0
- package/dist/cli/tui/clipboard.js +153 -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 +183 -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 +23 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +45 -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 +34 -7
- 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 +4 -3
- 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 { 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;
|
|
@@ -83,8 +92,10 @@ export class ClaudeSessionService {
|
|
|
83
92
|
sessionCwds = new Map();
|
|
84
93
|
hostedSubagents = new Set();
|
|
85
94
|
downloadedFileResourceSessions = new Set();
|
|
95
|
+
runtimeCwd;
|
|
86
96
|
constructor(options) {
|
|
87
97
|
this.options = options;
|
|
98
|
+
this.runtimeCwd = options.workspace?.cwd() ?? options.cwd;
|
|
88
99
|
this.schema = selectClaudeSchemaAdapter(options.claudeVersion);
|
|
89
100
|
this.scheduledPrompts =
|
|
90
101
|
options.tools && (options.scheduledToolNames?.length ?? 0) > 0
|
|
@@ -300,10 +311,18 @@ export class ClaudeSessionService {
|
|
|
300
311
|
this.worktreeManager?.bindSession(sessionId);
|
|
301
312
|
return this.executeTurn(sessionId, prompt, false, signal, name, images, documents);
|
|
302
313
|
}
|
|
314
|
+
async runShell(command, signal, sessionId = randomUUID(), name) {
|
|
315
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
316
|
+
return this.executeTurn(sessionId, `! ${command}`, false, signal, name, [], [], undefined, command);
|
|
317
|
+
}
|
|
303
318
|
async resume(sessionId, prompt, signal, name, images, documents, resumeSessionAt) {
|
|
304
319
|
this.worktreeManager?.bindSession(sessionId);
|
|
305
320
|
return this.executeTurn(sessionId, prompt, true, signal, name, images, documents, resumeSessionAt);
|
|
306
321
|
}
|
|
322
|
+
async resumeShell(sessionId, command, signal, name, resumeSessionAt) {
|
|
323
|
+
this.worktreeManager?.bindSession(sessionId);
|
|
324
|
+
return this.executeTurn(sessionId, `! ${command}`, true, signal, name, [], [], resumeSessionAt, command);
|
|
325
|
+
}
|
|
307
326
|
async promptSuggestion(sessionId, signal) {
|
|
308
327
|
const provider = this.provider();
|
|
309
328
|
const loaded = this.options.sessionPersistence === false
|
|
@@ -355,6 +374,28 @@ export class ClaudeSessionService {
|
|
|
355
374
|
}
|
|
356
375
|
return validPromptSuggestion(suggestion);
|
|
357
376
|
}
|
|
377
|
+
async sessionNameSuggestion(sessionId, signal) {
|
|
378
|
+
const provider = this.provider();
|
|
379
|
+
const loaded = await this.turnStore(sessionId).withLease((lease) => lease.load());
|
|
380
|
+
if (loaded.status === 'conflict' || loaded.value.entries.length === 0) {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
let suggestion = '';
|
|
384
|
+
for await (const event of provider.complete({
|
|
385
|
+
messages: [
|
|
386
|
+
...projectClaudeModelMessages(loaded.value.entries),
|
|
387
|
+
{ role: 'user', content: SESSION_NAME_INSTRUCTION },
|
|
388
|
+
],
|
|
389
|
+
...(this.options.effort ? { effort: this.options.effort } : {}),
|
|
390
|
+
...(signal ? { signal } : {}),
|
|
391
|
+
})) {
|
|
392
|
+
if (event.type === 'text-delta')
|
|
393
|
+
suggestion += event.delta;
|
|
394
|
+
if (event.type === 'tool-call')
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
return validSessionName(suggestion);
|
|
398
|
+
}
|
|
358
399
|
async sessions() {
|
|
359
400
|
const paths = this.paths(randomUUID());
|
|
360
401
|
let names;
|
|
@@ -456,6 +497,317 @@ export class ClaudeSessionService {
|
|
|
456
497
|
throw error;
|
|
457
498
|
}
|
|
458
499
|
}
|
|
500
|
+
async transcript(sessionId, resumeSessionAt) {
|
|
501
|
+
try {
|
|
502
|
+
const recovery = await this.store(sessionId).loadReadOnly();
|
|
503
|
+
if (recovery.entries.length === 0) {
|
|
504
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
505
|
+
}
|
|
506
|
+
const entries = resumeSessionAt === undefined
|
|
507
|
+
? recovery.entries
|
|
508
|
+
: selectClaudeTranscriptAtMessage(recovery.entries, resumeSessionAt);
|
|
509
|
+
return projectClaudeDisplayTranscript(entries);
|
|
510
|
+
}
|
|
511
|
+
catch (error) {
|
|
512
|
+
if (error.code === 'ENOENT') {
|
|
513
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
514
|
+
}
|
|
515
|
+
throw error;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
async rename(sessionId, name) {
|
|
519
|
+
this.assertWritable();
|
|
520
|
+
const normalized = name.trim();
|
|
521
|
+
if (!normalized)
|
|
522
|
+
throw new Error('Session name must not be empty');
|
|
523
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
524
|
+
const snapshot = await lease.load();
|
|
525
|
+
if (snapshot.entries.length === 0) {
|
|
526
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
527
|
+
}
|
|
528
|
+
if (this.hasSessionName(snapshot.entries, normalized))
|
|
529
|
+
return;
|
|
530
|
+
const appendResult = await lease.appendMany(snapshot.tail, this.sessionNameEntries(sessionId, normalized));
|
|
531
|
+
if (appendResult.status === 'conflict') {
|
|
532
|
+
throw new Error(`Claude transcript rename conflict: ${appendResult.reason}`);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
if (result.status === 'conflict') {
|
|
536
|
+
throw new Error(`Claude transcript rename conflict: ${result.reason}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
async changeCwd(sessionId, requestedCwd) {
|
|
540
|
+
this.assertWritable();
|
|
541
|
+
const previousCwd = this.activeCwd();
|
|
542
|
+
const expandedCwd = requestedCwd === '~'
|
|
543
|
+
? homedir()
|
|
544
|
+
: requestedCwd.startsWith('~/')
|
|
545
|
+
? join(homedir(), requestedCwd.slice(2))
|
|
546
|
+
: requestedCwd;
|
|
547
|
+
const cwd = await realpath(isAbsolute(expandedCwd) ? expandedCwd : join(previousCwd, expandedCwd));
|
|
548
|
+
if (!(await stat(cwd)).isDirectory()) {
|
|
549
|
+
throw new Error(`Not a directory: ${requestedCwd}`);
|
|
550
|
+
}
|
|
551
|
+
if (sessionId && this.options.sessionPersistence !== false) {
|
|
552
|
+
const sourcePaths = resolveClaudePaths({
|
|
553
|
+
configDir: this.options.configRoot,
|
|
554
|
+
cwd: this.sessionCwds.get(sessionId) ?? previousCwd,
|
|
555
|
+
sessionId,
|
|
556
|
+
});
|
|
557
|
+
const targetPaths = resolveClaudePaths({
|
|
558
|
+
configDir: this.options.configRoot,
|
|
559
|
+
cwd,
|
|
560
|
+
sessionId,
|
|
561
|
+
});
|
|
562
|
+
if (sourcePaths.sessionFile !== targetPaths.sessionFile) {
|
|
563
|
+
try {
|
|
564
|
+
await lstat(targetPaths.sessionFile);
|
|
565
|
+
throw new Error(`Claude transcript already exists at relocation target: ${targetPaths.sessionFile}`);
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
if (error.code !== 'ENOENT')
|
|
569
|
+
throw error;
|
|
570
|
+
}
|
|
571
|
+
const sourceStore = this.store(sessionId);
|
|
572
|
+
const relocated = await sourceStore.withLease(async (lease) => {
|
|
573
|
+
const snapshot = await lease.load();
|
|
574
|
+
if (snapshot.entries.length === 0) {
|
|
575
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
576
|
+
}
|
|
577
|
+
await mkdir(targetPaths.projectRoot, { recursive: true });
|
|
578
|
+
const relocationId = randomUUID();
|
|
579
|
+
const stagingFile = join(targetPaths.projectRoot, `.${sessionId}.${relocationId}.relocating`);
|
|
580
|
+
const stagingStore = new ClaudeTranscriptStore({
|
|
581
|
+
sessionFile: stagingFile,
|
|
582
|
+
lockFile: join(targetPaths.praxisRoot, 'locks', `${sessionId}.${relocationId}.lock`),
|
|
583
|
+
schema: this.schema,
|
|
584
|
+
});
|
|
585
|
+
let publishedIdentity;
|
|
586
|
+
try {
|
|
587
|
+
await copyFile(sourcePaths.sessionFile, stagingFile);
|
|
588
|
+
const staged = await stagingStore.withLease((stagingLease) => stagingLease.appendMany(snapshot.tail, [
|
|
589
|
+
{
|
|
590
|
+
type: 'relocated',
|
|
591
|
+
sessionId,
|
|
592
|
+
relocatedCwd: cwd,
|
|
593
|
+
},
|
|
594
|
+
...this.cdCommandEntries(sessionId, cwd, this.logicalTailUuid(snapshot.tail)),
|
|
595
|
+
]));
|
|
596
|
+
if (staged.status === 'conflict') {
|
|
597
|
+
throw new Error(`Claude transcript relocation conflict: ${staged.reason}`);
|
|
598
|
+
}
|
|
599
|
+
if (staged.value.status === 'conflict') {
|
|
600
|
+
throw new Error(`Claude transcript relocation conflict: ${staged.value.reason}`);
|
|
601
|
+
}
|
|
602
|
+
const stagingMetadata = await lstat(stagingFile);
|
|
603
|
+
await link(stagingFile, targetPaths.sessionFile);
|
|
604
|
+
publishedIdentity = {
|
|
605
|
+
dev: stagingMetadata.dev,
|
|
606
|
+
ino: stagingMetadata.ino,
|
|
607
|
+
};
|
|
608
|
+
await unlink(stagingFile);
|
|
609
|
+
await unlink(sourcePaths.sessionFile);
|
|
610
|
+
}
|
|
611
|
+
catch (error) {
|
|
612
|
+
await unlink(stagingFile).catch(() => undefined);
|
|
613
|
+
if (publishedIdentity) {
|
|
614
|
+
const targetMetadata = await lstat(targetPaths.sessionFile).catch(() => undefined);
|
|
615
|
+
if (targetMetadata?.dev === publishedIdentity.dev &&
|
|
616
|
+
targetMetadata.ino === publishedIdentity.ino) {
|
|
617
|
+
await unlink(targetPaths.sessionFile).catch(() => undefined);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
if (relocated.status === 'conflict') {
|
|
624
|
+
throw new Error(`Claude transcript relocation conflict: ${relocated.reason}`);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
else {
|
|
628
|
+
await this.appendCdCommand(sessionId, cwd);
|
|
629
|
+
}
|
|
630
|
+
this.sessionCwds.set(sessionId, cwd);
|
|
631
|
+
this.runtimeCwd = cwd;
|
|
632
|
+
this.options.workspace?.setCwd(cwd);
|
|
633
|
+
return cwd;
|
|
634
|
+
}
|
|
635
|
+
this.runtimeCwd = cwd;
|
|
636
|
+
this.options.workspace?.setCwd(cwd);
|
|
637
|
+
if (sessionId)
|
|
638
|
+
this.sessionCwds.set(sessionId, cwd);
|
|
639
|
+
return cwd;
|
|
640
|
+
}
|
|
641
|
+
async recordCdUsage(sessionId) {
|
|
642
|
+
this.assertWritable();
|
|
643
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
644
|
+
const snapshot = await lease.load();
|
|
645
|
+
if (snapshot.entries.length === 0) {
|
|
646
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
647
|
+
}
|
|
648
|
+
const promptId = randomUUID();
|
|
649
|
+
const firstUuid = randomUUID();
|
|
650
|
+
const timestamp = new Date().toISOString();
|
|
651
|
+
const common = {
|
|
652
|
+
isSidechain: false,
|
|
653
|
+
promptId,
|
|
654
|
+
timestamp,
|
|
655
|
+
userType: 'external',
|
|
656
|
+
entrypoint: 'cli',
|
|
657
|
+
cwd: this.activeCwd(),
|
|
658
|
+
sessionId,
|
|
659
|
+
version: this.options.claudeVersion,
|
|
660
|
+
gitBranch: null,
|
|
661
|
+
};
|
|
662
|
+
const appendResult = await lease.appendMany(snapshot.tail, [
|
|
663
|
+
{
|
|
664
|
+
...common,
|
|
665
|
+
parentUuid: this.logicalTailUuid(snapshot.tail),
|
|
666
|
+
type: 'user',
|
|
667
|
+
message: {
|
|
668
|
+
role: 'user',
|
|
669
|
+
content: '<command-name>/cd</command-name>\n <command-message>cd</command-message>\n <command-args></command-args>',
|
|
670
|
+
},
|
|
671
|
+
uuid: firstUuid,
|
|
672
|
+
},
|
|
673
|
+
{
|
|
674
|
+
...common,
|
|
675
|
+
parentUuid: firstUuid,
|
|
676
|
+
type: 'user',
|
|
677
|
+
message: {
|
|
678
|
+
role: 'user',
|
|
679
|
+
content: '<local-command-stdout>Usage: /cd <path></local-command-stdout>',
|
|
680
|
+
},
|
|
681
|
+
uuid: randomUUID(),
|
|
682
|
+
},
|
|
683
|
+
]);
|
|
684
|
+
if (appendResult.status === 'conflict') {
|
|
685
|
+
throw new Error(`Claude cd usage append conflict: ${appendResult.reason}`);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
if (result.status === 'conflict') {
|
|
689
|
+
throw new Error(`Claude cd usage conflict: ${result.reason}`);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
async compact(sessionId, signal, selection) {
|
|
693
|
+
this.assertWritable();
|
|
694
|
+
const provider = this.provider();
|
|
695
|
+
const result = await this.turnStore(sessionId).withLease(async (lease) => {
|
|
696
|
+
const snapshot = await lease.load();
|
|
697
|
+
if (snapshot.entries.length === 0) {
|
|
698
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
699
|
+
}
|
|
700
|
+
const activeEntries = selectClaudeActiveTranscript(snapshot.entries);
|
|
701
|
+
const allMessages = projectClaudeModelMessages(activeEntries);
|
|
702
|
+
let selectedEntries = activeEntries;
|
|
703
|
+
let logicalParentUuid = this.lastMessageUuid(activeEntries);
|
|
704
|
+
let preservedEntries = [];
|
|
705
|
+
if (selection) {
|
|
706
|
+
const targetIndex = activeEntries.findIndex((entry) => entry.uuid === selection.messageId);
|
|
707
|
+
if (targetIndex < 0) {
|
|
708
|
+
throw new Error(`No rewind point found with message.uuid of: ${selection.messageId}`);
|
|
709
|
+
}
|
|
710
|
+
if (selection.direction === 'from') {
|
|
711
|
+
selectedEntries = activeEntries.slice(targetIndex);
|
|
712
|
+
preservedEntries = activeEntries.slice(0, targetIndex);
|
|
713
|
+
logicalParentUuid = this.lastMessageUuid(preservedEntries);
|
|
714
|
+
if (!logicalParentUuid)
|
|
715
|
+
logicalParentUuid = selection.messageId;
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
selectedEntries = activeEntries.slice(0, targetIndex);
|
|
719
|
+
preservedEntries = activeEntries.slice(targetIndex);
|
|
720
|
+
const target = activeEntries[targetIndex];
|
|
721
|
+
logicalParentUuid =
|
|
722
|
+
typeof target?.parentUuid === 'string'
|
|
723
|
+
? target.parentUuid
|
|
724
|
+
: this.lastMessageUuid(selectedEntries);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
const selectedMessages = projectClaudeModelMessages(selectedEntries);
|
|
728
|
+
const messages = [
|
|
729
|
+
...selectedMessages,
|
|
730
|
+
...(selection?.context
|
|
731
|
+
? [
|
|
732
|
+
{
|
|
733
|
+
role: 'user',
|
|
734
|
+
content: `Additional summarization context: ${selection.context}`,
|
|
735
|
+
},
|
|
736
|
+
]
|
|
737
|
+
: []),
|
|
738
|
+
];
|
|
739
|
+
if (!logicalParentUuid || selectedMessages.length === 0) {
|
|
740
|
+
throw new Error('Cannot compact an empty Claude transcript');
|
|
741
|
+
}
|
|
742
|
+
if (findUnresolvedClaudeToolCalls(snapshot.entries).length > 0) {
|
|
743
|
+
throw new Error('Cannot compact a Claude session with unresolved tool calls');
|
|
744
|
+
}
|
|
745
|
+
this.options.eventSink?.({ type: 'state', state: 'compacting' });
|
|
746
|
+
const contextWindowTokens = this.contextBudget(provider)?.contextWindowTokens ??
|
|
747
|
+
provider.capabilities.contextWindowTokens ??
|
|
748
|
+
200_000;
|
|
749
|
+
const compacted = await (this.options.compactor ?? new ModelCompactor(provider)).compact({
|
|
750
|
+
messages,
|
|
751
|
+
targetTokens: Math.min(8192, Math.max(1, Math.floor(contextWindowTokens / 4))),
|
|
752
|
+
contextWindowTokens,
|
|
753
|
+
...(signal ? { signal } : {}),
|
|
754
|
+
});
|
|
755
|
+
if (signal?.aborted)
|
|
756
|
+
throw new AgentRunCancelledError();
|
|
757
|
+
const preTokens = estimateModelRequestTokens(allMessages);
|
|
758
|
+
const summary = compacted.summary;
|
|
759
|
+
const preservedMessages = projectClaudeModelMessages(preservedEntries);
|
|
760
|
+
const boundaryUuid = randomUUID();
|
|
761
|
+
const summaryUuid = randomUUID();
|
|
762
|
+
const uuids = [boundaryUuid, summaryUuid];
|
|
763
|
+
const entries = createClaudeCompactEntries({
|
|
764
|
+
sessionId,
|
|
765
|
+
logicalParentUuid,
|
|
766
|
+
summary,
|
|
767
|
+
preTokens,
|
|
768
|
+
postTokens: estimateModelRequestTokens([
|
|
769
|
+
{ role: 'user', content: formatClaudeCompactSummary(summary) },
|
|
770
|
+
...preservedMessages,
|
|
771
|
+
]),
|
|
772
|
+
previousCumulativeDroppedTokens: getCumulativeDroppedTokens(snapshot.entries),
|
|
773
|
+
durationMs: compacted.durationMs,
|
|
774
|
+
cwd: this.activeCwd(),
|
|
775
|
+
claudeVersion: this.options.claudeVersion,
|
|
776
|
+
gitBranch: null,
|
|
777
|
+
trigger: 'manual',
|
|
778
|
+
...(selection
|
|
779
|
+
? {
|
|
780
|
+
summarizeMetadata: {
|
|
781
|
+
messagesSummarized: selectedMessages.length,
|
|
782
|
+
direction: selection.direction === 'to' ? 'up_to' : selection.direction,
|
|
783
|
+
},
|
|
784
|
+
preservedUuids: preservedEntries.flatMap((entry) => typeof entry.uuid === 'string' ? [entry.uuid] : []),
|
|
785
|
+
}
|
|
786
|
+
: {}),
|
|
787
|
+
createUuid: () => uuids.shift() ?? randomUUID(),
|
|
788
|
+
});
|
|
789
|
+
const appendResult = await lease.appendMany(snapshot.tail, entries);
|
|
790
|
+
if (appendResult.status === 'conflict') {
|
|
791
|
+
throw new Error(`Claude transcript append conflict: ${appendResult.reason}`);
|
|
792
|
+
}
|
|
793
|
+
this.options.eventSink?.({
|
|
794
|
+
type: 'compact-boundary',
|
|
795
|
+
trigger: 'manual',
|
|
796
|
+
preTokens,
|
|
797
|
+
uuid: boundaryUuid,
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
summary,
|
|
801
|
+
usage: compacted.usage,
|
|
802
|
+
preTokens,
|
|
803
|
+
...(selection ? { messagesSummarized: selectedMessages.length } : {}),
|
|
804
|
+
};
|
|
805
|
+
});
|
|
806
|
+
if (result.status === 'conflict') {
|
|
807
|
+
throw new Error(`Claude transcript compact conflict: ${result.reason}`);
|
|
808
|
+
}
|
|
809
|
+
return result.value;
|
|
810
|
+
}
|
|
459
811
|
async fork(parentSessionId, sessionId = randomUUID(), resumeSessionAt) {
|
|
460
812
|
this.assertSessionPersistence();
|
|
461
813
|
this.assertWritable();
|
|
@@ -496,13 +848,100 @@ export class ClaudeSessionService {
|
|
|
496
848
|
throw new Error(`Claude file rewind conflict: ${result.reason}`);
|
|
497
849
|
}
|
|
498
850
|
}
|
|
499
|
-
async
|
|
851
|
+
async rewindPoints(sessionId) {
|
|
852
|
+
const result = await this.turnStore(sessionId).withLease((lease) => lease.load());
|
|
853
|
+
if (result.status === 'conflict') {
|
|
854
|
+
throw new Error(`Claude transcript rewind conflict: ${result.reason}`);
|
|
855
|
+
}
|
|
856
|
+
if (result.value.entries.length === 0) {
|
|
857
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
858
|
+
}
|
|
859
|
+
const active = selectClaudeActiveTranscript(result.value.entries);
|
|
860
|
+
const snapshots = new Set(active
|
|
861
|
+
.filter((entry) => entry.type === 'file-history-snapshot' &&
|
|
862
|
+
typeof entry.messageId === 'string')
|
|
863
|
+
.map((entry) => entry.messageId));
|
|
864
|
+
const changes = new Map();
|
|
865
|
+
for (const entry of active) {
|
|
866
|
+
if (entry.type !== 'file-history-delta' ||
|
|
867
|
+
typeof entry.snapshotMessageId !== 'string' ||
|
|
868
|
+
typeof entry.trackingPath !== 'string') {
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
const paths = changes.get(entry.snapshotMessageId) ?? new Set();
|
|
872
|
+
paths.add(entry.trackingPath);
|
|
873
|
+
changes.set(entry.snapshotMessageId, paths);
|
|
874
|
+
}
|
|
875
|
+
const points = [];
|
|
876
|
+
let branchMessageId;
|
|
877
|
+
for (const entry of active) {
|
|
878
|
+
if ((entry.type === 'user' || entry.type === 'assistant') &&
|
|
879
|
+
typeof entry.uuid === 'string') {
|
|
880
|
+
const message = entry.message;
|
|
881
|
+
if (typeof message === 'object' &&
|
|
882
|
+
message !== null &&
|
|
883
|
+
!Array.isArray(message)) {
|
|
884
|
+
const role = message.role;
|
|
885
|
+
const content = message.content;
|
|
886
|
+
if (entry.type === 'user' &&
|
|
887
|
+
role === 'user' &&
|
|
888
|
+
typeof content === 'string' &&
|
|
889
|
+
entry.isCompactSummary !== true &&
|
|
890
|
+
entry.isMeta !== true &&
|
|
891
|
+
typeof entry.sourceToolAssistantUUID !== 'string' &&
|
|
892
|
+
!content.startsWith('<bash-input>') &&
|
|
893
|
+
!content.startsWith('<local-command-') &&
|
|
894
|
+
!content.startsWith('<command-name>')) {
|
|
895
|
+
points.push({
|
|
896
|
+
messageId: entry.uuid,
|
|
897
|
+
prompt: content,
|
|
898
|
+
...(typeof entry.timestamp === 'string'
|
|
899
|
+
? { timestamp: entry.timestamp }
|
|
900
|
+
: {}),
|
|
901
|
+
...(branchMessageId === undefined ? {} : { branchMessageId }),
|
|
902
|
+
fileChanges: [...(changes.get(entry.uuid) ?? [])],
|
|
903
|
+
fileRestoreAvailable: snapshots.has(entry.uuid),
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
if (role === 'user' || role === 'assistant') {
|
|
907
|
+
branchMessageId = entry.uuid;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return points;
|
|
913
|
+
}
|
|
914
|
+
async setPermissionMode(sessionId, permissionMode) {
|
|
915
|
+
this.assertSessionPersistence();
|
|
916
|
+
this.assertWritable();
|
|
917
|
+
const result = await this.store(sessionId).withLease(async (lease) => {
|
|
918
|
+
const snapshot = await lease.load();
|
|
919
|
+
if (snapshot.entries.length === 0) {
|
|
920
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
921
|
+
}
|
|
922
|
+
this.options.interactiveTools?.restore(sessionId, snapshot.entries);
|
|
923
|
+
const entry = {
|
|
924
|
+
type: 'permission-mode',
|
|
925
|
+
permissionMode,
|
|
926
|
+
sessionId,
|
|
927
|
+
};
|
|
928
|
+
await this.append(lease, snapshot.tail, entry);
|
|
929
|
+
await this.options.interactiveTools?.setMode(sessionId, permissionMode);
|
|
930
|
+
});
|
|
931
|
+
if (result.status === 'conflict') {
|
|
932
|
+
throw new Error(`Claude permission mode update conflict: ${result.reason}`);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async executeTurn(sessionId, prompt, requireExisting, signal, name, images = [], documents = [], resumeSessionAt, shellCommand) {
|
|
500
936
|
this.assertWritable();
|
|
501
937
|
if (prompt.length === 0 && images.length === 0 && documents.length === 0)
|
|
502
938
|
throw new Error('Prompt must not be empty');
|
|
503
939
|
if (name !== undefined && name.length === 0) {
|
|
504
940
|
throw new Error('Session name must not be empty');
|
|
505
941
|
}
|
|
942
|
+
if (shellCommand !== undefined && shellCommand.trim().length === 0) {
|
|
943
|
+
throw new Error('Shell command must not be empty');
|
|
944
|
+
}
|
|
506
945
|
await this.ensureFileResources(sessionId, signal);
|
|
507
946
|
this.worktreeManager?.bindSession(sessionId);
|
|
508
947
|
if (this.options.initialWorktree) {
|
|
@@ -575,7 +1014,8 @@ export class ClaudeSessionService {
|
|
|
575
1014
|
if (this.options.maxBudgetUsd !== undefined && !initialPricing) {
|
|
576
1015
|
throw new Error(`Cannot enforce --max-budget-usd: no pricing is configured for model ${provider.model ?? 'praxis/provider'}`);
|
|
577
1016
|
}
|
|
578
|
-
|
|
1017
|
+
const shellInputUuid = shellCommand === undefined ? null : randomUUID();
|
|
1018
|
+
let currentPromptId = shellInputUuid;
|
|
579
1019
|
let lastAssistantUuid = null;
|
|
580
1020
|
const fileHistory = this.options.fileCheckpointing &&
|
|
581
1021
|
this.options.sessionPersistence !== false
|
|
@@ -1047,9 +1487,13 @@ export class ClaudeSessionService {
|
|
|
1047
1487
|
]
|
|
1048
1488
|
: []),
|
|
1049
1489
|
];
|
|
1050
|
-
const expansion =
|
|
1051
|
-
?
|
|
1052
|
-
|
|
1490
|
+
const expansion = shellCommand === undefined
|
|
1491
|
+
? this.options.extensions
|
|
1492
|
+
? await this.options.extensions.expandPromptAsync(prompt, signal, toolResultDirectory)
|
|
1493
|
+
: { userMessages: [prompt] }
|
|
1494
|
+
: {
|
|
1495
|
+
userMessages: [`<bash-input>${shellCommand}</bash-input>`],
|
|
1496
|
+
};
|
|
1053
1497
|
const expandedMessages = expansion.messages ?? expansion.userMessages.map((text) => ({ text }));
|
|
1054
1498
|
const attachmentIndex = expansion.messages
|
|
1055
1499
|
? expandedMessages.length - 1
|
|
@@ -1086,7 +1530,37 @@ export class ClaudeSessionService {
|
|
|
1086
1530
|
? { documents }
|
|
1087
1531
|
: {}),
|
|
1088
1532
|
}));
|
|
1533
|
+
const agentMentionMessages = shellCommand === undefined
|
|
1534
|
+
? (this.options.extensions?.agentMentionMessages(prompt) ?? [])
|
|
1535
|
+
: [];
|
|
1536
|
+
const injectAgentMentionContext = (messages) => {
|
|
1537
|
+
if (agentMentionMessages.length === 0)
|
|
1538
|
+
return [...messages];
|
|
1539
|
+
let insertionIndex = messages.length;
|
|
1540
|
+
let foundPrompt = false;
|
|
1541
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
1542
|
+
const message = messages[index];
|
|
1543
|
+
if (message?.role === 'user' &&
|
|
1544
|
+
typeof message.content === 'string' &&
|
|
1545
|
+
message.content.endsWith(prompt)) {
|
|
1546
|
+
insertionIndex = index;
|
|
1547
|
+
foundPrompt = true;
|
|
1548
|
+
break;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
if (!foundPrompt)
|
|
1552
|
+
return [...messages];
|
|
1553
|
+
return [
|
|
1554
|
+
...messages.slice(0, insertionIndex),
|
|
1555
|
+
...agentMentionMessages.map((content) => ({
|
|
1556
|
+
role: 'user',
|
|
1557
|
+
content,
|
|
1558
|
+
})),
|
|
1559
|
+
...messages.slice(insertionIndex),
|
|
1560
|
+
];
|
|
1561
|
+
};
|
|
1089
1562
|
const injectDynamicContext = (messages) => injectFirstUserMessageContext(messages, assembledContext?.firstUserMessageContext);
|
|
1563
|
+
const injectTurnContext = (messages) => injectAgentMentionContext(injectDynamicContext(messages));
|
|
1090
1564
|
let compactionAnchorUuid = this.lastMessageUuid(snapshot.entries);
|
|
1091
1565
|
const compactIfNeeded = async (pendingMessages = [], preservedUserMessages = []) => {
|
|
1092
1566
|
if (!budget)
|
|
@@ -1094,13 +1568,13 @@ export class ClaudeSessionService {
|
|
|
1094
1568
|
const historyMessages = projectClaudeModelMessages(snapshot.entries);
|
|
1095
1569
|
const predicted = budget.evaluate([
|
|
1096
1570
|
...contextMessages,
|
|
1097
|
-
...
|
|
1571
|
+
...injectTurnContext([...historyMessages, ...pendingMessages]),
|
|
1098
1572
|
], definitions);
|
|
1099
1573
|
if (!predicted.shouldCompact)
|
|
1100
1574
|
return;
|
|
1101
1575
|
const irreducibleMessages = [
|
|
1102
1576
|
...contextMessages,
|
|
1103
|
-
...
|
|
1577
|
+
...injectTurnContext([
|
|
1104
1578
|
...pendingMessages,
|
|
1105
1579
|
...preservedUserMessages.map((content) => ({
|
|
1106
1580
|
role: 'user',
|
|
@@ -1146,7 +1620,7 @@ export class ClaudeSessionService {
|
|
|
1146
1620
|
const boundaryUuid = randomUUID();
|
|
1147
1621
|
const summaryUuid = randomUUID();
|
|
1148
1622
|
const timestamp = new Date().toISOString();
|
|
1149
|
-
const preTokens = budget.evaluate([...contextMessages, ...
|
|
1623
|
+
const preTokens = budget.evaluate([...contextMessages, ...injectTurnContext(historyMessages)], definitions).estimatedTokens;
|
|
1150
1624
|
const compactEntries = (postTokens) => {
|
|
1151
1625
|
const uuids = [boundaryUuid, summaryUuid];
|
|
1152
1626
|
return createClaudeCompactEntries({
|
|
@@ -1187,13 +1661,10 @@ export class ClaudeSessionService {
|
|
|
1187
1661
|
...provisionalEntries,
|
|
1188
1662
|
...replayEntries,
|
|
1189
1663
|
]);
|
|
1190
|
-
const afterHistory = budget.evaluate([...contextMessages, ...
|
|
1664
|
+
const afterHistory = budget.evaluate([...contextMessages, ...injectTurnContext(compactedHistory)], definitions);
|
|
1191
1665
|
const afterPending = budget.evaluate([
|
|
1192
1666
|
...contextMessages,
|
|
1193
|
-
...
|
|
1194
|
-
...compactedHistory,
|
|
1195
|
-
...pendingMessages,
|
|
1196
|
-
]),
|
|
1667
|
+
...injectTurnContext([...compactedHistory, ...pendingMessages]),
|
|
1197
1668
|
], definitions);
|
|
1198
1669
|
budget.assertFits(afterPending);
|
|
1199
1670
|
const entries = [
|
|
@@ -1224,25 +1695,26 @@ export class ClaudeSessionService {
|
|
|
1224
1695
|
};
|
|
1225
1696
|
await compactIfNeeded(pendingUserMessages);
|
|
1226
1697
|
for (const [index, message] of expandedMessages.entries()) {
|
|
1698
|
+
if (shellCommand !== undefined)
|
|
1699
|
+
break;
|
|
1227
1700
|
const messageImages = [
|
|
1228
1701
|
...(index === attachmentIndex ? images : []),
|
|
1229
1702
|
...(message.images ?? []),
|
|
1230
1703
|
];
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
:
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
], this.translationContext(sessionId, snapshot));
|
|
1704
|
+
const persistenceEvent = messageImages.length > 0 ||
|
|
1705
|
+
(index === attachmentIndex && documents.length > 0)
|
|
1706
|
+
? {
|
|
1707
|
+
type: 'user-message',
|
|
1708
|
+
text: message.text,
|
|
1709
|
+
images: messageImages,
|
|
1710
|
+
...(index === attachmentIndex && documents.length > 0
|
|
1711
|
+
? { documents }
|
|
1712
|
+
: {}),
|
|
1713
|
+
}
|
|
1714
|
+
: index === 0
|
|
1715
|
+
? { type: 'user-text', text: message.text }
|
|
1716
|
+
: { type: 'user-text-block', text: message.text };
|
|
1717
|
+
const [userEntry] = translateProviderEvents([persistenceEvent], this.translationContext(sessionId, snapshot));
|
|
1246
1718
|
if (!userEntry)
|
|
1247
1719
|
throw new Error('Could not translate user prompt');
|
|
1248
1720
|
if (currentPromptId === null && typeof userEntry.uuid === 'string') {
|
|
@@ -1275,18 +1747,90 @@ export class ClaudeSessionService {
|
|
|
1275
1747
|
throw new Error(`UserPromptSubmit hook error: ${outcome.blockedReason}`);
|
|
1276
1748
|
}
|
|
1277
1749
|
}
|
|
1750
|
+
let shellUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1751
|
+
if (shellCommand !== undefined) {
|
|
1752
|
+
const call = {
|
|
1753
|
+
id: `shell_${randomUUID().replaceAll('-', '')}`,
|
|
1754
|
+
name: 'Bash',
|
|
1755
|
+
input: { command: shellCommand },
|
|
1756
|
+
};
|
|
1757
|
+
this.options.eventSink?.({
|
|
1758
|
+
type: 'shell-command',
|
|
1759
|
+
callId: call.id,
|
|
1760
|
+
command: shellCommand,
|
|
1761
|
+
});
|
|
1762
|
+
let shellResult;
|
|
1763
|
+
try {
|
|
1764
|
+
shellResult = await runtime.executeDirectToolCall(call, {
|
|
1765
|
+
cwd: this.activeCwd(),
|
|
1766
|
+
toolResultDirectory,
|
|
1767
|
+
messages: projectClaudeModelMessages(snapshot.entries),
|
|
1768
|
+
observer: {
|
|
1769
|
+
assistantCompleted: async () => undefined,
|
|
1770
|
+
toolCompleted: async () => undefined,
|
|
1771
|
+
followUpUserMessagesCompleted: observer.followUpUserMessagesCompleted,
|
|
1772
|
+
},
|
|
1773
|
+
...(this.options.approveTool
|
|
1774
|
+
? { approveTool: this.options.approveTool }
|
|
1775
|
+
: {}),
|
|
1776
|
+
...(signal ? { signal } : {}),
|
|
1777
|
+
});
|
|
1778
|
+
}
|
|
1779
|
+
catch (error) {
|
|
1780
|
+
this.options.eventSink?.({
|
|
1781
|
+
type: 'shell-cancelled',
|
|
1782
|
+
callId: call.id,
|
|
1783
|
+
});
|
|
1784
|
+
throw error;
|
|
1785
|
+
}
|
|
1786
|
+
shellUsage = shellResult.usage ?? shellUsage;
|
|
1787
|
+
const stdout = shellResult.processOutput?.stdout ??
|
|
1788
|
+
(shellResult.isError ? '' : shellResult.content);
|
|
1789
|
+
const stderr = shellResult.processOutput?.stderr ??
|
|
1790
|
+
(shellResult.isError ? shellResult.content : '');
|
|
1791
|
+
const shellUuids = [shellInputUuid ?? randomUUID(), randomUUID()];
|
|
1792
|
+
const [inputEntry, outputEntry] = translateProviderEvents([
|
|
1793
|
+
{ type: 'bash-input', command: shellCommand },
|
|
1794
|
+
{ type: 'bash-output', stdout, stderr },
|
|
1795
|
+
], {
|
|
1796
|
+
...this.translationContext(sessionId, snapshot),
|
|
1797
|
+
createUuid: () => shellUuids.shift() ?? randomUUID(),
|
|
1798
|
+
});
|
|
1799
|
+
if (!inputEntry || !outputEntry) {
|
|
1800
|
+
throw new Error('Could not translate shell command result');
|
|
1801
|
+
}
|
|
1802
|
+
const shellAppend = await lease.appendMany(snapshot.tail, [
|
|
1803
|
+
inputEntry,
|
|
1804
|
+
outputEntry,
|
|
1805
|
+
]);
|
|
1806
|
+
if (shellAppend.status === 'conflict') {
|
|
1807
|
+
throw new Error(`Claude transcript append conflict: ${shellAppend.reason}`);
|
|
1808
|
+
}
|
|
1809
|
+
snapshot = {
|
|
1810
|
+
entries: [...snapshot.entries, inputEntry, outputEntry],
|
|
1811
|
+
tail: shellAppend.tail,
|
|
1812
|
+
};
|
|
1813
|
+
currentTurnUserMessages.push(`<bash-stdout>${stdout}</bash-stdout><bash-stderr>${stderr}</bash-stderr>`);
|
|
1814
|
+
this.options.eventSink?.({
|
|
1815
|
+
type: 'shell-result',
|
|
1816
|
+
callId: call.id,
|
|
1817
|
+
stdout,
|
|
1818
|
+
stderr,
|
|
1819
|
+
isError: shellResult.isError,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1278
1822
|
if (budget) {
|
|
1279
1823
|
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
1280
1824
|
budget.assertFits(budget.evaluate([
|
|
1281
1825
|
...contextMessages,
|
|
1282
|
-
...
|
|
1826
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1283
1827
|
], definitions));
|
|
1284
1828
|
}
|
|
1285
1829
|
let stopHookActive = false;
|
|
1286
1830
|
const runtimeRequest = {
|
|
1287
1831
|
messages: [
|
|
1288
1832
|
...contextMessages,
|
|
1289
|
-
...
|
|
1833
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1290
1834
|
],
|
|
1291
1835
|
cwd: this.activeCwd(),
|
|
1292
1836
|
toolResultDirectory,
|
|
@@ -1301,7 +1845,7 @@ export class ClaudeSessionService {
|
|
|
1301
1845
|
await compactIfNeeded([], currentTurnUserMessages ?? []);
|
|
1302
1846
|
return [
|
|
1303
1847
|
...contextMessages,
|
|
1304
|
-
...
|
|
1848
|
+
...injectTurnContext(projectClaudeModelMessages(snapshot.entries)),
|
|
1305
1849
|
];
|
|
1306
1850
|
},
|
|
1307
1851
|
...(this.options.hooks ||
|
|
@@ -1373,7 +1917,7 @@ export class ClaudeSessionService {
|
|
|
1373
1917
|
lastPrompt: prompt,
|
|
1374
1918
|
leafUuid: finalLeafUuid,
|
|
1375
1919
|
}));
|
|
1376
|
-
const totalUsage = mergeUsage(mergeUsage(recoveryUsage, compactionUsage), result.usage);
|
|
1920
|
+
const totalUsage = mergeUsage(mergeUsage(mergeUsage(recoveryUsage, compactionUsage), shellUsage), result.usage);
|
|
1377
1921
|
return {
|
|
1378
1922
|
sessionId,
|
|
1379
1923
|
text: structuredCapture && structuredCapture.calls === 1
|
|
@@ -1604,7 +2148,7 @@ export class ClaudeSessionService {
|
|
|
1604
2148
|
: pathFromCwd;
|
|
1605
2149
|
}
|
|
1606
2150
|
activeCwd() {
|
|
1607
|
-
return this.options.workspace?.cwd() ?? this.
|
|
2151
|
+
return this.options.workspace?.cwd() ?? this.runtimeCwd;
|
|
1608
2152
|
}
|
|
1609
2153
|
restoreWorktree(entries) {
|
|
1610
2154
|
const latest = [...entries]
|
|
@@ -1620,10 +2164,74 @@ export class ClaudeSessionService {
|
|
|
1620
2164
|
paths(sessionId) {
|
|
1621
2165
|
return resolveClaudePaths({
|
|
1622
2166
|
configDir: this.options.configRoot,
|
|
1623
|
-
cwd: this.sessionCwds.get(sessionId) ?? this.
|
|
2167
|
+
cwd: this.sessionCwds.get(sessionId) ?? this.activeCwd(),
|
|
1624
2168
|
sessionId,
|
|
1625
2169
|
});
|
|
1626
2170
|
}
|
|
2171
|
+
async appendCdCommand(sessionId, cwd) {
|
|
2172
|
+
const result = await this.store(sessionId).withLease(async (lease) => {
|
|
2173
|
+
const snapshot = await lease.load();
|
|
2174
|
+
if (snapshot.entries.length === 0) {
|
|
2175
|
+
throw new Error(`Claude session not found: ${sessionId}`);
|
|
2176
|
+
}
|
|
2177
|
+
const appendResult = await lease.appendMany(snapshot.tail, this.cdCommandEntries(sessionId, cwd, this.logicalTailUuid(snapshot.tail)));
|
|
2178
|
+
if (appendResult.status === 'conflict') {
|
|
2179
|
+
throw new Error(`Claude local command append conflict: ${appendResult.reason}`);
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
if (result.status === 'conflict') {
|
|
2183
|
+
throw new Error(`Claude local command conflict: ${result.reason}`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
cdCommandEntries(sessionId, cwd, parentUuid) {
|
|
2187
|
+
const firstUuid = randomUUID();
|
|
2188
|
+
const secondUuid = randomUUID();
|
|
2189
|
+
const timestamp = new Date().toISOString();
|
|
2190
|
+
const common = {
|
|
2191
|
+
isSidechain: false,
|
|
2192
|
+
timestamp,
|
|
2193
|
+
userType: 'external',
|
|
2194
|
+
entrypoint: 'cli',
|
|
2195
|
+
cwd,
|
|
2196
|
+
sessionId,
|
|
2197
|
+
version: this.options.claudeVersion,
|
|
2198
|
+
gitBranch: null,
|
|
2199
|
+
};
|
|
2200
|
+
return [
|
|
2201
|
+
{
|
|
2202
|
+
...common,
|
|
2203
|
+
type: 'system',
|
|
2204
|
+
subtype: 'local_command',
|
|
2205
|
+
isMeta: false,
|
|
2206
|
+
level: 'info',
|
|
2207
|
+
parentUuid,
|
|
2208
|
+
uuid: firstUuid,
|
|
2209
|
+
content: `<command-name>/cd</command-name>\n <command-message>cd</command-message>\n <command-args>${cwd}</command-args>`,
|
|
2210
|
+
},
|
|
2211
|
+
{
|
|
2212
|
+
...common,
|
|
2213
|
+
type: 'system',
|
|
2214
|
+
subtype: 'local_command',
|
|
2215
|
+
isMeta: false,
|
|
2216
|
+
level: 'info',
|
|
2217
|
+
parentUuid: firstUuid,
|
|
2218
|
+
uuid: secondUuid,
|
|
2219
|
+
content: `<local-command-stdout>Moved to ${cwd}</local-command-stdout>`,
|
|
2220
|
+
},
|
|
2221
|
+
{
|
|
2222
|
+
...common,
|
|
2223
|
+
parentUuid: secondUuid,
|
|
2224
|
+
promptId: randomUUID(),
|
|
2225
|
+
type: 'user',
|
|
2226
|
+
message: {
|
|
2227
|
+
role: 'user',
|
|
2228
|
+
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>`,
|
|
2229
|
+
},
|
|
2230
|
+
isMeta: true,
|
|
2231
|
+
uuid: randomUUID(),
|
|
2232
|
+
},
|
|
2233
|
+
];
|
|
2234
|
+
}
|
|
1627
2235
|
store(sessionId) {
|
|
1628
2236
|
const paths = this.paths(sessionId);
|
|
1629
2237
|
return new ClaudeTranscriptStore({
|