@akira-tl/forgerelay 0.2.4 → 0.2.6

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.
@@ -8,7 +8,7 @@ const MAX_COMMAND_YIELD_MS = 300_000;
8
8
  const MAX_POLL_YIELD_MS = 300_000;
9
9
  const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;
10
10
  const DEFAULT_BUFFER_CHARACTERS = 1_000_000;
11
- const COMPLETED_SESSION_TTL_MS = 24 * 60 * 60 * 1_000;
11
+ const COMPLETED_PROCESS_TTL_MS = 24 * 60 * 60 * 1_000;
12
12
  const DEFAULT_COLUMNS = 80;
13
13
  const DEFAULT_ROWS = 24;
14
14
  function boundedInteger(value, fallback, maximum) {
@@ -27,6 +27,19 @@ function terminalSize(value, fallback) {
27
27
  }
28
28
  return value;
29
29
  }
30
+ export function resolveProcessId(processId, legacySessionId) {
31
+ if (processId !== undefined && legacySessionId !== undefined && processId !== legacySessionId) {
32
+ throw new Error("processId and deprecated sessionId must identify the same process when both are provided.");
33
+ }
34
+ const resolved = processId ?? legacySessionId;
35
+ if (resolved === undefined) {
36
+ throw new Error("A processId is required. Deprecated sessionId remains accepted for compatibility.");
37
+ }
38
+ if (!Number.isInteger(resolved) || resolved < 1) {
39
+ throw new Error("processId must be a positive integer.");
40
+ }
41
+ return resolved;
42
+ }
30
43
  function processEnvironment(input) {
31
44
  return {
32
45
  ...Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined && (input?.codexCi || entry[0] !== "CODEX_CI"))),
@@ -129,110 +142,120 @@ function truncateOutput(output, maxCharacters) {
129
142
  truncated: true,
130
143
  };
131
144
  }
