remote-codex 0.11.44 → 0.11.49

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 (63) hide show
  1. package/README.md +84 -5
  2. package/apps/relay-server/dist/index.js +18 -2
  3. package/apps/supervisor-api/dist/index.js +10034 -2782
  4. package/apps/supervisor-web/dist/assets/c-BIGW1oBm.js +1 -0
  5. package/apps/supervisor-web/dist/assets/{core-DODVy7wn.js → core-B0prQzhr.js} +1 -1
  6. package/apps/supervisor-web/dist/assets/cpp-DIPi6g--.js +1 -0
  7. package/apps/supervisor-web/dist/assets/csharp-DSvCPggb.js +1 -0
  8. package/apps/supervisor-web/dist/assets/go-C27-OAKa.js +1 -0
  9. package/apps/supervisor-web/dist/assets/index-DZI1aSXo.js +22 -0
  10. package/apps/supervisor-web/dist/assets/index-Dy8PgXgw.css +1 -0
  11. package/apps/supervisor-web/dist/assets/java-CylS5w8V.js +1 -0
  12. package/apps/supervisor-web/dist/assets/{markdown-vendor-RZk8L7-L.js → markdown-vendor-BG6PurxI.js} +33 -33
  13. package/apps/supervisor-web/dist/assets/ruby-B6AkvBWc.js +1 -0
  14. package/apps/supervisor-web/dist/assets/rust-B1yitclQ.js +1 -0
  15. package/apps/supervisor-web/dist/assets/{shellscript-CEILq0vU.js → shellscript-DfDnw5Jg.js} +1 -1
  16. package/apps/supervisor-web/dist/assets/thread-ui-gcslNXur.js +3968 -0
  17. package/apps/supervisor-web/dist/assets/xml-sdJ4AIDG.js +1 -0
  18. package/apps/supervisor-web/dist/index.html +4 -4
  19. package/bin/remote-codex.mjs +426 -35
  20. package/docs/windows-device-manager.zh.md +143 -0
  21. package/docs/windows-device-setup.zh.md +263 -0
  22. package/docs/windows-one-click-installer-research.zh.md +481 -0
  23. package/docs/windows.md +87 -0
  24. package/package.json +20 -4
  25. package/packages/acp/src/agent-catalog.test.ts +57 -0
  26. package/packages/acp/src/agent-catalog.ts +361 -0
  27. package/packages/acp/src/catalog-runtime.test.ts +99 -0
  28. package/packages/acp/src/catalog-runtime.ts +507 -0
  29. package/packages/acp/src/codex-environment.test.ts +58 -0
  30. package/packages/acp/src/codex-environment.ts +33 -0
  31. package/packages/acp/src/index.ts +5 -0
  32. package/packages/acp/src/item-mapper.test.ts +137 -0
  33. package/packages/acp/src/item-mapper.ts +473 -0
  34. package/packages/acp/src/runtimeAdapter.test.ts +79 -0
  35. package/packages/acp/src/runtimeAdapter.ts +1193 -0
  36. package/packages/acp/src/terminal-service.test.ts +31 -0
  37. package/packages/acp/src/terminal-service.ts +137 -0
  38. package/packages/agent-runtime/src/runtime-errors.ts +1 -0
  39. package/packages/agent-runtime/src/types.ts +8 -0
  40. package/packages/claude/src/runtimeAdapter.test.ts +2 -2
  41. package/packages/claude/src/runtimeAdapter.ts +10 -19
  42. package/packages/codex/src/appServerManager.test.ts +47 -0
  43. package/packages/codex/src/appServerManager.ts +6 -2
  44. package/packages/codex/src/runtimeAdapter.test.ts +9 -2
  45. package/packages/db/migrations/0030_thread_agent_id.sql +9 -0
  46. package/packages/db/src/repositories.ts +3 -0
  47. package/packages/db/src/schema.ts +1 -0
  48. package/packages/opencode/src/historyItems.ts +7 -6
  49. package/packages/opencode/src/runtimeAdapter.ts +7 -11
  50. package/packages/process-runtime/src/index.test.ts +132 -0
  51. package/packages/process-runtime/src/index.ts +253 -0
  52. package/packages/shared/src/agent-providers.ts +10 -1
  53. package/packages/shared/src/index.ts +36 -0
  54. package/scripts/service-manager.mjs +112 -4
  55. package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
  56. package/scripts/windows/build-device-manager.ps1 +64 -0
  57. package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
  58. package/scripts/windows/relay-smoke.ps1 +16 -0
  59. package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
  60. package/scripts/windows/validate-real-codex.mjs +680 -0
  61. package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
  62. package/apps/supervisor-web/dist/assets/index-CO8cU7a0.js +0 -21
  63. package/apps/supervisor-web/dist/assets/thread-ui-Dmrigdek.js +0 -3714
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { AcpTerminalService } from './terminal-service';
4
+
5
+ describe('AcpTerminalService', () => {
6
+ it('parses agents that send command and argv as one ACP command string', async () => {
7
+ const service = new AcpTerminalService(() => process.cwd());
8
+ const created = service.create({
9
+ sessionId: 'session-1',
10
+ command: `"${process.execPath}" -e "process.stdout.write('ACP_TERMINAL_OK')"`,
11
+ args: [],
12
+ outputByteLimit: 1_000,
13
+ });
14
+ const exit = await service.waitForExit({
15
+ sessionId: 'session-1',
16
+ terminalId: created.terminalId,
17
+ });
18
+ const output = service.output({
19
+ sessionId: 'session-1',
20
+ terminalId: created.terminalId,
21
+ });
22
+
23
+ expect(exit.exitCode).toBe(0);
24
+ expect(output.output).toBe('ACP_TERMINAL_OK');
25
+ expect(output.truncated).toBe(false);
26
+ service.release({
27
+ sessionId: 'session-1',
28
+ terminalId: created.terminalId,
29
+ });
30
+ });
31
+ });
@@ -0,0 +1,137 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+
4
+ import type * as acp from '@agentclientprotocol/sdk';
5
+
6
+ import {
7
+ parseCommandLine,
8
+ spawnProcess,
9
+ } from '../../process-runtime/src/index';
10
+
11
+ const DEFAULT_OUTPUT_BYTE_LIMIT = 1024 * 1024;
12
+
13
+ interface AcpTerminalState {
14
+ child: ChildProcess;
15
+ chunks: Buffer[];
16
+ outputByteLimit: number;
17
+ exitStatus: acp.TerminalExitStatus | null;
18
+ exitPromise: Promise<acp.WaitForTerminalExitResponse>;
19
+ resolveExit: (status: acp.WaitForTerminalExitResponse) => void;
20
+ }
21
+
22
+ function retainedOutput(state: AcpTerminalState) {
23
+ const complete = Buffer.concat(state.chunks);
24
+ if (complete.byteLength <= state.outputByteLimit) {
25
+ return { output: complete.toString('utf8'), truncated: false };
26
+ }
27
+
28
+ let start = complete.byteLength - state.outputByteLimit;
29
+ while (start < complete.byteLength && (complete[start]! & 0xc0) === 0x80) {
30
+ start += 1;
31
+ }
32
+ return {
33
+ output: complete.subarray(start).toString('utf8'),
34
+ truncated: true,
35
+ };
36
+ }
37
+
38
+ export class AcpTerminalService {
39
+ private readonly terminals = new Map<string, AcpTerminalState>();
40
+
41
+ constructor(private readonly sessionCwd: (sessionId: string) => string | null) {}
42
+
43
+ create(params: acp.CreateTerminalRequest): acp.CreateTerminalResponse {
44
+ const terminalId = randomUUID();
45
+ const cwd = params.cwd ?? this.sessionCwd(params.sessionId) ?? process.cwd();
46
+ const parsed = params.args && params.args.length > 0
47
+ ? { command: params.command, args: params.args }
48
+ : parseCommandLine(params.command);
49
+ const child = spawnProcess({
50
+ command: parsed.command,
51
+ args: parsed.args,
52
+ cwd,
53
+ env: {
54
+ ...process.env,
55
+ ...Object.fromEntries((params.env ?? []).map((entry) => [entry.name, entry.value])),
56
+ },
57
+ stdio: ['ignore', 'pipe', 'pipe'],
58
+ });
59
+ let resolveExit!: (status: acp.WaitForTerminalExitResponse) => void;
60
+ const exitPromise = new Promise<acp.WaitForTerminalExitResponse>((resolve) => {
61
+ resolveExit = resolve;
62
+ });
63
+ const state: AcpTerminalState = {
64
+ child,
65
+ chunks: [],
66
+ outputByteLimit: Math.max(1, params.outputByteLimit ?? DEFAULT_OUTPUT_BYTE_LIMIT),
67
+ exitStatus: null,
68
+ exitPromise,
69
+ resolveExit,
70
+ };
71
+ this.terminals.set(terminalId, state);
72
+
73
+ const append = (chunk: Buffer | string) => {
74
+ state.chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
75
+ };
76
+ child.stdout?.on('data', append);
77
+ child.stderr?.on('data', append);
78
+ child.on('error', (error) => append(`${error.message}\n`));
79
+ child.on('close', (code, signal) => {
80
+ const exitStatus: acp.TerminalExitStatus = {
81
+ exitCode: code,
82
+ signal,
83
+ };
84
+ state.exitStatus = exitStatus;
85
+ state.resolveExit(exitStatus);
86
+ });
87
+
88
+ return { terminalId };
89
+ }
90
+
91
+ output(params: acp.TerminalOutputRequest): acp.TerminalOutputResponse {
92
+ const state = this.require(params.terminalId);
93
+ const output = retainedOutput(state);
94
+ return {
95
+ ...output,
96
+ ...(state.exitStatus ? { exitStatus: state.exitStatus } : {}),
97
+ };
98
+ }
99
+
100
+ waitForExit(params: acp.WaitForTerminalExitRequest) {
101
+ return this.require(params.terminalId).exitPromise;
102
+ }
103
+
104
+ kill(params: acp.KillTerminalRequest): acp.KillTerminalResponse {
105
+ const state = this.require(params.terminalId);
106
+ if (!state.exitStatus) {
107
+ state.child.kill('SIGTERM');
108
+ }
109
+ return {};
110
+ }
111
+
112
+ release(params: acp.ReleaseTerminalRequest): acp.ReleaseTerminalResponse {
113
+ const state = this.require(params.terminalId);
114
+ if (!state.exitStatus) {
115
+ state.child.kill('SIGTERM');
116
+ }
117
+ this.terminals.delete(params.terminalId);
118
+ return {};
119
+ }
120
+
121
+ stop() {
122
+ for (const state of this.terminals.values()) {
123
+ if (!state.exitStatus) {
124
+ state.child.kill('SIGTERM');
125
+ }
126
+ }
127
+ this.terminals.clear();
128
+ }
129
+
130
+ private require(terminalId: string) {
131
+ const state = this.terminals.get(terminalId);
132
+ if (!state) {
133
+ throw new Error(`ACP terminal not found: ${terminalId}`);
134
+ }
135
+ return state;
136
+ }
137
+ }
@@ -73,6 +73,7 @@ export function isRemoteThreadBootstrapError(error: unknown) {
73
73
  }
