@akira-tl/forgerelay 0.2.3 → 0.2.4

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.
package/dist/server.js CHANGED
@@ -21,10 +21,10 @@ import { attachHookReports, HookRunner, runToolWithHooks } from "./hooks.js";
21
21
  import { buildServerInstructions, buildShellMutationPolicy, buildToolDescriptions, toolNames, } from "./mcp/server-instructions.js";
22
22
  import { createOpenAIIncomingArtifactAdapter, } from "./incoming-artifacts.js";
23
23
  import { logEvent, requestIp, requestPath, commandPreview, sessionIdPrefix, workspaceLogLabel, } from "./logger.js";
24
- import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, runShellTool, writeFileTool, } from "./pi-tools.js";
24
+ import { editFileTool, findFilesTool, grepFilesTool, listDirectoryTool, readFileTool, writeFileTool, } from "./pi-tools.js";
25
25
  import { SingleUserOAuthProvider } from "./oauth-provider.js";
26
26
  import { McpSessionRegistry, } from "./mcp-sessions.js";
27
- import { ProcessSessionManager } from "./process-sessions.js";
27
+ import { ProcessSessionManager, } from "./process-sessions.js";
28
28
  import { createReviewCheckpointManager } from "./review-checkpoints.js";
29
29
  import { openAiConversationScopeId } from "./request-meta.js";
30
30
  import { shutdownHttpServer } from "./server-shutdown.js";
@@ -314,6 +314,45 @@ function processResult(snapshot) {
314
314
  : `Process exited with code ${snapshot.exitCode ?? "unknown"}.`;
315
315
  return snapshot.output ? `${snapshot.output.replace(/\n$/, "")}\n${status}` : status;
316
316
  }
