@myagentroam/node 0.9.3 → 0.9.5

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 (66) 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 +25 -0
  4. package/dist/codex-app-server.js +104 -7
  5. package/dist/connector.d.ts +1 -0
  6. package/dist/connector.js +21 -12
  7. package/dist/database.d.ts +2 -0
  8. package/dist/native-session-history.d.ts +3 -2
  9. package/dist/native-session-history.js +199 -30
  10. package/dist/opencode-server.d.ts +1 -0
  11. package/dist/opencode-server.js +1 -0
  12. package/dist/runner/abstract-runner.d.ts +11 -2
  13. package/dist/runner/abstract-runner.js +4 -0
  14. package/dist/runner/claude/managed-run-controller.js +3 -0
  15. package/dist/runner/claude-code-runner.d.ts +2 -1
  16. package/dist/runner/claude-code-runner.js +4 -0
  17. package/dist/runner/codex/managed-run-controller.d.ts +9 -0
  18. package/dist/runner/codex/managed-run-controller.js +63 -17
  19. package/dist/runner/codex-runner.d.ts +3 -1
  20. package/dist/runner/codex-runner.js +23 -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.d.ts +2 -0
  27. package/dist/runtime-state.js +12 -7
  28. package/dist/service/conversation-history-service.d.ts +10 -1
  29. package/dist/service/conversation-history-service.js +272 -42
  30. package/dist/service/conversation-segment-service.js +7 -1
  31. package/dist/service/external-session-resume-service.d.ts +2 -0
  32. package/dist/service/external-session-resume-service.js +45 -34
  33. package/dist/service/native-session-projection-service.d.ts +1 -0
  34. package/dist/service/native-session-projection-service.js +4 -0
  35. package/dist/service/native-session-watch-service.d.ts +26 -2
  36. package/dist/service/native-session-watch-service.js +258 -42
  37. package/dist/service/node-request-service.js +2 -1
  38. package/dist/service/run-event-service.js +16 -5
  39. package/dist/service/run-workbench-service.d.ts +2 -4
  40. package/dist/service/run-workbench-service.js +2 -7
  41. package/dist/service/session-command-service.js +1 -1
  42. package/dist/service/session-identity-service.d.ts +1 -1
  43. package/dist/service/session-identity-service.js +1 -1
  44. package/dist/service/session-lifecycle-service.js +7 -12
  45. package/dist/service/session-message-service.d.ts +1 -1
  46. package/dist/service/session-message-service.js +104 -83
  47. package/dist/service/skill-directory-service.d.ts +1 -0
  48. package/dist/service/skill-directory-service.js +40 -6
  49. package/dist/service/workspace-change-service.js +1 -1
  50. package/dist/service/workspace-domain-state.d.ts +3 -0
  51. package/dist/service/workspace-domain-state.js +1 -0
  52. package/dist/service/workspace-file-service.d.ts +1 -0
  53. package/dist/service/workspace-file-service.js +14 -1
  54. package/dist/service/workspace-queue-workbench-service.d.ts +29 -2
  55. package/dist/service/workspace-queue-workbench-service.js +73 -8
  56. package/dist/service/workspace-watch-service.d.ts +7 -2
  57. package/dist/service/workspace-watch-service.js +32 -6
  58. package/dist/service/workspace-workbench-service.d.ts +14 -0
  59. package/dist/service/workspace-workbench-service.js +215 -16
  60. package/dist/util/personal-instructions.d.ts +2 -0
  61. package/dist/util/personal-instructions.js +31 -0
  62. package/dist/util/runner-native-session-parsers.d.ts +3 -1
  63. package/dist/util/runner-native-session-parsers.js +35 -17
  64. package/dist/workspace.d.ts +14 -8
  65. package/dist/workspace.js +98 -50
  66. package/package.json +2 -2
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { readFile, readdir, unlink } from 'node:fs/promises';
2
+ import { readFile, readdir, stat, unlink } from 'node:fs/promises';
3
3
  import { homedir } from 'node:os';
4
4
  import { basename, join, resolve } from 'node:path';
5
5
  import { forEachJsonlRecord, listJsonlFiles, mapConcurrent, readJsonlWindow } from './native-jsonl-reader.js';
@@ -10,9 +10,16 @@ const TRANSCRIPT_READ_BYTES = 4 * 1024 * 1024;
10
10
  const CONTEXT_USAGE_READ_BYTES = 256 * 1024;
11
11
  const CLAUDE_CONTEXT_USAGE_READ_BYTES = [CONTEXT_USAGE_READ_BYTES, 1024 * 1024, 4 * 1024 * 1024];
12
12
  const TRANSCRIPT_INDEX_TTL_MS = 60 * 60_000;
