@myagentroam/node 0.9.4 → 0.9.6

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.
Files changed (61) hide show
  1. package/dist/claude-agent-sdk.d.ts +1 -0
  2. package/dist/claude-agent-sdk.js +9 -0
  3. package/dist/codex-app-server.d.ts +1 -0
  4. package/dist/codex-app-server.js +4 -2
  5. package/dist/connector.d.ts +1 -0
  6. package/dist/connector.js +16 -11
  7. package/dist/native-jsonl-reader.d.ts +21 -0
  8. package/dist/native-jsonl-reader.js +89 -0
  9. package/dist/native-session-history.d.ts +20 -3
  10. package/dist/native-session-history.js +245 -38
  11. package/dist/opencode-server.d.ts +1 -0
  12. package/dist/opencode-server.js +1 -0
  13. package/dist/runner/abstract-runner.d.ts +11 -2
  14. package/dist/runner/abstract-runner.js +4 -0
  15. package/dist/runner/claude/managed-run-controller.js +3 -0
  16. package/dist/runner/claude-code-runner.d.ts +2 -1
  17. package/dist/runner/claude-code-runner.js +4 -0
  18. package/dist/runner/codex/managed-run-controller.js +6 -0
  19. package/dist/runner/codex-runner.d.ts +2 -1
  20. package/dist/runner/codex-runner.js +13 -2
  21. package/dist/runner/opencode/managed-run-controller.js +4 -0
  22. package/dist/runner/opencode-runner.d.ts +10 -3
  23. package/dist/runner/opencode-runner.js +87 -14
  24. package/dist/runner/runner-registry.js +16 -1
  25. package/dist/runtime-command-detector.js +1 -6
  26. package/dist/runtime-state.js +1 -0
  27. package/dist/service/conversation-history-service.d.ts +6 -5
  28. package/dist/service/conversation-history-service.js +151 -55
  29. package/dist/service/external-session-resume-service.d.ts +2 -0
  30. package/dist/service/external-session-resume-service.js +45 -34
  31. package/dist/service/native-session-projection-service.d.ts +1 -0
  32. package/dist/service/native-session-projection-service.js +4 -0
  33. package/dist/service/native-session-watch-service.d.ts +18 -0
  34. package/dist/service/native-session-watch-service.js +232 -34
  35. package/dist/service/run-workbench-service.d.ts +2 -4
  36. package/dist/service/run-workbench-service.js +2 -7
  37. package/dist/service/session-command-service.js +1 -1
  38. package/dist/service/session-identity-service.d.ts +1 -1
  39. package/dist/service/session-identity-service.js +1 -1
  40. package/dist/service/session-lifecycle-service.js +3 -0
  41. package/dist/service/session-message-service.d.ts +0 -1
  42. package/dist/service/session-message-service.js +15 -23
  43. package/dist/service/skill-directory-service.d.ts +1 -0
  44. package/dist/service/skill-directory-service.js +40 -6
  45. package/dist/service/workspace-domain-state.d.ts +3 -0
  46. package/dist/service/workspace-domain-state.js +1 -0
  47. package/dist/service/workspace-file-service.d.ts +1 -0
  48. package/dist/service/workspace-file-service.js +14 -1
  49. package/dist/service/workspace-queue-workbench-service.d.ts +17 -0
  50. package/dist/service/workspace-queue-workbench-service.js +66 -6
  51. package/dist/service/workspace-watch-service.d.ts +7 -2
  52. package/dist/service/workspace-watch-service.js +32 -6
  53. package/dist/service/workspace-workbench-service.d.ts +14 -0
  54. package/dist/service/workspace-workbench-service.js +215 -16
  55. package/dist/util/personal-instructions.d.ts +2 -0
  56. package/dist/util/personal-instructions.js +31 -0
  57. package/dist/util/runner-native-session-parsers.d.ts +1 -0
  58. package/dist/util/runner-native-session-parsers.js +76 -12
  59. package/dist/workspace.d.ts +14 -8
  60. package/dist/workspace.js +65 -42
  61. package/package.json +2 -2
