@myagentroam/node 0.1.8 → 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 (79) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.d.ts +2 -0
  8. package/dist/connector.js +43 -14
  9. package/dist/database.d.ts +23 -0
  10. package/dist/database.js +84 -2
  11. package/dist/migrations/v004.d.ts +4 -0
  12. package/dist/migrations/v004.js +35 -0
  13. package/dist/native-session-history.js +72 -14
  14. package/dist/opencode-server.d.ts +1 -0
  15. package/dist/opencode-server.js +12 -0
  16. package/dist/runner/abstract-runner.d.ts +13 -3
  17. package/dist/runner/abstract-runner.js +78 -4
  18. package/dist/runner/claude/managed-run-controller.js +18 -7
  19. package/dist/runner/claude-code-runner.d.ts +1 -0
  20. package/dist/runner/claude-code-runner.js +17 -4
  21. package/dist/runner/codex/conversation-parser.d.ts +2 -2
  22. package/dist/runner/codex/conversation-parser.js +30 -12
  23. package/dist/runner/codex/managed-run-controller.js +11 -8
  24. package/dist/runner/codex-runner.d.ts +17 -3
  25. package/dist/runner/codex-runner.js +225 -12
  26. package/dist/runner/opencode/conversation-parser.d.ts +11 -7
  27. package/dist/runner/opencode/conversation-parser.js +15 -15
  28. package/dist/runner/opencode/managed-run-controller.d.ts +2 -1
  29. package/dist/runner/opencode/managed-run-controller.js +36 -12
  30. package/dist/runner/opencode-runner.d.ts +2 -1
  31. package/dist/runner/opencode-runner.js +23 -3
  32. package/dist/runner/runner-registry.d.ts +1 -1
  33. package/dist/runner/runner-registry.js +2 -2
  34. package/dist/runner-command-engine.js +1 -1
  35. package/dist/runner-profiles.js +29 -41
  36. package/dist/runner-usage.js +5 -5
  37. package/dist/runtime-command-detector.d.ts +3 -0
  38. package/dist/runtime-command-detector.js +78 -0
  39. package/dist/service/conversation-history-service.js +55 -24
  40. package/dist/service/conversation-segment-service.d.ts +20 -0
  41. package/dist/service/conversation-segment-service.js +155 -0
  42. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  43. package/dist/service/mcp-installation-verifier.js +87 -0
  44. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  45. package/dist/service/mcp-node-operation-service.js +90 -0
  46. package/dist/service/mcp-package-installer.d.ts +8 -0
  47. package/dist/service/mcp-package-installer.js +136 -0
  48. package/dist/service/native-session-watch-service.d.ts +1 -0
  49. package/dist/service/native-session-watch-service.js +21 -4
  50. package/dist/service/node-request-service.js +2 -1
  51. package/dist/service/run-attachment-service.d.ts +3 -2
  52. package/dist/service/run-attachment-service.js +21 -11
  53. package/dist/service/run-event-service.d.ts +1 -0
  54. package/dist/service/run-event-service.js +8 -2
  55. package/dist/service/runner-service.d.ts +2 -0
  56. package/dist/service/runner-service.js +5 -2
  57. package/dist/service/session-catalog-service.js +4 -4
  58. package/dist/service/session-lifecycle-service.js +7 -4
  59. package/dist/service/session-presentation-service.d.ts +1 -0
  60. package/dist/service/session-presentation-service.js +8 -1
  61. package/dist/service/session-query-service.d.ts +4 -0
  62. package/dist/service/session-query-service.js +6 -1
  63. package/dist/service/skill-directory-service.js +3 -26
  64. package/dist/service/skill-install-service.js +4 -74
  65. package/dist/service/skill-node-operation-service.js +2 -1
  66. package/dist/service/workbench-manifest-service.d.ts +2 -0
  67. package/dist/service/workspace-git-exclude-service.d.ts +4 -0
  68. package/dist/service/workspace-git-exclude-service.js +119 -0
  69. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  70. package/dist/service/workspace-queue-workbench-service.js +5 -3
  71. package/dist/service/workspace-service.js +2 -0
  72. package/dist/supervisor.js +2 -1
  73. package/dist/terminal.js +1 -1
  74. package/dist/util/node-operation-parsers.js +2 -2
  75. package/dist/util/runner-native-session-parsers.d.ts +2 -3
  76. package/dist/util/runner-native-session-parsers.js +5 -14
  77. package/dist/util/safe-error.d.ts +2 -0
  78. package/dist/util/safe-error.js +5 -0
  79. package/package.json +2 -2