13
+ const DISCOVERY_SNAPSHOT_TTL_MS = 15_000;
14
+ const TRANSCRIPT_DETAIL_CACHE_TTL_MS = 60_000;
15
+ const TRANSCRIPT_DETAIL_CACHE_MAX_ENTRIES = 16;
13
16
  const transcriptIndex = new Map();
14
17
  const transcriptIndexTimers = new Map();
18
+ const transcriptDetailCache = new Map();
19
+ const transcriptDetailReads = new Map();
15
20
  const discoveryReads = new Map();
21
+ const discoverySnapshots = new Map();
22
+ const discoveryGenerations = new Map();
16
23
  const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000;
17
24
  const CLAUDE_CONTEXT_WINDOW_ENV = 'MAR_CLAUDE_CONTEXT_WINDOW_TOKENS';
18
25
  /**
@@ -26,61 +33,128 @@ export async function discoverNativeSessions(runner, workspacePath) {
26
33
  }
27
34
  /**
28
35
  * Reads every supported transcript header before applying a Workspace filter.
29
- * The caller owns in-process caching; a fixed file-count cutoff would silently
30
- * hide older sessions from a project with a large global history.
36
+ * A short Runner-level snapshot is shared across Workspaces and refreshes only
37
+ * files whose size or modification time changed. A fixed file-count cutoff
38
+ * would silently hide older sessions from a project with a large global history.
31
39
  */
32
40
  export async function discoverAllNativeSessions(runner) {
33
- const current = discoveryReads.get(runner);
34
- if (current !== undefined)
41
+ const key = discoveryKey(runner);
42
+ const snapshot = discoverySnapshots.get(key);
43
+ if (snapshot !== undefined && snapshot.expiresAt > Date.now()) {
44
+ snapshot.cacheHits += 1;
45
+ return snapshot.sessions;
46
+ }
47
+ const current = discoveryReads.get(key);
48
+ if (current !== undefined) {
49
+ nodeLog('native.session.discovery.single-flight-joined', { runner });
35
50
  return current;
36
- const discovery = performNativeSessionDiscovery(runner).finally(() => discoveryReads.delete(runner));
37
- discoveryReads.set(runner, discovery);
51
+ }
52
+ const generation = discoveryGenerations.get(key) ?? 0;
53
+ const discovery = performNativeSessionDiscovery(runner, snapshot)
54
+ .then((next) => {
55
+ if ((discoveryGenerations.get(key) ?? 0) === generation)
56
+ discoverySnapshots.set(key, next);
57
+ return next.sessions;
58
+ })
59
+ .finally(() => {
60
+ if (discoveryReads.get(key) === discovery)
61
+ discoveryReads.delete(key);
62
+ });
63
+ discoveryReads.set(key, discovery);
38
64
  return discovery;
39
65
  }
