@myagentroam/node 0.9.4 → 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 (59) 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-session-history.d.ts +3 -2
  8. package/dist/native-session-history.js +199 -30
  9. package/dist/opencode-server.d.ts +1 -0
  10. package/dist/opencode-server.js +1 -0
  11. package/dist/runner/abstract-runner.d.ts +11 -2
  12. package/dist/runner/abstract-runner.js +4 -0
  13. package/dist/runner/claude/managed-run-controller.js +3 -0
  14. package/dist/runner/claude-code-runner.d.ts +2 -1
  15. package/dist/runner/claude-code-runner.js +4 -0
  16. package/dist/runner/codex/managed-run-controller.js +6 -0
  17. package/dist/runner/codex-runner.d.ts +2 -1
  18. package/dist/runner/codex-runner.js +11 -1
  19. package/dist/runner/opencode/managed-run-controller.js +4 -0
  20. package/dist/runner/opencode-runner.d.ts +10 -3
  21. package/dist/runner/opencode-runner.js +87 -14
  22. package/dist/runner/runner-registry.js +16 -1
  23. package/dist/runtime-command-detector.js +1 -6
  24. package/dist/runtime-state.js +1 -0
  25. package/dist/service/conversation-history-service.d.ts +5 -4
  26. package/dist/service/conversation-history-service.js +110 -52
  27. package/dist/service/external-session-resume-service.d.ts +2 -0
  28. package/dist/service/external-session-resume-service.js +45 -34
  29. package/dist/service/native-session-projection-service.d.ts +1 -0
  30. package/dist/service/native-session-projection-service.js +4 -0
  31. package/dist/service/native-session-watch-service.d.ts +18 -0
  32. package/dist/service/native-session-watch-service.js +232 -34
  33. package/dist/service/run-workbench-service.d.ts +2 -4
  34. package/dist/service/run-workbench-service.js +2 -7
  35. package/dist/service/session-command-service.js +1 -1
  36. package/dist/service/session-identity-service.d.ts +1 -1
  37. package/dist/service/session-identity-service.js +1 -1
  38. package/dist/service/session-lifecycle-service.js +3 -0
  39. package/dist/service/session-message-service.d.ts +0 -1
  40. package/dist/service/session-message-service.js +15 -23
  41. package/dist/service/skill-directory-service.d.ts +1 -0
  42. package/dist/service/skill-directory-service.js +40 -6
  43. package/dist/service/workspace-domain-state.d.ts +3 -0
  44. package/dist/service/workspace-domain-state.js +1 -0
  45. package/dist/service/workspace-file-service.d.ts +1 -0
  46. package/dist/service/workspace-file-service.js +14 -1
  47. package/dist/service/workspace-queue-workbench-service.d.ts +17 -0
  48. package/dist/service/workspace-queue-workbench-service.js +66 -6
  49. package/dist/service/workspace-watch-service.d.ts +7 -2
  50. package/dist/service/workspace-watch-service.js +32 -6
  51. package/dist/service/workspace-workbench-service.d.ts +14 -0
  52. package/dist/service/workspace-workbench-service.js +215 -16
  53. package/dist/util/personal-instructions.d.ts +2 -0
  54. package/dist/util/personal-instructions.js +31 -0
  55. package/dist/util/runner-native-session-parsers.d.ts +1 -0
  56. package/dist/util/runner-native-session-parsers.js +8 -1
  57. package/dist/workspace.d.ts +14 -8
  58. package/dist/workspace.js +65 -42
  59. package/package.json +2 -2
@@ -23,6 +23,7 @@ export interface ClaudeQueryInput {
23
23
  readonly onChannelReply?: (content: string) => void;
24
24
  readonly environment?: Readonly<Record<string, string>>;
25
25
  readonly mcpServers?: NonNullable<Options['mcpServers']>;
26
+ readonly personalInstructions?: string;
26
27
  }
27
28
  /** Image blocks accepted by the Claude Agent SDK message input. */
