@myagentroam/node 0.9.0 → 0.9.2

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 (50) hide show
  1. package/dist/connector.d.ts +2 -0
  2. package/dist/connector.js +38 -12
  3. package/dist/native-session-history.js +72 -14
  4. package/dist/runner/abstract-runner.d.ts +1 -1
  5. package/dist/runner/abstract-runner.js +2 -2
  6. package/dist/runner/claude/managed-run-controller.js +17 -7
  7. package/dist/runner/claude-code-runner.js +3 -4
  8. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  9. package/dist/runner/codex/conversation-parser.js +30 -12
  10. package/dist/runner/codex/managed-run-controller.js +7 -6
  11. package/dist/runner/codex-runner.d.ts +1 -1
  12. package/dist/runner/codex-runner.js +10 -8
  13. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  14. package/dist/runner/opencode/conversation-parser.js +15 -15
  15. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  16. package/dist/runner/opencode/managed-run-controller.js +26 -12
  17. package/dist/runner-command-engine.js +1 -1
  18. package/dist/runner-profiles.js +25 -17
  19. package/dist/runner-usage.js +5 -5
  20. package/dist/service/conversation-history-service.js +55 -24
  21. package/dist/service/conversation-segment-service.d.ts +20 -0
  22. package/dist/service/conversation-segment-service.js +155 -0
  23. package/dist/service/mcp-installation-verifier.js +1 -1
  24. package/dist/service/native-session-watch-service.d.ts +1 -0
  25. package/dist/service/native-session-watch-service.js +21 -4
  26. package/dist/service/node-request-service.js +2 -1
  27. package/dist/service/run-attachment-service.d.ts +3 -2
  28. package/dist/service/run-attachment-service.js +21 -11
  29. package/dist/service/run-event-service.d.ts +1 -0
  30. package/dist/service/run-event-service.js +8 -2
  31. package/dist/service/session-catalog-service.js +4 -4
  32. package/dist/service/session-presentation-service.d.ts +1 -0
  33. package/dist/service/session-presentation-service.js +8 -1
  34. package/dist/service/session-query-service.d.ts +4 -0
  35. package/dist/service/session-query-service.js +6 -1
  36. package/dist/service/skill-directory-service.js +3 -26
  37. package/dist/service/skill-install-service.js +4 -74
  38. package/dist/service/skill-node-operation-service.js +2 -1
  39. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  40. package/dist/service/workspace-git-exclude-service.js +119 -0
  41. package/dist/service/workspace-queue-workbench-service.js +3 -2
  42. package/dist/service/workspace-service.js +2 -0
  43. package/dist/supervisor.js +2 -1
  44. package/dist/terminal.js +1 -1
  45. package/dist/util/node-operation-parsers.js +2 -2
  46. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  47. package/dist/util/runner-native-session-parsers.js +5 -14
  48. package/dist/util/safe-error.d.ts +2 -0
  49. package/dist/util/safe-error.js +5 -0
  50. package/package.json +2 -2
@@ -44,7 +44,7 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
44
44
  readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "MANAGED_ACTIVE" | "EXTERNAL_ACTIVE" | "IDLE" | undefined>;
45
45
  acceptsTruncatedNativeHistory(): boolean;
46
46
  readManagedRunnerTitle(session: NodeAgentSession, nativeSessionId: string): Promise<string | undefined>;
47
- nativeImageRoot(session: NodeAgentSession): string | undefined;
47
+ nativeImageRoots(session: NodeAgentSession): readonly string[];
48
48
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
49
49
  protected profileForValidation(): RunnerProfile;
