praxis-agent 0.32.0 → 0.34.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/dist/application/background-agent-manager.d.ts +48 -3
- package/dist/application/background-agent-manager.js +249 -73
- package/dist/application/background-task-runtime.d.ts +20 -1
- package/dist/application/background-task-runtime.js +45 -6
- package/dist/application/managed-worktree.d.ts +8 -0
- package/dist/application/managed-worktree.js +54 -2
- package/dist/application/session-service.d.ts +38 -0
- package/dist/application/session-service.js +590 -70
- package/dist/application/subagent-service.d.ts +34 -7
- package/dist/application/subagent-service.js +694 -135
- package/dist/cli/interactive.d.ts +1 -0
- package/dist/cli/interactive.js +26 -1
- package/dist/cli/tui/task-panel.d.ts +1 -1
- package/dist/cli/tui/task-panel.js +2 -1
- package/dist/cli-runtime.d.ts +1 -0
- package/dist/cli-runtime.js +111 -31
- package/dist/compatibility/claude/history.d.ts +8 -0
- package/dist/compatibility/claude/history.js +54 -0
- package/dist/compatibility/claude/interruption.d.ts +16 -0
- package/dist/compatibility/claude/interruption.js +147 -0
- package/dist/compatibility/claude/projection.d.ts +4 -0
- package/dist/compatibility/claude/projection.js +82 -0
- package/dist/compatibility/claude/schema.js +22 -0
- package/dist/compatibility/claude/session-metadata.d.ts +44 -0
- package/dist/compatibility/claude/session-metadata.js +180 -0
- package/dist/compatibility/claude/sidechain.d.ts +5 -0
- package/dist/compatibility/claude/sidechain.js +13 -4
- package/dist/persistence/claude-session-index.d.ts +39 -0
- package/dist/persistence/claude-session-index.js +175 -0
- package/dist/persistence/claude-sidechain-store.js +19 -3
- package/dist/persistence/claude-transcript-store.d.ts +6 -2
- package/dist/persistence/claude-transcript-store.js +78 -3
- package/dist/persistence/in-memory-transcript-store.js +1 -0
- package/dist/persistence/subagent-lifecycle-store.d.ts +66 -0
- package/dist/persistence/subagent-lifecycle-store.js +299 -0
- package/package.json +2 -1
|
@@ -18,13 +18,16 @@ const APPENDABLE_ENTRY_TYPES = new Set([
|
|
|
18
18
|
'assistant',
|
|
19
19
|
'attachment',
|
|
20
20
|
'custom-title',
|
|
21
|
+
'ai-title',
|
|
21
22
|
'file-history-delta',
|
|
22
23
|
'file-history-snapshot',
|
|
23
24
|
'last-prompt',
|
|
25
|
+
'mode',
|
|
24
26
|
'permission-mode',
|
|
25
27
|
'pr-link',
|
|
26
28
|
'queue-operation',
|
|
27
29
|
'relocated',
|
|
30
|
+
'tag',
|
|
28
31
|
'system',
|
|
29
32
|
'user',
|
|
30
33
|
'worktree-state',
|
|
@@ -732,6 +735,25 @@ function validateAppendableEntry(entry) {
|
|
|
732
735
|
}
|
|
733
736
|
return;
|
|
734
737
|
}
|
|
738
|
+
if (entry.type === 'ai-title') {
|
|
739
|
+
if (!isNonEmptyString(entry.aiTitle) ||
|
|
740
|
+
!isNonEmptyString(entry.sessionId)) {
|
|
741
|
+
throw new Error('Claude ai-title entry has invalid metadata');
|
|
742
|
+
}
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (entry.type === 'tag') {
|
|
746
|
+
if (!isNonEmptyString(entry.tag) || !isNonEmptyString(entry.sessionId)) {
|
|
747
|
+
throw new Error('Claude tag entry has invalid metadata');
|
|
748
|
+
}
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (entry.type === 'mode') {
|
|
752
|
+
if (!isNonEmptyString(entry.mode) || !isNonEmptyString(entry.sessionId)) {
|
|
753
|
+
throw new Error('Claude mode entry has invalid metadata');
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
735
757
|
if (entry.type === 'agent-color') {
|
|
736
758
|
if (!isAgentColorValue(entry.agentColor) ||
|
|
737
759
|
!isNonEmptyString(entry.sessionId)) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ClaudeTranscriptEntry } from './schema.js';
|
|
2
|
+
export interface ClaudeSessionPrLink {
|
|
3
|
+
prNumber: number;
|
|
4
|
+
prUrl?: string;
|
|
5
|
+
prRepository?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ClaudeSessionMetadata {
|
|
8
|
+
title?: string;
|
|
9
|
+
titleSource?: 'custom-title' | 'ai-title';
|
|
10
|
+
customTitle?: string;
|
|
11
|
+
aiTitle?: string;
|
|
12
|
+
tag?: string;
|
|
13
|
+
agentName?: string;
|
|
14
|
+
agentColor?: string;
|
|
15
|
+
agentSetting?: string;
|
|
16
|
+
permissionMode?: string;
|
|
17
|
+
mode?: string;
|
|
18
|
+
worktreeSession?: Readonly<Record<string, unknown>>;
|
|
19
|
+
prLink?: ClaudeSessionPrLink;
|
|
20
|
+
lastPrompt?: string;
|
|
21
|
+
lastPromptLeafUuid?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const CLAUDE_DURABLE_METADATA_TYPES: readonly ["custom-title", "ai-title", "tag", "agent-name", "agent-color", "agent-setting", "permission-mode", "mode", "worktree-state", "pr-link", "last-prompt"];
|
|
24
|
+
export type ClaudeDurableMetadataType = (typeof CLAUDE_DURABLE_METADATA_TYPES)[number];
|
|
25
|
+
export declare function isClaudeDurableMetadataType(type: string): type is ClaudeDurableMetadataType;
|
|
26
|
+
/**
|
|
27
|
+
* Reduces Claude's append-only metadata records. Each metadata type is
|
|
28
|
+
* independently last-wins, while a user-authored custom title always takes
|
|
29
|
+
* precedence over an AI title regardless of append order.
|
|
30
|
+
*
|
|
31
|
+
* Unknown records remain untouched in the transcript but never become
|
|
32
|
+
* authoritative session state here.
|
|
33
|
+
*/
|
|
34
|
+
export declare function reduceClaudeSessionMetadata(entries: readonly ClaudeTranscriptEntry[], sessionId?: string): ClaudeSessionMetadata;
|
|
35
|
+
/** Returns one current record per durable metadata type for a tail snapshot. */
|
|
36
|
+
export declare function createClaudeDurableMetadataSnapshot(entries: readonly ClaudeTranscriptEntry[], sessionId: string, options?: {
|
|
37
|
+
trustLastPromptReference?: boolean;
|
|
38
|
+
}): ClaudeTranscriptEntry[];
|
|
39
|
+
/** Keeps the last known full baseline while overlaying metadata visible in a
|
|
40
|
+
* newer bounded head/tail observation. Structural entries are included only
|
|
41
|
+
* to validate or safely advance the committed last-prompt hint. */
|
|
42
|
+
export declare function mergeClaudeDurableMetadataSnapshot(baseline: readonly ClaudeTranscriptEntry[], observed: readonly ClaudeTranscriptEntry[], sessionId: string): ClaudeTranscriptEntry[];
|
|
43
|
+
export declare function createClaudeTagEntry(sessionId: string, tag: string): ClaudeTranscriptEntry;
|
|
44
|
+
//# sourceMappingURL=session-metadata.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export const CLAUDE_DURABLE_METADATA_TYPES = [
|
|
2
|
+
'custom-title',
|
|
3
|
+
'ai-title',
|
|
4
|
+
'tag',
|
|
5
|
+
'agent-name',
|
|
6
|
+
'agent-color',
|
|
7
|
+
'agent-setting',
|
|
8
|
+
'permission-mode',
|
|
9
|
+
'mode',
|
|
10
|
+
'worktree-state',
|
|
11
|
+
'pr-link',
|
|
12
|
+
'last-prompt',
|
|
13
|
+
];
|
|
14
|
+
const DURABLE_METADATA_TYPE_SET = new Set(CLAUDE_DURABLE_METADATA_TYPES);
|
|
15
|
+
export function isClaudeDurableMetadataType(type) {
|
|
16
|
+
return DURABLE_METADATA_TYPE_SET.has(type);
|
|
17
|
+
}
|
|
18
|
+
function nonEmptyString(value) {
|
|
19
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
20
|
+
}
|
|
21
|
+
function record(value) {
|
|
22
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
23
|
+
? value
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reduces Claude's append-only metadata records. Each metadata type is
|
|
28
|
+
* independently last-wins, while a user-authored custom title always takes
|
|
29
|
+
* precedence over an AI title regardless of append order.
|
|
30
|
+
*
|
|
31
|
+
* Unknown records remain untouched in the transcript but never become
|
|
32
|
+
* authoritative session state here.
|
|
33
|
+
*/
|
|
34
|
+
export function reduceClaudeSessionMetadata(entries, sessionId) {
|
|
35
|
+
const latest = new Map();
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (!isClaudeDurableMetadataType(entry.type))
|
|
38
|
+
continue;
|
|
39
|
+
if (sessionId !== undefined &&
|
|
40
|
+
typeof entry.sessionId === 'string' &&
|
|
41
|
+
entry.sessionId !== sessionId) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
latest.set(entry.type, entry);
|
|
45
|
+
}
|
|
46
|
+
const customTitle = nonEmptyString(latest.get('custom-title')?.customTitle);
|
|
47
|
+
const aiTitle = nonEmptyString(latest.get('ai-title')?.aiTitle);
|
|
48
|
+
const tag = nonEmptyString(latest.get('tag')?.tag);
|
|
49
|
+
const agentName = nonEmptyString(latest.get('agent-name')?.agentName);
|
|
50
|
+
const agentColor = nonEmptyString(latest.get('agent-color')?.agentColor);
|
|
51
|
+
const agentSetting = nonEmptyString(latest.get('agent-setting')?.agentSetting);
|
|
52
|
+
const permissionMode = nonEmptyString(latest.get('permission-mode')?.permissionMode);
|
|
53
|
+
const mode = nonEmptyString(latest.get('mode')?.mode);
|
|
54
|
+
const worktreeSession = record(latest.get('worktree-state')?.worktreeSession);
|
|
55
|
+
const pr = latest.get('pr-link');
|
|
56
|
+
const prNumber = pr?.prNumber;
|
|
57
|
+
const prUrl = nonEmptyString(pr?.prUrl);
|
|
58
|
+
const prRepository = nonEmptyString(pr?.prRepository);
|
|
59
|
+
const prLink = Number.isSafeInteger(prNumber) && Number(prNumber) > 0
|
|
60
|
+
? {
|
|
61
|
+
prNumber: Number(prNumber),
|
|
62
|
+
...(prUrl === undefined ? {} : { prUrl }),
|
|
63
|
+
...(prRepository === undefined ? {} : { prRepository }),
|
|
64
|
+
}
|
|
65
|
+
: undefined;
|
|
66
|
+
const lastPromptEntry = latest.get('last-prompt');
|
|
67
|
+
const lastPrompt = nonEmptyString(lastPromptEntry?.lastPrompt);
|
|
68
|
+
const lastPromptLeafUuid = nonEmptyString(lastPromptEntry?.leafUuid);
|
|
69
|
+
return {
|
|
70
|
+
...(customTitle === undefined ? {} : { customTitle }),
|
|
71
|
+
...(aiTitle === undefined ? {} : { aiTitle }),
|
|
72
|
+
...(customTitle !== undefined
|
|
73
|
+
? { title: customTitle, titleSource: 'custom-title' }
|
|
74
|
+
: aiTitle !== undefined
|
|
75
|
+
? { title: aiTitle, titleSource: 'ai-title' }
|
|
76
|
+
: {}),
|
|
77
|
+
...(tag === undefined ? {} : { tag }),
|
|
78
|
+
...(agentName === undefined ? {} : { agentName }),
|
|
79
|
+
...(agentColor === undefined ? {} : { agentColor }),
|
|
80
|
+
...(agentSetting === undefined ? {} : { agentSetting }),
|
|
81
|
+
...(permissionMode === undefined ? {} : { permissionMode }),
|
|
82
|
+
...(mode === undefined ? {} : { mode }),
|
|
83
|
+
...(worktreeSession === undefined ? {} : { worktreeSession }),
|
|
84
|
+
...(prLink === undefined ? {} : { prLink }),
|
|
85
|
+
...(lastPrompt === undefined ? {} : { lastPrompt }),
|
|
86
|
+
...(lastPromptLeafUuid === undefined ? {} : { lastPromptLeafUuid }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Returns one current record per durable metadata type for a tail snapshot. */
|
|
90
|
+
export function createClaudeDurableMetadataSnapshot(entries, sessionId, options = {}) {
|
|
91
|
+
const latest = new Map();
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!isClaudeDurableMetadataType(entry.type))
|
|
94
|
+
continue;
|
|
95
|
+
if (typeof entry.sessionId === 'string' && entry.sessionId !== sessionId) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
latest.set(entry.type, entry);
|
|
99
|
+
}
|
|
100
|
+
// A later AI title must never obscure a user rename in a bounded tail.
|
|
101
|
+
if (latest.has('custom-title'))
|
|
102
|
+
latest.delete('ai-title');
|
|
103
|
+
const lastPrompt = latest.get('last-prompt');
|
|
104
|
+
if (lastPrompt !== undefined) {
|
|
105
|
+
const committedLeafUuid = nonEmptyString(lastPrompt.leafUuid);
|
|
106
|
+
const committedLeaf = entries.find((entry) => entry.isSidechain !== true &&
|
|
107
|
+
typeof entry.uuid === 'string' &&
|
|
108
|
+
entry.uuid === committedLeafUuid &&
|
|
109
|
+
(entry.type === 'assistant' ||
|
|
110
|
+
(entry.type === 'system' &&
|
|
111
|
+
entry.subtype === 'local_command' &&
|
|
112
|
+
typeof entry.content === 'string' &&
|
|
113
|
+
entry.content.startsWith('<local-command-stdout>'))));
|
|
114
|
+
const trustedCommittedLeafUuid = committedLeaf?.uuid ??
|
|
115
|
+
(options.trustLastPromptReference ? committedLeafUuid : undefined);
|
|
116
|
+
if (trustedCommittedLeafUuid === undefined) {
|
|
117
|
+
latest.delete('last-prompt');
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
// Metadata-only re-appends never promote a newer assistant: it may be
|
|
121
|
+
// an abandoned or failed branch. A completed local command is itself a
|
|
122
|
+
// committed observable result, so it may safely advance the hint when
|
|
123
|
+
// it descends from the last committed turn.
|
|
124
|
+
const byUuid = new Map(entries.flatMap((entry) => entry.isSidechain !== true && typeof entry.uuid === 'string'
|
|
125
|
+
? [[entry.uuid, entry]]
|
|
126
|
+
: []));
|
|
127
|
+
const localCommandLeaf = [...entries].reverse().find((entry) => {
|
|
128
|
+
if (entry.isSidechain === true ||
|
|
129
|
+
entry.type !== 'system' ||
|
|
130
|
+
entry.subtype !== 'local_command' ||
|
|
131
|
+
typeof entry.uuid !== 'string' ||
|
|
132
|
+
typeof entry.content !== 'string' ||
|
|
133
|
+
!entry.content.startsWith('<local-command-stdout>')) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
let parentUuid = typeof entry.parentUuid === 'string' ? entry.parentUuid : null;
|
|
137
|
+
const seen = new Set();
|
|
138
|
+
while (parentUuid !== null && !seen.has(parentUuid)) {
|
|
139
|
+
if (parentUuid === trustedCommittedLeafUuid)
|
|
140
|
+
return true;
|
|
141
|
+
seen.add(parentUuid);
|
|
142
|
+
const parent = byUuid.get(parentUuid);
|
|
143
|
+
if (parent?.type !== 'system' || parent.subtype !== 'local_command') {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
parentUuid =
|
|
147
|
+
typeof parent?.parentUuid === 'string' ? parent.parentUuid : null;
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
if (localCommandLeaf === undefined) {
|
|
152
|
+
latest.set('last-prompt', lastPrompt);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const snapshot = {
|
|
156
|
+
...lastPrompt,
|
|
157
|
+
leafUuid: localCommandLeaf.uuid,
|
|
158
|
+
};
|
|
159
|
+
delete snapshot.lastPrompt;
|
|
160
|
+
latest.set('last-prompt', snapshot);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return CLAUDE_DURABLE_METADATA_TYPES.flatMap((type) => {
|
|
165
|
+
const entry = latest.get(type);
|
|
166
|
+
return entry === undefined ? [] : [{ ...entry, sessionId }];
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Keeps the last known full baseline while overlaying metadata visible in a
|
|
170
|
+
* newer bounded head/tail observation. Structural entries are included only
|
|
171
|
+
* to validate or safely advance the committed last-prompt hint. */
|
|
172
|
+
export function mergeClaudeDurableMetadataSnapshot(baseline, observed, sessionId) {
|
|
173
|
+
return createClaudeDurableMetadataSnapshot([...baseline, ...observed], sessionId, { trustLastPromptReference: true });
|
|
174
|
+
}
|
|
175
|
+
export function createClaudeTagEntry(sessionId, tag) {
|
|
176
|
+
if (tag.length === 0)
|
|
177
|
+
throw new Error('Session tag must not be empty');
|
|
178
|
+
return { type: 'tag', tag, sessionId };
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=session-metadata.js.map
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ModelUsage } from '../../core/runtime.js';
|
|
2
2
|
import type { ClaudeTranscriptEntry } from './schema.js';
|
|
3
|
+
export declare function isClaudeAgentId(agentId: string): boolean;
|
|
4
|
+
export declare function assertClaudeAgentId(agentId: string): void;
|
|
3
5
|
export interface ClaudeSidechainPaths {
|
|
4
6
|
sessionId: string;
|
|
5
7
|
agentId: string;
|
|
@@ -12,6 +14,7 @@ export interface ClaudeSidechainPathOptions {
|
|
|
12
14
|
subdirectory?: string;
|
|
13
15
|
}
|
|
14
16
|
export interface ClaudeSidechainMetadata {
|
|
17
|
+
readonly [key: string]: unknown;
|
|
15
18
|
agentType: string;
|
|
16
19
|
description: string;
|
|
17
20
|
toolUseId: string;
|
|
@@ -19,6 +22,8 @@ export interface ClaudeSidechainMetadata {
|
|
|
19
22
|
name?: string;
|
|
20
23
|
permissionMode?: ClaudeSidechainPermissionMode;
|
|
21
24
|
isolation?: 'worktree';
|
|
25
|
+
parentAgentId?: string;
|
|
26
|
+
worktreePath?: string;
|
|
22
27
|
}
|
|
23
28
|
export type ClaudeSidechainPermissionMode = 'acceptEdits' | 'auto' | 'bypassPermissions' | 'default' | 'dontAsk' | 'plan';
|
|
24
29
|
export declare function resolveClaudeSidechainPaths(projectRoot: string, sessionId: string, agentId: string, options?: ClaudeSidechainPathOptions): ClaudeSidechainPaths;
|
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
import { resolve } from 'node:path';
|
|
2
2
|
import { isClaudeSessionId } from './paths.js';
|
|
3
|
-
|
|
3
|
+
/** Claude uses a stable 16-hex suffix and may prefix it with a bounded label.
|
|
4
|
+
* Keeping the accepted alphabet filename-safe makes path construction a pure
|
|
5
|
+
* validation boundary rather than a sanitizing rewrite. */
|
|
6
|
+
const CLAUDE_AGENT_ID_PATTERN = /^a(?:[A-Za-z0-9][A-Za-z0-9_-]{0,62}-)?[0-9a-f]{16}$/u;
|
|
7
|
+
export function isClaudeAgentId(agentId) {
|
|
8
|
+
return CLAUDE_AGENT_ID_PATTERN.test(agentId);
|
|
9
|
+
}
|
|
10
|
+
export function assertClaudeAgentId(agentId) {
|
|
11
|
+
if (!isClaudeAgentId(agentId)) {
|
|
12
|
+
throw new Error(`Invalid Claude agent ID: ${agentId}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
4
15
|
export function resolveClaudeSidechainPaths(projectRoot, sessionId, agentId, options = {}) {
|
|
5
16
|
if (!isClaudeSessionId(sessionId)) {
|
|
6
17
|
throw new Error(`Invalid Claude session ID: ${sessionId}`);
|
|
7
18
|
}
|
|
8
|
-
|
|
9
|
-
throw new Error(`Invalid Claude agent ID: ${agentId}`);
|
|
10
|
-
}
|
|
19
|
+
assertClaudeAgentId(agentId);
|
|
11
20
|
const subdirectory = options.subdirectory;
|
|
12
21
|
if (subdirectory !== undefined &&
|
|
13
22
|
(subdirectory.length === 0 ||
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ClaudeSchemaAdapter, ClaudeTranscriptEntry } from '../compatibility/claude/schema.js';
|
|
2
|
+
import type { TranscriptParseIssue } from './claude-transcript-store.js';
|
|
3
|
+
import type { TranscriptTail } from './claude-transcript-store.js';
|
|
4
|
+
export declare const CLAUDE_SESSION_INDEX_HEAD_BYTES: number;
|
|
5
|
+
export declare const CLAUDE_SESSION_INDEX_TAIL_BYTES: number;
|
|
6
|
+
export declare const CLAUDE_SESSION_INDEX_CONCURRENCY = 32;
|
|
7
|
+
export interface ClaudeSessionIndex {
|
|
8
|
+
entries: ClaudeTranscriptEntry[];
|
|
9
|
+
tailEntries: ClaudeTranscriptEntry[];
|
|
10
|
+
issue: TranscriptParseIssue | null;
|
|
11
|
+
byteLength: number;
|
|
12
|
+
newlineTerminated: boolean;
|
|
13
|
+
updatedAt: string;
|
|
14
|
+
tail: TranscriptTail;
|
|
15
|
+
}
|
|
16
|
+
export interface ClaudeSessionIndexRequest {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
path: string;
|
|
19
|
+
}
|
|
20
|
+
export type ClaudeSessionIndexResult = ClaudeSessionIndexRequest & ({
|
|
21
|
+
index: ClaudeSessionIndex;
|
|
22
|
+
error?: never;
|
|
23
|
+
} | {
|
|
24
|
+
index?: never;
|
|
25
|
+
error: unknown;
|
|
26
|
+
});
|
|
27
|
+
export declare class ClaudeSessionIndexCandidateError extends Error {
|
|
28
|
+
readonly name = "ClaudeSessionIndexCandidateError";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Reads only bounded head/tail windows for session discovery. A partial final
|
|
32
|
+
* line is treated as an in-progress append and ignored. Full JSONL and graph
|
|
33
|
+
* validation remains the responsibility of inspect/resume.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readClaudeSessionIndex(path: string, schema: ClaudeSchemaAdapter): Promise<ClaudeSessionIndex>;
|
|
36
|
+
/** Reads a discovery set with a fixed worker pool so large projects cannot
|
|
37
|
+
* exhaust the process file-descriptor limit. Results preserve input order. */
|
|
38
|
+
export declare function readClaudeSessionIndexes(requests: readonly ClaudeSessionIndexRequest[], schema: ClaudeSchemaAdapter, concurrency?: number): Promise<ClaudeSessionIndexResult[]>;
|
|
39
|
+
//# sourceMappingURL=claude-session-index.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, open } from 'node:fs/promises';
|
|
3
|
+
import { TextDecoder } from 'node:util';
|
|
4
|
+
import { isClaudeDurableLastPromptSnapshot } from '../compatibility/claude/history.js';
|
|
5
|
+
export const CLAUDE_SESSION_INDEX_HEAD_BYTES = 64 * 1024;
|
|
6
|
+
export const CLAUDE_SESSION_INDEX_TAIL_BYTES = 128 * 1024;
|
|
7
|
+
export const CLAUDE_SESSION_INDEX_CONCURRENCY = 32;
|
|
8
|
+
const UTF8_DECODER = new TextDecoder('utf-8', { fatal: true });
|
|
9
|
+
function logicalTailUuid(entries) {
|
|
10
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
11
|
+
const entry = entries[index];
|
|
12
|
+
if (!entry)
|
|
13
|
+
continue;
|
|
14
|
+
if (entry.type === 'last-prompt' &&
|
|
15
|
+
isClaudeDurableLastPromptSnapshot(entries, index)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (typeof entry.uuid === 'string')
|
|
19
|
+
return entry.uuid;
|
|
20
|
+
if (typeof entry.leafUuid === 'string')
|
|
21
|
+
return entry.leafUuid;
|
|
22
|
+
}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
export class ClaudeSessionIndexCandidateError extends Error {
|
|
26
|
+
name = 'ClaudeSessionIndexCandidateError';
|
|
27
|
+
}
|
|
28
|
+
function completeLines(source, absoluteStart, discardLeadingPartial, discardTrailingPartial, lineNumberStart, tail) {
|
|
29
|
+
let start = 0;
|
|
30
|
+
if (discardLeadingPartial) {
|
|
31
|
+
const newline = source.indexOf(0x0a);
|
|
32
|
+
if (newline < 0)
|
|
33
|
+
return [];
|
|
34
|
+
start = newline + 1;
|
|
35
|
+
}
|
|
36
|
+
let end = source.length;
|
|
37
|
+
if (discardTrailingPartial && source.at(-1) !== 0x0a) {
|
|
38
|
+
const newline = source.lastIndexOf(0x0a);
|
|
39
|
+
if (newline < start)
|
|
40
|
+
return [];
|
|
41
|
+
end = newline + 1;
|
|
42
|
+
}
|
|
43
|
+
const lines = [];
|
|
44
|
+
let lineStart = start;
|
|
45
|
+
for (let index = start; index < end; index += 1) {
|
|
46
|
+
if (source[index] !== 0x0a)
|
|
47
|
+
continue;
|
|
48
|
+
if (index > lineStart) {
|
|
49
|
+
lines.push({
|
|
50
|
+
source: source.subarray(lineStart, index),
|
|
51
|
+
byteOffset: absoluteStart + lineStart,
|
|
52
|
+
lineNumber: lineNumberStart === null ? null : lineNumberStart + lines.length,
|
|
53
|
+
tail,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
lineStart = index + 1;
|
|
57
|
+
}
|
|
58
|
+
if (!discardTrailingPartial && lineStart < end) {
|
|
59
|
+
lines.push({
|
|
60
|
+
source: source.subarray(lineStart, end),
|
|
61
|
+
byteOffset: absoluteStart + lineStart,
|
|
62
|
+
lineNumber: lineNumberStart === null ? null : lineNumberStart + lines.length,
|
|
63
|
+
tail,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return lines;
|
|
67
|
+
}
|
|
68
|
+
async function readWindow(handle, length, position) {
|
|
69
|
+
const buffer = Buffer.alloc(length);
|
|
70
|
+
const { bytesRead } = await handle.read(buffer, 0, length, position);
|
|
71
|
+
return buffer.subarray(0, bytesRead);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Reads only bounded head/tail windows for session discovery. A partial final
|
|
75
|
+
* line is treated as an in-progress append and ignored. Full JSONL and graph
|
|
76
|
+
* validation remains the responsibility of inspect/resume.
|
|
77
|
+
*/
|
|
78
|
+
export async function readClaudeSessionIndex(path, schema) {
|
|
79
|
+
const pathMetadata = await lstat(path);
|
|
80
|
+
if (!pathMetadata.isFile())
|
|
81
|
+
throw new ClaudeSessionIndexCandidateError(`Expected a regular file: ${path}`);
|
|
82
|
+
const handle = await open(path, 'r');
|
|
83
|
+
try {
|
|
84
|
+
const metadata = await handle.stat();
|
|
85
|
+
const byteLength = metadata.size;
|
|
86
|
+
const wholeFileThreshold = CLAUDE_SESSION_INDEX_HEAD_BYTES + CLAUDE_SESSION_INDEX_TAIL_BYTES;
|
|
87
|
+
let lines;
|
|
88
|
+
let newlineTerminated = true;
|
|
89
|
+
if (byteLength <= wholeFileThreshold) {
|
|
90
|
+
const source = await readWindow(handle, byteLength, 0);
|
|
91
|
+
newlineTerminated = source.length === 0 || source.at(-1) === 0x0a;
|
|
92
|
+
lines = completeLines(source, 0, false, !newlineTerminated, 1, true);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const head = await readWindow(handle, CLAUDE_SESSION_INDEX_HEAD_BYTES, 0);
|
|
96
|
+
const tailStart = byteLength - CLAUDE_SESSION_INDEX_TAIL_BYTES;
|
|
97
|
+
const tail = await readWindow(handle, CLAUDE_SESSION_INDEX_TAIL_BYTES, tailStart);
|
|
98
|
+
newlineTerminated = tail.length === 0 || tail.at(-1) === 0x0a;
|
|
99
|
+
lines = [
|
|
100
|
+
...completeLines(head, 0, false, true, 1, false),
|
|
101
|
+
...completeLines(tail, tailStart, true, !newlineTerminated, null, true),
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
const entries = [];
|
|
105
|
+
const tailEntries = [];
|
|
106
|
+
let issue = null;
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
try {
|
|
109
|
+
const entry = schema.parse(UTF8_DECODER.decode(line.source));
|
|
110
|
+
entries.push(entry);
|
|
111
|
+
if (line.tail)
|
|
112
|
+
tailEntries.push(entry);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
issue = {
|
|
116
|
+
lineNumber: line.lineNumber,
|
|
117
|
+
byteOffset: line.byteOffset,
|
|
118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
119
|
+
};
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
entries,
|
|
125
|
+
tailEntries,
|
|
126
|
+
issue,
|
|
127
|
+
byteLength,
|
|
128
|
+
newlineTerminated,
|
|
129
|
+
updatedAt: metadata.mtime.toISOString(),
|
|
130
|
+
tail: {
|
|
131
|
+
byteLength,
|
|
132
|
+
lastLineHash: lines.at(-1) === undefined
|
|
133
|
+
? null
|
|
134
|
+
: createHash('sha256')
|
|
135
|
+
.update(lines.at(-1)?.source ?? Buffer.alloc(0))
|
|
136
|
+
.digest('hex'),
|
|
137
|
+
lastUuid: logicalTailUuid(entries),
|
|
138
|
+
newlineTerminated,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await handle.close();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Reads a discovery set with a fixed worker pool so large projects cannot
|
|
147
|
+
* exhaust the process file-descriptor limit. Results preserve input order. */
|
|
148
|
+
export async function readClaudeSessionIndexes(requests, schema, concurrency = CLAUDE_SESSION_INDEX_CONCURRENCY) {
|
|
149
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
150
|
+
throw new Error('Claude session index concurrency must be a positive integer');
|
|
151
|
+
}
|
|
152
|
+
const results = new Array(requests.length);
|
|
153
|
+
let nextIndex = 0;
|
|
154
|
+
const worker = async () => {
|
|
155
|
+
while (nextIndex < requests.length) {
|
|
156
|
+
const index = nextIndex;
|
|
157
|
+
nextIndex += 1;
|
|
158
|
+
const request = requests[index];
|
|
159
|
+
if (request === undefined)
|
|
160
|
+
continue;
|
|
161
|
+
try {
|
|
162
|
+
results[index] = {
|
|
163
|
+
...request,
|
|
164
|
+
index: await readClaudeSessionIndex(request.path, schema),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
results[index] = { ...request, error };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, requests.length) }, async () => worker()));
|
|
173
|
+
return results;
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=claude-session-index.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mkdir, open, readFile, rm } from 'node:fs/promises';
|
|
2
|
+
import { isClaudeAgentId } from '../compatibility/claude/sidechain.js';
|
|
2
3
|
import { ClaudeTranscriptStore, } from './claude-transcript-store.js';
|
|
3
4
|
export class ClaudeSidechainStore {
|
|
4
5
|
paths;
|
|
@@ -72,7 +73,8 @@ export class ClaudeSidechainStore {
|
|
|
72
73
|
if (typeof record.agentType !== 'string' ||
|
|
73
74
|
typeof record.description !== 'string' ||
|
|
74
75
|
typeof record.toolUseId !== 'string' ||
|
|
75
|
-
|
|
76
|
+
!Number.isSafeInteger(record.spawnDepth) ||
|
|
77
|
+
Number(record.spawnDepth) < 1 ||
|
|
76
78
|
(record.name !== undefined && typeof record.name !== 'string') ||
|
|
77
79
|
(record.permissionMode !== undefined &&
|
|
78
80
|
(typeof record.permissionMode !== 'string' ||
|
|
@@ -84,14 +86,22 @@ export class ClaudeSidechainStore {
|
|
|
84
86
|
'dontAsk',
|
|
85
87
|
'plan',
|
|
86
88
|
].includes(record.permissionMode))) ||
|
|
87
|
-
(record.isolation !== undefined && record.isolation !== 'worktree')
|
|
89
|
+
(record.isolation !== undefined && record.isolation !== 'worktree') ||
|
|
90
|
+
(record.parentAgentId !== undefined &&
|
|
91
|
+
(typeof record.parentAgentId !== 'string' ||
|
|
92
|
+
!isClaudeAgentId(record.parentAgentId))) ||
|
|
93
|
+
(record.worktreePath !== undefined &&
|
|
94
|
+
(typeof record.worktreePath !== 'string' ||
|
|
95
|
+
record.worktreePath.length === 0 ||
|
|
96
|
+
record.worktreePath.includes('\0')))) {
|
|
88
97
|
throw new Error('Claude sidechain metadata is invalid');
|
|
89
98
|
}
|
|
90
99
|
return {
|
|
100
|
+
...record,
|
|
91
101
|
agentType: record.agentType,
|
|
92
102
|
description: record.description,
|
|
93
103
|
toolUseId: record.toolUseId,
|
|
94
|
-
spawnDepth: record.spawnDepth,
|
|
104
|
+
spawnDepth: Number(record.spawnDepth),
|
|
95
105
|
...(record.name === undefined ? {} : { name: record.name }),
|
|
96
106
|
...(record.permissionMode === undefined
|
|
97
107
|
? {}
|
|
@@ -101,6 +111,12 @@ export class ClaudeSidechainStore {
|
|
|
101
111
|
...(record.isolation === undefined
|
|
102
112
|
? {}
|
|
103
113
|
: { isolation: record.isolation }),
|
|
114
|
+
...(record.parentAgentId === undefined
|
|
115
|
+
? {}
|
|
116
|
+
: { parentAgentId: record.parentAgentId }),
|
|
117
|
+
...(record.worktreePath === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: { worktreePath: record.worktreePath }),
|
|
104
120
|
};
|
|
105
121
|
}
|
|
106
122
|
}
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import type { ClaudeSchemaAdapter, ClaudeTranscriptEntry } from '../compatibility/claude/schema.js';
|
|
2
|
+
import { type ClaudeSessionIndex } from './claude-session-index.js';
|
|
2
3
|
export interface TranscriptTail {
|
|
3
4
|
byteLength: number;
|
|
4
5
|
lastLineHash: string | null;
|
|
5
6
|
lastUuid: string | null;
|
|
6
7
|
newlineTerminated: boolean;
|
|
7
|
-
branchParentUuid?: string;
|
|
8
|
+
branchParentUuid?: string | null;
|
|
8
9
|
}
|
|
9
10
|
export interface TranscriptSnapshot {
|
|
10
11
|
entries: ClaudeTranscriptEntry[];
|
|
11
12
|
tail: TranscriptTail;
|
|
12
13
|
}
|
|
13
14
|
export interface TranscriptParseIssue {
|
|
14
|
-
lineNumber: number;
|
|
15
|
+
lineNumber: number | null;
|
|
15
16
|
byteOffset: number;
|
|
16
17
|
message: string;
|
|
17
18
|
}
|
|
@@ -46,8 +47,10 @@ export type TranscriptReserveResult = {
|
|
|
46
47
|
};
|
|
47
48
|
export interface ClaudeTranscriptLease {
|
|
48
49
|
load(): Promise<TranscriptSnapshot>;
|
|
50
|
+
loadIndex?(): Promise<ClaudeSessionIndex>;
|
|
49
51
|
append(expectedTail: TranscriptTail, entry: ClaudeTranscriptEntry): Promise<TranscriptAppendResult>;
|
|
50
52
|
appendMany(expectedTail: TranscriptTail, entries: readonly ClaudeTranscriptEntry[]): Promise<TranscriptAppendResult>;
|
|
53
|
+
appendMetadataSnapshot(expectedTail: TranscriptTail, entries: readonly ClaudeTranscriptEntry[]): Promise<TranscriptAppendResult>;
|
|
51
54
|
}
|
|
52
55
|
export type TranscriptLeaseResult<T> = {
|
|
53
56
|
status: 'completed';
|
|
@@ -80,6 +83,7 @@ export declare class ClaudeTranscriptStore {
|
|
|
80
83
|
append(expectedTail: TranscriptTail, entry: ClaudeTranscriptEntry): Promise<TranscriptAppendResult>;
|
|
81
84
|
private appendUnderLease;
|
|
82
85
|
private appendManyUnderLease;
|
|
86
|
+
private appendMetadataSnapshotUnderLease;
|
|
83
87
|
private serializeNewSidechain;
|
|
84
88
|
}
|
|
85
89
|
//# sourceMappingURL=claude-transcript-store.d.ts.map
|