28
29
  export interface ClaudeImageAttachment {
@@ -64,6 +64,15 @@ export class ClaudeAgentSdkAdapter {
64
64
  // This is the SDK's documented API-side public summary mode. We never
65
65
  // enable or forward raw thinking deltas to the Workbench.
66
66
  settings: { showThinkingSummaries: true },
67
+ ...(input.personalInstructions === undefined
68
+ ? {}
69
+ : {
70
+ systemPrompt: {
71
+ type: 'preset',
72
+ preset: 'claude_code',
73
+ append: input.personalInstructions
74
+ }
75
+ }),
67
76
  abortController,
68
77
  canUseTool: input.onPermission,
69
78
  ...(input.mcpServers !== undefined || channelEnabled
@@ -50,6 +50,7 @@ export interface CodexRunConfiguration {
50
50
  readonly serviceTier?: 'fast';
51
51
  readonly collaborationMode?: 'default' | 'plan';
52
52
  readonly mcpServers?: unknown;
53
+ readonly developerInstructions?: string;
53
54
  }
54
55
  interface CodexRpcErrorPayload {
55
56
  readonly code?: unknown;
@@ -358,6 +358,9 @@ export class CodexAppServerClient {
358
358
  function codexThreadConfiguration(cwd, configuration) {
359
359
  return {
360
360
  ...modelOption(configuration.model),
361
+ ...(configuration.developerInstructions === undefined
362
+ ? {}
363
+ : { developerInstructions: configuration.developerInstructions }),
361
364
  ...(configuration.mcpServers === undefined
362
365
  ? {}
363
366
  : { config: { mcp_servers: configuration.mcpServers } }),
@@ -376,8 +379,7 @@ function codexTurnConfiguration(cwd, configuration) {
376
379
  mode: configuration.collaborationMode,
377
380
  settings: {
378
381
  model: configuration.model ?? 'gpt-5.6-sol',
379
- reasoningEffort: configuration.effort ?? null,
380
- developerInstructions: null
382
+ reasoningEffort: configuration.effort ?? null
381
383
  }
382
384
  }
383
385
  }),
@@ -3,6 +3,7 @@ export type { NodeConnectorOptions } from './connector/node-connector-options.js
3
3
  export { nodeConnectEndpoint, validatedCapabilities } from './runner/codex/conversation-parser.js';
4
4
  export declare function nextReconnectDelay(attempt: number): number;
5
5
  export declare class NodeConnector {
6
+ private readonly nodeGeneration;
6
7
  private readonly controlChannel;
7
8
  private readonly controlMessages;
8
9
  private readonly nodeRequests;
package/dist/connector.js CHANGED
@@ -86,6 +86,7 @@ function upgradeMetadata(metadata, status) {
86
86
  return next;
87
87
  }
88
88
  export class NodeConnector {
89
+ nodeGeneration = crypto.randomUUID().replace(/-/g, '');
89
90
  controlChannel;
90
91
  controlMessages;
91
92
  nodeRequests;
@@ -153,7 +154,7 @@ export class NodeConnector {
153
154
  discover: (workspaceId) => this.discoverWorkspaceSessions(workspaceId),
154
155
  listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
155
156
  active: (workspaceId) => this.workspaceWatchService.topicActive(workspaceId, 'sessions'),
156
- emitRevision: (workspaceId, revision) => this.emitWorkbenchEvent('workspace', { workspaceId, topic: 'sessions', revision }),
157
+ emitRevision: (workspaceId) => void this.workspaceWorkbenchService?.refreshTopic(workspaceId, 'sessions'),
157
158
  onPending: (workspaceId, waitMs) => nodeLog('native.session.discovery.pending', {
158
159
  nodeId: this.config?.nodeId,
159
160
  workspaceId,
@@ -192,8 +193,9 @@ export class NodeConnector {
192
193
  });
193
194
  nativeSessionWatchService = new NativeSessionWatchService({
194
195
  resolve: async (sessionId) => this.runtime.getAgentSession(sessionId) ??
195
- this.nativeSessionProjectionService.resolve(sessionId),
196
+ this.nativeSessionProjectionService.resolveCurrent(sessionId),
196
197
  managedActive: (session) => hasManagedActiveRun(this.runtime, session),
198
+ suspended: (sessionId) => this.externalSessionResumeService.transitioning(sessionId),
197
199
  refreshActivity: (session) => this.conversationHistoryService.refreshExternalActivity(session),
198
200
  present: (session) => this.sessionPresentationService.present(session),
199
201
  readPage: (session, limit) => this.conversationHistoryService.read(session, { limit }),
@@ -203,7 +205,10 @@ export class NodeConnector {
203
205
  emitPage: (session, page) => this.emitWorkbenchEvent('conversation', {
204
206
  sessionId: session.id,
205
207
  snapshot: this.conversationSegmentService.projectInitial(session, page, 10)
206
- })
208
+ }),
209
+ nodeId: () => this.config?.nodeId ?? 'runtime-node',
210
+ nodeGeneration: () => this.nodeGeneration,
211
+ emitWatchEvent: (payload) => this.send('watch.event', payload)
207
212
  });
208
213
  constructor(options = {}) {
209
214
  this.config = options.config;
@@ -396,10 +401,8 @@ export class NodeConnector {
396
401
  readImage: async (runId, imageIndex) => this.runAttachmentService.read(runId, imageIndex) ??
397
402
  (await this.runAttachmentService.readNative(runId, imageIndex)),
398
403
  respondUserInput: (runId, requestId, answers) => this.runnerInteractionService.respondUserInput(runId, requestId, answers),
399
- deleteQueuedStart: (runId) => this.workspaceQueueWorkbenchService.remove(runId),
400
- clearImages: (runId) => this.runAttachmentService.clear(runId),
401
- cleanupAttachments: (runId) => this.runAttachmentService.cleanup(runId),
402
- emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
404
+ cancelQueued: (runId) => this.workspaceQueueWorkbenchService.cancelQueued(runId),
405
+ pauseQueue: (sessionId) => this.workspaceQueueWorkbenchService.pauseForInterrupt(sessionId),
403
406
  emitRun: (runId, eventType, payload, status) => this.runEventBridge.emitRun(runId, eventType, payload, status),
404
407
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
405
408
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId)
@@ -447,7 +450,7 @@ export class NodeConnector {
447
450
  migrateRunnerState: (previousId, nextId) => this.runEventBridge.migrateRunnerSessionIdentity(previousId, nextId),
448
451
  emitSession: (session) => this.emitWorkbenchEvent('session', { session }),
449
452
  emitRun: (run) => this.emitWorkbenchEvent('run', { run }),
450
- emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
453
+ emitQueue: (sessionId) => this.workspaceQueueWorkbenchService.publish(sessionId),
451
454
  emitTurn: (turn) => this.emitConversationTurn(turn)
452
455
  });
453
456
  this.sessionLifecycleService = new SessionLifecycleService({
@@ -501,7 +504,6 @@ export class NodeConnector {
501
504
  emitSession: (session) => this.emitWorkbenchEvent('session', {
502
505
  session: this.sessionPresentationService.present(session)
503
506
  }),
504
- emitQueue: (workspaceId, sessionId) => this.runEventBridge.emitQueue(workspaceId, sessionId),
505
507
  channelToken: (sessionId) => {
506
508
  const session = this.runtime.getAgentSession(sessionId);
507
509
  return session === undefined
@@ -650,7 +652,7 @@ export class NodeConnector {
650
652
  watches: this.workspaceWatchService,
651
653
  sessions: this.workspaceSessionService,
652
654
  changes: this.workspaceChangeService,
653
- emit: (workspaceId, topic, revision) => this.emitWorkbenchEvent('workspace', { workspaceId, topic, revision }),
655
+ emit: (workspaceId, topic) => void this.workspaceWorkbenchService?.refreshTopic(workspaceId, topic),
654
656
  send: (type, payload, replyTo) => this.send(type, payload, replyTo)
655
657
  });
656
658
  this.runEventBridge = new NodeRunEventBridge({
@@ -711,7 +713,10 @@ export class NodeConnector {
711
713
  uploads: this.workspaceUploadService,
712
714
  changes: this.workspaceChangeService,
713
715
  listSessions: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
714
- invalidate: (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId)
716
+ invalidate: (workspaceId) => this.workspaceCoordinator.invalidate(workspaceId),
717
+ nodeId: () => this.config?.nodeId ?? 'runtime-node',
718
+ nodeGeneration: () => this.nodeGeneration,
719
+ emitWatchEvent: (payload) => this.send('watch.event', payload)
715
720
  });
716
721
  this.operationRouter.registerAll(this.runnerService.operations());
717
722
  this.operationRouter.registerAll(this.terminalService.operations());
@@ -78,8 +78,9 @@ export interface NativeClaudeContextUsage {
78
78
  export declare function discoverNativeSessions(runner: RunnerName, workspacePath: string): Promise<readonly NativeSessionHistory[]>;
79
79
  /**
80
80
  * Reads every supported transcript header before applying a Workspace filter.
81
- * The caller owns in-process caching; a fixed file-count cutoff would silently
82
- * hide older sessions from a project with a large global history.
81
+ * A short Runner-level snapshot is shared across Workspaces and refreshes only
82
+ * files whose size or modification time changed. A fixed file-count cutoff
83
+ * would silently hide older sessions from a project with a large global history.
83
84
  */
84
85
  export declare function discoverAllNativeSessions(runner: RunnerName): Promise<readonly NativeSessionHistory[]>;
85
86
  /** Reads one already-discovered external session again to follow appended JSONL records. */
@@ -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');
@@ -354,6 +354,9 @@ export class CodexManagedRunController {
354
354
  access: payload.access,
355
355
  collaborationMode: payload.collaborationMode,
356
356
  serviceTier: payload.serviceTier,
357
+ ...(typeof payload.personalInstructions === 'string'
358
+ ? { personalInstructions: payload.personalInstructions }
359
+ : {}),
357
360
  mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment)
358
361
  }, attachments);
359
362
  }
@@ -370,6 +373,9 @@ export class CodexManagedRunController {
370
373
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
371
374
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
372
375
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
376
+ ...(payload.personalInstructions === undefined
377
+ ? {}
378
+ : { developerInstructions: payload.personalInstructions }),
373
379
  ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
374
380
  };
375
381
  const collaborationMode = payload.collaborationMode === 'plan' || payload.collaborationMode === 'default'
@@ -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>;