@co0ontty/wand 4.57.0 → 4.58.0

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 (177) hide show
  1. package/dist/build-info.json +3 -3
  2. package/dist/ensure-node-pty-helper.d.ts +0 -1
  3. package/dist/ensure-node-pty-helper.js +0 -5
  4. package/dist/git-quick-commit.js +18 -19
  5. package/dist/github-connector.d.ts +11 -5
  6. package/dist/github-connector.js +105 -33
  7. package/dist/missions.d.ts +1 -1
  8. package/dist/missions.js +17 -4
  9. package/dist/server-file-routes.js +43 -2
  10. package/dist/server-github-routes.js +50 -53
  11. package/dist/server-mission-routes.js +11 -10
  12. package/dist/server-task-routes.d.ts +14 -2
  13. package/dist/server-task-routes.js +248 -84
  14. package/dist/server-workspace-routes.js +90 -16
  15. package/dist/server.js +2 -20
  16. package/dist/storage.d.ts +23 -6
  17. package/dist/storage.js +130 -53
  18. package/dist/structured-exec-host.d.ts +13 -2
  19. package/dist/structured-exec-host.js +71 -36
  20. package/dist/structured-exec-pump.d.ts +1 -1
  21. package/dist/structured-exec-pump.js +14 -8
  22. package/dist/task-types.d.ts +15 -0
  23. package/dist/terminal-daemon-client.d.ts +2 -0
  24. package/dist/terminal-daemon-client.js +7 -1
  25. package/dist/terminal-daemon-server.js +17 -18
  26. package/dist/tui/ipc-server.js +3 -1
  27. package/dist/types.d.ts +1 -0
  28. package/dist/wand-task-sync.d.ts +9 -0
  29. package/dist/wand-task-sync.js +50 -0
  30. package/dist/web-ui/content/scripts.js +85 -85
  31. package/dist/web-ui/content/styles.css +1 -1
  32. package/dist/web-ui/embedded-assets.js +2 -2
  33. package/dist/ws-broadcast.js +5 -1
  34. package/package.json +2 -3
  35. package/dist/taskboard-bridge.d.ts +0 -11
  36. package/dist/taskboard-bridge.js +0 -371
  37. package/vendor/codex-taskboard/LICENSE +0 -201
  38. package/vendor/codex-taskboard/dist/web/assets/AiChat-BNn4VJ5a.js +0 -12
  39. package/vendor/codex-taskboard/dist/web/assets/GanttView-D8wnfGrG.css +0 -1
  40. package/vendor/codex-taskboard/dist/web/assets/GanttView-rGWhuEX9.js +0 -39
  41. package/vendor/codex-taskboard/dist/web/assets/abnfDiagram-N423BO3Z-DbFG6lbA.js +0 -1
  42. package/vendor/codex-taskboard/dist/web/assets/arc-BjSQqbzd.js +0 -1
  43. package/vendor/codex-taskboard/dist/web/assets/architecture-TIHT7OUA-C9-t8p4o.js +0 -1
  44. package/vendor/codex-taskboard/dist/web/assets/architectureDiagram-T3A2C74G-Db4kJZwO.js +0 -36
  45. package/vendor/codex-taskboard/dist/web/assets/array-BifhSqXX.js +0 -1
  46. package/vendor/codex-taskboard/dist/web/assets/blockDiagram-VBNYF7ZC-C-BywuWV.js +0 -132
  47. package/vendor/codex-taskboard/dist/web/assets/c4Diagram-5PPSVZJV-Dq-80rjP.js +0 -10
  48. package/vendor/codex-taskboard/dist/web/assets/channel-C7mnz8Xc.js +0 -1
  49. package/vendor/codex-taskboard/dist/web/assets/chunk-2GRJ4B5K-B2G57us9.js +0 -1
  50. package/vendor/codex-taskboard/dist/web/assets/chunk-2Q5K7J3B-C1jixKkw.js +0 -1
  51. package/vendor/codex-taskboard/dist/web/assets/chunk-3NCLNEKW-DVGIRib_.js +0 -1
  52. package/vendor/codex-taskboard/dist/web/assets/chunk-4I5QYGJK-kNoGFqr0.js +0 -1
  53. package/vendor/codex-taskboard/dist/web/assets/chunk-5RXB4S5H-4iv6-xM6.js +0 -231
  54. package/vendor/codex-taskboard/dist/web/assets/chunk-5VM5RSS4-ZNzvKenW.js +0 -15
  55. package/vendor/codex-taskboard/dist/web/assets/chunk-6Q2QTUOP-1J34vxHr.js +0 -88
  56. package/vendor/codex-taskboard/dist/web/assets/chunk-7BUUIJ7U-Bb538aSH.js +0 -1
  57. package/vendor/codex-taskboard/dist/web/assets/chunk-7Z6QIM7H-CDQpQlxX.js +0 -10
  58. package/vendor/codex-taskboard/dist/web/assets/chunk-GF5L2VYU-Dg92oHHl.js +0 -206
  59. package/vendor/codex-taskboard/dist/web/assets/chunk-I66GZJ75-Bk8_gFs2.js +0 -125
  60. package/vendor/codex-taskboard/dist/web/assets/chunk-J7OUQ5F2-CJRcenlr.js +0 -2
  61. package/vendor/codex-taskboard/dist/web/assets/chunk-JQJVKLGR-BLBV_Sus.js +0 -156
  62. package/vendor/codex-taskboard/dist/web/assets/chunk-JWPE2WC7-DVXcaiue.js +0 -1
  63. package/vendor/codex-taskboard/dist/web/assets/chunk-KBJHAD2P-Da9Yjz0p.js +0 -1
  64. package/vendor/codex-taskboard/dist/web/assets/chunk-KEIR6QF5-Dj-OpFgW.js +0 -161
  65. package/vendor/codex-taskboard/dist/web/assets/chunk-NSK5VX7P-Cb56tnGK.js +0 -2
  66. package/vendor/codex-taskboard/dist/web/assets/chunk-QR6OTTB3-BwoszTxn.js +0 -62
  67. package/vendor/codex-taskboard/dist/web/assets/chunk-RYQCIY6F-D_L2RdcQ.js +0 -1
  68. package/vendor/codex-taskboard/dist/web/assets/chunk-UBXNYLIW-YX5mN8cl.js +0 -1
  69. package/vendor/codex-taskboard/dist/web/assets/chunk-W5SLKNZC-DlOLhhDO.js +0 -1
  70. package/vendor/codex-taskboard/dist/web/assets/chunk-WRU74C26-B7SBmP0K.js +0 -70
  71. package/vendor/codex-taskboard/dist/web/assets/chunk-XXDRQBXY-BuE3VzE_.js +0 -1
  72. package/vendor/codex-taskboard/dist/web/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +0 -1
  73. package/vendor/codex-taskboard/dist/web/assets/chunk-ZIRB5QZD-C6fEPe3t.js +0 -32
  74. package/vendor/codex-taskboard/dist/web/assets/classDiagram-JCYQIIEL-KIdCVvbA.js +0 -1
  75. package/vendor/codex-taskboard/dist/web/assets/classDiagram-v2-OCEON4UE-KIdCVvbA.js +0 -1
  76. package/vendor/codex-taskboard/dist/web/assets/cose-bilkent-JH36ORCC-BzQvy5TC.js +0 -1
  77. package/vendor/codex-taskboard/dist/web/assets/cynefin-VYW2F7L2-D1_5O1XR.js +0 -1
  78. package/vendor/codex-taskboard/dist/web/assets/cynefinDiagram-MW4NZA55-DOW_U5QN.js +0 -62
  79. package/vendor/codex-taskboard/dist/web/assets/cytoscape.esm-DnGd5Fuo.js +0 -321
  80. package/vendor/codex-taskboard/dist/web/assets/dagre-VZM6K2ZE-0Dntc3NT.js +0 -4
  81. package/vendor/codex-taskboard/dist/web/assets/dagre-dpRSp0QF.js +0 -1
  82. package/vendor/codex-taskboard/dist/web/assets/defaultLocale-C8Fc0cco.js +0 -1
  83. package/vendor/codex-taskboard/dist/web/assets/diagram-7IWD3JNH-D7GleYAM.js +0 -30
  84. package/vendor/codex-taskboard/dist/web/assets/diagram-B4RE2ZJO-DCXTe6q5.js +0 -3
  85. package/vendor/codex-taskboard/dist/web/assets/diagram-LBJQPF4R-ZFJyXEXl.js +0 -24
  86. package/vendor/codex-taskboard/dist/web/assets/diagram-Q27KOJAE-DUGCuGd9.js +0 -24
  87. package/vendor/codex-taskboard/dist/web/assets/diagram-UB23O5K3-DRmp4Nng.js +0 -41
  88. package/vendor/codex-taskboard/dist/web/assets/dist-D9sYb5Oa.js +0 -1
  89. package/vendor/codex-taskboard/dist/web/assets/ebnfDiagram-BXEA7PRR-CkKoaf7T.js +0 -1
  90. package/vendor/codex-taskboard/dist/web/assets/erDiagram-JOGREHBK-nBX5z4R4.js +0 -85
  91. package/vendor/codex-taskboard/dist/web/assets/eventmodeling-45OFAUF4-pexmK65N.js +0 -1
  92. package/vendor/codex-taskboard/dist/web/assets/flowDiagram-UKHOOZJN-aroohRbL.js +0 -1
  93. package/vendor/codex-taskboard/dist/web/assets/ganttDiagram-PKOTCBZU-BOJWUTXa.js +0 -292
  94. package/vendor/codex-taskboard/dist/web/assets/gitGraph-TEB2WS4Q-DxnuhSZU.js +0 -1
  95. package/vendor/codex-taskboard/dist/web/assets/gitGraphDiagram-DS77QQ5N-Cte975TC.js +0 -106
  96. package/vendor/codex-taskboard/dist/web/assets/graphlib-DS17s2tU.js +0 -1
  97. package/vendor/codex-taskboard/dist/web/assets/index-Docmx4uQ.css +0 -1
  98. package/vendor/codex-taskboard/dist/web/assets/index-Du07lEGz.js +0 -121
  99. package/vendor/codex-taskboard/dist/web/assets/info-DKCQHKI2-DOTjgYxk.js +0 -1
  100. package/vendor/codex-taskboard/dist/web/assets/infoDiagram-6WML65LV-SpyV9UJd.js +0 -2
  101. package/vendor/codex-taskboard/dist/web/assets/init-D6jRqBbL.js +0 -1
  102. package/vendor/codex-taskboard/dist/web/assets/ishikawaDiagram-WSZJBQD7-T0PZwoUZ.js +0 -70
  103. package/vendor/codex-taskboard/dist/web/assets/journeyDiagram-NVQOT4AX-B88b8UgX.js +0 -139
  104. package/vendor/codex-taskboard/dist/web/assets/kanban-definition-27J2QSJJ-CIlfNnl_.js +0 -89
  105. package/vendor/codex-taskboard/dist/web/assets/katex-B7rAX3Vi.js +0 -257
  106. package/vendor/codex-taskboard/dist/web/assets/line-fWrwPZhB.js +0 -1
  107. package/vendor/codex-taskboard/dist/web/assets/linear-B7l8qgEw.js +0 -1
  108. package/vendor/codex-taskboard/dist/web/assets/map-BaFkSB1l.js +0 -1
  109. package/vendor/codex-taskboard/dist/web/assets/mermaid-parser.core-C5gls2nC.js +0 -7
  110. package/vendor/codex-taskboard/dist/web/assets/mermaid.core-POz2Xcvr.js +0 -11
  111. package/vendor/codex-taskboard/dist/web/assets/mindmap-definition-FAOFIHXS-BM0-egZ5.js +0 -96
  112. package/vendor/codex-taskboard/dist/web/assets/ordinal-hYBb2elL.js +0 -1
  113. package/vendor/codex-taskboard/dist/web/assets/packet-7NZHBO7P-DQZzyVos.js +0 -1
  114. package/vendor/codex-taskboard/dist/web/assets/path-BWPyau1x.js +0 -1
  115. package/vendor/codex-taskboard/dist/web/assets/pegDiagram-VL7TDLO6-B8aEwl-7.js +0 -1
  116. package/vendor/codex-taskboard/dist/web/assets/pie-RZYD4A2V-DRUflilR.js +0 -1
  117. package/vendor/codex-taskboard/dist/web/assets/pieDiagram-7S7Q4E2Y-0cDDxlua.js +0 -39
  118. package/vendor/codex-taskboard/dist/web/assets/purify.es-Bwt4ulIK.js +0 -3
  119. package/vendor/codex-taskboard/dist/web/assets/quadrantDiagram-CIZ2JOQS-CBgb0YZ0.js +0 -7
  120. package/vendor/codex-taskboard/dist/web/assets/radar-I7S5WNFK-tRocoKxq.js +0 -1
  121. package/vendor/codex-taskboard/dist/web/assets/railroad-3IZDKUUU-DaS4ZlX_.js +0 -1
  122. package/vendor/codex-taskboard/dist/web/assets/railroad-abnf-AHOZXSZD-BN1T5dBT.js +0 -1
  123. package/vendor/codex-taskboard/dist/web/assets/railroad-ebnf-EBAXGLYW-Cuf8qyKs.js +0 -1
  124. package/vendor/codex-taskboard/dist/web/assets/railroad-peg-LSFZ7HO6-B511_EYq.js +0 -1
  125. package/vendor/codex-taskboard/dist/web/assets/railroadDiagram-AXF67PYL-QtPo-JZb.js +0 -1
  126. package/vendor/codex-taskboard/dist/web/assets/requirementDiagram-LRYGKXZP-CuBIF85r.js +0 -84
  127. package/vendor/codex-taskboard/dist/web/assets/rolldown-runtime-QTnfLwEv.js +0 -1
  128. package/vendor/codex-taskboard/dist/web/assets/rough.esm-CSKSodPl.js +0 -1
  129. package/vendor/codex-taskboard/dist/web/assets/sankeyDiagram-W5VNT64P-CRA-b_za.js +0 -40
  130. package/vendor/codex-taskboard/dist/web/assets/sequenceDiagram-SI44F4Z6-CoddVWwY.js +0 -162
  131. package/vendor/codex-taskboard/dist/web/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +0 -1
  132. package/vendor/codex-taskboard/dist/web/assets/src-_wZywoZs.js +0 -1
  133. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-OKZ733FA-D8DMM2ln.js +0 -1
  134. package/vendor/codex-taskboard/dist/web/assets/stateDiagram-v2-UEYNNEHI-CVp0eYAS.js +0 -1
  135. package/vendor/codex-taskboard/dist/web/assets/swimlanes-SLNWSIFB-C1fC7EFu.js +0 -2
  136. package/vendor/codex-taskboard/dist/web/assets/swimlanesDiagram-ULZ7WXOC-CPd58Xso.js +0 -8
  137. package/vendor/codex-taskboard/dist/web/assets/timeline-definition-Z64GVDOM-BUBv8aKP.js +0 -120
  138. package/vendor/codex-taskboard/dist/web/assets/treeView-QDETBFTQ-C-Bve2kY.js +0 -1
  139. package/vendor/codex-taskboard/dist/web/assets/treemap-6X3UGDF4-BcphHDdV.js +0 -1
  140. package/vendor/codex-taskboard/dist/web/assets/vennDiagram-T6HMQDX7-0wQnwAyA.js +0 -34
  141. package/vendor/codex-taskboard/dist/web/assets/wardley-OPB4EBWU-CJZ4BevB.js +0 -1
  142. package/vendor/codex-taskboard/dist/web/assets/wardleyDiagram-T6FBY63Y-U9nwPrkN.js +0 -78
  143. package/vendor/codex-taskboard/dist/web/assets/xychartDiagram-ELKLHX3M-CyCqnzgE.js +0 -7
  144. package/vendor/codex-taskboard/dist/web/codex-agent-logo.png +0 -0
  145. package/vendor/codex-taskboard/dist/web/codex-app-icon.png +0 -0
  146. package/vendor/codex-taskboard/dist/web/favicon.svg +0 -7
  147. package/vendor/codex-taskboard/dist/web/index.html +0 -20
  148. package/vendor/codex-taskboard/server/ai-chat-catalog.mjs +0 -1031
  149. package/vendor/codex-taskboard/server/ai-chat-process.mjs +0 -462
  150. package/vendor/codex-taskboard/server/ai-chat.mjs +0 -1237
  151. package/vendor/codex-taskboard/server/ai-turn-owner.mjs +0 -39
  152. package/vendor/codex-taskboard/server/app.mjs +0 -3211
  153. package/vendor/codex-taskboard/server/cloud-config.mjs +0 -188
  154. package/vendor/codex-taskboard/server/cloud-proxy.mjs +0 -310
  155. package/vendor/codex-taskboard/server/codex-app-server.mjs +0 -376
  156. package/vendor/codex-taskboard/server/codex-slash-commands-0.139.0.json +0 -62
  157. package/vendor/codex-taskboard/server/database.mjs +0 -2845
  158. package/vendor/codex-taskboard/server/index.mjs +0 -41
  159. package/vendor/codex-taskboard/server/jira-config.mjs +0 -181
  160. package/vendor/codex-taskboard/server/jira-integration.mjs +0 -487
  161. package/vendor/codex-taskboard/server/project-summary.mjs +0 -159
  162. package/vendor/codex-taskboard/shared/api-fields.mjs +0 -163
  163. package/vendor/codex-taskboard/shared/codex-environment.mjs +0 -5
  164. package/vendor/codex-taskboard/shared/codex-executable.mjs +0 -77
  165. package/vendor/codex-taskboard/shared/codex-permissions.mjs +0 -9
  166. package/vendor/codex-taskboard/shared/composer-reference.d.mts +0 -19
  167. package/vendor/codex-taskboard/shared/composer-reference.mjs +0 -90
  168. package/vendor/codex-taskboard/shared/domain.mjs +0 -34
  169. package/vendor/codex-taskboard/shared/executable-command.mjs +0 -13
  170. package/vendor/codex-taskboard/shared/process-tree.mjs +0 -27
  171. package/vendor/codex-taskboard/shared/task-input.mjs +0 -308
  172. package/vendor/codex-taskboard/shared/task-records.mjs +0 -199
  173. package/vendor/codex-taskboard/shared/task-relations.mjs +0 -55
  174. package/vendor/codex-taskboard/shared/taskboard-automation.mjs +0 -277
  175. package/vendor/codex-taskboard/skills/manage-taskboard/SKILL.md +0 -45
  176. package/vendor/codex-taskboard/skills/manage-taskboard/agents/openai.yaml +0 -4
  177. package/vendor/codex-taskboard/skills/manage-taskboard/references/cli.md +0 -211
