@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
@@ -5,6 +5,7 @@ import { AbstractRunner } from './abstract-runner.js';
5
5
  import { isWithinWorkspace } from './codex/conversation-parser.js';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { openCodeConversationPage } from './opencode/conversation-parser.js';
8
+ import { nodeLog } from '../operational.js';
8
9
  const PROFILE_ENTRY_TTL_MS = 30 * 60_000;
9
10
  export class OpenCodeRunner extends AbstractRunner {
10
11
  client;
@@ -14,6 +15,8 @@ export class OpenCodeRunner extends AbstractRunner {
14
15
  modelsByWorkspace = new Map();
15
16
  contextLimitsByWorkspace = new Map();
16
17
  profileTimersByWorkspace = new Map();
18
+ profileCache = new Map();
19
+ profileRefreshes = new Map();
17
20
  execution = new Map();
18
21
  managedTurns = new Map();
19
22
  environmentClients = new Map();
@@ -35,25 +38,79 @@ export class OpenCodeRunner extends AbstractRunner {
35
38
  if (capabilities.openCode?.available !== true || workspace === undefined) {
36
39
  return this.profile(capabilities);
37
40
  }
41
+ const cacheKey = this.profileCacheKey(workspace.path, environment);
42
+ const now = Date.now();
43
+ for (const [key, entry] of this.profileCache)
44
+ if (entry.expiresAt <= now)
45
+ this.profileCache.delete(key);
46
+ const cached = this.profileCache.get(cacheKey);
47
+ if (cached !== undefined) {
48
+ nodeLog('runner.opencode.profile.cache-hit', {
49
+ workspaceId: workspace.id,
50
+ models: cached.models.length,
51
+ remainingTtlMs: cached.expiresAt - now
52
+ });
53
+ this.modelsByWorkspace.set(workspace.path, cached.models);
54
+ this.contextLimitsByWorkspace.set(workspace.path, cached.contextLimits);
55
+ this.scheduleProfileExpiry(workspace.path);
56
+ return { ...this.profile(capabilities), models: cached.models };
57
+ }
58
+ const current = this.profileRefreshes.get(cacheKey);
59
+ if (current !== undefined) {
60
+ nodeLog('runner.opencode.profile.single-flight-joined', { workspaceId: workspace.id });
61
+ return current;
62
+ }
63
+ const refresh = this.fetchProfile(capabilities, workspace, environment, cacheKey).finally(() => {
64
+ if (this.profileRefreshes.get(cacheKey) === refresh)
65
+ this.profileRefreshes.delete(cacheKey);
66
+ });
67
+ this.profileRefreshes.set(cacheKey, refresh);
68
+ return refresh;
69
+ }
70
+ async fetchProfile(capabilities, workspace, environment, cacheKey) {
38
71
  const client = this.managedClient(environment);
72
+ const startedAt = performance.now();
39
73
  try {
40
74
  const catalog = await client.providers(workspace.path);
41
75
  const models = openCodeProfileModels(catalog);
76
+ const contextLimits = openCodeContextLimits(catalog);
42
77
  this.modelsByWorkspace.set(workspace.path, models);
43
- this.contextLimitsByWorkspace.set(workspace.path, openCodeContextLimits(catalog));
78
+ this.contextLimitsByWorkspace.set(workspace.path, contextLimits);
79
+ this.profileCache.set(cacheKey, {
80
+ expiresAt: Date.now() + PROFILE_ENTRY_TTL_MS,
81
+ workspacePath: workspace.path,
82
+ environmentFingerprint: this.environmentFingerprint(environment),
83
+ models,
84
+ contextLimits
85
+ });
44
86
  this.scheduleProfileExpiry(workspace.path);
87
+ nodeLog('runner.opencode.profile.providers.completed', {
88
+ workspaceId: workspace.id,
89
+ models: models.length,
90
+ durationMs: Math.round(performance.now() - startedAt)
91
+ });
45
92
  return { ...this.profile(capabilities), models };
46
93
  }
47
94
  catch {
48
- this.modelsByWorkspace.delete(workspace.path);
49
- this.contextLimitsByWorkspace.delete(workspace.path);
50
- this.clearProfileTimer(workspace.path);
95
+ nodeLog('runner.opencode.profile.providers.failed', {
96
+ workspaceId: workspace.id,
97
+ durationMs: Math.round(performance.now() - startedAt)
98
+ });
51
99
  }
52
100
  finally {
53
101
  this.releaseManagedClient(client);
54
102
  }
55
103
  return this.profile(capabilities);
56
104
  }
105
+ profileCacheKey(workspacePath, environment) {
106
+ return createHash('sha256')
107
+ .update(JSON.stringify({ workspacePath, environment: this.environmentFingerprint(environment) }))
108
+ .digest('hex');
109
+ }
110
+ environmentFingerprint(environment) {
111
+ const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
112
+ return createHash('sha256').update(JSON.stringify(entries)).digest('hex');
113
+ }
57
114
  available(capabilities) {
58
115
  return capabilities.openCode?.available === true;
59
116
  }
@@ -68,22 +125,18 @@ export class OpenCodeRunner extends AbstractRunner {
68
125
  openCode: { available: true }
69
126
  });
70
127
  }