132
- export class ProcessSessionManager {
133
- sessions = new Map();
145
+ export class ProcessManager {
146
+ processes = new Map();
134
147
  completedByWorkspace = new Map();
135
148
  maxBufferCharacters;
136
- completedSessionTtlMs;
149
+ completedProcessTtlMs;
137
150
  maxStartYieldMs;
138
- nextSessionId = 1;
151
+ monotonicNow;
152
+ nextProcessId = 1;
139
153
  constructor(options = {}) {
140
154
  this.maxBufferCharacters = options.maxBufferCharacters ?? DEFAULT_BUFFER_CHARACTERS;
141
- this.completedSessionTtlMs = options.completedSessionTtlMs ?? COMPLETED_SESSION_TTL_MS;
155
+ this.completedProcessTtlMs = options.completedProcessTtlMs
156
+ ?? options.completedSessionTtlMs
157
+ ?? COMPLETED_PROCESS_TTL_MS;
142
158
  this.maxStartYieldMs = options.maxStartYieldMs ?? MAX_START_YIELD_MS;
159
+ this.monotonicNow = options.monotonicNow ?? (() => performance.now());
143
160
  }
144
161
  async start(input) {
145
- const session = this.createSession(input);
146
- this.sessions.set(session.id, session);
162
+ const processEntry = this.createProcess(input);
163
+ this.processes.set(processEntry.id, processEntry);
147
164
  try {
148
165
  if (input.tty && process.platform !== "win32")
149
- await this.startPty(session, input);
166
+ await this.startPty(processEntry, input);
150
167
  else
151
- this.startPipe(session, input);
168
+ this.startPipe(processEntry, input);
152
169
  }
153
170
  catch (error) {
154
- this.sessions.delete(session.id);
171
+ this.processes.delete(processEntry.id);
155
172
  throw error;
156
173
  }
157
174
  const yieldTimeMs = boundedInteger(input.yieldTimeMs, DEFAULT_EXEC_YIELD_MS, this.maxStartYieldMs);
158
- await this.waitForExit(session, yieldTimeMs);
159
- if (session.running)
160
- session.background = true;
161
- const snapshot = this.consume(session, input.maxOutputTokens);
175
+ await this.waitForExit(processEntry, yieldTimeMs);
176
+ if (processEntry.running)
177
+ processEntry.background = true;
178
+ const snapshot = this.consume(processEntry, input.maxOutputTokens);
162
179
  if (!snapshot.running)
163
- this.removeSession(session.id);
180
+ this.removeProcess(processEntry.id);
164
181
  return snapshot;
165
182
  }
166
183
  async write(input) {
167
- const session = this.getOwnedSession(input.workspaceId, input.sessionId);
184
+ const processId = resolveProcessId(input.processId, input.sessionId);
185
+ const processEntry = this.getOwnedProcess(input.workspaceId, processId);
168
186
  const chars = input.chars ?? "";
169
187
  const interactionRequested = chars.length > 0 || input.columns !== undefined || input.rows !== undefined;
170
188
  if (input.columns !== undefined || input.rows !== undefined) {
171
- session.columns = terminalSize(input.columns, session.columns);
172
- session.rows = terminalSize(input.rows, session.rows);
173
- if (!session.process?.resize) {
174
- throw new Error(`Process session ${session.id} is not a PTY and cannot be resized.`);
189
+ processEntry.columns = terminalSize(input.columns, processEntry.columns);
190
+ processEntry.rows = terminalSize(input.rows, processEntry.rows);
191
+ if (!processEntry.process?.resize) {
192
+ throw new Error(`Process ${processEntry.id} is not a PTY and cannot be resized.`);
175
193
  }
176
- session.process.resize(session.columns, session.rows);
194
+ processEntry.process.resize(processEntry.columns, processEntry.rows);
177
195
  }
178
- const interruptRequested = chars.includes("\u0003") && session.running;
196
+ const interruptRequested = chars.includes("\u0003") && processEntry.running;
179
197
  if (interruptRequested) {
180
- session.process?.kill("SIGINT");
198
+ processEntry.process?.kill("SIGINT");
181
199
  }
182
200
  const writableChars = chars.replaceAll("\u0003", "");
183
- if (writableChars && session.running)
184
- session.process?.write(writableChars);
185
- if ((interactionRequested || !session.buffer.hasOutput()) && session.running) {
201
+ if (writableChars && processEntry.running)
202
+ processEntry.process?.write(writableChars);
203
+ if ((interactionRequested || !processEntry.buffer.hasOutput()) && processEntry.running) {
186
204
  const fallback = interactionRequested ? DEFAULT_INTERACTIVE_YIELD_MS : DEFAULT_POLL_YIELD_MS;
187
205
  const maximum = interactionRequested ? MAX_COMMAND_YIELD_MS : MAX_POLL_YIELD_MS;
188
206
  const yieldTimeMs = boundedInteger(input.yieldTimeMs, fallback, maximum);
189
- await this.waitForExit(session, yieldTimeMs);
207
+ await this.waitForExit(processEntry, yieldTimeMs);
190
208
  }
191
- const snapshot = this.consume(session, input.maxOutputTokens);
192
- if (!session.running)
193
- this.removeSession(session.id);
209
+ const snapshot = this.consume(processEntry, input.maxOutputTokens);
210
+ if (!processEntry.running)
211
+ this.removeProcess(processEntry.id);
194
212
  return snapshot;
195
213
  }
196
214
  activeWorkspaceIds() {
197
- return new Set([...this.sessions.values()].map((session) => session.workspaceId));
215
+ return new Set([...this.processes.values()].map((processEntry) => processEntry.workspaceId));
198
216
  }
199
- takeCompleted(workspaceId, maxOutputTokens, excludeSessionId) {
200
- const sessionIds = this.completedByWorkspace.get(workspaceId) ?? [];
201
- if (sessionIds.length === 0)
217
+ takeCompleted(workspaceId, maxOutputTokens, excludeProcessId) {
218
+ const processIds = this.completedByWorkspace.get(workspaceId) ?? [];
219
+ if (processIds.length === 0)
202
220
  return [];
203
221
  const completed = [];
204
- for (const sessionId of sessionIds) {
205
- if (sessionId === excludeSessionId)
222
+ for (const processId of processIds) {
223
+ if (processId === excludeProcessId)
206
224
  continue;
207
- const session = this.sessions.get(sessionId);
208
- if (!session || session.running)
225
+ const processEntry = this.processes.get(processId);
226
+ if (!processEntry || processEntry.running)
209
227
  continue;
210
- const snapshot = this.consume(session, maxOutputTokens);
211
- completed.push({ ...snapshot, sessionId: session.id, command: session.command });
212
- this.removeSession(session.id);
228
+ const snapshot = this.consume(processEntry, maxOutputTokens);
229
+ completed.push({
230
+ ...snapshot,
231
+ processId: processEntry.id,
232
+ sessionId: processEntry.id,
233
+ command: processEntry.command,
234
+ });
235
+ this.removeProcess(processEntry.id);
213
236
  }
214
237
  return completed;
215
238
  }
216
- terminate(workspaceId, sessionId) {
217
- const session = this.getOwnedSession(workspaceId, sessionId);
218
- if (session.running)
219
- session.process?.kill("SIGTERM");
239
+ terminate(workspaceId, processId) {
240
+ const processEntry = this.getOwnedProcess(workspaceId, processId);
241
+ if (processEntry.running)
242
+ processEntry.process?.kill("SIGTERM");
220
243
  }
221
244
  shutdown() {
222
- for (const session of this.sessions.values()) {
223
- if (session.cleanupTimer)
224
- clearTimeout(session.cleanupTimer);
225
- if (session.running)
226
- session.process?.kill("SIGTERM");
245
+ for (const processEntry of this.processes.values()) {
246
+ if (processEntry.cleanupTimer)
247
+ clearTimeout(processEntry.cleanupTimer);
248
+ if (processEntry.running)
249
+ processEntry.process?.kill("SIGTERM");
227
250
  }
228
- this.sessions.clear();
251
+ this.processes.clear();
229
252
  this.completedByWorkspace.clear();
230
253
  }
231
- async waitForExit(session, yieldTimeMs) {
254
+ async waitForExit(processEntry, yieldTimeMs) {
232
255
  let timer;
233
256
  try {
234
257
  await Promise.race([
235
- session.exitPromise,
258
+ processEntry.exitPromise,
236
259
  new Promise((resolve) => {
237
260
  timer = setTimeout(resolve, yieldTimeMs);
238
261
  }),
@@ -243,16 +266,16 @@ export class ProcessSessionManager {
243
266
  clearTimeout(timer);
244
267
  }
245
268
  }
246
- createSession(input) {
269
+ createProcess(input) {
247
270
  let resolveExit = () => undefined;
248
271
  const exitPromise = new Promise((resolve) => {
249
272
  resolveExit = resolve;
250
273
  });
251
274
  return {
252
- id: this.nextSessionId++,
275
+ id: this.nextProcessId++,
253
276
  workspaceId: input.workspaceId,
254
277
  command: input.command,
255
- startedAt: Date.now(),
278
+ startedAtMonotonic: this.monotonicNow(),
256
279
  columns: terminalSize(input.columns, DEFAULT_COLUMNS),
257
280
  rows: terminalSize(input.rows, DEFAULT_ROWS),
258
281
  buffer: new HeadTailBuffer(this.maxBufferCharacters),
@@ -262,7 +285,7 @@ export class ProcessSessionManager {
262
285
  resolveExit,
263
286
  };
264
287
  }
265
- startPipe(session, input) {
288
+ startPipe(processEntry, input) {
266
289
  const shell = resolveShellCommand(input.command);
267
290
  const detached = process.platform !== "win32";
268
291
  const child = spawn(input.command, {
@@ -277,17 +300,17 @@ export class ProcessSessionManager {
277
300
  detached,
278
301
  shell: shell.executable,
279
302
  });
280
- session.process = {
303
+ processEntry.process = {
281
304
  write: (data) => child.stdin.write(data),
282
305
  kill: (signal = "SIGTERM") => terminateProcessTree(child, signal, detached),
283
306
  resize: input.tty ? () => undefined : undefined,
284
307
  };
285
- child.stdout.on("data", (data) => this.append(session, data.toString("utf8")));
286
- child.stderr.on("data", (data) => this.append(session, data.toString("utf8")));
287
- child.on("error", (error) => this.append(session, `${error.message}\n`));
288
- child.on("close", (code, signal) => this.finish(session, code ?? undefined, signal ?? undefined));
308
+ child.stdout.on("data", (data) => this.append(processEntry, data.toString("utf8")));
309
+ child.stderr.on("data", (data) => this.append(processEntry, data.toString("utf8")));
310
+ child.on("error", (error) => this.append(processEntry, `${error.message}\n`));
311
+ child.on("close", (code, signal) => this.finish(processEntry, code ?? undefined, signal ?? undefined));
289
312
  }
290
- async startPty(session, input) {
313
+ async startPty(processEntry, input) {
291
314
  let nodePty;
292
315
  try {
293
316
  nodePty = await import("node-pty");
@@ -306,80 +329,84 @@ export class ProcessSessionManager {
306
329
  codexCi: input.codexCi,
307
330
  }),
308
331
  name: "xterm-256color",
309
- cols: session.columns,
310
- rows: session.rows,
332
+ cols: processEntry.columns,
333
+ rows: processEntry.rows,
311
334
  });
312
335
  }
313
336
  catch (error) {
314
337
  throw error;
315
338
  }
316
- session.process = {
339
+ processEntry.process = {
317
340
  write: (data) => pty.write(data),
318
341
  kill: (signal) => pty.kill(signal),
319
342
  resize: (columns, rows) => pty.resize(columns, rows),
320
343
  };
321
- pty.onData((data) => this.append(session, data));
344
+ pty.onData((data) => this.append(processEntry, data));
322
345
  pty.onExit(({ exitCode, signal }) => {
323
- this.finish(session, exitCode, signal === 0 ? undefined : String(signal));
346
+ this.finish(processEntry, exitCode, signal === 0 ? undefined : String(signal));
324
347
  });
325
348
  }
326
- finish(session, exitCode, signal) {
327
- if (!session.running)
349
+ finish(processEntry, exitCode, signal) {
350
+ if (!processEntry.running)
328
351
  return;
329
- session.running = false;
330
- session.exitCode = exitCode;
331
- session.signal = signal;
332
- session.resolveExit();
333
- if (session.background) {
334
- const completed = this.completedByWorkspace.get(session.workspaceId) ?? [];
335
- if (!completed.includes(session.id)) {
336
- completed.push(session.id);
337
- this.completedByWorkspace.set(session.workspaceId, completed);
352
+ processEntry.running = false;
353
+ processEntry.exitCode = exitCode;
354
+ processEntry.signal = signal;
355
+ processEntry.resolveExit();
356
+ if (processEntry.background) {
357
+ const completed = this.completedByWorkspace.get(processEntry.workspaceId) ?? [];
358
+ if (!completed.includes(processEntry.id)) {
359
+ completed.push(processEntry.id);
360
+ this.completedByWorkspace.set(processEntry.workspaceId, completed);
338
361
  }
339
362
  }
340
- session.cleanupTimer = setTimeout(() => this.removeSession(session.id), this.completedSessionTtlMs);
341
- session.cleanupTimer.unref();
363
+ processEntry.cleanupTimer = setTimeout(() => this.removeProcess(processEntry.id), this.completedProcessTtlMs);
364
+ processEntry.cleanupTimer.unref();
342
365
  }
343
- append(session, output) {
344
- session.buffer.append(output);
366
+ append(processEntry, output) {
367
+ processEntry.buffer.append(output);
345
368
  }
346
- consume(session, maxOutputTokens) {
369
+ consume(processEntry, maxOutputTokens) {
347
370
  const limit = boundedInteger(maxOutputTokens, DEFAULT_MAX_OUTPUT_TOKENS, 100_000);
348
371
  const maxCharacters = Math.max(256, limit * 4);
349
- const buffered = session.buffer.drain(maxCharacters);
372
+ const buffered = processEntry.buffer.drain(maxCharacters);
373
+ const processId = processEntry.running ? processEntry.id : undefined;
350
374
  return {
351
- sessionId: session.running ? session.id : undefined,
375
+ processId,
376
+ sessionId: processId,
352
377
  output: buffered.output,
353
378
  outputTruncated: buffered.truncated,
354
- running: session.running,
355
- exitCode: session.exitCode,
356
- signal: session.signal,
357
- wallTimeMs: Date.now() - session.startedAt,
379
+ running: processEntry.running,
380
+ exitCode: processEntry.exitCode,
381
+ signal: processEntry.signal,
382
+ wallTimeMs: Math.max(0, Math.round(this.monotonicNow() - processEntry.startedAtMonotonic)),
358
383
  };
359
384
  }
360
- getOwnedSession(workspaceId, sessionId) {
361
- const session = this.sessions.get(sessionId);
362
- if (!session)
363
- throw new Error(`Unknown process session: ${sessionId}`);
364
- if (session.workspaceId !== workspaceId) {
365
- throw new Error(`Process session ${sessionId} does not belong to workspace ${workspaceId}.`);
385
+ getOwnedProcess(workspaceId, processId) {
386
+ const processEntry = this.processes.get(processId);
387
+ if (!processEntry)
388
+ throw new Error(`Unknown process: ${processId}`);
389
+ if (processEntry.workspaceId !== workspaceId) {
390
+ throw new Error(`Process ${processId} does not belong to workspace ${workspaceId}.`);
366
391
  }
367
- return session;
392
+ return processEntry;
368
393
  }
369
- removeSession(sessionId) {
370
- const session = this.sessions.get(sessionId);
371
- if (session?.cleanupTimer)
372
- clearTimeout(session.cleanupTimer);
373
- this.sessions.delete(sessionId);
374
- if (!session)
394
+ removeProcess(processId) {
395
+ const processEntry = this.processes.get(processId);
396
+ if (processEntry?.cleanupTimer)
397
+ clearTimeout(processEntry.cleanupTimer);
398
+ this.processes.delete(processId);
399
+ if (!processEntry)
375
400
  return;
376
- const completed = this.completedByWorkspace.get(session.workspaceId);
401
+ const completed = this.completedByWorkspace.get(processEntry.workspaceId);
377
402
  if (!completed)
378
403
  return;
379
- const remaining = completed.filter((id) => id !== sessionId);
404
+ const remaining = completed.filter((id) => id !== processId);
380
405
  if (remaining.length > 0)
381
- this.completedByWorkspace.set(session.workspaceId, remaining);
406
+ this.completedByWorkspace.set(processEntry.workspaceId, remaining);
382
407
  else
383
- this.completedByWorkspace.delete(session.workspaceId);
408
+ this.completedByWorkspace.delete(processEntry.workspaceId);
384
409
  }
385
410
  }
411
+ /** @deprecated Use ProcessManager. */
412
+ export { ProcessManager as ProcessSessionManager };
package/dist/roots.js CHANGED
@@ -33,7 +33,7 @@ export function assertAllowedPath(path, allowedRoots) {
33
33
  throw new AccessDeniedError(`Path is outside allowed roots: ${path}`);
34
34
  }
35
35
  export function resolveAllowedPath(inputPath, cwd, allowedRoots) {
36
- const absolutePath = resolve(cwd, inputPath);
36
+ const absolutePath = resolve(cwd, expandHomePath(inputPath));
37
37
  return assertAllowedPath(absolutePath, allowedRoots);
38
38
  }
39
39
  export async function resolveCanonicalAllowedPath(inputPath, cwd, allowedRoots) {