@@ -1,8 +1,10 @@
1
- import { listGitHistoryRefs, readGitHistory, readGitHistoryFileDiff, readGitHistoryFiles, readCurrentChangeDiff, readRestrictedDiff, resolveGitRepository, restoreCurrentChange } from '../workspace.js';
1
+ import { listGitHistoryRefs, readGitHistory, readGitHistoryFileDiff, readGitHistoryFileContentRange, readGitHistoryFiles, readCurrentChangeDiff, readRestrictedDiff, resolveGitRepository, restoreCurrentChange } from '../workspace.js';
2
2
  import { WorkspaceContentSearchService } from './workspace-content-search-service.js';
3
3
  export class WorkspaceWorkbenchService {
4
4
  options;
5
5
  contentSearch;
6
+ topicRefreshes = new Map();
7
+ dirtyTopics = new Set();
6
8
  constructor(options) {
7
9
  this.options = options;
8
10
  this.contentSearch = new WorkspaceContentSearchService(options.files);
@@ -13,6 +15,7 @@ export class WorkspaceWorkbenchService {
13
15
  'workspace.unwatch': (data) => this.unwatch(record(data)),
14
16
  'workspace.files.list': (data) => this.listFiles(record(data)),
15
17
  'workspace.file.read': (data) => this.readFile(record(data)),
18
+ 'workspace.file.content.read': (data) => this.readFileContent(record(data)),
16
19
  'workspace.files.search': (data) => this.searchFiles(record(data)),
17
20
  'workspace.files.mutate': (data) => this.mutateFile(record(data)),
18
21
  'workspace.content.search': (data) => this.searchContent(record(data)),
@@ -31,44 +34,220 @@ export class WorkspaceWorkbenchService {
31
34
  'workspace.git.history.list': (data) => this.gitHistory(record(data)),
32
35
  'workspace.git.commit.files': (data) => this.gitCommitFiles(record(data)),
33
36
  'workspace.git.commit.file.diff': (data) => this.gitCommitFileDiff(record(data)),
37
+ 'workspace.git.commit.file.content.read': (data) => this.gitCommitFileContent(record(data)),
34
38
  'workspace.diff': (data) => this.diff(record(data))
35
39
  };
36
40
  }
41
+ async refreshTopic(workspaceId, topic) {
42
+ const key = `${workspaceId}:${topic}`;
43
+ const active = this.topicRefreshes.get(key);
44
+ if (active !== undefined) {
45
+ this.dirtyTopics.add(key);
46
+ return active;
47
+ }
48
+ const refresh = this.performTopicRefresh(workspaceId, topic).catch(() => {
49
+ this.emitTopicFailure(workspaceId, topic);
50
+ });
51
+ this.topicRefreshes.set(key, refresh);
52
+ try {
53
+ await refresh;
54
+ }
55
+ finally {
56
+ if (this.topicRefreshes.get(key) === refresh)
57
+ this.topicRefreshes.delete(key);
58
+ if (this.dirtyTopics.delete(key))
59
+ void this.refreshTopic(workspaceId, topic);
60
+ }
61
+ }
62
+ emitTopicFailure(workspaceId, topic) {
63
+ for (const watch of this.options.watch.activeWatches(workspaceId)) {
64
+ const subscribed = topic === 'files' ? watch.topics.files !== undefined : watch.topics[topic] === true;
65
+ if (!subscribed)
66
+ continue;
67
+ this.options.emitWatchEvent({
68
+ type: 'watch.failed',
69
+ workbenchConnectionId: watch.workbenchConnectionId,
70
+ watchType: 'workspace',
71
+ watchId: watch.watchId,
72
+ nodeGeneration: watch.nodeGeneration,
73
+ nodeId: this.options.nodeId(),
74
+ workspaceId,
75
+ topic,
76
+ code: 'WORKSPACE_WATCH_SNAPSHOT_FAILED',
77
+ message: 'Workspace watch snapshot failed.'
78
+ });
79
+ }
80
+ }
81
+ async performTopicRefresh(workspaceId, topic) {
82
+ const workspace = this.options.requireWorkspace(workspaceId);
83
+ const watches = this.options.watch.activeWatches(workspaceId);
84
+ if (topic === 'sessions') {
85
+ const subscribers = watches.filter((watch) => watch.topics.sessions);
86
+ if (subscribers.length === 0)
87
+ return;
88
+ const payload = await this.options.listSessions(workspace.id);
89
+ for (const watch of subscribers)
90
+ this.deliverSnapshot(workspace, watch, topic, payload);
91
+ return;
92
+ }
93
+ if (topic === 'changes') {
94
+ const subscribers = watches.filter((watch) => watch.topics.changes);
95
+ if (subscribers.length === 0)
96
+ return;
97
+ const payload = workspace.kind === 'GIT_WORKSPACE' ? await this.options.changes.current(workspace) : null;
98
+ for (const watch of subscribers)
99
+ this.deliverSnapshot(workspace, watch, topic, payload);
100
+ return;
101
+ }
102
+ const views = new Map();
103
+ for (const watch of watches) {
104
+ if (watch.topics.files === undefined)
105
+ continue;
106
+ const key = JSON.stringify(watch.topics.files);
107
+ views.set(key, [...(views.get(key) ?? []), watch]);
108
+ }
109
+ await Promise.all([...views.values()].map(async (subscribers) => {
110
+ const view = subscribers[0]?.topics.files;
111
+ if (view === undefined)
112
+ return;
113
+ const payload = { files: await this.options.files.list(workspace, view) };
114
+ for (const watch of subscribers)
115
+ this.deliverSnapshot(workspace, watch, topic, payload);
116
+ }));
117
+ }
37
118
  async watch(input) {
38
119
  if (typeof input.workspaceId !== 'string' ||
39
120
  typeof input.watchId !== 'string' ||
121
+ typeof input.workbenchConnectionId !== 'string' ||
40
122
  !/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId) ||
41
123
  (input.mode !== 'initial' && input.mode !== 'renew'))
42
124
  throw new Error('WORKSPACE_WATCH_INVALID');
43
125
  const workspace = this.options.requireWorkspace(input.workspaceId);
44
126
  if (input.mode === 'renew') {
45
- const watch = this.options.watch.renew(workspace.id, this.options.watch.key(input));
127
+ const watch = this.options.watch.renew(workspace.id, this.options.watch.key(input), input.watchId);
46
128
  if (watch === undefined)
47
129
  throw new Error('WORKSPACE_WATCH_NOT_FOUND');
48
- return { renewed: true };
130
+ return this.accepted(watch);
49
131
  }
50
132
  const topics = this.options.watch.parseTopics(input);
51
- const snapshot = {};
52
- if (topics.sessions)
53
- snapshot.sessions = await this.options.listSessions(workspace.id);
54
- if (topics.files !== undefined)
55
- snapshot.files = { files: await this.options.files.list(workspace, topics.files) };
56
- if (topics.changes)
57
- snapshot.changes =
58
- workspace.kind === 'GIT_WORKSPACE' ? await this.options.changes.current(workspace) : null;
59
- if (topics.files !== undefined || topics.changes)
60
- await this.options.files.index(workspace).observe();
61
- const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), topics);
62
- return { watch: { expiresAt: watch.expiresAt }, snapshot };
133
+ const existing = this.options.watch.existing(workspace.id, this.options.watch.key(input));
134
+ if (existing?.watchId === input.watchId &&
135
+ JSON.stringify(existing.topics) === JSON.stringify(topics)) {
136
+ this.options.watch.renew(workspace.id, this.options.watch.key(input), input.watchId);
137
+ return this.accepted(existing);
138
+ }
139
+ const watch = this.options.watch.create(workspace.id, input.watchId, this.options.watch.sessionHash(input), input.workbenchConnectionId, this.options.nodeGeneration(), topics);
140
+ void this.createInitialSnapshots(workspace, watch);
141
+ return this.accepted(watch);
63
142
  }