@@ -1,1237 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
-
5
- import { signalProcessTree } from "../shared/process-tree.mjs";
6
- import { resolveCodexPermissions } from "../shared/codex-permissions.mjs";
7
- import { ApiError } from "../shared/api-fields.mjs";
8
- import {
9
- ComposerCatalog,
10
- discoverAppServerAiCatalog,
11
- discoverAiCatalog,
12
- loadSlashCommands,
13
- resolveAiWorkspace,
14
- } from "./ai-chat-catalog.mjs";
15
- import { CodexAppServer, CodexHostAppServer } from "./codex-app-server.mjs";
16
- import {
17
- buildCodexArgs,
18
- buildCodexPrompt,
19
- normalizeCodexEvent,
20
- spawnCodexTurn,
21
- } from "./ai-chat-process.mjs";
22
-
23
- const SANDBOXES = new Set(["read-only", "workspace-write", "danger-full-access"]);
24
- const ERROR_CONTENT_LIMIT = 65_536;
25
- const AGENT_DISPATCH_PROTOCOL = "taskboard.agent.v1";
26
- const SKILL_MARKER = "\uFFFC";
27
- const CODEX_IMAGE_TYPES = new Set([
28
- "image/gif",
29
- "image/jpeg",
30
- "image/png",
31
- "image/webp",
32
- ]);
33
-
34
- function cappedError(value) {
35
- const message = value instanceof Error ? value.message : String(value ?? "");
36
- return message.slice(0, ERROR_CONTENT_LIMIT);
37
- }
38
-
39
- function agentDispatchText(agent) {
40
- return [
41
- `Taskboard private agent dispatch (${AGENT_DISPATCH_PROTOCOL}):`,
42
- `Use the configured Taskboard agent ${JSON.stringify(agent.name)} (id ${JSON.stringify(agent.id)}) for this request.`,
43
- "This is Taskboard product-private routing context, not a Codex App Server UserInput type.",
44
- ].join("\n");
45
- }
46
-
47
- function signalProcessGroup(child, signal) {
48
- signalProcessTree(child, signal);
49
- }
50
-
51
- function wait(milliseconds) {
52
- return new Promise((resolve) => {
53
- const timer = setTimeout(resolve, milliseconds);
54
- timer.unref();
55
- });
56
- }
57
-
58
- function appServerThreadSettings(thread, resolved) {
59
- const permission = resolveCodexPermissions(thread.sandbox);
60
- return {
61
- model: thread.model,
62
- cwd: resolved.workspacePath,
63
- runtimeWorkspaceRoots: [resolved.workspacePath, ...resolved.addDirectories],
64
- approvalPolicy: permission.approvalPolicy,
65
- ...(permission.reviewer ? { approvalsReviewer: permission.reviewer } : {}),
66
- sandbox: permission.sandbox,
67
- };
68
- }
69
-
70
- function codexTargetFromOrigin(origin) {
71
- if (
72
- origin?.codexProjectKind !== "remote"
73
- || !origin.codexProjectId
74
- || !origin.codexHostId
75
- || !origin.workspacePath
76
- ) return undefined;
77
- return {
78
- codexProjectId: origin.codexProjectId,
79
- codexProjectKind: "remote",
80
- codexHostId: origin.codexHostId,
81
- workspacePath: origin.workspacePath,
82
- };
83
- }
84
-
85
- function normalizedAppServerItem(item) {
86
- if (!item || typeof item !== "object") return null;
87
- const itemId = typeof item.id === "string" ? item.id.slice(0, ERROR_CONTENT_LIMIT) : "";
88
- const baseData = { status: item.status ?? "completed", ...(itemId ? { itemId } : {}) };
89
- if (item.type === "agentMessage") {
90
- return {
91
- type: "agent_message",
92
- role: "assistant",
93
- content: cappedError(item.text),
94
- data: baseData,
95
- };
96
- }
97
- if (item.type === "commandExecution") {
98
- return {
99
- type: "command_execution",
100
- role: "activity",
101
- content: cappedError(item.command),
102
- data: {
103
- ...baseData,
104
- command: cappedError(item.command),
105
- ...(typeof item.aggregatedOutput === "string"
106
- ? { output: cappedError(item.aggregatedOutput) }
107
- : {}),
108
- ...(Number.isInteger(item.exitCode) ? { exitCode: item.exitCode } : {}),
109
- },
110
- };
111
- }
112
- if (item.type === "fileChange") {
113
- const files = Array.isArray(item.changes)
114
- ? item.changes.flatMap((change) => (
115
- typeof change?.path === "string" ? [change.path.slice(0, ERROR_CONTENT_LIMIT)] : []
116
- ))
117
- : [];
118
- return {
119
- type: "file_change",
120
- role: "activity",
121
- content: files.join("\n").slice(0, ERROR_CONTENT_LIMIT),
122
- data: { ...baseData, files },
123
- };
124
- }
125
- if (item.type === "mcpToolCall") {
126
- return {
127
- type: "mcp_tool_call",
128
- role: item.error ? "error" : "activity",
129
- content: `${item.server ?? ""}.${item.tool ?? ""}`.replace(/^\.|\.$/g, ""),
130
- data: {
131
- ...baseData,
132
- ...(typeof item.server === "string" ? { server: item.server } : {}),
133
- ...(typeof item.tool === "string" ? { tool: item.tool } : {}),
134
- },
135
- };
136
- }
137
- return null;
138
- }
139
-
140
- export class AiChatService {
141
- constructor(options) {
142
- this.database = options.database;
143
- this.codexExecutable = options.codexExecutable;
144
- this.codexStatePath = options.codexStatePath;
145
- this.manageTaskboardSkillPath = options.manageTaskboardSkillPath;
146
- this.processEnv = options.processEnv ?? process.env;
147
- this.killGraceMs = options.killGraceMs ?? 1_000;
148
- this.appServer = options.appServer ?? new CodexAppServer({
149
- executable: this.codexExecutable,
150
- processEnv: this.processEnv,
151
- });
152
- this.composerCatalog = options.composerCatalog ?? new ComposerCatalog({
153
- appServer: this.appServer,
154
- issueSlashCommands: () => loadSlashCommands(),
155
- });
156
- this.remoteAppServerFactory = options.remoteAppServerFactory
157
- ?? ((hostId) => new CodexHostAppServer({ hostId }));
158
- this.remoteRuntimes = new Map();
159
- this.resolveContext = options.resolveContext ?? (async (projectId, issueId) => {
160
- const resolved = await resolveAiWorkspace(projectId, this.codexStatePath, this.database);
161
- let issue;
162
- if (issueId !== undefined) {
163
- issue = this.database.getTask(issueId);
164
- if (!issue || issue.projectId !== projectId || issue.archivedAt != null) {
165
- throw new ApiError(
166
- 404,
167
- "AI_CHAT_ISSUE_NOT_FOUND",
168
- `Task '${issueId}' is not an active task in project '${projectId}'`,
169
- );
170
- }
171
- }
172
- return { ...resolved, issue };
173
- });
174
- this.active = new Map();
175
- this.listeners = new Map();
176
- this.completions = new Map();
177
- this.unsubscribeAppServer = this.appServer.subscribe((notification, child) => {
178
- this.#handleAppServerNotification(this.appServer, notification, child);
179
- });
180
- }
181
-
182
- #runtimeForTarget(target) {
183
- if (target?.codexProjectKind !== "remote") {
184
- return { appServer: this.appServer, composerCatalog: this.composerCatalog };
185
- }
186
- let runtime = this.remoteRuntimes.get(target.codexHostId);
187
- if (runtime) return runtime;
188
- const appServer = this.remoteAppServerFactory(target.codexHostId);
189
- const composerCatalog = new ComposerCatalog({
190
- appServer,
191
- issueSlashCommands: () => loadSlashCommands(),
192
- configuredAgents: async () => ({ agents: [], available: false }),
193
- });
194
- const unsubscribe = appServer.subscribe((notification) => {
195
- this.#handleAppServerNotification(appServer, notification);
196
- });
197
- runtime = { appServer, composerCatalog, unsubscribe };
198
- this.remoteRuntimes.set(target.codexHostId, runtime);
199
- return runtime;
200
- }
201
-
202
- #runtimeForThread(thread) {
203
- return this.#runtimeForTarget(codexTargetFromOrigin(thread.origin));
204
- }
205
-
206
- listThreads() {
207
- return this.database.listAiChatThreads();
208
- }
209
-
210
- getThread(threadId) {
211
- const thread = this.database.getAiChatThread(threadId);
212
- if (!thread) {
213
- throw new ApiError(
214
- 404,
215
- "AI_CHAT_THREAD_NOT_FOUND",
216
- `AI chat thread '${threadId}' does not exist`,
217
- );
218
- }
219
- return thread;
220
- }
221
-
222
- getThreadSummary(threadId) {
223
- return {
224
- thread: this.getThread(threadId),
225
- runs: this.database.listAiChatRuns(threadId),
226
- };
227
- }
228
-
229
- getThreadSnapshot(threadId) {
230
- const thread = this.getThread(threadId);
231
- return {
232
- thread,
233
- events: this.database.listAiChatEvents(threadId),
234
- runs: this.database.listAiChatRuns(threadId),
235
- };
236
- }
237
-
238
- composerCatalogForThread(thread) {
239
- return this.#runtimeForThread(thread).composerCatalog;
240
- }
241
-
242
- getRun(runId) {
243
- const run = this.database.getAiChatRun(runId);
244
- if (!run) {
245
- throw new ApiError(404, "AI_CHAT_RUN_NOT_FOUND", `AI chat run '${runId}' does not exist`);
246
- }
247
- return run;
248
- }
249
-
250
- subscribe(threadId, listener) {
251
- let listeners = this.listeners.get(threadId);
252
- if (!listeners) {
253
- listeners = new Set();
254
- this.listeners.set(threadId, listeners);
255
- }
256
- listeners.add(listener);
257
- return () => {
258
- listeners.delete(listener);
259
- if (listeners.size === 0) this.listeners.delete(threadId);
260
- };
261
- }
262
-
263
- async #catalogForWorkspace(workspacePath) {
264
- return discoverAiCatalog({
265
- codexExecutable: this.codexExecutable,
266
- workspacePath,
267
- processEnv: this.processEnv,
268
- });
269
- }
270
-
271
- async getCatalog(projectId, resolvedContext, codexTarget) {
272
- const resolved = resolvedContext ?? await this.resolveContext(projectId, undefined, codexTarget);
273
- if (resolved.codexProjectKind === "remote") {
274
- const { appServer } = this.#runtimeForTarget(resolved);
275
- return discoverAppServerAiCatalog({ appServer, workspacePath: resolved.workspacePath });
276
- }
277
- return this.#catalogForWorkspace(resolved.workspacePath);
278
- }
279
-
280
- async getComposerCandidates({
281
- projectId,
282
- threadId,
283
- trigger,
284
- query,
285
- codexProjectId,
286
- codexProjectKind,
287
- codexHostId,
288
- workspacePath,
289
- }) {
290
- let thread;
291
- let codexTarget = codexProjectKind === "remote"
292
- ? { codexProjectId, codexProjectKind, codexHostId, workspacePath }
293
- : undefined;
294
- if (threadId !== undefined) {
295
- try {
296
- thread = this.getThread(threadId);
297
- } catch (error) {
298
- if (error instanceof ApiError && error.code === "AI_CHAT_THREAD_NOT_FOUND") {
299
- throw new ApiError(
300
- 400,
301
- "INVALID_COMPOSER_QUERY",
302
- "Composer thread does not exist",
303
- );
304
- }
305
- throw error;
306
- }
307
- if (projectId !== undefined && thread.origin.projectId !== projectId) {
308
- throw new ApiError(
309
- 400,
310
- "INVALID_COMPOSER_QUERY",
311
- "Composer thread does not belong to the selected project",
312
- );
313
- }
314
- projectId = thread.origin.projectId;
315
- codexTarget = codexTargetFromOrigin(thread.origin);
316
- }
317
-
318
- if (projectId === undefined) {
319
- const response = await this.composerCatalog.candidates({ workspacePath: null, trigger, query });
320
- return { ...response, candidates: response.candidates.filter((candidate) => (
321
- candidate.dispatch?.handlerId !== "compact-conversation"
322
- )) };
323
- }
324
-
325
- let resolved;
326
- try {
327
- resolved = await this.resolveContext(projectId, thread?.origin.issueId, codexTarget);
328
- } catch (error) {
329
- if (error instanceof ApiError && ["PROJECT_NOT_FOUND", "AI_CHAT_ISSUE_NOT_FOUND"].includes(error.code)) {
330
- throw new ApiError(400, "INVALID_COMPOSER_QUERY", "Composer project is invalid");
331
- }
332
- throw error;
333
- }
334
- if (thread && resolved.workspacePath !== thread.origin.workspacePath) {
335
- throw new ApiError(
336
- 400,
337
- "INVALID_COMPOSER_QUERY",
338
- "Composer thread workspace no longer matches the selected project",
339
- );
340
- }
341
- const { composerCatalog } = this.#runtimeForTarget(resolved);
342
- const response = await composerCatalog.candidates({
343
- workspacePath: resolved.workspacePath,
344
- trigger,
345
- query,
346
- });
347
- return {
348
- ...response,
349
- candidates: response.candidates.filter((candidate) => (
350
- candidate.dispatch?.handlerId !== "compact-conversation"
351
- || (thread?.codexThreadId && thread.status !== "running")
352
- )),
353
- };
354
- }
355
-
356
- async compactThread(threadId) {
357
- const thread = this.getThread(threadId);
358
- if (thread.status === "running") {
359
- throw new ApiError(409, "AI_CHAT_THREAD_RUNNING", "Cannot compact a running conversation");
360
- }
361
- if (!thread.codexThreadId) {
362
- throw new ApiError(409, "AI_CHAT_THREAD_NOT_STARTED", "Conversation has not started");
363
- }
364
- await this.#runtimeForThread(thread).appServer.compactThread(thread.codexThreadId);
365
- return this.getThread(threadId);
366
- }
367
-
368
- async createThread(input) {
369
- const codexTarget = input.codexProjectKind === "remote" ? input : undefined;
370
- const resolved = await this.resolveContext(input.projectId, input.issueId, codexTarget);
371
- const catalog = await this.getCatalog(input.projectId, resolved, codexTarget);
372
- const model = this.#resolveModel(catalog, input.model);
373
- const reasoningEffort = input.reasoningEffort ?? model.defaultReasoningEffort;
374
- this.#validateReasoningEffort(model, reasoningEffort);
375
- const sandbox = input.sandbox ?? "workspace-write";
376
- this.#validateSandbox(sandbox);
377
-
378
- const issue = resolved.issue;
379
-
380
- return this.database.createAiChatThread({
381
- title: input.title ?? issue?.identifier ?? "New conversation",
382
- origin: {
383
- projectId: resolved.project.id,
384
- projectName: resolved.project.name,
385
- workspacePath: resolved.workspacePath,
386
- ...(resolved.codexProjectKind === "remote" ? {
387
- codexProjectId: resolved.codexProjectId,
388
- codexProjectKind: resolved.codexProjectKind,
389
- codexHostId: resolved.codexHostId,
390
- } : {}),
391
- ...(issue ? { issueId: issue.id, issueIdentifier: issue.identifier } : {}),
392
- },
393
- model: model.slug,
394
- reasoningEffort,
395
- sandbox,
396
- });
397
- }
398
-
399
- async updateThread(threadId, changes) {
400
- let thread = this.getThread(threadId);
401
- const changesSettings = ["model", "reasoningEffort", "sandbox"].some(
402
- (key) => Object.hasOwn(changes, key),
403
- );
404
- const wasActive = changesSettings && this.#threadIsActive(thread);
405
-
406
- if (Object.hasOwn(changes, "sandbox")) this.#validateSandbox(changes.sandbox);
407
- if (Object.hasOwn(changes, "model") || Object.hasOwn(changes, "reasoningEffort")) {
408
- const catalog = await this.getCatalog(
409
- thread.origin.projectId,
410
- undefined,
411
- codexTargetFromOrigin(thread.origin),
412
- );
413
- thread = this.getThread(threadId);
414
- const model = this.#resolveModel(catalog, changes.model ?? thread.model);
415
- const reasoningEffort = changes.reasoningEffort ?? thread.reasoningEffort;
416
- this.#validateReasoningEffort(model, reasoningEffort);
417
- }
418
- if (wasActive || (changesSettings && this.#threadIsActive(thread))) {
419
- throw new ApiError(
420
- 409,
421
- "THREAD_BUSY",
422
- `AI chat thread '${threadId}' has a running turn`,
423
- );
424
- }
425
-
426
- return this.database.updateAiChatThread(threadId, changes);
427
- }
428
-
429
- deleteThread(threadId) {
430
- const thread = this.getThread(threadId);
431
- if (this.#threadIsActive(thread)) {
432
- throw new ApiError(
433
- 409,
434
- "THREAD_BUSY",
435
- `AI chat thread '${threadId}' has a running turn`,
436
- );
437
- }
438
- return this.database.deleteAiChatThread(threadId);
439
- }
440
-
441
- async startTurn(threadId, input) {
442
- let thread = this.getThread(threadId);
443
- if (this.#threadIsActive(thread)) {
444
- throw new ApiError(
445
- 409,
446
- "THREAD_BUSY",
447
- `AI chat thread '${threadId}' has a running turn`,
448
- );
449
- }
450
- if (input?.contractVersion === "composer.v1") {
451
- return this.#startComposerTurn(thread, input);
452
- }
453
- this.#validateTurnInput(input);
454
- if (thread.sandbox === "danger-full-access" && input.dangerFullAccessConfirmed !== true) {
455
- throw new ApiError(
456
- 400,
457
- "DANGER_CONFIRMATION_REQUIRED",
458
- "danger-full-access must be confirmed for every turn",
459
- );
460
- }
461
-
462
- const codexTarget = codexTargetFromOrigin(thread.origin);
463
- const resolved = await this.resolveContext(
464
- thread.origin.projectId,
465
- thread.origin.issueId,
466
- codexTarget,
467
- );
468
- const catalog = await this.getCatalog(thread.origin.projectId, resolved, codexTarget);
469
-
470
- thread = this.getThread(threadId);
471
- if (this.#threadIsActive(thread)) {
472
- throw new ApiError(
473
- 409,
474
- "THREAD_BUSY",
475
- `AI chat thread '${threadId}' has a running turn`,
476
- );
477
- }
478
- if (thread.sandbox === "danger-full-access" && input.dangerFullAccessConfirmed !== true) {
479
- throw new ApiError(
480
- 400,
481
- "DANGER_CONFIRMATION_REQUIRED",
482
- "danger-full-access must be confirmed for every turn",
483
- );
484
- }
485
- const model = this.#resolveModel(catalog, thread.model);
486
- this.#validateReasoningEffort(model, thread.reasoningEffort);
487
- if (resolved.workspacePath !== thread.origin.workspacePath) {
488
- throw new ApiError(
489
- 409,
490
- "PROJECT_WORKSPACE_CHANGED",
491
- "The project's device workspace no longer matches this conversation",
492
- );
493
- }
494
-
495
- const skillIds = input.skillIds ?? [];
496
- const availableSkills = new Map(
497
- catalog.skills
498
- .filter((skill) => skill.id !== "manage-taskboard")
499
- .map((skill) => [skill.id, skill]),
500
- );
501
- for (const skillId of skillIds) {
502
- if (!availableSkills.has(skillId)) {
503
- throw new ApiError(400, "INVALID_SKILL", `Unknown or unavailable skill '${skillId}'`);
504
- }
505
- }
506
- const selectedSkills = skillIds.map((skillId) => availableSkills.get(skillId));
507
-
508
- if (resolved.codexProjectKind === "remote") {
509
- return this.#startRemoteTurn(thread, input, resolved, selectedSkills);
510
- }
511
-
512
- const attachments = input.attachments ?? [];
513
- const {
514
- temporaryDirectory,
515
- attachmentPaths,
516
- imagePaths,
517
- } = await this.#writeTurnAttachments(attachments);
518
- try {
519
- const args = buildCodexArgs(thread, resolved.addDirectories, imagePaths);
520
- const prompt = buildCodexPrompt(
521
- thread,
522
- {
523
- message: input.message,
524
- skills: selectedSkills,
525
- attachmentPaths,
526
- },
527
- this.manageTaskboardSkillPath,
528
- );
529
- const run = this.database.createAiChatRun({ threadId });
530
- this.#emit(threadId, { type: "ai.run", run });
531
- const userEventData = {};
532
- if (skillIds.length > 0) userEventData.skillIds = skillIds;
533
- if (attachments.length > 0) {
534
- userEventData.attachments = attachments.map(({ filename, contentType, size }) => ({
535
- filename,
536
- contentType,
537
- size,
538
- }));
539
- }
540
- const userEvent = this.database.insertAiChatEvent({
541
- threadId,
542
- runId: run.id,
543
- type: "user_message",
544
- role: "user",
545
- content: input.message,
546
- data: Object.keys(userEventData).length > 0 ? userEventData : undefined,
547
- });
548
- this.#emit(threadId, { type: "ai.event", event: userEvent });
549
-
550
- const resumingThreadId = thread.codexThreadId;
551
- let startedThreadId = null;
552
- let terminalOutcome = null;
553
- let terminalError = "";
554
- let pendingError = "";
555
- const { child, completion } = spawnCodexTurn({
556
- executable: this.codexExecutable,
557
- args,
558
- prompt,
559
- env: this.processEnv,
560
- onRawEvent: (raw) => {
561
- const normalized = normalizeCodexEvent(raw);
562
- if (!normalized) return;
563
- if (normalized.kind === "thread.started") {
564
- if (
565
- (resumingThreadId && normalized.threadId !== resumingThreadId)
566
- || (startedThreadId && normalized.threadId !== startedThreadId)
567
- ) {
568
- throw new Error("Codex returned an unexpected thread id");
569
- }
570
- startedThreadId = normalized.threadId;
571
- this.database.updateAiChatThread(threadId, { codexThreadId: normalized.threadId });
572
- return;
573
- }
574
- const event = this.database.insertAiChatEvent({
575
- threadId,
576
- runId: run.id,
577
- type: normalized.type,
578
- role: normalized.role,
579
- content: normalized.content,
580
- data: normalized.data,
581
- });
582
- if (raw.type === "turn.completed" && terminalOutcome === null) {
583
- terminalOutcome = "completed";
584
- } else if (raw.type === "turn.failed") {
585
- terminalOutcome = "failed";
586
- terminalError ||= normalized.content;
587
- } else if (raw.type === "error") {
588
- pendingError ||= normalized.content;
589
- }
590
- this.#emit(threadId, { type: "ai.event", event });
591
- },
592
- });
593
-
594
- const active = { child, threadId, interrupted: false, temporaryDirectory };
595
- this.active.set(run.id, active);
596
- const finalization = completion.then(
597
- (result) => this.#finishRun({
598
- run,
599
- active,
600
- result,
601
- resumingThreadId,
602
- startedThreadId: () => startedThreadId,
603
- terminalOutcome: () => terminalOutcome,
604
- terminalError: () => terminalError,
605
- pendingError: () => pendingError,
606
- }),
607
- (error) => this.#finishRun({
608
- run,
609
- active,
610
- error,
611
- resumingThreadId,
612
- startedThreadId: () => startedThreadId,
613
- terminalOutcome: () => terminalOutcome,
614
- terminalError: () => terminalError,
615
- pendingError: () => pendingError,
616
- }),
617
- );
618
- this.completions.set(run.id, finalization);
619
- void finalization.finally(() => this.completions.delete(run.id)).catch(() => {});
620
- return run;
621
- } catch (error) {
622
- if (temporaryDirectory) {
623
- await rm(temporaryDirectory, { recursive: true, force: true });
624
- }
625
- throw error;
626
- }
627
- }
628
-
629
- async interrupt(runId) {
630
- let run = this.getRun(runId);
631
- if (run.status !== "running") return run;
632
-
633
- const active = this.active.get(runId);
634
- if (!active) {
635
- run = this.database.updateAiChatRun(runId, {
636
- status: "interrupted",
637
- error: "Interrupted",
638
- finishedAt: new Date().toISOString(),
639
- });
640
- this.#emit(run.threadId, { type: "ai.run", run });
641
- return run;
642
- }
643
-
644
- active.interrupted = true;
645
- if (active.kind === "app-server") {
646
- if (active.turnId) {
647
- try {
648
- await active.appServer.interruptTurn({
649
- threadId: active.appServerThreadId,
650
- turnId: active.turnId,
651
- });
652
- } catch {}
653
- }
654
- const completion = this.completions.get(runId);
655
- if (completion) {
656
- await Promise.race([completion.catch(() => {}), wait(this.killGraceMs + 25)]);
657
- }
658
- if (this.active.has(runId)) await this.#finishAppServerRun(active, "interrupted");
659
- return this.getRun(runId);
660
- }
661
- signalProcessGroup(active.child, "SIGTERM");
662
- const timer = setTimeout(() => {
663
- if (this.active.has(runId)) signalProcessGroup(active.child, "SIGKILL");
664
- }, this.killGraceMs);
665
- timer.unref();
666
-
667
- const completion = this.completions.get(runId);
668
- if (completion) {
669
- await Promise.race([completion.catch(() => {}), wait(this.killGraceMs + 25)]);
670
- }
671
- return this.getRun(runId);
672
- }
673
-
674
- async close() {
675
- const entries = [...this.active.entries()];
676
- for (const [, active] of entries) {
677
- active.interrupted = true;
678
- if (active.kind === "app-server") {
679
- if (active.turnId) {
680
- void active.appServer.interruptTurn({
681
- threadId: active.appServerThreadId,
682
- turnId: active.turnId,
683
- }).catch(() => {});
684
- }
685
- } else {
686
- signalProcessGroup(active.child, "SIGTERM");
687
- }
688
- }
689
-
690
- const completions = entries
691
- .map(([runId]) => this.completions.get(runId))
692
- .filter(Boolean);
693
- if (completions.length > 0) {
694
- const settled = Promise.allSettled(completions);
695
- await Promise.race([settled, wait(this.killGraceMs)]);
696
- for (const [runId, active] of entries) {
697
- if (active.kind !== "app-server" && this.active.has(runId)) {
698
- signalProcessGroup(active.child, "SIGKILL");
699
- }
700
- }
701
- for (const [, active] of entries) {
702
- if (active.kind === "app-server" && this.active.has(active.run.id)) {
703
- await this.#finishAppServerRun(active, "interrupted");
704
- }
705
- }
706
- await settled;
707
- }
708
- for (const [, active] of entries) {
709
- if (active.kind === "app-server" && this.active.has(active.run.id)) {
710
- await this.#finishAppServerRun(active, "interrupted");
711
- }
712
- }
713
- this.unsubscribeAppServer();
714
- this.composerCatalog.close();
715
- await this.appServer.close();
716
- for (const runtime of this.remoteRuntimes.values()) {
717
- runtime.unsubscribe();
718
- runtime.composerCatalog.close();
719
- await runtime.appServer.close();
720
- }
721
- this.remoteRuntimes.clear();
722
- this.listeners.clear();
723
- }
724
-
725
- #resolveModel(catalog, requestedModel) {
726
- const model = requestedModel === undefined
727
- ? catalog.models[0]
728
- : catalog.models.find((candidate) => candidate.slug === requestedModel);
729
- if (!model) {
730
- throw new ApiError(
731
- 400,
732
- "INVALID_MODEL",
733
- requestedModel === undefined
734
- ? "Codex did not provide an available model"
735
- : `Unknown model '${requestedModel}'`,
736
- );
737
- }
738
- return model;
739
- }
740
-
741
- #validateReasoningEffort(model, reasoningEffort) {
742
- if (!model.supportedReasoningEfforts.includes(reasoningEffort)) {
743
- throw new ApiError(
744
- 400,
745
- "INVALID_REASONING_EFFORT",
746
- `Reasoning effort '${reasoningEffort}' is not supported by model '${model.slug}'`,
747
- );
748
- }
749
- }
750
-
751
- #validateSandbox(sandbox) {
752
- if (!SANDBOXES.has(sandbox)) {
753
- throw new ApiError(
754
- 400,
755
- "INVALID_SANDBOX",
756
- "'sandbox' must be read-only, workspace-write, or danger-full-access",
757
- );
758
- }
759
- }
760
-
761
- #validateTurnInput(input) {
762
- if (
763
- !input
764
- || typeof input.message !== "string"
765
- || input.message.length > 100_000
766
- || (
767
- input.message.trim() === ""
768
- && (!Array.isArray(input.attachments) || input.attachments.length === 0)
769
- )
770
- ) {
771
- throw new ApiError(
772
- 400,
773
- "INVALID_MESSAGE",
774
- "A message or at least one attachment is required",
775
- );
776
- }
777
- if (
778
- input.skillIds !== undefined
779
- && (
780
- !Array.isArray(input.skillIds)
781
- || input.skillIds.length > 20
782
- || input.skillIds.some((skillId) => typeof skillId !== "string" || !skillId)
783
- )
784
- ) {
785
- throw new ApiError(
786
- 400,
787
- "INVALID_SKILL",
788
- "'skillIds' must contain at most 20 skill ids",
789
- );
790
- }
791
- }
792
-
793
- async #startAppServerRun({
794
- thread,
795
- resolved,
796
- appServer,
797
- userInput,
798
- userEvent,
799
- temporaryDirectory = null,
800
- }) {
801
- const settings = appServerThreadSettings(thread, resolved);
802
- let appServerThreadId = thread.codexThreadId;
803
- if (appServerThreadId) {
804
- const resumed = await appServer.resumeThread({
805
- threadId: appServerThreadId,
806
- ...settings,
807
- });
808
- if (resumed?.thread?.id !== appServerThreadId) {
809
- throw new Error("Codex returned an unexpected resumed thread id");
810
- }
811
- } else {
812
- const started = await appServer.startThread(settings);
813
- appServerThreadId = started?.thread?.id;
814
- if (typeof appServerThreadId !== "string" || !appServerThreadId) {
815
- throw new Error("Codex did not provide a thread id");
816
- }
817
- this.database.updateAiChatThread(thread.id, { codexThreadId: appServerThreadId });
818
- }
819
-
820
- const run = this.database.createAiChatRun({ threadId: thread.id });
821
- this.#emit(thread.id, { type: "ai.run", run });
822
- const storedUserEvent = this.database.insertAiChatEvent({
823
- threadId: thread.id,
824
- runId: run.id,
825
- type: "user_message",
826
- role: "user",
827
- ...userEvent,
828
- });
829
- this.#emit(thread.id, { type: "ai.event", event: storedUserEvent });
830
-
831
- let resolveCompletion;
832
- const completion = new Promise((resolve) => { resolveCompletion = resolve; });
833
- const active = {
834
- kind: "app-server",
835
- run,
836
- threadId: thread.id,
837
- appServer,
838
- appServerChild: appServer === this.appServer ? appServer.child : undefined,
839
- appServerThreadId,
840
- turnId: null,
841
- interrupted: false,
842
- temporaryDirectory,
843
- resolveCompletion,
844
- };
845
- this.active.set(run.id, active);
846
- const finalization = completion.finally(() => this.completions.delete(run.id));
847
- this.completions.set(run.id, finalization);
848
- try {
849
- const started = await appServer.startTurn({
850
- threadId: appServerThreadId,
851
- input: userInput,
852
- effort: thread.reasoningEffort,
853
- });
854
- const turnId = started?.turn?.id;
855
- if (typeof turnId !== "string" || !turnId) {
856
- throw new Error("Codex did not provide a turn id");
857
- }
858
- active.turnId = turnId;
859
- } catch (error) {
860
- await this.#finishAppServerRun(active, "failed", error);
861
- throw error;
862
- }
863
- return run;
864
- }
865
-
866
- #remoteAttachmentInput(attachment) {
867
- const url = `data:${attachment.contentType};base64,${attachment.data.toString("base64")}`;
868
- if (CODEX_IMAGE_TYPES.has(attachment.contentType)) return { type: "image", url };
869
- if (attachment.contentType.startsWith("audio/")) return { type: "audio", url };
870
- return {
871
- type: "text",
872
- text: `\n\nAttached file ${attachment.filename}: ${url}`,
873
- };
874
- }
875
-
876
- async #startRemoteTurn(thread, input, resolved, selectedSkills) {
877
- const userInput = [];
878
- const messageParts = input.message.split(SKILL_MARKER);
879
- for (const [index, text] of messageParts.entries()) {
880
- if (text) userInput.push({ type: "text", text });
881
- const skill = selectedSkills[index];
882
- if (skill) userInput.push({ type: "skill", name: skill.id, path: skill.path });
883
- }
884
- for (const attachment of input.attachments ?? []) {
885
- userInput.push(this.#remoteAttachmentInput(attachment));
886
- }
887
- const userEventData = {};
888
- if (selectedSkills.length > 0) userEventData.skillIds = selectedSkills.map((skill) => skill.id);
889
- if ((input.attachments ?? []).length > 0) {
890
- userEventData.attachments = input.attachments.map(({ filename, contentType, size }) => ({
891
- filename,
892
- contentType,
893
- size,
894
- }));
895
- }
896
- return this.#startAppServerRun({
897
- thread,
898
- resolved,
899
- appServer: this.#runtimeForTarget(resolved).appServer,
900
- userInput,
901
- userEvent: {
902
- content: input.message,
903
- data: Object.keys(userEventData).length > 0 ? userEventData : undefined,
904
- },
905
- });
906
- }
907
-
908
- async #startComposerTurn(thread, input) {
909
- if (thread.sandbox === "danger-full-access" && input.dangerFullAccessConfirmed !== true) {
910
- throw new ApiError(
911
- 400,
912
- "DANGER_CONFIRMATION_REQUIRED",
913
- "danger-full-access must be confirmed for every turn",
914
- );
915
- }
916
-
917
- const codexTarget = codexTargetFromOrigin(thread.origin);
918
- const resolved = await this.resolveContext(
919
- thread.origin.projectId,
920
- thread.origin.issueId,
921
- codexTarget,
922
- );
923
- const runtime = this.#runtimeForTarget(resolved);
924
- thread = this.getThread(thread.id);
925
- if (this.#threadIsActive(thread)) {
926
- throw new ApiError(409, "THREAD_BUSY", `AI chat thread '${thread.id}' has a running turn`);
927
- }
928
- if (resolved.workspacePath !== thread.origin.workspacePath) {
929
- throw new ApiError(
930
- 409,
931
- "PROJECT_WORKSPACE_CHANGED",
932
- "The project's device workspace no longer matches this conversation",
933
- );
934
- }
935
-
936
- const nodes = input.document.nodes;
937
- const unsupportedIndex = nodes.findIndex((node) => !["text", "skill", "agent"].includes(node.type));
938
- if (unsupportedIndex >= 0) {
939
- throw new ApiError(
940
- 422,
941
- "COMPOSER_NODE_UNSUPPORTED",
942
- `Unsupported composer node at index ${unsupportedIndex}`,
943
- { nodeIndex: unsupportedIndex },
944
- );
945
- }
946
- const resolvedReferences = nodes.some((node) => node.type === "skill" || node.type === "agent")
947
- ? await runtime.composerCatalog.resolveReferences({
948
- workspacePath: resolved.workspacePath,
949
- revision: input.revision,
950
- nodes,
951
- })
952
- : nodes.map(() => null);
953
- const attachments = input.attachments ?? [];
954
- if (
955
- nodes.every((node) => node.type !== "text" || node.text.trim() === "")
956
- && !nodes.some((node) => node.type === "skill" || node.type === "agent")
957
- && attachments.length === 0
958
- ) {
959
- throw new ApiError(
960
- 400,
961
- "INVALID_COMPOSER_DOCUMENT",
962
- "A composer message or at least one attachment is required",
963
- );
964
- }
965
-
966
- const { temporaryDirectory, attachmentPaths } = resolved.codexProjectKind === "remote"
967
- ? { temporaryDirectory: null, attachmentPaths: [] }
968
- : await this.#writeTurnAttachments(attachments);
969
- try {
970
- const userInput = nodes.flatMap((node, nodeIndex) => {
971
- if (node.type === "text") return { type: "text", text: node.text };
972
- const reference = resolvedReferences[nodeIndex];
973
- if (node.type === "skill") {
974
- return [
975
- { type: "text", text: `$${reference.name}` },
976
- { type: "skill", name: reference.name, path: reference.path },
977
- ];
978
- }
979
- return { type: "text", text: agentDispatchText(reference) };
980
- });
981
- for (const [index, attachment] of attachments.entries()) {
982
- if (resolved.codexProjectKind === "remote") {
983
- userInput.push(this.#remoteAttachmentInput(attachment));
984
- continue;
985
- }
986
- const attachmentPath = attachmentPaths[index];
987
- if (CODEX_IMAGE_TYPES.has(attachment.contentType)) {
988
- userInput.push({ type: "localImage", path: attachmentPath });
989
- } else {
990
- userInput.push({ type: "text", text: `\n\nAttached file: ${attachmentPath}` });
991
- }
992
- }
993
-
994
- const agentDispatches = nodes.flatMap((node, nodeIndex) => {
995
- if (node.type !== "agent") return [];
996
- const reference = resolvedReferences[nodeIndex];
997
- return [{ nodeIndex, id: reference.id, name: reference.name }];
998
- });
999
- const run = await this.#startAppServerRun({
1000
- thread,
1001
- resolved,
1002
- appServer: runtime.appServer,
1003
- userInput,
1004
- temporaryDirectory,
1005
- userEvent: {
1006
- content: nodes.map((node) => (
1007
- node.type === "text" ? node.text : `@${node.label}`
1008
- )).join(""),
1009
- data: {
1010
- contractVersion: "composer.v1",
1011
- revision: input.revision,
1012
- document: input.document,
1013
- ...(agentDispatches.length > 0
1014
- ? {
1015
- dispatchProtocol: AGENT_DISPATCH_PROTOCOL,
1016
- agentDispatches,
1017
- }
1018
- : {}),
1019
- ...(attachments.length > 0
1020
- ? {
1021
- attachments: attachments.map(({ filename, contentType, size }) => ({
1022
- filename,
1023
- contentType,
1024
- size,
1025
- })),
1026
- }
1027
- : {}),
1028
- },
1029
- },
1030
- });
1031
- return run;
1032
- } catch (error) {
1033
- if (temporaryDirectory && ![...this.active.values()].some(
1034
- (active) => active.temporaryDirectory === temporaryDirectory,
1035
- )) {
1036
- await rm(temporaryDirectory, { recursive: true, force: true });
1037
- }
1038
- throw error;
1039
- }
1040
- }
1041
-
1042
- #handleAppServerNotification(appServer, notification, child) {
1043
- const params = notification?.params;
1044
- if (!params || typeof params !== "object") return;
1045
- if (notification.method === "app-server/terminated") {
1046
- // A remote bridge disconnect does not establish that the remote turn stopped.
1047
- if (appServer !== this.appServer || !child) return;
1048
- for (const active of this.active.values()) {
1049
- if (
1050
- active.kind !== "app-server"
1051
- || active.appServer !== appServer
1052
- || active.appServerChild !== child
1053
- ) continue;
1054
- void this.#finishAppServerRun(
1055
- active,
1056
- active.interrupted ? "interrupted" : "failed",
1057
- params.message,
1058
- );
1059
- }
1060
- return;
1061
- }
1062
- const active = [...this.active.values()].find((candidate) => (
1063
- candidate.kind === "app-server"
1064
- && candidate.appServer === appServer
1065
- && candidate.appServerChild === child
1066
- && candidate.appServerThreadId === params.threadId
1067
- && (!candidate.turnId || !params.turnId || candidate.turnId === params.turnId)
1068
- ));
1069
- if (!active) return;
1070
-
1071
- if (notification.method === "turn/started") {
1072
- if (typeof params.turn?.id === "string") active.turnId = params.turn.id;
1073
- return;
1074
- }
1075
- if (notification.method === "item/completed") {
1076
- const normalized = normalizedAppServerItem(params.item);
1077
- if (!normalized) return;
1078
- const event = this.database.insertAiChatEvent({
1079
- threadId: active.threadId,
1080
- runId: active.run.id,
1081
- ...normalized,
1082
- });
1083
- this.#emit(active.threadId, { type: "ai.event", event });
1084
- return;
1085
- }
1086
- if (notification.method !== "turn/completed") return;
1087
- const status = params.turn?.status;
1088
- if (active.interrupted || status === "interrupted") {
1089
- void this.#finishAppServerRun(active, "interrupted");
1090
- } else if (status === "completed") {
1091
- void this.#finishAppServerRun(active, "completed");
1092
- } else {
1093
- void this.#finishAppServerRun(
1094
- active,
1095
- "failed",
1096
- params.turn?.error?.message ?? "Codex reported a failed turn",
1097
- );
1098
- }
1099
- }
1100
-
1101
- async #finishAppServerRun(active, status, error) {
1102
- if (!this.active.delete(active.run.id)) return this.getRun(active.run.id);
1103
- let publicError = null;
1104
- if (status === "interrupted") publicError = "Interrupted";
1105
- if (status === "failed") publicError = cappedError(error) || "Codex turn failed";
1106
- try {
1107
- if (status === "failed") {
1108
- const errorEvent = this.database.insertAiChatEvent({
1109
- threadId: active.threadId,
1110
- runId: active.run.id,
1111
- type: "error",
1112
- role: "error",
1113
- content: publicError,
1114
- data: { status: "failed" },
1115
- });
1116
- this.#emit(active.threadId, { type: "ai.event", event: errorEvent });
1117
- }
1118
- const run = this.database.updateAiChatRun(active.run.id, {
1119
- status,
1120
- exitCode: null,
1121
- error: publicError,
1122
- finishedAt: new Date().toISOString(),
1123
- });
1124
- this.#emit(active.threadId, { type: "ai.run", run });
1125
- return run;
1126
- } finally {
1127
- if (active.temporaryDirectory) {
1128
- await rm(active.temporaryDirectory, { recursive: true, force: true });
1129
- }
1130
- active.resolveCompletion();
1131
- }
1132
- }
1133
-
1134
- async #writeTurnAttachments(attachments) {
1135
- if (attachments.length === 0) {
1136
- return { temporaryDirectory: null, attachmentPaths: [], imagePaths: [] };
1137
- }
1138
- const temporaryDirectory = await mkdtemp(
1139
- path.join(os.tmpdir(), "codex-taskboard-ai-turn-"),
1140
- );
1141
- try {
1142
- const attachmentPaths = [];
1143
- const imagePaths = [];
1144
- for (const [index, attachment] of attachments.entries()) {
1145
- const attachmentPath = path.join(
1146
- temporaryDirectory,
1147
- `attachment-${index + 1}-${attachment.filename}`,
1148
- );
1149
- await writeFile(attachmentPath, attachment.data, { flag: "wx", mode: 0o600 });
1150
- attachmentPaths.push(attachmentPath);
1151
- if (CODEX_IMAGE_TYPES.has(attachment.contentType)) imagePaths.push(attachmentPath);
1152
- }
1153
- return { temporaryDirectory, attachmentPaths, imagePaths };
1154
- } catch (error) {
1155
- await rm(temporaryDirectory, { recursive: true, force: true });
1156
- throw error;
1157
- }
1158
- }
1159
-
1160
- #threadIsActive(thread) {
1161
- return Boolean(thread.currentRun)
1162
- || [...this.active.values()].some((active) => active.threadId === thread.id);
1163
- }
1164
-
1165
- async #finishRun({
1166
- run,
1167
- active,
1168
- result,
1169
- error,
1170
- resumingThreadId,
1171
- startedThreadId,
1172
- terminalOutcome,
1173
- terminalError,
1174
- pendingError,
1175
- }) {
1176
- let status;
1177
- let publicError = null;
1178
- if (active.interrupted) {
1179
- status = "interrupted";
1180
- publicError = "Interrupted";
1181
- } else if (error) {
1182
- status = "failed";
1183
- publicError = cappedError(error) || "Codex turn failed";
1184
- } else if (terminalOutcome() === "failed") {
1185
- status = "failed";
1186
- publicError = terminalError() || "Codex reported a failed turn";
1187
- } else if (result.exitCode !== 0) {
1188
- status = "failed";
1189
- publicError = result.exitCode === null
1190
- ? `Codex exited due to signal ${result.signal ?? "unknown"}`
1191
- : `Codex exited with code ${result.exitCode}`;
1192
- } else if (terminalOutcome() !== "completed") {
1193
- status = "failed";
1194
- publicError = pendingError() || "Codex exited without reporting turn completion";
1195
- } else if (!resumingThreadId && !startedThreadId()) {
1196
- status = "failed";
1197
- publicError = "Codex did not provide a thread id";
1198
- } else {
1199
- status = "completed";
1200
- }
1201
-
1202
- try {
1203
- if (status === "failed" && terminalOutcome() !== "failed") {
1204
- const errorEvent = this.database.insertAiChatEvent({
1205
- threadId: run.threadId,
1206
- runId: run.id,
1207
- type: "error",
1208
- role: "error",
1209
- content: cappedError(publicError),
1210
- data: { status: "failed" },
1211
- });
1212
- this.#emit(run.threadId, { type: "ai.event", event: errorEvent });
1213
- }
1214
- const updated = this.database.updateAiChatRun(run.id, {
1215
- status,
1216
- exitCode: result?.exitCode ?? null,
1217
- error: publicError === null ? null : cappedError(publicError),
1218
- finishedAt: new Date().toISOString(),
1219
- });
1220
- this.#emit(run.threadId, { type: "ai.run", run: updated });
1221
- return updated;
1222
- } finally {
1223
- this.active.delete(run.id);
1224
- if (active.temporaryDirectory) {
1225
- await rm(active.temporaryDirectory, { recursive: true, force: true });
1226
- }
1227
- }
1228
- }
1229
-
1230
- #emit(threadId, event) {
1231
- for (const listener of this.listeners.get(threadId) ?? []) {
1232
- try {
1233
- listener(event);
1234
- } catch {}
1235
- }
1236
- }
1237
- }