@@ -30,6 +30,7 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
30
30
  protected profileForValidation(): RunnerProfile;
31
31
  supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace): boolean;
32
32
  defaultConfiguration(workspace?: NodeWorkspace): RunnerDefaultConfiguration;
33
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
33
34
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
34
35
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
35
36
  readExternalActivity(session: NodeAgentSession): Promise<"UNAVAILABLE" | "EXTERNAL_ACTIVE" | "IDLE">;
@@ -55,6 +56,6 @@ export declare class OpenCodeRunner extends AbstractRunner<'opencode'> {
55
56
  maxTokens: number;
56
57
  } | undefined>;
57
58
  stop(): Promise<void>;
58
- managedClient(environment: Readonly<Record<string, string>>): OpenCodeServerClient;
59
+ managedClient(environment: Readonly<Record<string, string>>, isolationKey?: string): OpenCodeServerClient;
59
60
  releaseManagedClient(client: OpenCodeServerClient): void;
60
61
  }
@@ -87,6 +87,24 @@ export class OpenCodeRunner extends AbstractRunner {
87
87
  access: 'default'
88
88
  };
89
89
  }
90
+ mcpConfiguration(raw, secrets) {
91
+ const servers = super.mcpConfiguration(raw, secrets);
92
+ return Object.fromEntries(servers.map((server) => [
93
+ server.name,
94
+ server.transport === 'STDIO'
95
+ ? {
96
+ type: 'local',
97
+ command: [server.command, ...(server.args ?? [])],
98
+ environment: { ...(server.environment ?? {}) }
99
+ }
100
+ : {
101
+ type: 'remote',
102
+ url: server.url,
103
+ headers: { ...(server.headers ?? {}) },
104
+ oauth: false
105
+ }
106
+ ]));
107
+ }
90
108
  async discoverSessions(context, workspace) {
91
109
  const result = [];
92
110
  for (const target of workspace === undefined ? context.workspaces : [workspace]) {
@@ -270,11 +288,13 @@ export class OpenCodeRunner extends AbstractRunner {
270
288
  await Promise.allSettled(stopping);
271
289
  this.stoppingClients.clear();
272
290
  }
273
- managedClient(environment) {
291
+ managedClient(environment, isolationKey) {
274
292
  const entries = Object.entries(environment).sort(([left], [right]) => left.localeCompare(right));
275
- if (entries.length === 0)
293
+ if (entries.length === 0 && isolationKey === undefined)
276
294
  return this.client;
277
- const fingerprint = createHash('sha256').update(JSON.stringify(entries)).digest('hex');
295
+ const fingerprint = createHash('sha256')
296
+ .update(JSON.stringify({ entries, isolationKey }))
297
+ .digest('hex');
278
298
  const existing = this.environmentClients.get(fingerprint);
279
299
  if (existing !== undefined) {
280
300
  existing.leases += 1;
@@ -10,7 +10,7 @@ export declare class RunnerRegistry {
10
10
  product(name: unknown): AbstractRunner<RunnerName> | undefined;
11
11
  profiles(capabilities: NodeCapabilities): readonly RunnerProfile[];
12
12
  refreshProfiles(capabilities: NodeCapabilities, workspace?: import('../database.js').NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<readonly RunnerProfile[]>;
13
- supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: import('../database.js').NodeWorkspace): boolean;
13
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: import('../database.js').NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
14
14
  startManagedRun(envelope: NodeEnvelope): boolean;
15
15
  cancelManagedRun(runId: string, intent: RunnerCancellationIntent): boolean;
16
16
  decideApproval(input: RunnerApprovalDecision): boolean;
@@ -38,8 +38,8 @@ export class RunnerRegistry {
38
38
  const profiles = await Promise.all(this.advertised().map((runner) => runner.refreshProfile(capabilities, workspace, environment)));
39
39
  return profiles.flatMap((profile) => (profile === undefined ? [] : [profile]));
40
40
  }
41
- supportsConfiguration(configuration, workspace) {
42
- return (this.product(configuration.runner)?.supportsConfiguration(configuration, workspace) === true);
41
+ supportsConfiguration(configuration, workspace, environment) {
42
+ return (this.product(configuration.runner)?.supportsConfiguration(configuration, workspace, environment) === true);
43
43
  }
44
44
  startManagedRun(envelope) {
45
45
  const runnerName = envelope.payload.runner;
@@ -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}`;
@@ -1,57 +1,41 @@
1
- // Codex reasoning efforts follow the official Codex model surface. Keep this
2
- // shared so an alias such as GPT-5.6 Luna cannot accidentally lose `high`.
3
- const CODEX_REASONING_EFFORTS = ['low', 'medium', 'high', 'xhigh'];
4
1
  const CLAUDE_MODEL_ENVIRONMENT = {
5
2
  opus: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
6
3
  sonnet: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
7
4
  haiku: 'ANTHROPIC_DEFAULT_HAIKU_MODEL'
8
5
  };
9
6
  /**
10
- * The Runner APIs used by MAR do not expose a portable model-list endpoint.
11
- * Keep the supported, product-visible choices in one Node-owned definition so
12
- * the browser never guesses models, effort levels, permission modes, or slash
13
- * commands. Model aliases for Claude are accepted by the Agent SDK and avoid
14
- * coupling the UI to a vendor version suffix.
7
+ * Keep product-visible permission modes and slash commands in one Node-owned
8
+ * definition. Codex models are supplied by App Server `model/list`; Claude
9
+ * aliases are accepted by the Agent SDK and avoid coupling the UI to a vendor
10
+ * version suffix.
15
11
  */
16
12
  const definitions = {
17
13
  codex: {
18
14
  runner: 'codex',
19
- models: [
20
- {
21
- id: 'gpt-5.6-sol',
22
- label: 'GPT-5.6 Sol',
23
- supportedEfforts: [...CODEX_REASONING_EFFORTS]
24
- },
15
+ models: [],
16
+ accessOptions: [
25
17
  {
26
- id: 'gpt-5.6-terra',
27
- label: 'GPT-5.6 Terra',
28
- supportedEfforts: [...CODEX_REASONING_EFFORTS]
18
+ id: 'on-request',
19
+ label: 'On-request',
20
+ description: 'Ask when additional permission is required.'
29
21
  },
30
- {
31
- id: 'gpt-5.6-luna',
32
- label: 'GPT-5.6 Luna',
33
- supportedEfforts: [...CODEX_REASONING_EFFORTS]
34
- }
35
- ],
36
- accessOptions: [
37
- { id: 'on-request', label: 'On-request', description: '由 Codex 在需要额外权限时请求确认。' },
38
22
  {
39
23
  id: 'workspace-write',
40
24
  label: 'Workspace write',
41
- description: '允许 Codex 在当前工作区内写入文件。'
25
+ description: 'Allow writes in the current Workspace.'
42
26
  },
43
27
  {
44
28
  id: 'full-access',
45
29
  label: 'Full access',
46
- description: '允许 Codex 以完整权限运行,且不再逐项确认。'
30
+ description: 'Allow full access without individual confirmations.'
47
31
  },
48
- { id: 'read-only', label: 'Read-only', description: '限制 Codex 为只读沙箱。' }
32
+ { id: 'read-only', label: 'Read-only', description: 'Restrict Codex to a read-only sandbox.' }
49
33
  ],
50
34
  commands: [
51
35
  {
52
36
  id: 'compact',
53
37
  label: '/compact',
54
- description: '立即压缩当前会话上下文。',
38
+ description: 'Compact the current session context.',
55
39
  inputHint: '/compact',
56
40
  interaction: 'IMMEDIATE_ACTION'
57
41
  },
@@ -59,14 +43,14 @@ const definitions = {
59
43
  id: 'plan',
60
44
  label: '/plan',
61
45
  stateLabel: 'Plan Mode',
62
- description: '开启或关闭协作规划模式。',
46
+ description: 'Toggle collaborative Plan Mode.',
63
47
  inputHint: '/plan',
64
48
  interaction: 'TOGGLE'
65
49
  },
66
50
  {
67
51
  id: 'fast',
68
52
  label: '/fast',
69
- description: '开启或关闭当前模型可用的快速服务层。',
53
+ description: 'Toggle the fast service tier when supported.',
70
54
  inputHint: '/fast',
71
55
  interaction: 'TOGGLE'
72
56
  }
@@ -94,21 +78,25 @@ const definitions = {
94
78
  { id: 'haiku', label: 'Claude Haiku', supportedEfforts: ['low', 'medium'] }
95
79
  ],
96
80
  accessOptions: [
97
- { id: 'default', label: 'Default', description: ' Claude Code 默认规则确认危险操作。' },
81
+ { id: 'default', label: 'Default', description: 'Use Claude Code default confirmations.' },
98
82
  {
99
83
  id: 'accept-edits',
100
84
  label: 'Accept edits',
101
- description: '自动接受当前工作区内的文件编辑。'
85
+ description: 'Automatically accept edits in the current Workspace.'
86
+ },
87
+ {
88
+ id: 'bypass',
89
+ label: 'Bypass permissions',
90
+ description: 'Bypass permission confirmations.'
102
91
  },
103
- { id: 'bypass', label: 'Bypass permissions', description: '跳过 Claude Code 的权限确认。' },
104
- { id: 'plan', label: 'Plan Mode', description: '以 Claude Code 的计划模式运行,不执行工具。' }
92
+ { id: 'plan', label: 'Plan Mode', description: 'Run in Plan Mode without executing tools.' }
105
93
  ],
106
94
  commands: [
107
95
  {
108
96
  id: 'compact',
109
97
  runner: 'claude-code',
110
98
  label: '/compact',
111
- description: ' Claude Agent SDK 在当前会话中压缩上下文。',
99
+ description: 'Compact context through the Claude Agent SDK.',
112
100
  inputHint: '/compact',
113
101
  source: 'CLAUDE_AGENT_SDK',
114
102
  interaction: 'IMMEDIATE_ACTION',
@@ -120,7 +108,7 @@ const definitions = {
120
108
  runner: 'claude-code',
121
109
  label: '/plan',
122
110
  stateLabel: 'Plan Mode',
123
- description: '开启或关闭标签式规划模式,只读规划并输出计划卡。',
111
+ description: 'Toggle read-only tagged Plan Mode.',
124
112
  inputHint: '/plan',
125
113
  source: 'CLAUDE_AGENT_SDK',
126
114
  interaction: 'TOGGLE',
@@ -136,12 +124,12 @@ const definitions = {
136
124
  {
137
125
  id: 'default',
138
126
  label: 'Default',
139
- description: ' OpenCode 和当前 Workspace 配置确认权限。'
127
+ description: 'Use OpenCode and Workspace permission configuration.'
140
128
  },
141
129
  {
142
130
  id: 'auto',
143
131
  label: 'Auto approve',
144
- description: '自动允许 OpenCode 请求的工具权限。'
132
+ description: 'Automatically allow requested tool permissions.'
145
133
  }
146
134
  ],
147
135
  commands: [
@@ -149,7 +137,7 @@ const definitions = {
149
137
  id: 'compact',
150
138
  runner: 'opencode',
151
139
  label: '/compact',
152
- description: '通过 OpenCode 压缩当前会话上下文。',
140
+ description: 'Compact context through OpenCode.',
153
141
  inputHint: '/compact',
154
142
  source: 'OPENCODE_SERVER',
155
143
  interaction: 'IMMEDIATE_ACTION',
@@ -161,7 +149,7 @@ const definitions = {
161
149
  runner: 'opencode',
162
150
  label: '/plan',
163
151
  stateLabel: 'Plan Mode',
164
- description: '开启或关闭 OpenCode 原生 Plan Agent',
152
+ description: 'Toggle the native OpenCode Plan Agent.',
165
153
  inputHint: '/plan',
166
154
  source: 'OPENCODE_SERVER',
167
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
  }
@@ -0,0 +1,3 @@
1
+ import type { RuntimeCommand } from '@myagentroam/protocol';
2
+ export declare function detectRuntimeCommands(): Promise<RuntimeCommand[]>;
3
+ export declare function normalizeVersion(id: RuntimeCommand['id'], value: string): string | undefined;
@@ -0,0 +1,78 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ const execFileAsync = promisify(execFile);
4
+ const COMMANDS = [
5
+ { id: 'npx', candidates: ['npx'], versionArguments: ['--version'] },
6
+ { id: 'git', candidates: ['git'], versionArguments: ['--version'] },
7
+ { id: 'python', candidates: ['python3', 'python'], versionArguments: ['--version'] },
8
+ { id: 'pip', candidates: ['pip3', 'pip'], versionArguments: ['--version'] }
9
+ ];
10
+ export async function detectRuntimeCommands() {
11
+ return Promise.all(COMMANDS.map(detectCommand));
12
+ }
13
+ async function detectCommand(descriptor) {
14
+ for (const command of descriptor.candidates) {
15
+ try {
16
+ const [{ stdout, stderr }, path] = await Promise.all([
17
+ executeVersion(command, descriptor.versionArguments),
18
+ resolveCommandPath(command)
19
+ ]);
20
+ const version = normalizeVersion(descriptor.id, `${stdout}${stderr}`.trim().split(/\r?\n/u)[0]?.trim() ?? '');
21
+ return {
22
+ id: descriptor.id,
23
+ command,
24
+ available: true,
25
+ ...(version === undefined || version === '' ? {} : { version }),
26
+ ...(path === undefined ? {} : { path })
27
+ };
28
+ }
29
+ catch {
30
+ // Try the platform-compatible fallback command name.
31
+ }
32
+ }
33
+ return {
34
+ id: descriptor.id,
35
+ command: descriptor.candidates[0],
36
+ available: false
37
+ };
38
+ }
39
+ export function normalizeVersion(id, value) {
40
+ const normalized = value.trim();
41
+ if (normalized === '')
42
+ return undefined;
43
+ const patterns = {
44
+ git: /^git version\s+(\S+)/iu,
45
+ python: /^python\s+(\S+)/iu,
46
+ pip: /^pip\s+(\S+)/iu
47
+ };
48
+ const match = patterns[id]?.exec(normalized);
49
+ return (match?.[1] ?? normalized).slice(0, 512);
50
+ }
51
+ async function executeVersion(command, args) {
52
+ const executable = process.platform === 'win32' ? process.env['ComSpec'] || 'cmd.exe' : command;
53
+ const commandArgs = process.platform === 'win32'
54
+ ? ['/d', '/s', '/c', [command, ...args].map(quoteWindowsArgument).join(' ')]
55
+ : [...args];
56
+ return execFileAsync(executable, commandArgs, {
57
+ timeout: 5_000,
58
+ maxBuffer: 64 * 1024,
59
+ windowsHide: true
60
+ });
61
+ }
62
+ function quoteWindowsArgument(value) {
63
+ return `"${value.replaceAll('"', '""')}"`;
64
+ }
65
+ async function resolveCommandPath(command) {
66
+ try {
67
+ const resolver = process.platform === 'win32' ? 'where.exe' : 'which';
68
+ const { stdout } = await execFileAsync(resolver, [command], {
69
+ timeout: 2_000,
70
+ maxBuffer: 64 * 1024,
71
+ windowsHide: true
72
+ });
73
+ return stdout.trim().split(/\r?\n/u)[0]?.trim().slice(0, 2048) || undefined;
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
@@ -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
+ }
@@ -0,0 +1,155 @@
1
+ const TURN_PAGE_SIZE = 30;
2
+ export function conversationSegments(turn) {
3
+ const groups = [];
4
+ let current = [];
5
+ for (const item of turn.items) {
6
+ current.push(item);
7
+ if (item.kind !== 'assistant_message')
8
+ continue;
9
+ groups.push(current);
10
+ current = [];
11
+ }
12
+ if (current.length > 0 || groups.length === 0)
13
+ groups.push(current);
14
+ return groups.map((items, index) => {
15
+ const id = `${turn.id}:segment:${index}`;
16
+ const latest = index === groups.length - 1;
17
+ const projectedItems = items.map((item) => ({
18
+ ...item,
19
+ segmentId: id,
20
+ segmentIndex: index
21
+ }));
22
+ return {
23
+ id,
24
+ sessionId: turn.sessionId,
25
+ turnId: turn.id,
26
+ runId: turn.runId,
27
+ index,
28
+ status: (latest ? turn.status : 'SUCCEEDED'),
29
+ ...(latest && turn.rewind !== undefined ? { rewind: turn.rewind } : {}),
30
+ items: projectedItems,
31
+ startedAt: projectedItems[0]?.startedAt ?? turn.startedAt,
32
+ completedAt: projectedItems.at(-1)?.completedAt ?? null
33
+ };
34
+ });
35
+ }
36
+ export function conversationSegmentIdentityForItem(turn, itemId, itemKind) {
37
+ let index = 0;
38
+ const runtimeItemId = turn.runId === null ? itemId : `${turn.runId}:${itemId}`;
39
+ for (const item of turn.items) {
40
+ if (item.id === itemId || item.id === runtimeItemId)
41
+ return { id: `${turn.id}:segment:${index}`, index };
42
+ if (item.kind === 'assistant_message')
43
+ index += 1;
44
+ }
45
+ if (itemKind === 'assistant_message' && turn.items.at(-1)?.kind === 'assistant_message')
46
+ index -= 1;
47
+ return { id: `${turn.id}:segment:${index}`, index };
48
+ }
49
+ export class ConversationSegmentService {
50
+ readTurns;
51
+ constructor(readTurns) {
52
+ this.readTurns = readTurns;
53
+ }
54
+ projectInitial(session, page, limit) {
55
+ const all = page.turns.flatMap(conversationSegments);
56
+ const segments = all.slice(-limit);
57
+ return {
58
+ snapshotSequence: page.snapshotSequence,
59
+ source: page.source,
60
+ readAt: page.readAt,
61
+ latestVisibleAt: page.latestVisibleAt,
62
+ unit: 'SEGMENT',
63
+ segments,
64
+ nextCursor: segments.length === 0 || (segments.length === all.length && page.nextCursor === null)
65
+ ? null
66
+ : encodeCursor({
67
+ version: 1,
68
+ sessionId: session.id,
69
+ beforeSegmentId: segments[0].id
70
+ })
71
+ };
72
+ }
73
+ async read(session, input) {
74
+ const limit = Math.min(Math.max(input.limit ?? 30, 1), 500);
75
+ const boundary = input.cursor === undefined ? undefined : decodeCursor(input.cursor, session.id);
76
+ const collected = [];
77
+ const visited = new Set();
78
+ let turnCursor;
79
+ let locatingBoundary = boundary !== undefined;
80
+ let hasOlder = false;
81
+ let metadata;
82
+ while (collected.length < limit) {
83
+ const page = await this.readTurns(session, {
84
+ ...(turnCursor === undefined ? {} : { cursor: turnCursor }),
85
+ limit: TURN_PAGE_SIZE
86
+ });
87
+ metadata ??= {
88
+ snapshotSequence: page.snapshotSequence,
89
+ source: page.source,
90
+ readAt: page.readAt,
91
+ latestVisibleAt: page.latestVisibleAt
92
+ };
93
+ const pageSegments = page.turns.flatMap(conversationSegments);
94
+ let eligible = pageSegments;
95
+ if (locatingBoundary) {
96
+ const boundaryIndex = pageSegments.findIndex((segment) => segment.id === boundary?.beforeSegmentId);
97
+ if (boundaryIndex >= 0) {
98
+ eligible = pageSegments.slice(0, boundaryIndex);
99
+ locatingBoundary = false;
100
+ }
101
+ else
102
+ eligible = [];
103
+ }
104
+ if (!locatingBoundary && eligible.length > 0) {
105
+ const remaining = Math.max(0, limit - collected.length);
106
+ const take = eligible.slice(-remaining);
107
+ hasOlder = eligible.length > take.length || page.nextCursor !== null;
108
+ collected.unshift(...take);
109
+ }
110
+ if (collected.length >= limit || page.nextCursor === null)
111
+ break;
112
+ if (visited.has(page.nextCursor))
113
+ throw new Error('EVENT_CURSOR_INVALID');
114
+ visited.add(page.nextCursor);
115
+ turnCursor = page.nextCursor;
116
+ }
117
+ if (locatingBoundary)
118
+ throw new Error('EVENT_CURSOR_INVALID');
119
+ const nextCursor = collected.length === 0 || !hasOlder
120
+ ? null
121
+ : encodeCursor({
122
+ version: 1,
123
+ sessionId: session.id,
124
+ beforeSegmentId: collected[0].id
125
+ });
126
+ return {
127
+ ...(metadata ?? {
128
+ snapshotSequence: 0,
129
+ source: 'runtime',
130
+ readAt: Date.now(),
131
+ latestVisibleAt: 0
132
+ }),
133
+ unit: 'SEGMENT',
134
+ segments: collected,
135
+ nextCursor
136
+ };
137
+ }
138
+ }
139
+ function encodeCursor(cursor) {
140
+ return Buffer.from(JSON.stringify(cursor)).toString('base64url');
141
+ }
142
+ function decodeCursor(value, sessionId) {
143
+ try {
144
+ const decoded = JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
145
+ if (decoded.version !== 1 ||
146
+ decoded.sessionId !== sessionId ||
147
+ typeof decoded.beforeSegmentId !== 'string' ||
148
+ decoded.beforeSegmentId.length === 0)
149
+ throw new Error('EVENT_CURSOR_INVALID');
150
+ return decoded;
151
+ }
152
+ catch {
153
+ throw new Error('EVENT_CURSOR_INVALID');
154
+ }
155
+ }
@@ -0,0 +1,9 @@
1
+ import type { McpCatalogEntry, McpInstallationConfiguration } from '@myagentroam/protocol';
2
+ export declare class McpInstallationVerifier {
3
+ verify(input: {
4
+ readonly entry: McpCatalogEntry;
5
+ readonly configuration: McpInstallationConfiguration;
6
+ readonly secrets: Readonly<Record<string, string>>;
7
+ readonly cwd: string;
8
+ }): Promise<void>;
9
+ }