64
143
  unwatch(input) {
65
144
  if (typeof input.workspaceId !== 'string' ||
66
145
  typeof input.watchId !== 'string' ||
146
+ typeof input.workbenchConnectionId !== 'string' ||
67
147
  !/^[A-Za-z0-9_-]{8,128}$/.test(input.watchId))
68
148
  throw new Error('WORKSPACE_WATCH_INVALID');
69
- this.options.watch.remove(input.workspaceId, this.options.watch.key(input));
149
+ const key = this.options.watch.key(input);
150
+ const watch = this.options.watch.renew(input.workspaceId, key, input.watchId);
151
+ if (watch !== undefined)
152
+ this.options.watch.remove(input.workspaceId, key);
70
153
  return { stopped: true };
71
154
  }
155
+ accepted(watch) {
156
+ return {
157
+ accepted: true,
158
+ watchId: watch.watchId,
159
+ nodeGeneration: watch.nodeGeneration,
160
+ expiresAt: watch.expiresAt
161
+ };
162
+ }
163
+ async createInitialSnapshots(workspace, watch) {
164
+ const tasks = [];
165
+ const emit = async (topic, read) => {
166
+ try {
167
+ const payload = await read();
168
+ if (!this.options.watch.matches(workspace.id, `${watch.sessionHash}:${watch.workbenchConnectionId}`, watch.watchId))
169
+ return;
170
+ this.options.emitWatchEvent({
171
+ type: 'watch.workspace.snapshot',
172
+ workbenchConnectionId: watch.workbenchConnectionId,
173
+ watchId: watch.watchId,
174
+ nodeGeneration: watch.nodeGeneration,
175
+ nodeId: this.options.nodeId(),
176
+ workspaceId: workspace.id,
177
+ topic,
178
+ revision: this.options.watch.nextRevision(workspace.id),
179
+ snapshotSequence: this.options.watch.nextSnapshotSequence(),
180
+ expiresAt: watch.expiresAt,
181
+ payload
182
+ });
183
+ }
184
+ catch {
185
+ this.options.emitWatchEvent({
186
+ type: 'watch.failed',
187
+ workbenchConnectionId: watch.workbenchConnectionId,
188
+ watchType: 'workspace',
189
+ watchId: watch.watchId,
190
+ nodeGeneration: watch.nodeGeneration,
191
+ nodeId: this.options.nodeId(),
192
+ workspaceId: workspace.id,
193
+ topic,
194
+ code: 'WORKSPACE_WATCH_SNAPSHOT_FAILED',
195
+ message: 'Workspace watch snapshot failed.'
196
+ });
197
+ }
198
+ };
199
+ if (watch.topics.sessions)
200
+ tasks.push(emit('sessions', () => this.options.listSessions(workspace.id)));
201
+ if (watch.topics.files !== undefined)
202
+ tasks.push(emit('files', async () => ({
203
+ files: await this.options.files.list(workspace, watch.topics.files)
204
+ })));
205
+ if (watch.topics.changes)
206
+ tasks.push(emit('changes', async () => workspace.kind === 'GIT_WORKSPACE' ? this.options.changes.current(workspace) : null));
207
+ if (watch.topics.files !== undefined || watch.topics.changes)
208
+ void this.options.files
209
+ .index(workspace)
210
+ .observe()
211
+ .catch(() => undefined);
212
+ await Promise.all(tasks);
213
+ }
214
+ async emitSnapshot(workspace, watch, topic, read) {
215
+ try {
216
+ const payload = await read();
217
+ this.deliverSnapshot(workspace, watch, topic, payload);
218
+ }
219
+ catch {
220
+ this.options.emitWatchEvent({
221
+ type: 'watch.failed',
222
+ workbenchConnectionId: watch.workbenchConnectionId,
223
+ watchType: 'workspace',
224
+ watchId: watch.watchId,
225
+ nodeGeneration: watch.nodeGeneration,
226
+ nodeId: this.options.nodeId(),
227
+ workspaceId: workspace.id,
228
+ topic,
229
+ code: 'WORKSPACE_WATCH_SNAPSHOT_FAILED',
230
+ message: 'Workspace watch snapshot failed.'
231
+ });
232
+ }
233
+ }
234
+ deliverSnapshot(workspace, watch, topic, payload) {
235
+ if (!this.options.watch.matches(workspace.id, `${watch.sessionHash}:${watch.workbenchConnectionId}`, watch.watchId))
236
+ return;
237
+ this.options.emitWatchEvent({
238
+ type: 'watch.workspace.snapshot',
239
+ workbenchConnectionId: watch.workbenchConnectionId,
240
+ watchId: watch.watchId,
241
+ nodeGeneration: watch.nodeGeneration,
242
+ nodeId: this.options.nodeId(),
243
+ workspaceId: workspace.id,
244
+ topic,
245
+ revision: this.options.watch.nextRevision(workspace.id),
246
+ snapshotSequence: this.options.watch.nextSnapshotSequence(),
247
+ expiresAt: watch.expiresAt,
248
+ payload
249
+ });
250
+ }
72
251
  async listFiles(input) {
73
252
  const workspace = this.options.requireWorkspace(input.workspaceId);
74
253
  return { files: await this.options.files.list(workspace, input) };
@@ -112,6 +291,13 @@ export class WorkspaceWorkbenchService {
112
291
  async currentChanges(input) {
113
292
  return this.options.changes.current(this.options.requireWorkspace(input.workspaceId));
114
293
  }
294
+ async readFileContent(input) {
295
+ if (typeof input.workspaceId !== 'string')
296
+ throw new Error('WORKSPACE_NOT_FOUND');
297
+ return {
298
+ file: await this.options.files.readContent(this.options.requireWorkspace(input.workspaceId), input)
299
+ };
300
+ }
115
301
  async changeDiff(input) {
116
302
  if (typeof input.workspaceId !== 'string' ||
117
303
  typeof input.path !== 'string' ||
@@ -184,6 +370,19 @@ export class WorkspaceWorkbenchService {
184
370
  diff: await readGitHistoryFileDiff(await this.gitRepository(input), input.commit, input.path, this.refs(input), input.previousPath)
185
371
  };
186
372
  }
373
+ async gitCommitFileContent(input) {
374
+ if (typeof input.commit !== 'string' ||
375
+ typeof input.path !== 'string' ||
376
+ !Number.isSafeInteger(input.offset) ||
377
+ input.offset < 0 ||
378
+ !Number.isSafeInteger(input.limit) ||
379
+ input.limit < 1 ||
380
+ input.limit > 20 * 1024 * 1024)
381
+ throw new Error('FILE_RANGE_INVALID');
382
+ return {
383
+ file: await readGitHistoryFileContentRange(await this.gitRepository(input), input.commit, input.path, input.offset, input.limit, this.refs(input))
384
+ };
385
+ }
187
386
  async diff(input) {
188
387
  if (typeof input.workspaceId !== 'string' ||
189
388
  typeof input.fromRef !== 'string' ||
@@ -0,0 +1,2 @@
1
+ import type { RunnerName } from '@myagentroam/protocol';
2
+ export declare function personalInstructionsForRunner(value: unknown, runner: RunnerName): string | undefined;
@@ -0,0 +1,31 @@
1
+ const MAX_CONTENT_LENGTH = 20_000;
2
+ export function personalInstructionsForRunner(value, runner) {
3
+ if (value === undefined)
4
+ return undefined;
5
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
6
+ throw new Error('PERSONAL_INSTRUCTION_INVALID');
7
+ const record = value;
8
+ const instructions = {
9
+ common: content(record['common']),
10
+ codex: content(record['codex']),
11
+ claudeCode: content(record['claudeCode']),
12
+ openCode: content(record['openCode'])
13
+ };
14
+ const specialized = runner === 'codex'
15
+ ? instructions.codex
16
+ : runner === 'claude-code'
17
+ ? instructions.claudeCode
18
+ : instructions.openCode;
19
+ const merged = [instructions.common, specialized].filter((item) => item.length > 0).join('\n\n');
20
+ return merged.length === 0 ? undefined : merged;
21
+ }
22
+ function content(value) {
23
+ if (value === undefined)
24
+ return '';
25
+ if (typeof value !== 'string')
26
+ throw new Error('PERSONAL_INSTRUCTION_INVALID');
27
+ const normalized = value.trim();
28
+ if (normalized.length > MAX_CONTENT_LENGTH)
29
+ throw new Error('PERSONAL_INSTRUCTION_INVALID');
30
+ return normalized;
31
+ }
@@ -22,6 +22,7 @@ export declare function settleWithin<T>(promise: Promise<T>, milliseconds: numbe
22
22
  export declare function extractCodexThreads(result: unknown): readonly {
23
23
  readonly id: string;
24
24
  readonly source: string | undefined;
25
+ readonly derived: boolean;
25
26
  readonly cwd: unknown;
26
27
  readonly title: string | undefined;
27
28
  }[];
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import { fileURLToPath } from 'node:url';
2
3
  import { stripClaudePlanTag } from '../runner-command-engine.js';
3
4
  import { compactRunnerText, isClaudeCommandTool, isPlainRecord, nativeClaudeCommand, nativePageEnd } from './node-operation-parsers.js';
@@ -49,12 +50,17 @@ export function extractCodexThreads(result) {
49
50
  typeof value.source.kind === 'string'
50
51
  ? value.source.kind
51
52
  : undefined;
53
+ const sourceRecord = isPlainRecord(value.source) ? value.source : undefined;
54
+ const derived = source === 'subagent' ||
55
+ sourceRecord?.kind === 'subagent' ||
56
+ isPlainRecord(sourceRecord?.subagent) ||
57
+ isPlainRecord(sourceRecord?.subAgent);
52
58
  const title = typeof value.name === 'string' && value.name.trim().length > 0
53
59
  ? value.name.trim()
54
60
  : typeof value.preview === 'string' && value.preview.trim().length > 0
55
61
  ? value.preview.trim()
56
62
  : undefined;
57
- return [{ id: value.id, source, cwd: value.cwd, title }];
63
+ return [{ id: value.id, source, derived, cwd: value.cwd, title }];
58
64
  });
59
65
  }
60
66
  export function codexThreadActivity(result) {
@@ -106,6 +112,8 @@ export function claudeDiscoveredSession(value, fallbackCwd) {
106
112
  if (typeof value !== 'object' || value === null)
107
113
  return undefined;
108
114
  const record = value;
115
+ if (record.isSidechain === true)
116
+ return undefined;
109
117
  const externalSessionId = typeof record.sessionId === 'string'
110
118
  ? record.sessionId
111
119
  : typeof record.id === 'string'
@@ -145,26 +153,27 @@ export function deduplicateDirectSessions(sessions) {
145
153
  }
146
154
  export function nativeConversationPage(session, history, input, runtimeTurns = [], registerImages = () => []) {
147
155
  const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
156
+ const effectiveCursor = history.cursorReset === true ? undefined : input.cursor;
148
157
  const nativeTurns = nativeTranscriptTurns(history.items);
158
+ const nativeTurnIds = nativeTranscriptTurnIds(session.id, nativeTurns);
149
159
  const representedClientMessageIds = new Set(nativeTurns.flatMap((entries) => entries.flatMap(({ entry, index }) => {
150
160
  if (entry.kind !== 'message' || entry.role !== 'USER')
151
161
  return [];
152
162
  const clientMessageId = nativeUserClientMessageId(entry, entry.createdAt ?? index, runtimeTurns);
153
163
  return clientMessageId === null ? [] : [clientMessageId];
154
164
  })));
155
- const missingRuntimeErrorTurns = input.cursor === undefined
165
+ const missingRuntimeErrorTurns = effectiveCursor === undefined
156
166
  ? runtimeTurns.filter((turn) => {
157
167
  const clientMessageId = runtimeTurnClientMessageId(turn);
158
168
  return (turn.items.some((item) => item.kind === 'error') &&
159
169
  (clientMessageId === undefined || !representedClientMessageIds.has(clientMessageId)));
160
170
  })
161
171
  : [];
162
- const end = nativePageEnd(input.cursor, session.id, nativeTurns.length);
172
+ const end = nativePageEnd(effectiveCursor, session.id, nativeTurns.length);
163
173
  const start = Math.max(0, end - limit);
164
174
  const nativePageTurns = nativeTurns
165
175
  .slice(start, end)
166
176
  .map((entries, relativeIndex) => {
167
- const firstIndex = entries[0]?.index ?? 0;
168
177
  const nativeUser = entries.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
169
178
  const matchedClientMessageId = nativeUser?.kind === 'message'
170
179
  ? nativeUserClientMessageId(nativeUser, nativeUser.createdAt, runtimeTurns)
@@ -172,9 +181,10 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
172
181
  const runtimeTurn = typeof matchedClientMessageId === 'string'
173
182
  ? runtimeTurns.find((turn) => runtimeTurnClientMessageId(turn) === matchedClientMessageId)
174
183
  : undefined;
175
- const turnId = runtimeTurn?.id ?? `${session.id}:native:${firstIndex}`;
176
- const nativeItems = entries.map(({ entry, index }) => ({
177
- ...nativeTranscriptConversationItem(session, entry, index, turnId, runtimeTurns, registerImages),
184
+ const turnId = runtimeTurn?.id ?? nativeTurnIds[start + relativeIndex];
185
+ const itemIdentities = nativeTranscriptItemIdentities(entries);
186
+ const nativeItems = entries.map(({ entry, index }, entryIndex) => ({
187
+ ...nativeTranscriptConversationItem(session, entry, index, itemIdentities[entryIndex], turnId, runtimeTurns, registerImages),
178
188
  runId: runtimeTurn?.runId ?? null
179
189
  }));
180
190
  const runtimeErrors = (runtimeTurn?.items ?? [])
@@ -214,11 +224,33 @@ export function nativeConversationPage(session, history, input, runtimeTurns = [
214
224
  const turns = combined.slice(-limit);
215
225
  const nativePageIds = new Set(nativePageTurns.map((turn) => turn.id));
216
226
  const nextEnd = start + dropped.filter((turn) => nativePageIds.has(turn.id)).length;
227
+ const cursorBase = {
228
+ sessionId: session.id,
229
+ ...(history.snapshotEnd === undefined ? {} : { snapshotEnd: history.snapshotEnd }),
230
+ ...(history.windowEnd === undefined ? {} : { nextEnd: history.windowEnd }),
231
+ ...(history.fileAnchor === undefined ? {} : { fileAnchor: history.fileAnchor }),
232
+ ...(history.anchorBytes === undefined ? {} : { anchorBytes: history.anchorBytes }),
233
+ readLimit: history.windowReadLimit ?? limit
234
+ };
235
+ const precedingWindow = history.truncated === true &&
236
+ history.windowStart !== undefined &&
237
+ history.snapshotEnd !== undefined &&
238
+ history.fileAnchor !== undefined &&
239
+ history.anchorBytes !== undefined
240
+ ? Buffer.from(JSON.stringify({
241
+ sessionId: session.id,
242
+ snapshotEnd: history.snapshotEnd,
243
+ nextEnd: history.windowStart,
244
+ fileAnchor: history.fileAnchor,
245
+ anchorBytes: history.anchorBytes,
246
+ readLimit: history.windowReadLimit ?? limit
247
+ })).toString('base64url')
248
+ : null;
217
249
  return {
218
250
  turns,
219
251
  nextCursor: nextEnd > 0
220
- ? Buffer.from(JSON.stringify({ sessionId: session.id, end: nextEnd })).toString('base64url')
221
- : null
252
+ ? Buffer.from(JSON.stringify({ ...cursorBase, end: nextEnd })).toString('base64url')
253
+ : precedingWindow
222
254
  };
223
255
  }
224
256
  function runtimeTurnClientMessageId(turn) {
@@ -230,6 +262,38 @@ function runtimeTurnClientMessageId(turn) {
230
262
  function lastNativeItemId(entries) {
231
263
  return entries.findLast(({ entry }) => entry.nativeId !== undefined)?.entry.nativeId;
232
264
  }
265
+ function nativeTranscriptTurnIds(sessionId, turns) {
266
+ const occurrences = new Map();
267
+ return turns.map((entries) => {
268
+ const user = entries.find(({ entry }) => entry.kind === 'message' && entry.role === 'USER')?.entry;
269
+ const first = user ?? entries[0]?.entry;
270
+ const base = first?.nativeId ?? nativeTranscriptItemFingerprint(first);
271
+ const occurrence = occurrences.get(base) ?? 0;
272
+ occurrences.set(base, occurrence + 1);
273
+ return `${sessionId}:native:${base}${occurrence === 0 ? '' : `:${occurrence}`}`;
274
+ });
275
+ }
276
+ function nativeTranscriptItemIdentities(entries) {
277
+ const nativeIdCounts = new Map();
278
+ for (const { entry } of entries)
279
+ if (entry.nativeId !== undefined)
280
+ nativeIdCounts.set(entry.nativeId, (nativeIdCounts.get(entry.nativeId) ?? 0) + 1);
281
+ const occurrences = new Map();
282
+ return entries.map(({ entry }) => {
283
+ const base = entry.nativeId !== undefined && nativeIdCounts.get(entry.nativeId) === 1
284
+ ? entry.nativeId
285
+ : `${entry.nativeId === undefined ? '' : `${entry.nativeId}:`}${nativeTranscriptItemFingerprint(entry)}`;
286
+ const occurrence = occurrences.get(base) ?? 0;
287
+ occurrences.set(base, occurrence + 1);
288
+ return occurrence === 0 ? base : `${base}:${occurrence}`;
289
+ });
290
+ }
291
+ function nativeTranscriptItemFingerprint(entry) {
292
+ return createHash('sha256')
293
+ .update(JSON.stringify(entry ?? null))
294
+ .digest('base64url')
295
+ .slice(0, 24);
296
+ }
233
297
  function nativeTranscriptTurns(items) {
234
298
  const turns = [];
235
299
  let current = [];
@@ -244,7 +308,7 @@ function nativeTranscriptTurns(items) {
244
308
  turns.push(current);
245
309
  return turns;
246
310
  }
247
- function nativeTranscriptConversationItem(session, entry, index, turnId, runtimeTurns, registerImages) {
311
+ function nativeTranscriptConversationItem(session, entry, index, itemIdentity, turnId, runtimeTurns, registerImages) {
248
312
  const time = entry.createdAt;
249
313
  const isToolCall = entry.kind === 'tool_call';
250
314
  const isUnknown = entry.kind === 'unknown';
@@ -269,7 +333,7 @@ function nativeTranscriptConversationItem(session, entry, index, turnId, runtime
269
333
  : '';
270
334
  const planText = entry.kind === 'message' && entry.role !== 'USER' ? claudePlanText(entryText) : undefined;
271
335
  const item = {
272
- id: `${turnId}:item:${index}`,
336
+ id: `${turnId}:item:${itemIdentity}`,
273
337
  sessionId: session.id,
274
338
  turnId,
275
339
  runId: null,
@@ -294,7 +358,7 @@ function nativeTranscriptConversationItem(session, entry, index, turnId, runtime
294
358
  status: 'COMPLETED',
295
359
  payload: isUserInputRequest
296
360
  ? {
297
- requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${index}`}`,
361
+ requestId: `native:${entry.kind === 'tool_call' ? entry.toolUseId : `item-${itemIdentity}`}`,
298
362
  questions: userInputQuestions
299
363
  }
300
364
  : isReasoning
@@ -111,6 +111,13 @@ export declare function readWorkspaceTextFile(workspaceRoot: string, requestedPa
111
111
  readonly truncated: boolean;
112
112
  readonly nextOffset: number | null;
113
113
  }>;
114
+ export declare function readWorkspaceFileContentRange(workspaceRoot: string, requestedPath: string, offset?: number, limit?: number): Promise<{
115
+ readonly path: string;
116
+ readonly size: number;
117
+ readonly offset: number;
118
+ readonly contentBase64: string;
119
+ readonly nextOffset: number | null;
120
+ }>;
114
121
  /** Reads one absolute file only when the resolved target remains within a Node allowed root. */
115
122
  export declare function readAllowedTextFile(requestedPath: string, allowedRoots: readonly string[], offset?: number, limit?: number): Promise<{
116
123
  readonly path: string;
@@ -177,8 +184,13 @@ export declare function readGitHistoryFileDiff(repository: string, commit: strin
177
184
  readonly text?: string;
178
185
  readonly summary?: string;
179
186
  readonly truncated: boolean;
180
- readonly beforePreview?: BinaryDiffPreview;
181
- readonly afterPreview?: BinaryDiffPreview;
187
+ }>;
188
+ export declare function readGitHistoryFileContentRange(repository: string, commit: string, requestedPath: string, offset: number, limit: number, refs?: readonly string[]): Promise<{
189
+ readonly path: string;
190
+ readonly size: number;
191
+ readonly offset: number;
192
+ readonly contentBase64: string;
193
+ readonly nextOffset: number | null;
182
194
  }>;
183
195
  export declare function readCurrentChanges(workspacePath: string): Promise<readonly {
184
196
  readonly path: string;
@@ -192,15 +204,9 @@ export declare function readCurrentChangeDiff(workspacePath: string, requestedPa
192
204
  readonly truncated: boolean;
193
205
  readonly afterText?: string;
194
206
  readonly afterTruncated?: boolean;
195
- readonly beforePreview?: BinaryDiffPreview;
196
- readonly afterPreview?: BinaryDiffPreview;
197
207
  }>;
198
208
  /** Current Changes must not expose an external link. */
199
209
  export declare function isWorkspaceChangeVisible(workspacePath: string, requestedPath: string): Promise<boolean>;
200
- interface BinaryDiffPreview {
201
- readonly contentBase64?: string;
202
- readonly truncated: boolean;
203
- }
204
210
  export declare function restoreCurrentChange(workspacePath: string, requestedPath: string, state: 'STAGED' | 'UNSTAGED' | 'UNTRACKED'): Promise<void>;
205
211
  declare function gitChange(status: string): 'ADDED' | 'MODIFIED' | 'DELETED' | 'RENAMED' | 'COPIED' | 'TYPE_CHANGED';
206
212
  export declare class GitCommandError extends Error {