317
+ function completedProcessResult(snapshot) {
318
+ const status = snapshot.signal
319
+ ? `Background process ${snapshot.sessionId} exited after signal ${snapshot.signal}.`
320
+ : `Background process ${snapshot.sessionId} exited with code ${snapshot.exitCode ?? "unknown"}.`;
321
+ const command = `Command: ${snapshot.command}`;
322
+ const output = snapshot.output ? `\n${snapshot.output.replace(/\n$/, "")}` : "";
323
+ return `${status}\n${command}${output}`;
324
+ }
325
+ function attachCompletedProcessNotices(processSessions, workspaceId, result) {
326
+ if (result instanceof Error) {
327
+ const completed = processSessions.takeCompleted(workspaceId);
328
+ if (completed.length > 0) {
329
+ result.message = [
330
+ result.message,
331
+ ...completed.map((snapshot) => completedProcessResult(snapshot)),
332
+ ].join("\n\n");
333
+ }
334
+ return result;
335
+ }
336
+ if (typeof result !== "object" || result === null)
337
+ return result;
338
+ const content = result.content;
339
+ if (!Array.isArray(content))
340
+ return result;
341
+ const structured = result.structuredContent;
342
+ const currentSessionId = structured?.running === true && typeof structured.sessionId === "number"
343
+ ? structured.sessionId
344
+ : undefined;
345
+ const completed = processSessions.takeCompleted(workspaceId, undefined, currentSessionId);
346
+ if (completed.length === 0)
347
+ return result;
348
+ return {
349
+ ...result,
350
+ content: [
351
+ ...content,
352
+ ...completed.map((snapshot) => textBlock(completedProcessResult(snapshot))),
353
+ ],
354
+ };
355
+ }
317
356
  function processOutputSchema() {
318
357
  return resultOutputSchema({
319
358
  sessionId: z.number().optional(),
@@ -367,86 +406,89 @@ function workspaceHookInvocation(workspace) {
367
406
  function toolResultIsError(result) {
368
407
  return typeof result === "object" && result !== null && result.isError === true;
369
408
  }
370
- function registerCodexProcessTools(server, config, workspaces, processSessions, hooks) {
371
- registerAppTool(server, "exec_command", {
372
- title: "Execute command",
373
- description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
374
- inputSchema: {
375
- workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
376
- cmd: z.string().min(1).describe("Shell command to execute."),
377
- tty: z
378
- .boolean()
379
- .optional()
380
- .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
381
- columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
382
- rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
383
- workingDirectory: z
384
- .string()
385
- .optional()
386
- .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
387
- yieldTimeMs: z
388
- .number()
389
- .int()
390
- .min(0)
391
- .max(30_000)
392
- .optional()
393
- .describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
394
- maxOutputTokens: z
395
- .number()
396
- .int()
397
- .positive()
398
- .max(100_000)
399
- .optional()
400
- .describe("Approximate output token budget. Defaults to 10000."),
401
- },
402
- outputSchema: processOutputSchema(),
403
- ...toolWidgetDescriptorMeta(config, "shell"),
404
- annotations: SHELL_TOOL_ANNOTATIONS,
405
- }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }, extra) => {
406
- const workspace = workspaces.getWorkspace(workspaceId);
407
- return runToolWithHooks(hooks, {
408
- tool: "exec_command",
409
- invocation: workspaceHookInvocation(workspace),
410
- payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
411
- operation: async () => {
412
- const startedAt = performance.now();
413
- const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
414
- const snapshot = await processSessions.start({
415
- workspaceId,
416
- command: cmd,
417
- cwd,
418
- workspaceRoot: workspace.root,
419
- tty,
420
- columns,
421
- rows,
422
- yieldTimeMs,
423
- maxOutputTokens,
424
- });
425
- logToolCall(config, {
426
- tool: "exec_command",
427
- ...workspaceLogContext(workspace, extra.sessionId),
428
- workingDirectory: workingDirectory ?? ".",
429
- command: cmd,
430
- commandLength: cmd.length,
431
- exitCode: snapshot.exitCode,
432
- running: snapshot.running,
433
- processSessionId: snapshot.sessionId,
434
- success: snapshot.running || snapshot.exitCode === 0,
435
- durationMs: Math.round(performance.now() - startedAt),
436
- });
437
- return processToolResponse("exec_command", workspaceId, snapshot, {
438
- command: cmd,
439
- workingDirectory: workingDirectory ?? ".",
440
- running: snapshot.running,
441
- exitCode: snapshot.exitCode,
442
- wallTimeMs: snapshot.wallTimeMs,
443
- });
409
+ function registerProcessTools(server, config, workspaces, processSessions, hooks) {
410
+ if (config.toolMode === "codex") {
411
+ registerAppTool(server, "exec_command", {
412
+ title: "Execute command",
413
+ description: `Run a command inside an open workspace. Returns its result when it exits during the yield window, otherwise returns a sessionId for write_stdin. Use this for file inspection, tests, builds, package scripts, generators, formatters, and long-running processes. ${buildShellMutationPolicy()} Call open_workspace first and pass workspaceId.`,
414
+ inputSchema: {
415
+ workspaceId: z.string().describe("Workspace identifier returned by open_workspace."),
416
+ cmd: z.string().min(1).describe("Shell command to execute."),
417
+ tty: z
418
+ .boolean()
419
+ .optional()
420
+ .describe("Allocate a pseudo-terminal for interactive commands. Defaults to false."),
421
+ columns: z.number().int().min(1).max(1_000).optional().describe("Initial PTY width. Defaults to 80."),
422
+ rows: z.number().int().min(1).max(1_000).optional().describe("Initial PTY height. Defaults to 24."),
423
+ workingDirectory: z
424
+ .string()
425
+ .optional()
426
+ .describe("Working directory relative to the workspace root. Defaults to the workspace root."),
427
+ yieldTimeMs: z
428
+ .number()
429
+ .int()
430
+ .min(0)
431
+ .max(30_000)
432
+ .optional()
433
+ .describe("Milliseconds to wait before returning a running session. Defaults to 10000."),
434
+ maxOutputTokens: z
435
+ .number()
436
+ .int()
437
+ .positive()
438
+ .max(100_000)
439
+ .optional()
440
+ .describe("Approximate output token budget. Defaults to 10000."),
444
441
  },
442
+ outputSchema: processOutputSchema(),
443
+ ...toolWidgetDescriptorMeta(config, "shell"),
444
+ annotations: SHELL_TOOL_ANNOTATIONS,
445
+ }, async ({ workspaceId, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, maxOutputTokens }, extra) => {
446
+ const workspace = workspaces.getWorkspace(workspaceId);
447
+ return runToolWithHooks(hooks, {
448
+ tool: "exec_command",
449
+ invocation: workspaceHookInvocation(workspace),
450
+ payload: { command: cmd, workingDirectory: workingDirectory ?? "." },
451
+ operation: async () => {
452
+ const startedAt = performance.now();
453
+ const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
454
+ const snapshot = await processSessions.start({
455
+ workspaceId,
456
+ command: cmd,
457
+ cwd,
458
+ workspaceRoot: workspace.root,
459
+ tty,
460
+ columns,
461
+ rows,
462
+ yieldTimeMs,
463
+ maxOutputTokens,
464
+ codexCi: true,
465
+ });
466
+ logToolCall(config, {
467
+ tool: "exec_command",
468
+ ...workspaceLogContext(workspace, extra.sessionId),
469
+ workingDirectory: workingDirectory ?? ".",
470
+ command: cmd,
471
+ commandLength: cmd.length,
472
+ exitCode: snapshot.exitCode,
473
+ running: snapshot.running,
474
+ processSessionId: snapshot.sessionId,
475
+ success: snapshot.running || snapshot.exitCode === 0,
476
+ durationMs: Math.round(performance.now() - startedAt),
477
+ });
478
+ return processToolResponse("exec_command", workspaceId, snapshot, {
479
+ command: cmd,
480
+ workingDirectory: workingDirectory ?? ".",
481
+ running: snapshot.running,
482
+ exitCode: snapshot.exitCode,
483
+ wallTimeMs: snapshot.wallTimeMs,
484
+ });
485
+ },
486
+ });
445
487
  });
446
- });
488
+ }
447
489
  registerAppTool(server, "write_stdin", {
448
490
  title: "Write to process",
449
- description: "Poll or write characters to a process returned by exec_command. Omit chars or pass an empty string to poll. Pass \\u0003 to send Ctrl-C.",
491
+ description: "Poll or write characters to a running process returned by bash or exec_command. Omit chars or pass an empty string to poll. Waiting never kills the process; pass \\u0003 to explicitly send Ctrl-C.",
450
492
  inputSchema: {
451
493
  workspaceId: z.string().describe("Workspace identifier used to start the process."),
452
494
  sessionId: z.number().describe("Process session identifier returned by exec_command."),
@@ -457,9 +499,9 @@ function registerCodexProcessTools(server, config, workspaces, processSessions,
457
499
  .number()
458
500
  .int()
459
501
  .min(0)
460
- .max(30_000)
502
+ .max(300_000)
461
503
  .optional()
462
- .describe("Milliseconds to wait for process output or completion. Defaults to 10000."),
504
+ .describe("Milliseconds to keep waiting before returning again, max 300000. Polling defaults to 5000; interaction defaults to 250."),
463
505
  maxOutputTokens: z
464
506
  .number()
465
507
  .int()
@@ -515,7 +557,7 @@ function registerCodexProcessTools(server, config, workspaces, processSessions,
515
557
  }
516
558
  export function createMcpServer(config, workspaces, reviewCheckpoints, processSessions, localAgentProviders, incomingArtifactAdapters) {
517
559
  const toolDescriptions = buildToolDescriptions(config);
518
- const hooks = new HookRunner(config.hooks, config.logging);
560
+ const hooks = new HookRunner(config.hooks, config.logging, process.env, (workspaceId, result) => attachCompletedProcessNotices(processSessions, workspaceId, result));
519
561
  const server = new McpServer({
520
562
  name: "forgerelay",
521
563
  title: "ForgeRelay",
@@ -552,11 +594,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
552
594
  });
553
595
  registerAppTool(server, "open_workspace", {
554
596
  title: "Open workspace",
555
- description: "Open a local project directory as a coding workspace. The same directory reuses the same active workspaceId across requests. Default to checkout mode and only use mode=\"worktree\" when the user explicitly asks for isolated or parallel work. Managed worktrees use dedicated forgerelay/* branches and can later be safely closed into their original target branch with close_worktree. Existing managed worktree paths can also be reopened directly.",
597
+ description: "Open or resume a local coding workspace. A conversation keeps a stable workspaceId for a project, while different conversations normally receive different logical workspaceIds that may point at the same physical checkout or worktree. Pass workspaceId to explicitly resume an existing logical workspace in this conversation. Default to checkout mode and only use mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. Workspaces idle for more than two days are reported for user-directed cleanup or resumption.",
556
598
  inputSchema: {
557
599
  path: z
558
600
  .string()
559
- .describe("Absolute path, or a leading-tilde home path such as ~/project, to a local project directory inside an allowed root. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
601
+ .optional()
602
+ .describe("Project path to open. Required unless workspaceId is supplied. With mode=\"worktree\", this may also be a managed worktree path previously returned by ForgeRelay."),
603
+ workspaceId: z
604
+ .string()
605
+ .optional()
606
+ .describe("Existing logical workspace ID to resume in this conversation. When supplied, ForgeRelay resumes that workspace rather than allocating another ID."),
560
607
  mode: z
561
608
  .enum(["checkout", "worktree"])
562
609
  .optional()
@@ -568,7 +615,11 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
568
615
  newWorktree: z
569
616
  .boolean()
570
617
  .optional()
571
- .describe("When true, create another isolated managed worktree instead of reusing the existing worktree for this project and baseRef. Use only when the user explicitly requests a separate worktree."),
618
+ .describe("When true, create another isolated managed Git worktree instead of reusing the existing physical worktree. Use only when the user explicitly requests separate Git isolation."),
619
+ newWorkspace: z
620
+ .boolean()
621
+ .optional()
622
+ .describe("When true, allocate a fresh logical workspaceId for the same physical checkout or worktree and bind this conversation to it. Use only after the user explicitly requests a new logical workspace."),
572
623
  },
573
624
  outputSchema: {
574
625
  workspaceId: z.string(),
@@ -597,6 +648,16 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
597
648
  managed: z.boolean(),
598
649
  current: z.boolean(),
599
650
  })),
651
+ staleWorkspaces: z.array(z.object({
652
+ workspaceId: z.string(),
653
+ root: z.string(),
654
+ mode: z.enum(["checkout", "worktree"]),
655
+ lastUsedAt: z.string(),
656
+ idleMs: z.number().nonnegative(),
657
+ branch: z.string().optional(),
658
+ targetBranch: z.string().optional(),
659
+ managed: z.boolean(),
660
+ })),
600
661
  agentsFiles: z.array(workspaceAgentsFileOutputSchema).optional(),
601
662
  availableAgentsFiles: z.array(workspaceAvailableAgentsFileOutputSchema).optional(),
602
663
  skills: z.array(workspaceSkillOutputSchema).optional(),
@@ -612,10 +673,14 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
612
673
  idempotentHint: false,
613
674
  openWorldHint: false,
614
675
  },
615
- }, async ({ path, mode, baseRef, newWorktree }, { _meta, sessionId }) => {
676
+ }, async ({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, { _meta, sessionId }) => {
616
677
  const startedAt = performance.now();
617
- const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, mode, baseRef, newWorktree }, { conversationScopeId: openAiConversationScopeId(_meta) });
678
+ const { workspace, agentsFiles, availableAgentsFiles, hookReports, workspaceReused, includeBootstrapContext, } = await workspaces.openWorkspace({ path, workspaceId, mode, baseRef, newWorktree, newWorkspace }, {
679
+ conversationScopeId: openAiConversationScopeId(_meta),
680
+ protectedWorkspaceIds: processSessions.activeWorkspaceIds(),
681
+ });
618
682
  const knownWorktrees = await workspaces.listKnownWorktrees(workspace);
683
+ const staleWorkspaces = await workspaces.listStaleWorkspaces(workspace);
619
684
  if (config.widgets === "changes") {
620
685
  await reviewCheckpoints.initializeWorkspace({
621
686
  workspaceId: workspace.id,
@@ -701,6 +766,9 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
701
766
  knownWorktrees.length > 0
702
767
  ? `Known worktrees: ${knownWorktrees.map((worktree) => `${worktree.path} [${worktree.workspaceId}]${worktree.branch ? ` branch=${worktree.branch}` : ""}${worktree.targetBranch ? ` target=${worktree.targetBranch}` : ""}${worktree.current ? " (current)" : ""}`).join(", ")}`
703
768
  : undefined,
769
+ staleWorkspaces.length > 0
770
+ ? `Idle logical workspaces for this same physical workspace (>2 days): ${staleWorkspaces.map((stale) => `${stale.workspaceId} last-used=${stale.lastUsedAt}`).join(", ")}. Tell the user these are available to resume or explicitly close; do not clean them up automatically.`
771
+ : undefined,
704
772
  instruction,
705
773
  ].filter(Boolean).join("\n"),
706
774
  },
@@ -712,7 +780,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
712
780
  success: true,
713
781
  durationMs: Math.round(performance.now() - startedAt),
714
782
  });
715
- return attachHookReports({
783
+ return hooks.decorateResult(workspace.id, attachHookReports({
716
784
  content: resultContent,
717
785
  _meta: {
718
786
  tool: "open_workspace",
@@ -726,6 +794,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
726
794
  sourceRoot: workspace.sourceRoot,
727
795
  worktree: workspace.worktree,
728
796
  worktrees: knownWorktrees,
797
+ staleWorkspaces,
729
798
  agentsFiles: cardAgentsFiles,
730
799
  availableAgentsFiles: cardAvailableAgentsFiles,
731
800
  skills: cardSkills,
@@ -749,6 +818,7 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
749
818
  sourceRoot: workspace.sourceRoot,
750
819
  worktree: workspace.worktree,
751
820
  worktrees: knownWorktrees,
821
+ staleWorkspaces,
752
822
  ...(includeBootstrapContext
753
823
  ? {
754
824
  agentsFiles: loadedAgentsFiles,
@@ -761,7 +831,35 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
761
831
  : {}),
762
832
  instruction,
763
833
  },
764
- }, hookReports);
834
+ }, hookReports));
835
+ });
836
+ registerAppTool(server, toolNames.closeWorkspace, {
837
+ title: "Close logical workspace",
838
+ description: "Release one logical ForgeRelay workspaceId after the user explicitly chooses to clean it up. This never deletes checkout files. A worktree handle can be released only when another logical handle still anchors the same physical worktree; use close_worktree to finalize and remove the last managed worktree. Running or unconsumed background processes prevent closure.",
839
+ inputSchema: {
840
+ workspaceId: z.string().describe("Logical workspace ID to release."),
841
+ },
842
+ outputSchema: resultOutputSchema({ workspaceId: z.string() }),
843
+ _meta: {},
844
+ annotations: WRITE_TOOL_ANNOTATIONS,
845
+ }, async ({ workspaceId }) => {
846
+ const workspace = workspaces.getWorkspace(workspaceId);
847
+ return runToolWithHooks(hooks, {
848
+ tool: toolNames.closeWorkspace,
849
+ invocation: workspaceHookInvocation(workspace),
850
+ payload: { workspaceId },
851
+ operation: async () => {
852
+ if (processSessions.activeWorkspaceIds().has(workspaceId)) {
853
+ throw new Error(`Workspace ${workspaceId} still owns a running process or an unconsumed process completion. Poll or consume it before closing this workspace.`);
854
+ }
855
+ workspaces.closeWorkspace(workspaceId);
856
+ const result = `Closed logical workspace ${workspaceId}. Physical project files were not removed.`;
857
+ return {
858
+ content: [textBlock(result)],
859
+ structuredContent: { result, workspaceId },
860
+ };
861
+ },
862
+ });
765
863
  });
766
864
  registerAppTool(server, toolNames.closeWorktree, {
767
865
  title: "Close worktree",
@@ -795,6 +893,12 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
795
893
  payload: { commitMessage },
796
894
  afterCwd: (response) => response.structuredContent.sourceRoot,
797
895
  operation: async () => {
896
+ const busyWorkspaceIds = workspaces
897
+ .workspaceIdsForPhysicalWorkspace(workspace)
898
+ .filter((id) => processSessions.activeWorkspaceIds().has(id));
899
+ if (busyWorkspaceIds.length > 0) {
900
+ throw new Error(`Cannot close this worktree while logical workspace processes are still running or awaiting completion delivery: ${busyWorkspaceIds.join(", ")}.`);
901
+ }
798
902
  const startedAt = performance.now();
799
903
  const closed = await workspaces.closeWorktree(workspaceId, commitMessage);
800
904
  const result = [
@@ -1554,80 +1658,57 @@ export function createMcpServer(config, workspaces, reviewCheckpoints, processSe
1554
1658
  .string()
1555
1659
  .optional()
1556
1660
  .describe("Optional working directory relative to the workspace root. Defaults to the workspace root."),
1557
- timeout: z
1558
- .number()
1559
- .positive()
1560
- .max(300)
1561
- .optional()
1562
- .describe("Timeout in seconds. Defaults to 30, max 300."),
1563
1661
  },
1564
- outputSchema: resultOutputSchema(),
1662
+ outputSchema: processOutputSchema(),
1565
1663
  ...toolWidgetDescriptorMeta(config, "shell"),
1566
1664
  annotations: SHELL_TOOL_ANNOTATIONS,
1567
- }, async ({ workspaceId, workingDirectory, ...input }, extra) => {
1665
+ }, async ({ workspaceId, command, workingDirectory }, extra) => {
1568
1666
  const workspace = workspaces.getWorkspace(workspaceId);
1569
1667
  return runToolWithHooks(hooks, {
1570
1668
  tool: toolNames.shell,
1571
1669
  invocation: workspaceHookInvocation(workspace),
1572
1670
  payload: {
1573
- command: input.command,
1671
+ command,
1574
1672
  workingDirectory: workingDirectory ?? ".",
1575
- timeoutSeconds: input.timeout,
1576
1673
  },
1577
1674
  isFailure: toolResultIsError,
1578
1675
  operation: async () => {
1579
1676
  const startedAt = performance.now();
1580
1677
  const cwd = workspaces.resolveWorkingDirectory(workspace, workingDirectory);
1581
- const response = await runShellTool(input, {
1678
+ const snapshot = await processSessions.start({
1679
+ workspaceId,
1680
+ command,
1582
1681
  cwd,
1583
- root: workspace.root,
1682
+ workspaceRoot: workspace.root,
1683
+ yieldTimeMs: 300_000,
1584
1684
  });
1585
- if (response.isError) {
1586
- logFailedToolResponse(config, {
1587
- tool: toolNames.shell,
1588
- ...workspaceLogContext(workspace, extra.sessionId),
1589
- workingDirectory: workingDirectory ?? ".",
1590
- command: input.command,
1591
- commandLength: input.command.length,
1592
- }, response.content, startedAt);
1593
- return response;
1594
- }
1595
- const summary = {
1596
- command: input.command,
1597
- workingDirectory: workingDirectory ?? ".",
1598
- ...textSummary(response.content),
1599
- };
1600
1685
  logToolCall(config, {
1601
1686
  tool: toolNames.shell,
1602
1687
  ...workspaceLogContext(workspace, extra.sessionId),
1603
1688
  workingDirectory: workingDirectory ?? ".",
1604
- command: input.command,
1605
- commandLength: input.command.length,
1606
- success: true,
1689
+ command,
1690
+ commandLength: command.length,
1691
+ exitCode: snapshot.exitCode,
1692
+ running: snapshot.running,
1693
+ processSessionId: snapshot.sessionId,
1694
+ success: snapshot.running || (snapshot.exitCode === 0 && !snapshot.signal),
1607
1695
  durationMs: Math.round(performance.now() - startedAt),
1608
1696
  });
1609
- return {
1610
- ...response,
1611
- _meta: {
1612
- tool: toolNames.shell,
1613
- card: {
1614
- workspaceId,
1615
- path: workingDirectory,
1616
- summary,
1617
- payload: { content: response.content },
1618
- },
1619
- },
1620
- structuredContent: {
1621
- result: contentText(response.content),
1622
- },
1623
- };
1697
+ const response = processToolResponse(toolNames.shell, workspaceId, snapshot, {
1698
+ command,
1699
+ workingDirectory: workingDirectory ?? ".",
1700
+ running: snapshot.running,
1701
+ exitCode: snapshot.exitCode,
1702
+ wallTimeMs: snapshot.wallTimeMs,
1703
+ });
1704
+ return !snapshot.running && (snapshot.signal || snapshot.exitCode !== 0)
1705
+ ? { ...response, isError: true }
1706
+ : response;
1624
1707
  },
1625
1708
  });
1626
1709
  });
1627
1710
  }
1628
- if (config.toolMode === "codex") {
1629
- registerCodexProcessTools(server, config, workspaces, processSessions, hooks);
1630
- }
1711
+ registerProcessTools(server, config, workspaces, processSessions, hooks);
1631
1712
  if (config.artifactsEnabled && isArtifactDownloadSupportedPlatform()) {
1632
1713
  registerArtifactTools(server, {
1633
1714
  config,
@@ -79,6 +79,19 @@ export class SqliteWorkspaceStore {
79
79
  : query.where(and(...conditions)).all();
80
80
  return rows.map(rowToWorkspaceSession);
81
81
  }
82
+ deleteSession(id) {
83
+ this.database.db
84
+ .delete(workspaceSessions)
85
+ .where(eq(workspaceSessions.id, id))
86
+ .run();
87
+ }
88
+ listConversationBindings() {
89
+ return this.database.db
90
+ .select()
91
+ .from(workspaceConversationBindings)
92
+ .all()
93
+ .map(rowToWorkspaceConversationBinding);
94
+ }
82
95
  getConversationBinding(conversationScopeId, targetKey) {
83
96
  const row = this.database.db
84
97
  .select()