50
50
  defaultConfiguration(_workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
@@ -6,7 +6,7 @@ import { resolve } from 'node:path';
6
6
  import { createHash } from 'node:crypto';
7
7
  import { discoverAllNativeSessions } from '../native-session-history.js';
8
8
  import { readCodexNativeContextUsage } from '../native-session-history.js';
9
- import { codexActiveTurnId, codexOfficialConversationPage, codexThreadCwd, codexThreadActivity, deduplicateDirectSessions, externalResumeFailureDetail, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
9
+ import { codexActiveTurnId, codexOfficialConversationPage, codexThreadCwd, codexThreadActivity, deduplicateDirectSessions, extractCodexThreads, isWithinWorkspace, latestNativeActivity } from './codex/conversation-parser.js';
10
10
  import { codexSessionControl } from '../codex-app-server.js';
11
11
  import { extractId } from '../util/runner-native-session-parsers.js';
12
12
  import { nodeLog } from '../operational.js';
@@ -248,10 +248,13 @@ export class CodexRunner extends AbstractRunner {
248
248
  await this.start();
249
249
  return extractCodexThreads(await this.listThreads()).find((thread) => thread.id === nativeSessionId)?.title;
250
250
  }
251
- nativeImageRoot(session) {
252
- return session.externalSessionId === null
253
- ? undefined
254
- : resolve(homedir(), '.codex', 'generated_images', session.externalSessionId);
251
+ nativeImageRoots(session) {
252
+ return [
253
+ session.cwd,
254
+ ...(session.externalSessionId === null
255
+ ? []
256
+ : [resolve(homedir(), '.codex', 'generated_images', session.externalSessionId)])
257
+ ];
255
258
  }
256
259
  managedRunForNativeTurn(nativeTurnId) {
257
260
  return this.execution.managedTurns.get(nativeTurnId)?.runId;
@@ -313,11 +316,10 @@ export class CodexRunner extends AbstractRunner {
313
316
  }
314
317
  return { activity: 'IDLE', session: context.create(external) };
315
318
  }
316
- catch (error) {
319
+ catch {
317
320
  return {
318
321
  activity: 'UNAVAILABLE',
319
- failureCode: 'RUNNER_RESUME_FAILED',
320
- failureDetail: externalResumeFailureDetail(error)
322
+ failureCode: 'RUNNER_RESUME_FAILED'
321
323
  };
322
324
  }
323
325
  }
@@ -18,10 +18,14 @@ export declare function openCodeQuestions(value: unknown): {
18
18
  allowOther: boolean;
19
19
  multiSelect: boolean;
20
20
  }[];
21
- export declare function openCodePatchChanges(value: unknown): {
22
- path: string;
23
- change: string;
24
- additions: null;
25
- deletions: null;
26
- diffAvailable: boolean;
27
- }[];
21
+ export declare function openCodePatchToolPayload(): {
22
+ namespace: string;
23
+ toolName: string;
24
+ title: string;
25
+ inputSummary: null;
26
+ outputSummary: string;
27
+ outputSummaryCode: string;
28
+ progressLabel: null;
29
+ errorCode: null;
30
+ truncated: boolean;
31
+ };
@@ -1,4 +1,3 @@
1
- import { basename } from 'node:path';
2
1
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
3
2
  export function openCodeConversationPage(session, messages, input, managedTurn) {
4
3
  const records = messages.flatMap((value) => {
@@ -114,10 +113,11 @@ function partItem(session, turnId, raw, planMode, startedAt, completedAt, sequen
114
113
  if (part.type === 'compaction')
115
114
  return item(session, turnId, part.id, 'context_compaction', 'COMPLETED', {
116
115
  phase: 'COMPLETED',
117
- summary: '上下文已整理'
116
+ summary: 'Context compacted',
117
+ summaryCode: 'CONTEXT_COMPACTED'
118
118
  }, startedAt, completedAt, sequence);
119
119
  if (part.type === 'patch')
120
- return item(session, turnId, part.id, 'file_change', 'COMPLETED', { changes: openCodePatchChanges(part.files) }, startedAt, completedAt, sequence);
120
+ return item(session, turnId, part.id, 'tool_call', 'COMPLETED', openCodePatchToolPayload(), startedAt, completedAt, sequence);
121
121
  if (part.type === 'step-start' || part.type === 'step-finish')
122
122
  return undefined;
123
123
  if (part.type === 'text' || part.type === 'reasoning')
@@ -162,18 +162,18 @@ export function openCodeQuestions(value) {
162
162
  ];
163
163
  });
164
164
  }
165
- export function openCodePatchChanges(value) {
166
- return (Array.isArray(value) ? value : []).flatMap((path) => typeof path === 'string' && path.length > 0
167
- ? [
168
- {
169
- path: basename(path),
170
- change: 'MODIFIED',
171
- additions: null,
172
- deletions: null,
173
- diffAvailable: false
174
- }
175
- ]
176
- : []);
165
+ export function openCodePatchToolPayload() {
166
+ return {
167
+ namespace: 'opencode',
168
+ toolName: 'patch',
169
+ title: 'patch',
170
+ inputSummary: null,
171
+ outputSummary: 'OpenCode applied the patch',
172
+ outputSummaryCode: 'OPENCODE_PATCH_APPLIED',
173
+ progressLabel: null,
174
+ errorCode: null,
175
+ truncated: false
176
+ };
177
177
  }
178
178
  function item(session, turnId, nativeId, kind, status, payload, startedAt, completedAt, sequence) {
179
179
  return {
@@ -7,6 +7,7 @@ interface OpenCodeManagedRunHost {
7
7
  intercepted(): boolean;
8
8
  readonly active: Set<string>;
9
9
  parseAttachments(value: unknown): readonly RunnerImageAttachment[];
10
+ persistAttachments(cwd: string, runId: string, attachments: readonly RunnerImageAttachment[]): Promise<readonly string[]>;
10
11
  emit(runId: string, eventType: string, payload: unknown, status?: FakeRunnerStatus): void;
11
12
  emitItem(runId: string, item: OpenCodePublicItem): void;
12
13
  adopt(input: {
@@ -40,7 +41,7 @@ interface OpenCodeManagedRunHost {
40
41
  }
41
42
  type OpenCodePublicItem = {
42
43
  readonly itemId: string;
43
- readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'file_change' | 'unknown';
44
+ readonly kind: 'assistant_message' | 'plan' | 'reasoning_summary' | 'tool_call' | 'user_input_request' | 'context_compaction' | 'unknown';
44
45
  readonly status: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'FAILED';
45
46
  readonly payload: Record<string, unknown>;
46
47
  readonly merge?: boolean;
@@ -1,6 +1,7 @@
1
1
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
2
+ import { safeErrorCode } from '../../util/safe-error.js';
2
3
  import { boundedConversationItemId, compactRunnerText, compactRunnerValue } from '../codex/conversation-parser.js';
3
- import { openCodePatchChanges, openCodeQuestions } from './conversation-parser.js';
4
+ import { openCodePatchToolPayload, openCodeQuestions } from './conversation-parser.js';
4
5
  export class OpenCodeManagedRunController {
5
6
  runner;
6
7
  host;
@@ -29,10 +30,10 @@ export class OpenCodeManagedRunController {
29
30
  commandId: 'plan',
30
31
  active: true,
31
32
  label: 'Plan Mode',
32
- detail: '后续消息将由 OpenCode 原生 Plan Agent 只读分析并制定计划。',
33
+ detail: 'Subsequent messages use the OpenCode Plan Agent for read-only analysis.',
33
34
  closable: true,
34
35
  closeInput: '/plan',
35
- continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
36
+ continuation: { label: 'Implement', prompt: 'Implement the plan above.' }
36
37
  });
37
38
  return { command: command.id, states: this.host.commandStates(session.id) };
38
39
  }
@@ -56,7 +57,7 @@ export class OpenCodeManagedRunController {
56
57
  attachments = this.host.parseAttachments(payload.attachments);
57
58
  }
58
59
  catch (error) {
59
- this.host.emit(payload.runId, 'run.rejected', { code: error instanceof Error ? error.message : 'RUNNER_INPUT_INVALID' }, 'FAILED');
60
+ this.host.emit(payload.runId, 'run.rejected', { code: safeErrorCode(error, 'RUNNER_INPUT_INVALID') }, 'FAILED');
60
61
  return;
61
62
  }
62
63
  void this.run({
@@ -122,8 +123,8 @@ export class OpenCodeManagedRunController {
122
123
  return true;
123
124
  }
124
125
  async run(input) {
125
- // OpenCode 的动态 MCP 注册属于 Server 进程状态。加载 MCP Run 使用独立 Server,
126
- // 避免同一工作区的并行会话共享 MCP 进程、配置或浏览器上下文。
126
+ // OpenCode dynamic MCP registration belongs to Server process state. Runs that load MCP use an
127
+ // isolated Server so parallel sessions in one workspace do not share MCP processes or context.
127
128
  const client = this.runner.managedClient(input.environment, hasMcpConfiguration(input.mcp) ? input.runId : undefined);
128
129
  this.startingClients.set(input.runId, client);
129
130
  this.startingCwds.set(input.runId, input.cwd);
@@ -181,17 +182,20 @@ export class OpenCodeManagedRunController {
181
182
  return;
182
183
  }
183
184
  this.finish(input.runId, 'FAILED', 'run.failed', {
184
- code: error instanceof Error ? error.message : 'OPENCODE_EVENT_FAILED'
185
+ code: safeErrorCode(error, 'OPENCODE_EVENT_FAILED')
185
186
  });
186
187
  });
187
188
  await subscriptionReady;
188
189
  this.host.emit(input.runId, 'run.running', {}, 'RUNNING');
189
190
  // The initial SSE snapshot can contain an idle event from before this prompt.
190
191
  this.pendingIdle.delete(input.runId);
192
+ const attachmentPaths = input.attachments.length === 0
193
+ ? []
194
+ : await this.host.persistAttachments(input.cwd, input.runId, input.attachments);
191
195
  await client.prompt({
192
196
  directory: input.cwd,
193
197
  sessionID: nativeSessionId,
194
- text: input.input,
198
+ text: openCodeImageInput(input.input, attachmentPaths),
195
199
  model: input.model,
196
200
  variant: input.effort,
197
201
  agent: input.agent,
@@ -203,7 +207,7 @@ export class OpenCodeManagedRunController {
203
207
  }
204
208
  catch (error) {
205
209
  this.finish(input.runId, 'FAILED', 'run.failed', {
206
- code: error instanceof Error ? error.message : 'OPENCODE_RUN_START_FAILED'
210
+ code: safeErrorCode(error, 'OPENCODE_RUN_START_FAILED')
207
211
  });
208
212
  }
209
213
  }
@@ -366,6 +370,12 @@ export class OpenCodeManagedRunController {
366
370
  void this.host.captureSnapshot(runId, cwd);
367
371
  }
368
372
  }
373
+ function openCodeImageInput(input, attachmentPaths) {
374
+ if (attachmentPaths.length === 0)
375
+ return input;
376
+ const paths = attachmentPaths.map((path) => `- ${JSON.stringify(path)}`).join('\n');
377
+ return `${input}\n\nIf an inline image in this message is unavailable, use the read tool on the corresponding copy below. Do not read it again when the inline image is already visible:\n${paths}`;
378
+ }
369
379
  function hasMcpConfiguration(value) {
370
380
  return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
371
381
  }
@@ -425,14 +435,18 @@ function openCodePart(part, planMode = false) {
425
435
  itemId,
426
436
  kind: 'context_compaction',
427
437
  status: 'COMPLETED',
428
- payload: { phase: 'COMPLETED', summary: '上下文已整理' }
438
+ payload: {
439
+ phase: 'COMPLETED',
440
+ summary: 'Context compacted',
441
+ summaryCode: 'CONTEXT_COMPACTED'
442
+ }
429
443
  };
430
444
  if (part.type === 'patch')
431
445
  return {
432
446
  itemId,
433
- kind: 'file_change',
447
+ kind: 'tool_call',
434
448
  status: 'COMPLETED',
435
- payload: { changes: openCodePatchChanges(part.files) },
449
+ payload: openCodePatchToolPayload(),
436
450
  merge: true
437
451
  };
438
452
  if (part.type === 'step-start' || part.type === 'step-finish')
@@ -8,7 +8,7 @@ const CLAUDE_PLAN_TAG_CLOSE = '</plan_mode>';
8
8
  */
9
9
  export function withClaudePlanTag(input) {
10
10
  return `${CLAUDE_PLAN_TAG_OPEN}
11
- 当前处于 Plan Mode。只做只读分析与规划,不得创建、修改或删除文件,不得执行有副作用的命令。完成分析后,把最终计划完整包裹在 <proposed_plan> </proposed_plan> 标签中输出,不要在标签外重复计划正文。
11
+ You are in Plan Mode. Perform only read-only analysis and planning. Do not create, modify, or delete files, and do not run commands with side effects. Wrap the complete final plan in <proposed_plan> and </proposed_plan> tags without repeating the plan outside those tags.
12
12
  ${CLAUDE_PLAN_TAG_CLOSE}
13
13
 
14
14
  ${input}`;
@@ -14,24 +14,28 @@ const definitions = {
14
14
  runner: 'codex',
15
15
  models: [],
16
16
  accessOptions: [
17
- { id: 'on-request', label: 'On-request', description: '由 Codex 在需要额外权限时请求确认。' },
17
+ {
18
+ id: 'on-request',
19
+ label: 'On-request',
20
+ description: 'Ask when additional permission is required.'
21
+ },
18
22
  {
19
23
  id: 'workspace-write',
20
24
  label: 'Workspace write',
21
- description: '允许 Codex 在当前工作区内写入文件。'
25
+ description: 'Allow writes in the current Workspace.'
22
26
  },
23
27
  {
24
28
  id: 'full-access',
25
29
  label: 'Full access',
26
- description: '允许 Codex 以完整权限运行,且不再逐项确认。'
30
+ description: 'Allow full access without individual confirmations.'
27
31
  },
28
- { id: 'read-only', label: 'Read-only', description: '限制 Codex 为只读沙箱。' }
32
+ { id: 'read-only', label: 'Read-only', description: 'Restrict Codex to a read-only sandbox.' }
29
33
  ],
30
34
  commands: [
31
35
  {
32
36
  id: 'compact',
33
37
  label: '/compact',
34
- description: '立即压缩当前会话上下文。',
38
+ description: 'Compact the current session context.',
35
39
  inputHint: '/compact',
36
40
  interaction: 'IMMEDIATE_ACTION'
37
41
  },
@@ -39,14 +43,14 @@ const definitions = {
39
43
  id: 'plan',
40
44
  label: '/plan',
41
45
  stateLabel: 'Plan Mode',
42
- description: '开启或关闭协作规划模式。',
46
+ description: 'Toggle collaborative Plan Mode.',
43
47
  inputHint: '/plan',
44
48
  interaction: 'TOGGLE'
45
49
  },
46
50
  {
47
51
  id: 'fast',
48
52
  label: '/fast',
49
- description: '开启或关闭当前模型可用的快速服务层。',
53
+ description: 'Toggle the fast service tier when supported.',
50
54
  inputHint: '/fast',
51
55
  interaction: 'TOGGLE'
52
56
  }
@@ -74,21 +78,25 @@ const definitions = {
74
78
  { id: 'haiku', label: 'Claude Haiku', supportedEfforts: ['low', 'medium'] }
75
79
  ],
76
80
  accessOptions: [
77
- { id: 'default', label: 'Default', description: ' Claude Code 默认规则确认危险操作。' },
81
+ { id: 'default', label: 'Default', description: 'Use Claude Code default confirmations.' },
78
82
  {
79
83
  id: 'accept-edits',
80
84
  label: 'Accept edits',
81
- description: '自动接受当前工作区内的文件编辑。'
85
+ description: 'Automatically accept edits in the current Workspace.'
86
+ },
87
+ {
88
+ id: 'bypass',
89
+ label: 'Bypass permissions',
90
+ description: 'Bypass permission confirmations.'
82
91
  },
83
- { id: 'bypass', label: 'Bypass permissions', description: '跳过 Claude Code 的权限确认。' },
84
- { id: 'plan', label: 'Plan Mode', description: '以 Claude Code 的计划模式运行,不执行工具。' }
92
+ { id: 'plan', label: 'Plan Mode', description: 'Run in Plan Mode without executing tools.' }
85
93
  ],
86
94
  commands: [
87
95
  {
88
96
  id: 'compact',
89
97
  runner: 'claude-code',
90
98
  label: '/compact',
91
- description: ' Claude Agent SDK 在当前会话中压缩上下文。',
99
+ description: 'Compact context through the Claude Agent SDK.',
92
100
  inputHint: '/compact',
93
101
  source: 'CLAUDE_AGENT_SDK',
94
102
  interaction: 'IMMEDIATE_ACTION',
@@ -100,7 +108,7 @@ const definitions = {
100
108
  runner: 'claude-code',
101
109
  label: '/plan',
102
110
  stateLabel: 'Plan Mode',
103
- description: '开启或关闭标签式规划模式,只读规划并输出计划卡。',
111
+ description: 'Toggle read-only tagged Plan Mode.',
104
112
  inputHint: '/plan',
105
113
  source: 'CLAUDE_AGENT_SDK',
106
114
  interaction: 'TOGGLE',
@@ -116,12 +124,12 @@ const definitions = {
116
124
  {
117
125
  id: 'default',
118
126
  label: 'Default',
119
- description: ' OpenCode 和当前 Workspace 配置确认权限。'
127
+ description: 'Use OpenCode and Workspace permission configuration.'
120
128
  },
121
129
  {
122
130
  id: 'auto',
123
131
  label: 'Auto approve',
124
- description: '自动允许 OpenCode 请求的工具权限。'
132
+ description: 'Automatically allow requested tool permissions.'
125
133
  }
126
134
  ],
127
135
  commands: [
@@ -129,7 +137,7 @@ const definitions = {
129
137
  id: 'compact',
130
138
  runner: 'opencode',
131
139
  label: '/compact',
132
- description: '通过 OpenCode 压缩当前会话上下文。',
140
+ description: 'Compact context through OpenCode.',
133
141
  inputHint: '/compact',
134
142
  source: 'OPENCODE_SERVER',
135
143
  interaction: 'IMMEDIATE_ACTION',
@@ -141,7 +149,7 @@ const definitions = {
141
149
  runner: 'opencode',
142
150
  label: '/plan',
143
151
  stateLabel: 'Plan Mode',
144
- description: '开启或关闭 OpenCode 原生 Plan Agent',
152
+ description: 'Toggle the native OpenCode Plan Agent.',
145
153
  inputHint: '/plan',
146
154
  source: 'OPENCODE_SERVER',
147
155
  interaction: 'TOGGLE',
@@ -70,9 +70,9 @@ export class RunnerUsageReader {
70
70
  'Content-Type': 'application/json'
71
71
  });
72
72
  const windows = [
73
- usageWindow(value, 'five_hour', '5 小时额度'),
74
- usageWindow(value, 'seven_day', '7 天额度'),
75
- usageWindow(value, 'seven_day_sonnet', '7 Sonnet 额度')
73
+ usageWindow(value, 'five_hour', '5-hour limit'),
74
+ usageWindow(value, 'seven_day', '7-day limit'),
75
+ usageWindow(value, 'seven_day_sonnet', '7-day Sonnet limit')
76
76
  ].filter((window) => window !== undefined);
77
77
  if (windows.length === 0)
78
78
  throw new Error('USAGE_RESPONSE_INVALID');
@@ -158,9 +158,9 @@ function codexUsageWindow(value, key) {
158
158
  const window = record(record(value)?.rate_limit)?.[key];
159
159
  const duration = number(record(window)?.limit_window_seconds);
160
160
  const label = duration !== undefined && approximately(duration, 5 * 60 * 60)
161
- ? '5 小时额度'
161
+ ? '5-hour limit'
162
162
  : duration !== undefined && approximately(duration, 7 * 24 * 60 * 60)
163
- ? '7 天额度'
163
+ ? '7-day limit'
164
164
  : undefined;
165
165
  return label === undefined ? undefined : usageWindow(value, key, label);
166
166
  }
@@ -16,35 +16,66 @@ export class ConversationHistoryService {
16
16
  async read(session, input) {
17
17
  const snapshotSequence = this.options.snapshotSequence();
18
18
  const readAt = Date.now();
19
- const official = await this.readOfficial(session, input);
19
+ let official = await this.readOfficial(session, input);
20
20
  if (official !== undefined) {
21
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
22
+ const runtimeTurns = this.options.runtime.getAgentSession(session.id) === undefined
23
+ ? []
24
+ : this.options.runtime.listConversationTurns({ sessionId: session.id, limit: 500 }).turns;
25
+ const initialOfficialRunIds = new Set(official.turns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
26
+ const absentLocalTurns = runtimeTurns.filter((turn) => turn.runId === null || !initialOfficialRunIds.has(turn.runId));
27
+ const officialFloor = Math.min(...official.turns.map((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0)));
28
+ const missingLocalTurns = official.turns.length === 0
29
+ ? absentLocalTurns.slice(-limit)
30
+ : officialFloor > 0
31
+ ? absentLocalTurns.filter((turn) => Math.max(turn.startedAt ?? 0, turn.completedAt ?? 0) >= officialFloor)
32
+ : absentLocalTurns.slice(-1);
33
+ if (input.cursor === undefined &&
34
+ missingLocalTurns.length > 0 &&
35
+ official.turns.length + missingLocalTurns.length > limit) {
36
+ const reserved = Math.min(missingLocalTurns.length, Math.max(0, limit - 1));
37
+ const narrowed = await this.readOfficial(session, { ...input, limit: limit - reserved });
38
+ if (narrowed !== undefined)
39
+ official = narrowed;
40
+ }
21
41
  const officialTurns = await this.attachNativeImages(session, official.turns);
22
42
  const officialRunIds = new Set(officialTurns.flatMap((turn) => (turn.runId === null ? [] : [turn.runId])));
23
- const localTurns = this.options.runtime.getAgentSession(session.id) === undefined
24
- ? []
25
- : this.options.runtime
26
- .listConversationTurns({ sessionId: session.id, limit: 500 })
27
- .turns.flatMap((turn) => {
28
- if (turn.runId === null || !officialRunIds.has(turn.runId))
29
- return [turn];
30
- const pendingItems = turn.items.filter((item) => item.kind === 'user_input_request' && item.status === 'PENDING');
31
- return pendingItems.length === 0 ? [] : [{ ...turn, items: pendingItems }];
32
- });
33
- const localRequestIds = new Set(localTurns.flatMap((turn) => turn.items.flatMap((item) => {
34
- const requestId = item.kind === 'user_input_request' && isPlainRecord(item.payload)
35
- ? item.payload.requestId
36
- : undefined;
37
- return typeof requestId === 'string' ? [requestId] : [];
38
- })));
43
+ const pendingItemsByRunId = new Map();
44
+ for (const turn of runtimeTurns) {
45
+ if (turn.runId === null || !officialRunIds.has(turn.runId))
46
+ continue;
47
+ const pendingItems = turn.items.filter((item) => item.kind === 'user_input_request' && item.status === 'PENDING');
48
+ if (pendingItems.length > 0)
49
+ pendingItemsByRunId.set(turn.runId, pendingItems);
50
+ }
51
+ const availableLocalSlots = Math.max(0, limit - officialTurns.length);
52
+ const localCandidates = (input.cursor === undefined ? missingLocalTurns : []).sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
53
+ const localTurns = availableLocalSlots === 0 ? [] : localCandidates.slice(-availableLocalSlots);
54
+ const localRequestIds = new Set([
55
+ ...localTurns.flatMap((turn) => turn.items.flatMap((item) => {
56
+ const requestId = item.kind === 'user_input_request' && isPlainRecord(item.payload)
57
+ ? item.payload.requestId
58
+ : undefined;
59
+ return typeof requestId === 'string' ? [requestId] : [];
60
+ })),
61
+ ...[...pendingItemsByRunId.values()].flatMap((items) => items.flatMap((item) => {
62
+ const requestId = isPlainRecord(item.payload) ? item.payload.requestId : undefined;
63
+ return typeof requestId === 'string' ? [requestId] : [];
64
+ }))
65
+ ]);
39
66
  const turns = [
40
67
  ...officialTurns
41
- .map((turn) => ({
42
- ...turn,
43
- items: turn.items.filter((item) => item.kind !== 'user_input_request' ||
44
- !isPlainRecord(item.payload) ||
45
- typeof item.payload.requestId !== 'string' ||
46
- !localRequestIds.has(item.payload.requestId))
47
- }))
68
+ .map((turn) => {
69
+ const pendingItems = turn.runId === null ? [] : (pendingItemsByRunId.get(turn.runId) ?? []);
70
+ const items = [
71
+ ...turn.items.filter((item) => item.kind !== 'user_input_request' ||
72
+ !isPlainRecord(item.payload) ||
73
+ typeof item.payload.requestId !== 'string' ||
74
+ !localRequestIds.has(item.payload.requestId)),
75
+ ...pendingItems
76
+ ];
77
+ return { ...turn, items };
78
+ })
48
79
  .filter((turn) => turn.items.length > 0),
49
80
  ...localTurns
50
81
  ].sort((left, right) => (left.startedAt ?? 0) - (right.startedAt ?? 0));
@@ -0,0 +1,20 @@
1
+ import type { ConversationSegment, ConversationSegmentPage } from '@myagentroam/protocol';
2
+ import type { NodeAgentSession, NodeConversationTurn } from '../database.js';
3
+ import type { ConversationHistoryPage } from '../util/runner-native-session-parsers.js';
4
+ export declare function conversationSegments(turn: NodeConversationTurn): ConversationSegment[];
5
+ export declare function conversationSegmentIdentityForItem(turn: NodeConversationTurn, itemId: string, itemKind?: string): {
6
+ readonly id: string;
7
+ readonly index: number;
8
+ };
9
+ export declare class ConversationSegmentService {
10
+ private readonly readTurns;
11
+ constructor(readTurns: (session: NodeAgentSession, input: {
12
+ readonly cursor?: string;
13
+ readonly limit?: number;
14
+ }) => Promise<ConversationHistoryPage>);
15
+ projectInitial(session: NodeAgentSession, page: ConversationHistoryPage, limit: number): ConversationSegmentPage;
16
+ read(session: NodeAgentSession, input: {
17
+ readonly cursor?: string;
18
+ readonly limit?: number;
19
+ }): Promise<ConversationSegmentPage>;
20
+ }