74
74
 
75
75
  return (
76
+ (isRecord(error.details) && error.details.historyUnavailable === true) ||
76
77
  error.message.includes('includeTurns is unavailable before first user message') ||
77
78
  error.message.includes('is not materialized yet') ||
78
79
  error.message.includes('no rollout found for thread id') ||
@@ -1,5 +1,6 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import type {
3
+ AcpAgentOptionMetadataDto,
3
4
  AgentBackendIdDto,
4
5
  AgentBackendInstallationDto,
5
6
  ThreadHistoryItemDto,
@@ -159,6 +160,8 @@ export interface AgentModel {
159
160
  description: string;
160
161
  }>;
161
162
  defaultReasoningEffort: string | null;
163
+ selectionKind?: 'model' | 'agent';
164
+ acpAgent?: AcpAgentOptionMetadataDto | null;
162
165
  }
163
166
 
164
167
  export type AgentSessionStatus =
@@ -228,6 +231,7 @@ export interface ReadAgentSessionOptions {
228
231
 
229
232
  export interface StartAgentSessionInput {
230
233
  cwd: string;
234
+ agentId?: string | null;
231
235
  model: string;
232
236
  reasoningEffort?: string | null;
233
237
  approvalMode: 'yolo' | 'guarded';
@@ -237,6 +241,7 @@ export interface StartAgentSessionInput {
237
241
 
238
242
  export interface StartAgentSessionResult {
239
243
  provider: AgentProviderId;
244
+ agentId?: string | null;
240
245
  providerSessionId: string;
241
246
  model: string | null;
242
247
  reasoningEffort?: string | null;
@@ -549,6 +554,8 @@ export interface AgentRuntime extends EventEmitter {
549
554
  stop(): Promise<void>;
550
555
 
551
556
  listModels(): Promise<AgentModel[]>;
557
+ listAgentOptions?(): Promise<AgentModel[]>;
558
+ listModelsForAgent?(agentId: string, cwd: string): Promise<AgentModel[]>;
552
559
  listSessions(): Promise<AgentSessionSummary[]>;
553
560
  listLoadedSessions(): Promise<string[]>;
554
561
  readSession(
@@ -582,6 +589,7 @@ export interface AgentRuntime extends EventEmitter {
582
589
  getGoal?(providerSessionId: string): Promise<AgentGoal | null>;
583
590
  setGoal?(input: SetAgentGoalInput): Promise<AgentGoal>;
584
591
  clearGoal?(providerSessionId: string): Promise<boolean>;
592
+ installModel?(modelId: string): Promise<void>;
585
593
  getSubscriptionUsage?(): Promise<{
586
594
  provider: 'codex' | 'claude';
587
595
  authKind: 'subscription' | 'apiKey' | 'unknown';
@@ -1266,7 +1266,7 @@ describe('ClaudeRuntimeAdapter', () => {
1266
1266
  message: {
1267
1267
  role: 'user',
1268
1268
  content:
1269
- '<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent \"Audit docs\" finished</summary>\\n<result>Found one stale runbook.</result>\\n</task-notification>',
1269
+ '<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit docs" finished</summary>\\n<result>Found one stale runbook.</result>\\n</task-notification>',
1270
1270
  },
1271
1271
  parent_tool_use_id: null,
1272
1272
  uuid: '00000000-0000-4000-8000-000000000042' as any,
@@ -2380,7 +2380,7 @@ describe('ClaudeRuntimeAdapter', () => {
2380
2380
  message: {
2381
2381
  role: 'user',
2382
2382
  content:
2383
- '<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent \"Audit infra docs\" finished</summary>\\n<result>Infra docs need one migration checklist.</result>\\n</task-notification>',
2383
+ '<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit infra docs" finished</summary>\\n<result>Infra docs need one migration checklist.</result>\\n</task-notification>',
2384
2384
  },
2385
2385
  parent_tool_use_id: null,
2386
2386
  },
@@ -1,11 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { EventEmitter } from 'node:events';
3
- import { execFile } from 'node:child_process';
4
3
  import fs from 'node:fs/promises';
5
4
  import { createRequire } from 'node:module';
6
5
  import path from 'node:path';
7
6
  import { pathToFileURL } from 'node:url';
8
- import { promisify } from 'node:util';
9
7
 
10
8
  import type {
11
9
  AgentActionQuestion,
@@ -58,8 +56,9 @@ import {
58
56
  userMessageHistoryItem,
59
57
  userMessageToHistoryItem,
60
58
  } from './historyItems';
59
+ import { runProcess } from '../../process-runtime/src/index';
60
+ import { isPathInside } from '../../workspace/src/index';
61
61
 
62
- const execFileAsync = promisify(execFile);
63
62
 
64
63
  type ClaudePromptInput = string | AsyncIterable<SDKUserMessage>;
65
64
  type ClaudeMessageContent =
@@ -379,8 +378,7 @@ function resolvePromptAssetPath(assetPath: string, cwd: string | null | undefine
379
378
  const resolvedPath = path.isAbsolute(assetPath)
380
379
  ? path.normalize(assetPath)
381
380
  : path.resolve(cwd, assetPath);
382
- const relativePath = path.relative(cwd, resolvedPath);
383
- if (relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))) {
381
+ if (isPathInside(cwd, resolvedPath)) {
384
382
  return resolvedPath;
385
383
  }
386
384
  return null;
@@ -2566,12 +2564,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
2566
2564
 
2567
2565
  const relativePath = `./.temp/threads/${input.localThreadId}/claude-history-${safeAssetFilePart(input.messageId)}-${input.blockIndex}.${extension}`;
2568
2566
  const targetPath = path.resolve(input.workspacePath, relativePath);
2569
- const relativeToWorkspace = path.relative(input.workspacePath, targetPath);
2570
- if (
2571
- relativeToWorkspace === '' ||
2572
- relativeToWorkspace.startsWith('..') ||
2573
- path.isAbsolute(relativeToWorkspace)
2574
- ) {
2567
+ if (!isPathInside(input.workspacePath, targetPath)) {
2575
2568
  return null;
2576
2569
  }
2577
2570
 
@@ -2833,12 +2826,10 @@ async function importOptionalPackage(specifier: string) {
2833
2826
  }
2834
2827
 
2835
2828
  async function npmGlobalRoot() {
2836
- try {
2837
- const { stdout } = await execFileAsync('npm', ['root', '-g'], {
2838
- timeout: 3_000,
2839
- });
2840
- return stdout.trim() || null;
2841
- } catch {
2842
- return null;
2843
- }
2829
+ const result = await runProcess({
2830
+ command: 'npm',
2831
+ args: ['root', '-g'],
2832
+ timeoutMs: 3_000,
2833
+ });
2834
+ return result.code === 0 ? result.stdout.trim() || null : null;
2844
2835
  }
@@ -1,5 +1,8 @@
1
1
  import { EventEmitter } from 'node:events';
2
2
  import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
3
6
  import { PassThrough, Writable } from 'node:stream';
4
7
 
5
8
  import { describe, expect, it } from 'vitest';
@@ -80,6 +83,8 @@ class ScriptedChild extends EventEmitter {
80
83
  }
81
84
 
82
85
  describe('CodexAppServerManager', () => {
86
+ const windowsIt = it.runIf(process.platform === 'win32');
87
+
83
88
  it('starts against a newline-delimited JSON-RPC process', async () => {
84
89
  const script = [
85
90
  "const readline=require('node:readline');",
@@ -117,6 +122,48 @@ describe('CodexAppServerManager', () => {
117
122
  await manager.stop();
118
123
  });
119
124
 
125
+ windowsIt('starts an npm-style Codex command shim from a path with spaces', async () => {
126
+ const directory = await fs.mkdtemp(
127
+ path.join(os.tmpdir(), 'remote-codex app server shim '),
128
+ );
129
+ const scriptPath = path.join(directory, 'fake-codex.cjs');
130
+ const shimPath = path.join(directory, 'fake-codex.cmd');
131
+ const script = [
132
+ "const readline = require('node:readline');",
133
+ 'const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });',
134
+ "rl.on('line', (line) => {",
135
+ ' const message = JSON.parse(line);',
136
+ " if (message.method === 'initialize') {",
137
+ " process.stdout.write(JSON.stringify({ id: message.id, result: { userAgent: 'fake-cmd', codexHome: 'C:/tmp', platformFamily: 'windows', platformOs: 'windows' } }) + '\\n');",
138
+ ' }',
139
+ '});',
140
+ ].join('\n');
141
+
142
+ await fs.writeFile(scriptPath, script);
143
+ await fs.writeFile(
144
+ shimPath,
145
+ `@echo off\r\n"${process.execPath}" "%~dp0fake-codex.cjs" %*\r\n`,
146
+ );
147
+
148
+ const manager = new CodexAppServerManager({
149
+ command: shimPath,
150
+ startupTimeoutMs: 3_000,
151
+ clientInfo: {
152
+ name: 'test',
153
+ title: 'test',
154
+ version: '0.1.0',
155
+ },
156
+ });
157
+
158
+ try {
159
+ await manager.start();
160
+ expect(manager.getStatus().state).toBe('ready');
161
+ } finally {
162
+ await manager.stop();
163
+ await fs.rm(directory, { recursive: true, force: true });
164
+ }
165
+ });
166
+
120
167
  it('surfaces spawn failures as a failed status instead of crashing', async () => {
121
168
  class FailingChild extends EventEmitter {
122
169
  stdout = new PassThrough();
@@ -1,6 +1,8 @@
1
- import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process';
1
+ import { ChildProcessWithoutNullStreams } from 'node:child_process';
2
2
  import { EventEmitter } from 'node:events';
3
3
 
4
+ import { spawnProcess as spawnCrossPlatformProcess } from '../../process-runtime/src/index';
5
+
4
6
  import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
5
7
  import {
6
8
  AppServerStatusSnapshot,
@@ -250,7 +252,9 @@ export class CodexAppServerManager extends EventEmitter {
250
252
  this.spawnProcess =
251
253
  options.spawnProcess ??
252
254
  ((command: string, args: string[]) =>
253
- spawn(command, args, {
255
+ spawnCrossPlatformProcess({
256
+ command,
257
+ args,
254
258
  stdio: 'pipe'
255
259
  }) as unknown as ChildProcessWithoutNullStreams);
256
260
  }
@@ -1,4 +1,5 @@
1
1
  import { EventEmitter } from 'node:events';
2
+ import path from 'node:path';
2
3
 
3
4
  import { describe, expect, it } from 'vitest';
4
5
 
@@ -120,7 +121,10 @@ describe('CodexRuntimeAdapter', () => {
120
121
  { type: 'text', text: 'Inspect this ', text_elements: [] },
121
122
  {
122
123
  type: 'localImage',
123
- path: '/tmp/workspace/.temp/threads/thread-1/photo.png',
124
+ path: path.join(
125
+ path.resolve('/tmp/workspace'),
126
+ '.temp/threads/thread-1/photo.png',
127
+ ),
124
128
  },
125
129
  { type: 'text', text: ' then summarize.', text_elements: [] },
126
130
  ],
@@ -141,7 +145,10 @@ describe('CodexRuntimeAdapter', () => {
141
145
  { type: 'text', text: 'Inspect ', text_elements: [] },
142
146
  {
143
147
  type: 'localImage',
144
- path: '/tmp/workspace/.temp/threads/thread-1/photo.heic',
148
+ path: path.join(
149
+ path.resolve('/tmp/workspace'),
150
+ '.temp/threads/thread-1/photo.heic',
151
+ ),
145
152
  },
146
153
  { type: 'text', text: '.', text_elements: [] },
147
154
  ]);
@@ -0,0 +1,9 @@
1
+ ALTER TABLE threads ADD COLUMN agent_id TEXT;
2
+
3
+ UPDATE threads
4
+ SET agent_id = model,
5
+ model = NULL
6
+ WHERE provider = 'acp'
7
+ AND agent_id IS NULL
8
+ AND model IS NOT NULL
9
+ AND provider_session_id LIKE model || '::%';
@@ -29,6 +29,7 @@ export interface CreateThreadRecordInput {
29
29
  workspaceId: string;
30
30
  title: string;
31
31
  provider?: string;
32
+ agentId?: string | null;
32
33
  providerSessionId: string | null;
33
34
  providerTurnId?: string | null;
34
35
  model?: string | null;
@@ -47,6 +48,7 @@ export interface CreateThreadRecordInput {
47
48
 
48
49
  export interface UpdateThreadRecordInput {
49
50
  provider?: string;
51
+ agentId?: string | null;
50
52
  providerSessionId?: string | null;
51
53
  providerTurnId?: string | null;
52
54
  title?: string;
@@ -278,6 +280,7 @@ export function createThreadRecord(db: DatabaseClient, input: CreateThreadRecord
278
280
  id: randomUUID(),
279
281
  workspaceId: input.workspaceId,
280
282
  provider: input.provider ?? 'codex',
283
+ agentId: input.agentId ?? null,
281
284
  providerSessionId: input.providerSessionId,
282
285
  providerTurnId: input.providerTurnId ?? null,
283
286
  source: input.source ?? 'supervisor',
@@ -23,6 +23,7 @@ export const threads = sqliteTable('threads', {
23
23
  id: text('id').primaryKey(),
24
24
  workspaceId: text('workspace_id').notNull(),
25
25
  provider: text('provider').notNull().default('codex'),
26
+ agentId: text('agent_id'),
26
27
  providerSessionId: text('provider_session_id'),
27
28
  providerTurnId: text('provider_turn_id'),
28
29
  source: text('source').notNull().default('supervisor'),
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
 
3
+ import { isPathInside } from '../../workspace/src/index';
4
+
3
5
  import type {
4
6
  AgentHistoryItem,
5
7
  AgentTurn,
@@ -163,15 +165,14 @@ function displayPath(pathValue: string | null, options: OpenCodeHistoryItemMappi
163
165
 
164
166
  const root = path.resolve(options.workspacePath);
165
167
  const absolutePath = path.resolve(pathValue);
168
+ if (!isPathInside(root, absolutePath)) {
169
+ return pathValue;
170
+ }
166
171
  const relativePath = path.relative(root, absolutePath);
167
- if (
168
- !relativePath ||
169
- relativePath.startsWith('..') ||
170
- path.isAbsolute(relativePath)
171
- ) {
172
+ if (!relativePath) {
172
173
  return pathValue;
173
174
  }
174
- return relativePath;
175
+ return relativePath.split(path.sep).join('/');
175
176
  }
176
177
 
177
178
  function toolIsLowInformationPatch(
@@ -1,11 +1,9 @@
1
1
  import { EventEmitter } from 'node:events';
2
- import { execFile } from 'node:child_process';
3
2
  import fs from 'node:fs/promises';
4
3
  import net from 'node:net';
5
4
  import { createRequire } from 'node:module';
6
5
  import path from 'node:path';
7
6
  import { pathToFileURL } from 'node:url';
8
- import { promisify } from 'node:util';
9
7
 
10
8
  import type {
11
9
  AgentHistoryItem,
@@ -37,8 +35,8 @@ import {
37
35
  openCodeMessageToHistoryItems,
38
36
  openCodeMessagesToPlanUpdate,
39
37
  } from './historyItems';
38
+ import { runProcess } from '../../process-runtime/src/index';
40
39
 
41
- const execFileAsync = promisify(execFile);
42
40
  const openCodeWaitTimeoutMs = 1_500;
43
41
  const openCodePromptPollIntervalMs = 500;
44
42
  const openCodePromptTimeoutMs = 120_000;
@@ -1476,12 +1474,10 @@ function resolveOptionalPackage(
1476
1474
  }
1477
1475
 
1478
1476
  async function npmGlobalRoot() {
1479
- try {
1480
- const { stdout } = await execFileAsync('npm', ['root', '-g'], {
1481
- timeout: 3_000,
1482
- });
1483
- return stdout.trim() || null;
1484
- } catch {
1485
- return null;
1486
- }
1477
+ const result = await runProcess({
1478
+ command: 'npm',
1479
+ args: ['root', '-g'],
1480
+ timeoutMs: 3_000,
1481
+ });
1482
+ return result.code === 0 ? result.stdout.trim() || null : null;
1487
1483
  }
@@ -0,0 +1,132 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+
6
+ import { parseCommandLine, resolveExecutable, runProcess } from './index';
7
+
8
+ const temporaryDirectories: string[] = [];
9
+
10
+ afterEach(async () => {
11
+ await Promise.all(
12
+ temporaryDirectories.splice(0).map((directory) =>
13
+ fs.rm(directory, { recursive: true, force: true }),
14
+ ),
15
+ );
16
+ });
17
+
18
+ describe('runProcess', () => {
19
+ const windowsIt = it.runIf(process.platform === 'win32');
20
+
21
+ it('preserves argv values without shell interpretation', async () => {
22
+ const argument = 'space & (parentheses) 中文';
23
+ const result = await runProcess({
24
+ command: process.execPath,
25
+ args: ['-e', 'process.stdout.write(process.argv[1])', argument],
26
+ });
27
+
28
+ expect(result.code).toBe(0);
29
+ expect(result.stdout).toBe(argument);
30
+ expect(result.spawnError).toBeNull();
31
+ });
32
+
33
+ it('returns stderr and a non-zero exit code', async () => {
34
+ const result = await runProcess({
35
+ command: process.execPath,
36
+ args: ['-e', "process.stderr.write('failure'); process.exit(7)"],
37
+ });
38
+
39
+ expect(result.code).toBe(7);
40
+ expect(result.stderr).toBe('failure');
41
+ });
42
+
43
+ it('reports a missing executable', async () => {
44
+ const result = await runProcess({
45
+ command: `remote-codex-missing-${Date.now()}`,
46
+ });
47
+
48
+ expect(result.code).toBeNull();
49
+ expect(result.spawnError).not.toBeNull();
50
+ });
51
+
52
+ it('terminates a timed out process', async () => {
53
+ const result = await runProcess({
54
+ command: process.execPath,
55
+ args: ['-e', 'setInterval(() => {}, 1000)'],
56
+ timeoutMs: 50,
57
+ });
58
+
59
+ expect(result.timedOut).toBe(true);
60
+ });
61
+
62
+ it('bounds captured output', async () => {
63
+ const result = await runProcess({
64
+ command: process.execPath,
65
+ args: ['-e', "process.stdout.write('x'.repeat(100))"],
66
+ maxOutputBytes: 10,
67
+ });
68
+
69
+ expect(result.stdout).toBe('x'.repeat(10));
70
+ expect(result.outputTruncated).toBe(true);
71
+ });
72
+
73
+ windowsIt('executes an npm-style command shim from a path with spaces', async () => {
74
+ const directory = await fs.mkdtemp(
75
+ path.join(os.tmpdir(), 'remote-codex command shim '),
76
+ );
77
+ temporaryDirectories.push(directory);
78
+ const shimPath = path.join(directory, 'fake-codex.cmd');
79
+ await fs.writeFile(shimPath, '@echo off\r\necho %~1\r\n');
80
+
81
+ const result = await runProcess({
82
+ command: shimPath,
83
+ args: ['shim executed'],
84
+ timeoutMs: 3_000,
85
+ });
86
+
87
+ expect(result.code).toBe(0);
88
+ expect(result.stdout.trim()).toBe('shim executed');
89
+ expect(result.spawnError).toBeNull();
90
+ });
91
+ });
92
+
93
+ describe('parseCommandLine', () => {
94
+ it('parses built-in npm and quoted node commands without a shell', () => {
95
+ expect(parseCommandLine('npm install -g @openai/codex@latest')).toEqual({
96
+ command: 'npm',
97
+ args: ['install', '-g', '@openai/codex@latest'],
98
+ });
99
+ expect(parseCommandLine('node -e "process.exit(0)"')).toEqual({
100
+ command: 'node',
101
+ args: ['-e', 'process.exit(0)'],
102
+ });
103
+ });
104
+
105
+ it('does not interpret shell operators', () => {
106
+ expect(parseCommandLine('tool first && second')).toEqual({
107
+ command: 'tool',
108
+ args: ['first', '&&', 'second'],
109
+ });
110
+ });
111
+ });
112
+
113
+ describe('resolveExecutable', () => {
114
+ it('resolves an executable from PATH', async () => {
115
+ const resolved = await resolveExecutable(process.execPath);
116
+ expect(resolved).toBe(path.resolve(process.execPath));
117
+ });
118
+
119
+ it('uses Windows PATHEXT semantics for command shims', async () => {
120
+ const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex process '));
121
+ temporaryDirectories.push(directory);
122
+ const shimPath = path.join(directory, 'codex.CMD');
123
+ await fs.writeFile(shimPath, '@echo off\r\n');
124
+
125
+ const resolved = await resolveExecutable('codex', {
126
+ env: { PATH: directory, PATHEXT: '.EXE;.CMD' },
127
+ platform: 'win32',
128
+ });
129
+
130
+ expect(resolved?.toLowerCase()).toBe(shimPath.toLowerCase());
131
+ });
132
+ });