40
- async function performNativeSessionDiscovery(runner) {
66
+ async function performNativeSessionDiscovery(runner, previous) {
41
67
  const startedAt = performance.now();
42
68
  const found = new Map();
43
69
  nodeLog('native.session.discovery.started', { runner });
44
- const files = await listJsonlFiles(transcriptRoot(runner));
70
+ const files = (await listJsonlFiles(transcriptRoot(runner))).filter((path) => runner !== 'claude-code' || !path.split(/[\\/]/u).includes('subagents'));
45
71
  nodeLog('native.session.discovery.files-listed', {
46
72
  runner,
47
73
  files: files.length,
48
74
  durationMs: Math.round(performance.now() - startedAt)
49
75
  });
50
- const parsedFiles = await readDiscoveryHeaders(files, runner);
51
- for (const [index, parsed] of parsedFiles.entries()) {
76
+ let completed = 0;
77
+ let parsedCount = 0;
78
+ let reusedCount = 0;
79
+ let metadataFailures = 0;
80
+ let metadataWorkMs = 0;
81
+ let parseWorkMs = 0;
82
+ const discoveredFiles = await mapConcurrent(files, 32, async (path) => {
83
+ let fingerprint;
84
+ const metadataStartedAt = performance.now();
85
+ try {
86
+ const metadata = await stat(path);
87
+ fingerprint = `${metadata.size}:${metadata.mtimeMs}`;
88
+ }
89
+ catch {
90
+ metadataFailures += 1;
91
+ return undefined;
92
+ }
93
+ finally {
94
+ metadataWorkMs += performance.now() - metadataStartedAt;
95
+ }
96
+ const cached = previous?.files.get(path);
97
+ let parsed;
98
+ if (cached?.fingerprint === fingerprint) {
99
+ parsed = cached.parsed;
100
+ reusedCount += 1;
101
+ }
102
+ else {
103
+ const parseStartedAt = performance.now();
104
+ parsed = await readTranscript(path, runner, DISCOVERY_READ_BYTES);
105
+ parseWorkMs += performance.now() - parseStartedAt;
106
+ parsedCount += 1;
107
+ }
108
+ completed += 1;
109
+ if (completed % 250 === 0)
110
+ nodeLog('native.session.discovery.progress', {
111
+ runner,
112
+ completed,
113
+ files: files.length,
114
+ parsedFiles: parsedCount,
115
+ reusedFiles: reusedCount,
116
+ metadataFailures,
117
+ metadataWorkMs: Math.round(metadataWorkMs),
118
+ parseWorkMs: Math.round(parseWorkMs),
119
+ durationMs: Math.round(performance.now() - startedAt)
120
+ });
121
+ return { path, fingerprint, parsed };
122
+ });
123
+ const snapshotFiles = new Map();
124
+ for (const file of discoveredFiles) {
125
+ if (file === undefined)
126
+ continue;
127
+ snapshotFiles.set(file.path, { fingerprint: file.fingerprint, parsed: file.parsed });
128
+ const parsed = file.parsed;
52
129
  // Discovery only needs a session identity, cwd and an optional early
53
130
  // title. Loading entire JSONL files here made opening a Workspace depend
54
131
  // on the total history of every other project on the Node.
55
132
  if (parsed === undefined)
56
133
  continue;
57
134
  const cwd = canonical(parsed.cwd);
58
- setTranscriptIndex(indexKey(runner, cwd, parsed.externalSessionId), files[index]);
135
+ setTranscriptIndex(indexKey(runner, cwd, parsed.externalSessionId), file.path);
59
136
  found.set(`${cwd}\u0000${parsed.externalSessionId}`, parsed);
60
137
  }
61
138
  nodeLog('native.session.discovery.completed', {
62
139
  runner,
63
140
  files: files.length,
64
141
  sessions: found.size,
142
+ parsedFiles: parsedCount,
143
+ reusedFiles: reusedCount,
144
+ unparsedFiles: [...snapshotFiles.values()].filter((file) => file.parsed === undefined).length,
145
+ metadataFailures,
146
+ metadataWorkMs: Math.round(metadataWorkMs),
147
+ parseWorkMs: Math.round(parseWorkMs),
148
+ cacheMode: previous === undefined ? 'cold' : 'refresh',
149
+ previousCacheHits: previous?.cacheHits ?? 0,
65
150
  durationMs: Math.round(performance.now() - startedAt)
66
151
  });
67
- return [...found.values()];
68
- }
69
- async function readDiscoveryHeaders(files, runner) {
70
- let completed = 0;
71
- const startedAt = performance.now();
72
- return mapConcurrent(files, 32, async (path) => {
73
- const value = await readTranscript(path, runner, DISCOVERY_READ_BYTES);
74
- completed += 1;
75
- if (completed % 250 === 0)
76
- nodeLog('native.session.discovery.progress', {
77
- runner,
78
- completed,
79
- files: files.length,
80
- durationMs: Math.round(performance.now() - startedAt)
81
- });
82
- return value;
83
- });
152
+ return {
153
+ expiresAt: Date.now() + DISCOVERY_SNAPSHOT_TTL_MS,
154
+ files: snapshotFiles,
155
+ sessions: [...found.values()],
156
+ cacheHits: 0
157
+ };
84
158
  }
85
159
  /** Reads one already-discovered external session again to follow appended JSONL records. */
86
160
  export async function readNativeSession(runner, workspacePath, externalSessionId) {
@@ -88,7 +162,7 @@ export async function readNativeSession(runner, workspacePath, externalSessionId
88
162
  const path = getTranscriptIndex(indexKey(runner, wanted, externalSessionId));
89
163
  if (path === undefined)
90
164
  return undefined;
91
- const parsed = await readTranscript(path, runner, TRANSCRIPT_READ_BYTES);
165
+ const parsed = await readTranscriptDetail(path, runner);
92
166
  if (parsed === undefined ||
93
167
  parsed.externalSessionId !== externalSessionId ||
94
168
  canonical(parsed.cwd) !== wanted) {
@@ -97,6 +171,70 @@ export async function readNativeSession(runner, workspacePath, externalSessionId
97
171
  }
98
172
  return parsed;
99
173
  }
174
+ async function readTranscriptDetail(path, runner) {
175
+ const key = `${runner}\u0000${path}`;
176
+ let fingerprint;
177
+ try {
178
+ const metadata = await stat(path);
179
+ fingerprint = `${metadata.size}:${metadata.mtimeMs}`;
180
+ }
181
+ catch {
182
+ transcriptDetailCache.delete(key);
183
+ transcriptDetailReads.delete(key);
184
+ return undefined;
185
+ }
186
+ const cached = transcriptDetailCache.get(key);
187
+ if (cached?.fingerprint === fingerprint && cached.expiresAt > Date.now()) {
188
+ cached.expiresAt = Date.now() + TRANSCRIPT_DETAIL_CACHE_TTL_MS;
189
+ transcriptDetailCache.delete(key);
190
+ transcriptDetailCache.set(key, cached);
191
+ return cached.parsed;
192
+ }
193
+ transcriptDetailCache.delete(key);
194
+ const current = transcriptDetailReads.get(key);
195
+ if (current?.fingerprint === fingerprint) {
196
+ nodeLog('native.session.history.single-flight-joined', { runner });
197
+ return current.read;
198
+ }
199
+ const startedAt = performance.now();
200
+ const read = readTranscript(path, runner, TRANSCRIPT_READ_BYTES)
201
+ .then((parsed) => {
202
+ if (transcriptDetailReads.get(key)?.read === read) {
203
+ transcriptDetailCache.set(key, {
204
+ fingerprint,
205
+ parsed,
206
+ expiresAt: Date.now() + TRANSCRIPT_DETAIL_CACHE_TTL_MS
207
+ });
208
+ pruneTranscriptDetailCache();
209
+ }
210
+ const durationMs = Math.round(performance.now() - startedAt);
211
+ if (durationMs >= 250)
212
+ nodeLog('native.session.history.detail-parse.completed', {
213
+ runner,
214
+ bytes: Number(fingerprint.slice(0, fingerprint.indexOf(':'))),
215
+ durationMs
216
+ });
217
+ return parsed;
218
+ })
219
+ .finally(() => {
220
+ if (transcriptDetailReads.get(key)?.read === read)
221
+ transcriptDetailReads.delete(key);
222
+ });
223
+ transcriptDetailReads.set(key, { fingerprint, read });
224
+ return read;
225
+ }
226
+ function pruneTranscriptDetailCache() {
227
+ const now = Date.now();
228
+ for (const [key, cached] of transcriptDetailCache)
229
+ if (cached.expiresAt <= now)
230
+ transcriptDetailCache.delete(key);
231
+ while (transcriptDetailCache.size > TRANSCRIPT_DETAIL_CACHE_MAX_ENTRIES) {
232
+ const oldest = transcriptDetailCache.keys().next().value;
233
+ if (oldest === undefined)
234
+ return;
235
+ transcriptDetailCache.delete(oldest);
236
+ }
237
+ }
100
238
  /**
101
239
  * Codex records the most recent prompt token count alongside the model context
102
240
  * window in `event_msg/token_count`. Read only the tail of an already
@@ -232,12 +370,24 @@ export async function removeNativeSession(runner, workspacePath, externalSession
232
370
  throw error;
233
371
  }
234
372
  deleteTranscriptIndex(indexKey(runner, wanted, externalSessionId));
373
+ transcriptDetailCache.delete(`${runner}\u0000${path}`);
374
+ transcriptDetailReads.delete(`${runner}\u0000${path}`);
375
+ invalidateNativeDiscovery(runner);
235
376
  }
236
377
  function transcriptRoot(runner) {
237
378
  return runner === 'codex'
238
379
  ? join(homedir(), '.codex', 'sessions')
239
380
  : join(homedir(), '.claude', 'projects');
240
381
  }
382
+ function discoveryKey(runner) {
383
+ return `${runner}\u0000${transcriptRoot(runner)}`;
384
+ }
385
+ function invalidateNativeDiscovery(runner) {
386
+ const key = discoveryKey(runner);
387
+ discoveryGenerations.set(key, (discoveryGenerations.get(key) ?? 0) + 1);
388
+ discoverySnapshots.delete(key);
389
+ discoveryReads.delete(key);
390
+ }
241
391
  async function readTranscript(path, runner, byteLimit) {
242
392
  const window = await readJsonlWindow(path, { headBytes: byteLimit, tailBytes: byteLimit });
243
393
  if (window === undefined)
@@ -245,11 +395,14 @@ async function readTranscript(path, runner, byteLimit) {
245
395
  const { content, truncated } = window;
246
396
  let cwd;
247
397
  let id;
398
+ let derivedSession = false;
248
399
  const items = [];
249
400
  const toolIndexes = new Map();
250
401
  const pendingToolResults = new Map();
251
402
  const metaRecordIds = new Set();
252
403
  forEachJsonlRecord(content, (value) => {
404
+ if (derivedTranscriptRecord(runner, value))
405
+ derivedSession = true;
253
406
  cwd ??= workspacePath(value);
254
407
  id ??= sessionId(value);
255
408
  const recordId = typeof value['uuid'] === 'string' ? value['uuid'] : undefined;
@@ -284,6 +437,8 @@ async function readTranscript(path, runner, byteLimit) {
284
437
  items.push(item);
285
438
  }
286
439
  });
440
+ if (derivedSession)
441
+ return undefined;
287
442
  for (const result of pendingToolResults.values())
288
443
  items.push({
289
444
  kind: 'tool_call',
@@ -318,6 +473,20 @@ async function readTranscript(path, runner, byteLimit) {
318
473
  truncated
319
474
  };
320
475
  }
476
+ function derivedTranscriptRecord(runner, value) {
477
+ if (runner === 'claude-code')
478
+ return value['isSidechain'] === true;
479
+ if (runner !== 'codex' || value['type'] !== 'session_meta')
480
+ return false;
481
+ const payload = isRecord(value['payload']) ? value['payload'] : undefined;
482
+ if (payload === undefined)
483
+ return false;
484
+ if (payload['thread_source'] === 'subagent')
485
+ return true;
486
+ const source = payload['source'];
487
+ return (source === 'subagent' ||
488
+ (isRecord(source) && (source['kind'] === 'subagent' || isRecord(source['subagent']))));
489
+ }
321
490
  function workspacePath(record) {
322
491
  return (firstString(record, ['cwd', 'working_directory', 'workspacePath']) ??
323
492
  nestedString(record, ['payload', 'cwd']) ??
@@ -36,6 +36,7 @@ export declare class OpenCodeServerClient {
36
36
  readonly model: string;
37
37
  readonly variant?: string;
38
38
  readonly agent?: 'plan' | 'build';
39
+ readonly system?: string;
39
40
  readonly attachments?: readonly {
40
41
  readonly name: string;
41
42
  readonly mime: 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
@@ -169,6 +169,7 @@ export class OpenCodeServerClient {
169
169
  ? {}
170
170
  : { variant: input.variant }),
171
171
  ...(input.agent === undefined ? {} : { agent: input.agent }),
172
+ ...(input.system === undefined ? {} : { system: input.system }),
172
173
  parts: [
173
174
  { type: 'text', text: input.text },
174
175
  ...(input.attachments ?? []).map((attachment) => ({
@@ -50,7 +50,8 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
50
50
  readOfficialConversation(session: NodeAgentSession, input: {
51
51
  readonly cursor?: string;
52
52
  readonly limit?: number;
53
- }, managedTurn: (nativeTurnId: string) => NodeConversationTurn | undefined): Promise<RunnerOfficialConversationPage | undefined>;
53
+ }, managedTurn: (nativeTurnId: string) => NodeConversationTurn | undefined): Promise<RunnerConversationPage | undefined>;
54
+ readConversationHistory(readers: RunnerConversationHistoryReaders): Promise<RunnerConversationHistoryPage | undefined>;
54
55
  readExternalActivity(session: NodeAgentSession): Promise<SessionActivityState | undefined>;
55
56
  acceptsTruncatedNativeHistory(): boolean;
56
57
  nativeImageRoots(session: NodeAgentSession): readonly string[];
@@ -134,10 +135,18 @@ export interface RunnerProjectionTitleInput {
134
135
  readonly title?: string;
135
136
  readonly titleOrigin?: 'OFFICIAL' | 'NATIVE';
136
137
  }
137
- export interface RunnerOfficialConversationPage {
138
+ export interface RunnerConversationPage {
138
139
  readonly turns: readonly NodeConversationTurn[];
139
140
  readonly nextCursor: string | null;
140
141
  }
142
+ export type RunnerConversationHistorySource = 'native' | 'official';
143
+ export interface RunnerConversationHistoryReaders {
144
+ readonly native: () => Promise<RunnerConversationPage | undefined>;
145
+ readonly official: () => Promise<RunnerConversationPage | undefined>;
146
+ }
147
+ export interface RunnerConversationHistoryPage extends RunnerConversationPage {
148
+ readonly source: RunnerConversationHistorySource;
149
+ }
141
150
  export interface RunnerSessionPresentationContext {
142
151
  readonly managedActive: boolean;
143
152
  readonly externalActivity: SessionActivityState | undefined;
@@ -99,6 +99,10 @@ export class AbstractRunner {
99
99
  void [session, input, managedTurn];
100
100
  return undefined;
101
101
  }
102
+ async readConversationHistory(readers) {
103
+ const page = await readers.native();
104
+ return page === undefined ? undefined : { ...page, source: 'native' };
105
+ }
102
106
  async readExternalActivity(session) {
103
107
  void session;
104
108
  return undefined;
@@ -148,6 +148,9 @@ export class ClaudeManagedRunController {
148
148
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
149
149
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
150
150
  environment: secretEnvironment,
151
+ ...(typeof payload.personalInstructions === 'string'
152
+ ? { personalInstructions: payload.personalInstructions }
153
+ : {}),
151
154
  mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment),
152
155
  ...(attachments.length === 0
153
156
  ? {}
@@ -1,5 +1,5 @@
1
1
  import { ClaudeAgentSdkAdapter, type ClaudeChannelRunHandle, type ClaudeQueryInput, type ClaudeRunHandle } from '../claude-agent-sdk.js';
2
- import { AbstractRunner, type ExternalResumeContext, type ExternalResumeResult, type RunnerDiscoveryContext, type RunnerSessionPresentationContext, type RunnerSessionPresentation } from './abstract-runner.js';
2
+ import { AbstractRunner, type ExternalResumeContext, type ExternalResumeResult, type RunnerDiscoveryContext, type RunnerConversationHistoryPage, type RunnerConversationHistoryReaders, type RunnerSessionPresentationContext, type RunnerSessionPresentation } from './abstract-runner.js';
3
3
  import type { NodeCapabilities, RunnerDefaultConfiguration, RunnerProfile } from '@myagentroam/protocol';
4
4
  import { ClaudeChannelGate } from '../claude-channel.js';
5
5
  import type { ChannelDelivery } from '../claude-channel.js';
@@ -21,6 +21,7 @@ export declare class ClaudeCodeRunner extends AbstractRunner<'claude-code'> {
21
21
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
22
22
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
23
23
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeClaudeContextUsage | undefined>;
24
+ readConversationHistory(readers: RunnerConversationHistoryReaders): Promise<RunnerConversationHistoryPage | undefined>;
24
25
  forkNative(session: NodeAgentSession, boundary: string | null, title: string): Promise<string | null>;
25
26
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
26
27
  prepareMessageInput(input: string, collaborationMode: 'default' | 'plan'): string;
@@ -135,6 +135,10 @@ export class ClaudeCodeRunner extends AbstractRunner {
135
135
  ? Promise.resolve(undefined)
136
136
  : readClaudeNativeContextUsage(session.cwd, session.externalSessionId);
137
137
  }
138
+ async readConversationHistory(readers) {
139
+ const native = await readers.native();
140
+ return native === undefined ? undefined : { ...native, source: 'native' };
141
+ }
138
142
  forkNative(session, boundary, title) {
139
143
  if (session.externalSessionId === null)
140
144
  throw new Error('SESSION_REWIND_UNAVAILABLE');
@@ -6,6 +6,15 @@ import { CodexRunner } from '../codex-runner.js';
6
6
  type CodexImageInput = Extract<CodexComposerInput, {
7
7
  readonly type: 'localImage';
8
8
  }>;
9
+ interface CodexNotificationRun {
10
+ readonly threadId: string;
11
+ readonly turnId?: string;
12
+ }
13
+ export declare function resolveCodexNotificationRun<T extends CodexNotificationRun>(runs: ReadonlyMap<string, T>, input: {
14
+ readonly threadId?: string;
15
+ readonly turnId?: string;
16
+ readonly allowUnboundFallback?: boolean;
17
+ }): readonly [string, T] | undefined;
9
18
  export interface CodexManagedRunHost<TAttachment> {
10
19
  readonly available: () => boolean;
11
20
  readonly intercepted: () => boolean;
@@ -3,6 +3,25 @@ import { parseSecretEnvironment } from '../../util/secret-environment.js';
3
3
  import { safeErrorCode } from '../../util/safe-error.js';
4
4
  import { runnerErrorProjection } from '../../util/runner-error.js';
5
5
  import { boundedConversationItemId, codexImageAttachments, codexLiveConversationItem, codexUserInputQuestions, compactRunnerText, isPlainRecord } from './conversation-parser.js';
6
+ export function resolveCodexNotificationRun(runs, input) {
7
+ const entries = [...runs.entries()];
8
+ if (input.turnId !== undefined) {
9
+ const exact = entries.find(([, value]) => value.turnId === input.turnId &&
10
+ (input.threadId === undefined || value.threadId === input.threadId));
11
+ if (exact !== undefined)
12
+ return exact;
13
+ if (input.allowUnboundFallback !== true)
14
+ return undefined;
15
+ // `turn/started` can arrive before the `turn/start` response binds its native Turn ID. Only
16
+ // an unbound Run on the same thread is a valid fallback; an older bound Run is never valid.
17
+ return entries
18
+ .reverse()
19
+ .find(([, value]) => value.threadId === input.threadId && value.turnId === undefined);
20
+ }
21
+ if (input.threadId === undefined)
22
+ return undefined;
23
+ return entries.reverse().find(([, value]) => value.threadId === input.threadId);
24
+ }
6
25
  export class CodexManagedRunController {
7
26
  runner;
8
27
  host;
@@ -104,8 +123,11 @@ export class CodexManagedRunController {
104
123
  const threadId = typeof params.threadId === 'string' ? params.threadId : undefined;
105
124
  if (turnId === undefined && threadId === undefined)
106
125
  return;
107
- const run = [...this.runner.execution.runs.entries()].find(([, value]) => (turnId !== undefined && value.turnId === turnId) ||
108
- (threadId !== undefined && value.threadId === threadId));
126
+ const run = resolveCodexNotificationRun(this.runner.execution.runs, {
127
+ ...(turnId === undefined ? {} : { turnId }),
128
+ ...(threadId === undefined ? {} : { threadId }),
129
+ ...(notification.method === 'turn/started' ? { allowUnboundFallback: true } : {})
130
+ });
109
131
  if (run === undefined)
110
132
  return;
111
133
  const [runId] = run;
@@ -332,6 +354,9 @@ export class CodexManagedRunController {
332
354
  access: payload.access,
333
355
  collaborationMode: payload.collaborationMode,
334
356
  serviceTier: payload.serviceTier,
357
+ ...(typeof payload.personalInstructions === 'string'
358
+ ? { personalInstructions: payload.personalInstructions }
359
+ : {}),
335
360
  mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment)
336
361
  }, attachments);
337
362
  }
@@ -348,30 +373,51 @@ export class CodexManagedRunController {
348
373
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
349
374
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
350
375
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
376
+ ...(payload.personalInstructions === undefined
377
+ ? {}
378
+ : { developerInstructions: payload.personalInstructions }),
351
379
  ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
352
380
  };
353
- const threadResult = typeof payload.externalSessionId === 'string'
354
- ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
355
- : await this.runner.startThread(cwd, configuration);
356
- if (this.host.completeCancelled(runId, cwd)) {
357
- this.runner.releaseEnvironment(runId);
358
- return;
381
+ const collaborationMode = payload.collaborationMode === 'plan' || payload.collaborationMode === 'default'
382
+ ? payload.collaborationMode
383
+ : undefined;
384
+ const turnConfiguration = {
385
+ ...configuration,
386
+ ...(collaborationMode === undefined ? {} : { collaborationMode }),
387
+ ...(payload.serviceTier === 'fast' ? { serviceTier: 'fast' } : {})
388
+ };
389
+ const materializedAttachments = await this.host.materialize(runId, attachments);
390
+ let threadId;
391
+ let turnResult;
392
+ for (let attempt = 0; attempt < 2; attempt += 1) {
393
+ try {
394
+ const threadResult = typeof payload.externalSessionId === 'string'
395
+ ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
396
+ : await this.runner.startThread(cwd, configuration);
397
+ if (this.host.completeCancelled(runId, cwd)) {
398
+ this.runner.releaseEnvironment(runId);
399
+ return;
400
+ }
401
+ threadId = extractId(threadResult, 'thread');
402
+ this.runner.execution.runs.set(runId, { sessionId, threadId, cwd });
403
+ turnResult = await this.runner.startTurn(threadId, input, cwd, turnConfiguration, materializedAttachments);
404
+ break;
405
+ }
406
+ catch (error) {
407
+ this.runner.execution.runs.delete(runId);
408
+ if (attempt === 0 && (await this.runner.recoverMissingRollout(runId, error)))
409
+ continue;
410
+ throw error;
411
+ }
359
412
  }
360
- const threadId = extractId(threadResult, 'thread');
413
+ if (threadId === undefined || turnResult === undefined)
414
+ throw new Error('CODEX_RUN_FAILED');
361
415
  let publicSessionId = sessionId;
362
416
  if (typeof payload.externalSessionId !== 'string') {
363
417
  this.host.promoteSession(sessionId, threadId);
364
418
  publicSessionId = threadId;
365
419
  this.host.send('session.external-id', { sessionId, externalSessionId: threadId });
366
420
  }
367
- this.runner.execution.runs.set(runId, { sessionId: publicSessionId, threadId, cwd });
368
- const turnResult = await this.runner.startTurn(threadId, input, cwd, {
369
- ...configuration,
370
- ...(payload.collaborationMode === 'plan' || payload.collaborationMode === 'default'
371
- ? { collaborationMode: payload.collaborationMode }
372
- : {}),
373
- ...(payload.serviceTier === 'fast' ? { serviceTier: 'fast' } : {})
374
- }, await this.host.materialize(runId, attachments));
375
421
  const turnId = extractId(turnResult, 'turn');
376
422
  this.runner.execution.runs.set(runId, { sessionId: publicSessionId, threadId, turnId, cwd });
377
423
  this.runner.execution.managedTurns.set(turnId, { sessionId: publicSessionId, runId });
@@ -1,5 +1,5 @@
1
1
  import { CodexAppServerClient, type CodexComposerInput, type CodexRunConfiguration, type JsonRpcNotification } from '../codex-app-server.js';
2
- import { AbstractRunner, type ExternalResumeContext, type ExternalResumeResult, type RunnerDiscoveryContext, type RunnerSessionPresentationContext, type RunnerSessionPresentation, type RunnerProjectionTitleInput } from './abstract-runner.js';
2
+ import { AbstractRunner, type ExternalResumeContext, type ExternalResumeResult, type RunnerDiscoveryContext, type RunnerConversationHistoryPage, type RunnerConversationHistoryReaders, type RunnerSessionPresentationContext, type RunnerSessionPresentation, type RunnerProjectionTitleInput } from './abstract-runner.js';
3
3
  import type { NodeCapabilities, RunnerDefaultConfiguration, RunnerProfile } from '@myagentroam/protocol';
4
4
  import type { NodeAgentSession, NodeDatabase } from '../database.js';
5
5
  import type { NodeWorkspace } from '../database.js';
@@ -43,6 +43,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
43
43
  readonly turns: readonly import("../database.js").NodeConversationTurn[];
44
44
  readonly nextCursor: string | null;
45
45
  } | undefined>;
46
+ readConversationHistory(readers: RunnerConversationHistoryReaders): Promise<RunnerConversationHistoryPage | undefined>;
46
47
  readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "MANAGED_ACTIVE" | "EXTERNAL_ACTIVE" | "IDLE" | undefined>;
47
48
  acceptsTruncatedNativeHistory(): boolean;
48
49
  readManagedRunnerTitle(session: NodeAgentSession, nativeSessionId: string): Promise<string | undefined>;
@@ -58,6 +59,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
58
59
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
59
60
  onNotification(listener: (notification: JsonRpcNotification) => void): () => void;
60
61
  start(): Promise<void>;
62
+ recoverMissingRollout(runId: string, error: unknown): Promise<boolean>;
61
63
  prepareEnvironment(environment: Readonly<Record<string, string>>, runId: string): void;
62
64
  releaseEnvironment(runId: string): void;
63
65
  stop(): void;
@@ -1,4 +1,4 @@
1
- import { CodexAppServerClient } from '../codex-app-server.js';
1
+ import { CodexAppServerClient, isCodexRolloutMissingError } from '../codex-app-server.js';
2
2
  import { AbstractRunner } from './abstract-runner.js';
3
3
  import { declaredRunnerProfiles } from '../runner-profiles.js';
4
4
  import { homedir } from 'node:os';
@@ -250,6 +250,16 @@ export class CodexRunner extends AbstractRunner {
250
250
  return undefined;
251
251
  }
252
252
  }
253
+ async readConversationHistory(readers) {
254
+ // Codex JSONL is the low-latency durable record for normal history reads,
255
+ // while thread/read can take seconds on large threads. Keep the official
256
+ // API as a fallback because a transcript can be missing or bounded/truncated.
257
+ const native = await readers.native();
258
+ if (native !== undefined)
259
+ return { ...native, source: 'native' };
260
+ const official = await readers.official();
261
+ return official === undefined ? undefined : { ...official, source: 'official' };
262
+ }
253
263
  async readExternalActivity(session) {
254
264
  if (session.nativeControl !== 'EXTERNAL' || session.externalSessionId === null)
255
265
  return undefined;
@@ -355,7 +365,7 @@ export class CodexRunner extends AbstractRunner {
355
365
  }
356
366
  const sessions = [];
357
367
  for (const thread of threads) {
358
- if (thread.source !== 'cli' && thread.source !== 'vscode')
368
+ if (thread.derived || (thread.source !== 'cli' && thread.source !== 'vscode'))
359
369
  continue;
360
370
  let cwd;
361
371
  try {
@@ -451,6 +461,17 @@ export class CodexRunner extends AbstractRunner {
451
461
  start() {
452
462
  return this.client.start();
453
463
  }
464
+ async recoverMissingRollout(runId, error) {
465
+ if (!isCodexRolloutMissingError(error))
466
+ return false;
467
+ if ([...this.environmentRuns].some((activeRunId) => activeRunId !== runId))
468
+ return false;
469
+ if ([...this.execution.runs.keys()].some((activeRunId) => activeRunId !== runId))
470
+ return false;
471
+ nodeLog('runner.codex.rollout.recovering', { runId });
472
+ await this.client.restart();
473
+ return true;
474
+ }
454
475
  prepareEnvironment(environment, runId) {
455
476
  if (Object.keys(environment).length === 0 && this.environmentFingerprint === undefined) {
456
477
  this.environmentRuns.add(runId);
@@ -128,6 +128,9 @@ export class OpenCodeManagedRunController {
128
128
  attachments,
129
129
  access: typeof payload.access === 'string' ? payload.access : 'default',
130
130
  agent: payload.collaborationMode === 'plan' ? 'plan' : 'build',
131
+ ...(typeof payload.personalInstructions === 'string'
132
+ ? { personalInstructions: payload.personalInstructions }
133
+ : {}),
131
134
  mcp
132
135
  });
133
136
  }
@@ -251,6 +254,7 @@ export class OpenCodeManagedRunController {
251
254
  model: input.model,
252
255
  variant: input.effort,
253
256
  agent: input.agent,
257
+ ...(input.personalInstructions === undefined ? {} : { system: input.personalInstructions }),
254
258
  attachments: input.attachments
255
259
  });
256
260
  this.promptAdmitted.add(input.runId);