71
- supportsConfiguration(configuration, workspace) {
128
+ supportsConfiguration(configuration, workspace, environment = {}) {
72
129
  if (configuration.runner !== this.name)
73
130
  return false;
74
- const models = workspace === undefined
75
- ? [...this.modelsByWorkspace.values()].flat()
76
- : (this.modelsByWorkspace.get(workspace.path) ?? []);
131
+ const models = this.cachedModels(workspace, environment);
77
132
  const model = models.find((candidate) => candidate.id === configuration.model);
78
133
  return (model !== undefined &&
79
134
  (model.supportedEfforts.length === 0 ||
80
135
  model.supportedEfforts.includes(configuration.effort)) &&
81
136
  this.profileForValidation().accessOptions.some((candidate) => candidate.id === configuration.access));
82
137
  }
83
- defaultConfiguration(workspace) {
84
- const model = workspace === undefined
85
- ? this.modelsByWorkspace.values().next().value?.[0]
86
- : this.modelsByWorkspace.get(workspace.path)?.[0];
138
+ defaultConfiguration(workspace, environment = {}) {
139
+ const model = this.cachedModels(workspace, environment)[0];
87
140
  return {
88
141
  runner: this.name,
89
142
  model: model?.id ?? '',
@@ -91,6 +144,15 @@ export class OpenCodeRunner extends AbstractRunner {
91
144
  access: 'default'
92
145
  };
93
146
  }
147
+ cachedModels(workspace, environment) {
148
+ const fingerprint = this.environmentFingerprint(environment);
149
+ const now = Date.now();
150
+ return [...this.profileCache.values()]
151
+ .filter((entry) => entry.expiresAt > now &&
152
+ entry.environmentFingerprint === fingerprint &&
153
+ (workspace === undefined || entry.workspacePath === workspace.path))
154
+ .flatMap((entry) => entry.models);
155
+ }
94
156
  mcpConfiguration(raw, secrets) {
95
157
  const servers = super.mcpConfiguration(raw, secrets);
96
158
  return Object.fromEntries(servers.map((server) => [
@@ -121,7 +183,9 @@ export class OpenCodeRunner extends AbstractRunner {
121
183
  }
122
184
  for (const value of sessions) {
123
185
  const session = openCodeSession(value);
124
- if (session === undefined || !isWithinWorkspace(session.directory, target.path))
186
+ if (session === undefined ||
187
+ session.parentId !== undefined ||
188
+ !isWithinWorkspace(session.directory, target.path))
125
189
  continue;
126
190
  result.push(context.createProjection({
127
191
  workspace: target,
@@ -177,6 +241,10 @@ export class OpenCodeRunner extends AbstractRunner {
177
241
  return undefined;
178
242
  }
179
243
  }
244
+ async readConversationHistory(readers) {
245
+ const official = await readers.official();
246
+ return official === undefined ? undefined : { ...official, source: 'official' };
247
+ }
180
248
  managedRunForNativeTurn(nativeTurnId) {
181
249
  return this.managedTurns.get(nativeTurnId);
182
250
  }
@@ -286,6 +354,8 @@ export class OpenCodeRunner extends AbstractRunner {
286
354
  for (const timer of this.profileTimersByWorkspace.values())
287
355
  clearTimeout(timer);
288
356
  this.profileTimersByWorkspace.clear();
357
+ this.profileCache.clear();
358
+ this.profileRefreshes.clear();
289
359
  this.modelsByWorkspace.clear();
290
360
  this.contextLimitsByWorkspace.clear();
291
361
  const stopping = [
@@ -370,6 +440,9 @@ function openCodeSession(value) {
370
440
  id: record.id,
371
441
  directory: record.directory,
372
442
  title: record.title,
373
- updatedAt: typeof time?.updated === 'number' ? time.updated : 0
443
+ updatedAt: typeof time?.updated === 'number' ? time.updated : 0,
444
+ ...(typeof record.parentID === 'string' && record.parentID.length > 0
445
+ ? { parentId: record.parentID }
446
+ : {})
374
447
  };
375
448
  }
@@ -1,3 +1,4 @@
1
+ import { nodeLog } from '../operational.js';
1
2
  export class RunnerRegistry {
2
3
  runners = new Map();
3
4
  constructor(runners = []) {
@@ -35,7 +36,21 @@ export class RunnerRegistry {
35
36
  });
36
37
  }
37
38
  async refreshProfiles(capabilities, workspace, environment = {}) {
38
- const profiles = await Promise.all(this.advertised().map((runner) => runner.refreshProfile(capabilities, workspace, environment)));
39
+ const profiles = await Promise.all(this.advertised().map(async (runner) => {
40
+ const startedAt = performance.now();
41
+ try {
42
+ return await runner.refreshProfile(capabilities, workspace, environment);
43
+ }
44
+ finally {
45
+ const durationMs = Math.round(performance.now() - startedAt);
46
+ if (durationMs >= 100)
47
+ nodeLog('runner.profile.refresh.completed', {
48
+ runner: runner.name,
49
+ workspaceId: workspace?.id,
50
+ durationMs
51
+ });
52
+ }
53
+ }));
39
54
  return profiles.flatMap((profile) => (profile === undefined ? [] : [profile]));
40
55
  }
41
56
  supportsConfiguration(configuration, workspace, environment) {
@@ -50,18 +50,13 @@ export function normalizeVersion(id, value) {
50
50
  }
51
51
  async function executeVersion(command, args) {
52
52
  const executable = process.platform === 'win32' ? process.env['ComSpec'] || 'cmd.exe' : command;
53
- const commandArgs = process.platform === 'win32'
54
- ? ['/d', '/s', '/c', [command, ...args].map(quoteWindowsArgument).join(' ')]
55
- : [...args];
53
+ const commandArgs = process.platform === 'win32' ? ['/d', '/s', '/c', [command, ...args].join(' ')] : [...args];
56
54
  return execFileAsync(executable, commandArgs, {
57
55
  timeout: 5_000,
58
56
  maxBuffer: 64 * 1024,
59
57
  windowsHide: true
60
58
  });
61
59
  }
62
- function quoteWindowsArgument(value) {
63
- return `"${value.replaceAll('"', '""')}"`;
64
- }
65
60
  async function resolveCommandPath(command) {
66
61
  try {
67
62
  const resolver = process.platform === 'win32' ? 'where.exe' : 'which';
@@ -391,6 +391,7 @@ export class NodeRuntimeState {
391
391
  ? { ...item, payload: { ...item.payload, text: content } }
392
392
  : item)
393
393
  }));
394
+ this.bumpQueueVersion(run.sessionId);
394
395
  return run;
395
396
  }
396
397
  deleteQueuedRun(runId) {
@@ -20,22 +20,23 @@ interface ConversationHistoryServiceOptions {
20
20
  }
21
21
  export declare class ConversationHistoryService {
22
22
  private readonly options;
23
- private readonly activeOfficialReads;
23
+ private readonly activeHistoryReads;
24
24
  constructor(options: ConversationHistoryServiceOptions);
25
25
  read(session: NodeAgentSession, input: {
26
26
  readonly cursor?: string;
27
27
  readonly limit?: number;
28
28
  }): Promise<ConversationHistoryPage>;
29
+ private readRunnerHistory;
29
30
  private readManagedActive;
30
- private prefetchActiveOfficial;
31
- private takeActiveOfficialRead;
31
+ private prefetchActiveHistory;
32
+ private takeActiveHistoryRead;
32
33
  refreshExternalActivity(session: NodeAgentSession): Promise<void>;
33
- readNative(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeSessionHistory | undefined>;
34
+ readNative(session: NodeAgentSession, cursor?: string, limit?: number): Promise<import("../native-session-history.js").NativeSessionHistory | undefined>;
34
35
  private attachNativeImages;
35
36
  readOfficial(session: NodeAgentSession, input: {
36
37
  readonly cursor?: string;
37
38
  readonly limit?: number;
38
- }): Promise<import("../runner/abstract-runner.js").RunnerOfficialConversationPage | undefined>;
39
+ }): Promise<import("../runner/abstract-runner.js").RunnerConversationPage | undefined>;
39
40
  private managedRunnerTurn;
40
41
  private page;
41
42
  }
@@ -2,6 +2,7 @@ import { discoverNativeSessions, readNativeSession } from '../native-session-his
2
2
  import { coalesceImageGenerationItems } from '../runner/codex/conversation-parser.js';
3
3
  import { nativeConversationPage } from '../util/runner-native-session-parsers.js';
4
4
  import { isPlainRecord } from '../util/node-operation-parsers.js';
5
+ import { nodeLog } from '../operational.js';
5
6
  export function hasManagedActiveRun(runtime, session) {
6
7
  return (session.nativeControl === 'MAR_MANAGED' &&
7
8
  runtime
@@ -47,7 +48,7 @@ function isConversationHistorySource(value) {
47
48
  }
48
49
  export class ConversationHistoryService {
49
50
  options;
50
- activeOfficialReads = new Map();
51
+ activeHistoryReads = new Map();
51
52
  constructor(options) {
52
53
  this.options = options;
53
54
  }
@@ -57,7 +58,14 @@ export class ConversationHistoryService {
57
58
  const activeCursor = decodeActiveHistoryCursor(input.cursor, session.id);
58
59
  if (activeCursor !== undefined || this.options.hasManagedActiveRun(session))
59
60
  return this.readManagedActive(session, input, activeCursor, snapshotSequence, readAt);
60
- let official = await this.readOfficial(session, input);
61
+ const selected = await this.readRunnerHistory(session, input);
62
+ if (selected.page?.source === 'native')
63
+ return this.page(selected.page.nextCursor, selected.page.turns, {
64
+ snapshotSequence,
65
+ source: 'native',
66
+ readAt
67
+ });
68
+ let official = selected.page?.source === 'official' ? selected.page : undefined;
61
69
  if (official !== undefined) {
62
70
  const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
63
71
  const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
@@ -75,7 +83,7 @@ export class ConversationHistoryService {
75
83
  const reserved = Math.min(missingLocalTurns.length, Math.max(0, limit - 1));
76
84
  const narrowed = await this.readOfficial(session, { ...input, limit: limit - reserved });
77
85
  if (narrowed !== undefined)
78
- official = narrowed;
86
+ official = { ...narrowed, source: 'official' };
79
87
  }
80
88
  const officialTurns = await this.attachNativeImages(session, official.turns);
81
89
  const officialRunIds = new Set(officialTurns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
@@ -124,40 +132,68 @@ export class ConversationHistoryService {
124
132
  readAt
125
133
  });
126
134
  }
127
- if (session.externalSessionId !== null && !this.options.hasManagedActiveRun(session)) {
128
- const history = await this.readNative(session);
129
- if (history !== undefined) {
130
- if (history.truncated !== true ||
131
- this.options.runners.require(session.runner).acceptsTruncatedNativeHistory()) {
132
- const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
133
- ? []
134
- : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 })
135
- .turns;
136
- const page = nativeConversationPage(session, history, input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
137
- return this.page(page.nextCursor, page.turns, {
138
- snapshotSequence,
139
- source: 'native',
140
- readAt
141
- });
142
- }
143
- const runtimePage = this.options.runtime.listConversationTurns({
144
- sessionId: session.id,
145
- ...input
146
- });
147
- if (runtimePage.turns.length === 0)
148
- throw new Error('NATIVE_TRANSCRIPT_INCOMPLETE');
149
- return this.page(runtimePage.nextCursor, runtimePage.turns, {
150
- snapshotSequence,
151
- source: 'runtime',
152
- readAt
153
- });
154
- }
155
- if (session.nativeControl === 'EXTERNAL')
156
- throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
135
+ if (session.externalSessionId !== null && selected.incompleteNative) {
136
+ const runtimePage = this.options.runtime.listConversationTurns({
137
+ sessionId: session.id,
138
+ ...input
139
+ });
140
+ if (runtimePage.turns.length === 0)
141
+ throw new Error('NATIVE_TRANSCRIPT_INCOMPLETE');
142
+ return this.page(runtimePage.nextCursor, runtimePage.turns, {
143
+ snapshotSequence,
144
+ source: 'runtime',
145
+ readAt
146
+ });
157
147
  }
148
+ if (session.externalSessionId !== null && session.nativeControl === 'EXTERNAL')
149
+ throw new Error('NATIVE_TRANSCRIPT_UNAVAILABLE');
158
150
  const page = this.options.runtime.listConversationTurns({ sessionId: session.id, ...input });
159
151
  return this.page(page.nextCursor, page.turns, { snapshotSequence, source: 'runtime', readAt });
160
152
  }
153
+ async readRunnerHistory(session, input) {
154
+ const runner = this.options.runners.require(session.runner);
155
+ let incompleteNative = false;
156
+ const page = await runner.readConversationHistory({
157
+ native: async () => {
158
+ const startedAt = performance.now();
159
+ let history;
160
+ try {
161
+ history = await this.readNative(session, input.cursor, input.limit);
162
+ }
163
+ catch {
164
+ return undefined;
165
+ }
166
+ finally {
167
+ const durationMs = Math.round(performance.now() - startedAt);
168
+ if (durationMs >= 250)
169
+ nodeLog('native.session.history.native-read.completed', {
170
+ runner: session.runner,
171
+ sessionId: session.id,
172
+ durationMs
173
+ });
174
+ }
175
+ if (history === undefined)
176
+ return undefined;
177
+ if (history.truncated === true &&
178
+ (!runner.acceptsTruncatedNativeHistory() || history.items.length === 0)) {
179
+ incompleteNative = true;
180
+ return undefined;
181
+ }
182
+ const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
183
+ ? []
184
+ : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 })
185
+ .turns;
186
+ const native = nativeConversationPage(session, history, history.cursorReset === true
187
+ ? input.limit === undefined
188
+ ? {}
189
+ : { limit: input.limit }
190
+ : input, runtimeTurns, (images) => this.options.attachments.registerNative(session, images));
191
+ return native;
192
+ },
193
+ official: () => this.readOfficial(session, input)
194
+ });
195
+ return { page, incompleteNative };
196
+ }
161
197
  async readManagedActive(session, input, cursor, snapshotSequence, readAt) {
162
198
  const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
163
199
  const runtimeTurns = this.options.runtime.listConversationTurns({
@@ -166,26 +202,30 @@ export class ConversationHistoryService {
166
202
  }).turns;
167
203
  const beforeAt = cursor?.beforeAt ?? oldestTurnActivityAt(runtimeTurns);
168
204
  if (cursor?.stage === 'OFFICIAL') {
169
- const prefetched = this.takeActiveOfficialRead(session.id, limit, cursor.cursor);
170
- const official = await (prefetched ??
171
- this.readOfficial(session, {
205
+ const prefetched = this.takeActiveHistoryRead(session.id, limit, cursor.cursor);
206
+ const selected = await (prefetched ??
207
+ this.readRunnerHistory(session, {
172
208
  ...(cursor.cursor === null ? {} : { cursor: cursor.cursor }),
173
209
  limit
174
210
  }));
175
- if (official === undefined)
211
+ const history = selected.page;
212
+ if (history === undefined)
176
213
  return this.page(null, [], { snapshotSequence, source: 'runtime', readAt });
177
- const turns = (await this.attachNativeImages(session, official.turns)).filter((turn) => latestTurnActivityAt(turn) < beforeAt);
178
- return this.page(official.nextCursor === null
214
+ const hydratedTurns = history.source === 'official'
215
+ ? await this.attachNativeImages(session, history.turns)
216
+ : history.turns;
217
+ const turns = hydratedTurns.filter((turn) => latestTurnActivityAt(turn) < beforeAt);
218
+ return this.page(history.nextCursor === null
179
219
  ? null
180
220
  : encodeActiveHistoryCursor({
181
221
  sessionId: session.id,
182
222
  stage: 'OFFICIAL',
183
223
  beforeAt,
184
- cursor: official.nextCursor
185
- }), turns, { snapshotSequence, source: 'official', readAt });
224
+ cursor: history.nextCursor
225
+ }), turns, { snapshotSequence, source: history.source, readAt });
186
226
  }
187
227
  if (cursor === undefined && session.externalSessionId !== null)
188
- this.prefetchActiveOfficial(session, limit);
228
+ this.prefetchActiveHistory(session, limit);
189
229
  const runtimePage = this.options.runtime.listConversationTurns({
190
230
  sessionId: session.id,
191
231
  ...(cursor?.cursor === null || cursor === undefined ? {} : { cursor: cursor.cursor }),
@@ -213,29 +253,32 @@ export class ConversationHistoryService {
213
253
  deferredOlderHistory: true
214
254
  });
215
255
  }
216
- prefetchActiveOfficial(session, limit) {
256
+ prefetchActiveHistory(session, limit) {
217
257
  const now = Date.now();
218
- for (const [sessionId, read] of this.activeOfficialReads)
258
+ for (const [sessionId, read] of this.activeHistoryReads)
219
259
  if (read.createdAt + 60_000 <= now)
220
- this.activeOfficialReads.delete(sessionId);
221
- const current = this.activeOfficialReads.get(session.id);
260
+ this.activeHistoryReads.delete(sessionId);
261
+ const current = this.activeHistoryReads.get(session.id);
222
262
  if (current !== undefined && current.limit === limit)
223
263
  return;
224
- this.activeOfficialReads.set(session.id, {
264
+ this.activeHistoryReads.set(session.id, {
225
265
  createdAt: now,
226
266
  limit,
227
267
  // The active Run fast path must stay independent from an optional
228
268
  // background reader failure until the user explicitly requests older history.
229
- page: this.readOfficial(session, { limit }).catch(() => undefined)
269
+ page: this.readRunnerHistory(session, { limit }).catch(() => ({
270
+ page: undefined,
271
+ incompleteNative: false
272
+ }))
230
273
  });
231
274
  }
232
- takeActiveOfficialRead(sessionId, limit, cursor) {
275
+ takeActiveHistoryRead(sessionId, limit, cursor) {
233
276
  if (cursor !== null)
234
277
  return undefined;
235
- const read = this.activeOfficialReads.get(sessionId);
278
+ const read = this.activeHistoryReads.get(sessionId);
236
279
  if (read === undefined || read.limit !== limit || read.createdAt + 60_000 <= Date.now())
237
280
  return undefined;
238
- this.activeOfficialReads.delete(sessionId);
281
+ this.activeHistoryReads.delete(sessionId);
239
282
  return read.page;
240
283
  }
241
284
  async refreshExternalActivity(session) {
@@ -248,17 +291,26 @@ export class ConversationHistoryService {
248
291
  if (activity !== undefined)
249
292
  this.options.setExternalActivity(session.id, activity);
250
293
  }
251
- async readNative(session) {
294
+ async readNative(session, cursor, limit = 30) {
252
295
  if (session.externalSessionId === null)
253
296
  return undefined;
254
- const current = await readNativeSession(session.runner, session.cwd, session.externalSessionId);
297
+ const page = nativeHistoryPageCursor(cursor, session.id);
298
+ const current = await readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
255
299
  if (current !== undefined)
256
300
  return current;
257
301
  await discoverNativeSessions(session.runner, session.cwd);
258
- return readNativeSession(session.runner, session.cwd, session.externalSessionId);
302
+ return readNativeSession(session.runner, session.cwd, session.externalSessionId, page, limit);
259
303
  }
260
304
  async attachNativeImages(session, turns) {
305
+ const startedAt = performance.now();
261
306
  const history = await this.readNative(session);
307
+ const readDurationMs = Math.round(performance.now() - startedAt);
308
+ if (readDurationMs >= 250)
309
+ nodeLog('native.session.history.attachment-source-read.completed', {
310
+ runner: session.runner,
311
+ sessionId: session.id,
312
+ durationMs: readDurationMs
313
+ });
262
314
  if (history === undefined)
263
315
  return turns;
264
316
  const attachments = new Map();
@@ -285,7 +337,19 @@ export class ConversationHistoryService {
285
337
  const runner = this.options.runners.require(session.runner);
286
338
  if (session.externalSessionId === null || !runner.available(this.options.capabilities()))
287
339
  return undefined;
288
- return runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
340
+ const startedAt = performance.now();
341
+ try {
342
+ return await runner.readOfficialConversation(session, input, (nativeTurnId) => this.managedRunnerTurn(session, nativeTurnId));
343
+ }
344
+ finally {
345
+ const durationMs = Math.round(performance.now() - startedAt);
346
+ if (durationMs >= 250)
347
+ nodeLog('native.session.history.official-read.completed', {
348
+ runner: session.runner,
349
+ sessionId: session.id,
350
+ durationMs
351
+ });
352
+ }
289
353
  }
290
354
  managedRunnerTurn(session, nativeTurnId) {
291
355
  const runId = this.options.runners
@@ -306,6 +370,38 @@ export class ConversationHistoryService {
306
370
  };
307
371
  }
308
372
  }
373
+ function nativeHistoryPageCursor(cursor, sessionId) {
374
+ if (cursor === undefined)
375
+ return undefined;
376
+ try {
377
+ const value = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
378
+ return value.sessionId === sessionId &&
379
+ Number.isInteger(value.snapshotEnd) &&
380
+ Number.isInteger(value.nextEnd) &&
381
+ Number.isInteger(value.anchorBytes) &&
382
+ value.snapshotEnd >= 0 &&
383
+ value.nextEnd >= 0 &&
384
+ value.nextEnd <= value.snapshotEnd &&
385
+ value.anchorBytes >= 0 &&
386
+ value.anchorBytes <= value.snapshotEnd &&
387
+ (value.readLimit === undefined ||
388
+ (Number.isInteger(value.readLimit) &&
389
+ value.readLimit >= 1 &&
390
+ value.readLimit <= 500)) &&
391
+ typeof value.fileAnchor === 'string'
392
+ ? {
393
+ snapshotEnd: value.snapshotEnd,
394
+ nextEnd: value.nextEnd,
395
+ fileAnchor: value.fileAnchor,
396
+ anchorBytes: value.anchorBytes,
397
+ ...(value.readLimit === undefined ? {} : { readLimit: value.readLimit })
398
+ }
399
+ : undefined;
400
+ }
401
+ catch {
402
+ return undefined;
403
+ }
404
+ }
309
405
  const ACTIVE_HISTORY_CURSOR_PREFIX = 'mar-active-history:1:';
310
406
  function encodeActiveHistoryCursor(cursor) {
311
407
  return `${ACTIVE_HISTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor)).toString('base64url')}`;
@@ -18,7 +18,9 @@ interface ExternalSessionResumeServiceOptions {
18
18
  }
19
19
  export declare class ExternalSessionResumeService {
20
20
  private readonly options;
21
+ private readonly transitions;
21
22
  constructor(options: ExternalSessionResumeServiceOptions);
23
+ transitioning(sessionId: string): boolean;
22
24
  resume(external: NodeAgentSession, force?: boolean): Promise<NodeAgentSession | undefined>;
23
25
  failure(sessionId: string): {
24
26
  readonly code: string;