openfox 0.2.3 → 1.0.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 (64) hide show
  1. package/dist/agent-defaults/builder.agent.md +3 -7
  2. package/dist/agent-defaults/code-reviewer.agent.md +1 -2
  3. package/dist/agent-defaults/debugger.agent.md +1 -2
  4. package/dist/agent-defaults/planner.agent.md +2 -9
  5. package/dist/agent-defaults/test-generator.agent.md +1 -1
  6. package/dist/agent-defaults/verifier.agent.md +4 -5
  7. package/dist/ask-3RK5YJZE.js +19 -0
  8. package/dist/auto-compaction-7LNZ5J3W.js +23 -0
  9. package/dist/{branch.api-WDHKZJS6.js → branch.api-XADFZZAA.js} +2 -2
  10. package/dist/chat-handler-T7SLWZ4E.js +201 -0
  11. package/dist/chunk-22CTURMH.js +128 -0
  12. package/dist/chunk-2KSCZSXQ.js +680 -0
  13. package/dist/{chunk-YMUIRZUI.js → chunk-2TBNG7CS.js} +12 -12
  14. package/dist/{chunk-4Z2OZ6NX.js → chunk-3EHGGBWE.js} +29 -2
  15. package/dist/{chunk-L475ND2Z.js → chunk-3UQACISJ.js} +162 -52
  16. package/dist/{chunk-AF7XS4OI.js → chunk-5YQAXUKO.js} +4 -1
  17. package/dist/{chunk-MTX5Y3V3.js → chunk-7IOZFJBW.js} +2 -2
  18. package/dist/chunk-BU2ZYOEA.js +1067 -0
  19. package/dist/chunk-M2C5GEQR.js +137 -0
  20. package/dist/{chunk-U62ZECSQ.js → chunk-PNBH3RAX.js} +2 -2
  21. package/dist/chunk-QDEKU5RL.js +75 -0
  22. package/dist/chunk-QYP5HQV4.js +438 -0
  23. package/dist/{chunk-3WOUSUW7.js → chunk-RAZQTQK2.js} +1353 -1134
  24. package/dist/chunk-S4QKTRGJ.js +343 -0
  25. package/dist/{chunk-ARFK3RZW.js → chunk-TPT6HP4H.js} +1 -1
  26. package/dist/chunk-TVQOONDR.js +75 -0
  27. package/dist/{chunk-5ZUU23XF.js → chunk-U6BUT37A.js} +286 -33
  28. package/dist/chunk-VZXXJKSQ.js +314 -0
  29. package/dist/{chunk-26NRL3HX.js → chunk-XFXOSPYH.js} +3 -2
  30. package/dist/{chunk-N2XYX4ZA.js → chunk-XKFPU2FA.js} +13 -2
  31. package/dist/{chunk-FUUYYDVG.js → chunk-YB2574C6.js} +2841 -4507
  32. package/dist/cli/dev.js +4 -3
  33. package/dist/cli/index.js +4 -3
  34. package/dist/{config-BMXSDOZB.js → config-GIFCEJDU.js} +11 -6
  35. package/dist/events-G6CKHDNZ.js +114 -0
  36. package/dist/folding-O52L65VN.js +37 -0
  37. package/dist/{init-PLRTRSDC.js → init-MKTUIGZU.js} +46 -13
  38. package/dist/orchestrator-UGXX7JRQ.js +39 -0
  39. package/dist/package.json +9 -2
  40. package/dist/{paths-4BPXWB2W.js → paths-MM76AWYB.js} +2 -2
  41. package/dist/processor-KLHKDILM.js +236 -0
  42. package/dist/{project-creator-CWPKRIKI.js → project-creator-KAQXMZCT.js} +9 -8
  43. package/dist/projects-GSMMEVSO.js +19 -0
  44. package/dist/{protocol-BFFz3WM0.d.ts → protocol-BZ6OIASP.d.ts} +71 -50
  45. package/dist/protocol-WQENDK72.js +88 -0
  46. package/dist/{provider-ETVY64A2.js → provider-4KTWU6IV.js} +41 -9
  47. package/dist/runtime-config-2XJJIMSC.js +9 -0
  48. package/dist/{serve-TL34LE77.js → serve-UK6B6G2O.js} +38 -18
  49. package/dist/server/index.d.ts +81 -27
  50. package/dist/server/index.js +19 -11
  51. package/dist/{settings-D5O3TMLD.js → settings-NCLN4JDZ.js} +4 -4
  52. package/dist/shared/index.d.ts +2 -2
  53. package/dist/shared/index.js +1 -1
  54. package/dist/tools-SGBQE736.js +48 -0
  55. package/dist/vision-fallback-63VPBSEN.js +22 -0
  56. package/dist/web/assets/{index-DHmZmhnR.css → index-BsaaC0X0.css} +32 -1
  57. package/dist/web/assets/index-CvO7QkGh.js +141 -0
  58. package/dist/web/index.html +2 -2
  59. package/dist/web/sounds/typing.mp3 +0 -0
  60. package/dist/workflow-defaults/default.workflow.json +1 -1
  61. package/package.json +9 -2
  62. package/dist/chunk-L4XDA3V7.js +0 -218
  63. package/dist/tools-QMIIDJTV.js +0 -48
  64. package/dist/web/assets/index-CAD7Iv_d.js +0 -122
@@ -1,126 +1,121 @@
1
1
  import {
2
- AskUserInterrupt,
3
- BUILDER_KICKOFF_PROMPT,
4
- EventEmitter,
5
- PathAccessDeniedError,
2
+ createVerifierNudgeConfig,
3
+ runBuilderTurn,
4
+ runChatTurn
5
+ } from "./chunk-VZXXJKSQ.js";
6
+ import {
6
7
  TurnMetrics,
7
8
  agentExists,
8
9
  assembleAgentRequest,
9
- buildContextMessagesFromEventHistory,
10
- buildMessagesFromStoredEvents,
11
- buildSnapshotFromSessionState,
12
10
  cancelPathConfirmationsForSession,
13
- cancelQuestionsForSession,
14
11
  createChatDoneEvent,
15
- createChatDoneMessage,
16
- createChatErrorMessage,
17
- createChatMessageMessage,
18
- createContextStateMessage,
19
- createCriteriaUpdatedMessage,
20
- createErrorMessage,
21
12
  createMessageStartEvent,
22
- createModeChangedMessage,
23
- createPhaseChangedMessage,
24
- createProjectListMessage,
25
- createProjectStateMessage,
26
- createQueueStateMessage,
27
- createSessionListMessage,
28
- createSessionRunningMessage,
29
- createSessionStateMessage,
30
- createSettingsValueMessage,
31
13
  createToolRegistry,
32
14
  deleteAgent,
33
15
  deleteSkill,
34
16
  devServerManager,
35
- emitAssistantMessageStart,
36
- emitContextCompacted,
37
- emitContextState,
38
- emitCriteriaSet,
39
- emitCriterionUpdated,
40
- emitModeChanged,
41
- emitPhaseChanged,
42
- emitRunningChanged,
43
- emitSessionInitialized,
44
- emitUserMessage,
45
17
  ensureDefaultAgents,
46
18
  ensureDefaultSkills,
47
19
  executeSubAgent,
48
20
  findAgentById,
49
21
  findSkillById,
50
- getCurrentContextWindowId,
22
+ getAllInstructions,
51
23
  getDefaultAgentIds,
52
24
  getDefaultSkillIds,
53
- getEventStore,
25
+ getEnabledSkillMetadata,
54
26
  getModifiedDefaultAgentIds,
55
27
  getModifiedDefaultSkillIds,
56
- getRecentUserPromptsForSession,
57
- getRuntimeConfig,
58
- getSessionState,
59
- getSubAgents,
60
28
  getToolRegistryForAgent,
61
- initEventStore,
62
- isChatSendPayload,
63
- isCriteriaEditPayload,
64
- isModeSwitchPayload,
65
- isPathConfirmPayload,
66
- isProjectCreatePayload,
67
- isProjectCreateWithDirPayload,
68
- isProjectDeletePayload,
69
- isProjectLoadPayload,
70
- isProjectUpdatePayload,
71
- isQueueAsapPayload,
72
- isQueueCancelPayload,
73
- isQueueCompletionPayload,
74
- isSessionCreatePayload,
75
- isSessionLoadPayload,
76
- isSessionSetProviderPayload,
77
- isSettingsGetPayload,
78
- isSettingsSetPayload,
79
29
  isSkillEnabled,
80
30
  loadAllAgents,
81
31
  loadAllAgentsDefault,
82
32
  loadAllSkills,
83
33
  maybeAutoCompactContext,
84
- parseClientMessage,
85
34
  performManualContextCompaction,
86
- providePathConfirmation,
87
35
  restoreAllDefaultAgents,
88
36
  restoreAllDefaultSkills,
89
37
  restoreDefaultAgent,
90
38
  restoreDefaultSkill,
91
- runTopLevelAgentLoop,
92
39
  saveAgent,
93
40
  saveSkill,
94
- serializeServerMessage,
95
- setRuntimeConfig,
96
41
  setSkillEnabled,
97
- skillExists,
42
+ skillExists
43
+ } from "./chunk-YB2574C6.js";
44
+ import {
45
+ generateSessionName,
46
+ needsNameGeneration
47
+ } from "./chunk-QDEKU5RL.js";
48
+ import {
49
+ createProviderManager,
50
+ parseDefaultModelSelection
51
+ } from "./chunk-QYP5HQV4.js";
52
+ import {
53
+ createSession,
54
+ deleteSession,
55
+ emitAssistantMessageStart,
56
+ emitContextCompacted,
57
+ emitContextState,
58
+ emitCriteriaSet,
59
+ emitCriterionUpdated,
60
+ emitModeChanged,
61
+ emitPhaseChanged,
62
+ emitRunningChanged,
63
+ emitSessionInitialized,
64
+ emitUserMessage,
65
+ getCurrentContextWindowId,
66
+ getEventStore,
67
+ getSession,
68
+ getSessionState,
69
+ initEventStore,
70
+ listSessions,
71
+ listSessionsByProject,
72
+ updateSessionDangerLevel,
73
+ updateSessionMetadata,
74
+ updateSessionProvider,
75
+ updateSessionSummary
76
+ } from "./chunk-BU2ZYOEA.js";
77
+ import {
78
+ buildContextMessagesFromEventHistory,
79
+ buildMessagesFromStoredEvents,
80
+ foldPendingConfirmations
81
+ } from "./chunk-2KSCZSXQ.js";
82
+ import {
83
+ createChatErrorMessage,
84
+ createChatMessageMessage,
85
+ createContextStateMessage,
86
+ createErrorMessage,
87
+ createPhaseChangedMessage,
88
+ createQueueStateMessage,
89
+ createSessionRunningMessage,
90
+ createSessionStateMessage,
91
+ isAskAnswerPayload,
92
+ isChatSendPayload,
93
+ isSessionLoadPayload,
94
+ parseClientMessage,
95
+ serializeServerMessage,
98
96
  storedEventToServerMessage
99
- } from "./chunk-FUUYYDVG.js";
97
+ } from "./chunk-S4QKTRGJ.js";
100
98
  import {
101
- getSetting,
102
- setSetting
103
- } from "./chunk-MTX5Y3V3.js";
99
+ EventEmitter,
100
+ cancelQuestionsForSession,
101
+ provideAnswer
102
+ } from "./chunk-22CTURMH.js";
104
103
  import {
105
- createProject,
106
- deleteProject,
107
- getProject,
108
- listProjects,
109
- updateProject
110
- } from "./chunk-N2XYX4ZA.js";
104
+ getProject
105
+ } from "./chunk-XKFPU2FA.js";
111
106
  import {
112
- getDatabase,
113
107
  initDatabase
114
- } from "./chunk-4Z2OZ6NX.js";
108
+ } from "./chunk-3EHGGBWE.js";
115
109
  import {
116
110
  computeSessionStats
117
111
  } from "./chunk-QY7BMXWT.js";
118
112
  import {
119
113
  createServerMessage
120
- } from "./chunk-ARFK3RZW.js";
114
+ } from "./chunk-TPT6HP4H.js";
121
115
  import {
122
- createProviderManager
123
- } from "./chunk-L4XDA3V7.js";
116
+ getRuntimeConfig,
117
+ setRuntimeConfig
118
+ } from "./chunk-TVQOONDR.js";
124
119
  import {
125
120
  SessionNotFoundError,
126
121
  createLLMClient,
@@ -129,14 +124,14 @@ import {
129
124
  getBackendDisplayName,
130
125
  getLlmStatus,
131
126
  getModelProfile
132
- } from "./chunk-5ZUU23XF.js";
127
+ } from "./chunk-U6BUT37A.js";
128
+ import {
129
+ getGlobalConfigDir
130
+ } from "./chunk-5YQAXUKO.js";
133
131
  import {
134
132
  logger,
135
133
  setLogLevel
136
- } from "./chunk-U62ZECSQ.js";
137
- import {
138
- getGlobalConfigDir
139
- } from "./chunk-AF7XS4OI.js";
134
+ } from "./chunk-PNBH3RAX.js";
140
135
 
141
136
  // src/server/index.ts
142
137
  import express from "express";
@@ -157,92 +152,157 @@ var RULES = [
157
152
  {
158
153
  match: /ID\s*["']([a-z0-9-]+)["']:\s*["']([^"']+)["'][\s\S]*ID\s*["']([a-z0-9-]+)["']:\s*["']([^"']+)["']/i,
159
154
  tools: [
160
- { name: "add_criterion", arguments: { id: "$1", description: "$2" } },
161
- { name: "add_criterion", arguments: { id: "$3", description: "$4" } }
155
+ { name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } },
156
+ { name: "criterion", arguments: { action: "add", id: "$3", description: "$4" } }
162
157
  ],
163
158
  response: "Added both criteria."
164
159
  },
160
+ // Single criterion: "Add criterion ID "test-1" with description "The tests pass""
161
+ {
162
+ match: /Add criterion ID\s*["']([a-z0-9-]+)["']\s*with description\s*["']([^"']+)["'][\s\S]*/i,
163
+ tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
164
+ response: "Added the criterion."
165
+ },
165
166
  // Single criterion: ID "test-1" with/: description "The tests pass"
166
167
  {
167
168
  match: /ID\s*["']([a-z0-9-]+)["'].*description\s*["']([^"']+)["']/i,
168
- tools: [{ name: "add_criterion", arguments: { id: "$1", description: "$2" } }],
169
+ tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
169
170
  response: "Added the criterion."
170
171
  },
171
172
  {
172
- match: /Add these two acceptance criteria:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)\s*Use add_criterion for each\./i,
173
+ match: /Add these two acceptance criteria:\s*1\.\s*([^\n]+)\s*2\.\s*([^\n]+)\s*Use criterion for each\./i,
173
174
  tools: [
174
- { name: "add_criterion", arguments: { id: "criterion-1", description: "$1" } },
175
- { name: "add_criterion", arguments: { id: "criterion-2", description: "$2" } }
175
+ { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
176
+ { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
176
177
  ],
177
178
  response: "Added both criteria."
178
179
  },
179
180
  {
180
- match: /Add these two acceptance criteria:[\s\S]*?1\.\s*([^\n]+)[\s\S]*?2\.\s*([^\n]+)[\s\S]*?Use add_criterion for each one\./i,
181
+ match: /Add these two acceptance criteria:[\s\S]*?1\.\s*([^\n]+)[\s\S]*?2\.\s*([^\n]+)[\s\S]*?Use (?:add_)?criterion for each one\./i,
181
182
  tools: [
182
- { name: "add_criterion", arguments: { id: "criterion-1", description: "$1" } },
183
- { name: "add_criterion", arguments: { id: "criterion-2", description: "$2" } }
183
+ { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
184
+ { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
184
185
  ],
185
186
  response: "Added both criteria."
186
187
  },
187
188
  {
188
- match: /Add criterion:\s*([\s\S]+?)\s*Use add_criterion\.?/i,
189
- tools: [{ name: "add_criterion", arguments: { id: "$auto", description: "$1" } }],
189
+ match: /Add these two acceptance criteria:[\s\S]*?1\.\s*([^\n]+)[\s\S]*?2\.\s*([^\n]+)/i,
190
+ tools: [
191
+ { name: "criterion", arguments: { action: "add", id: "criterion-1", description: "$1" } },
192
+ { name: "criterion", arguments: { action: "add", id: "criterion-2", description: "$2" } }
193
+ ],
194
+ response: "Added both criteria."
195
+ },
196
+ {
197
+ match: /Add criterion:\s*([\s\S]+?)\s*Use criterion\.?/i,
198
+ tools: [{ name: "criterion", arguments: { action: "add", id: "$auto", description: "$1" } }],
190
199
  response: "Added the criterion."
191
200
  },
192
201
  // Single criterion: ID "test-1": "The tests pass" (colon format)
193
202
  {
194
203
  match: /ID\s*["']([a-z0-9-]+)["']\s*:\s*["']([^"']+)["']/i,
195
- tools: [{ name: "add_criterion", arguments: { id: "$1", description: "$2" } }],
204
+ tools: [{ name: "criterion", arguments: { action: "add", id: "$1", description: "$2" } }],
196
205
  response: "Added the criterion."
197
206
  },
198
207
  // complete_criterion to mark "id" as done
199
208
  {
200
209
  match: /complete_criterion.*mark\s*["']([a-z0-9-]+)["']/i,
201
- tools: [{ name: "complete_criterion", arguments: { id: "$1", reason: "Completed successfully" } }],
210
+ tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
202
211
  response: "Marked criterion as complete."
203
212
  },
204
213
  // complete_criterion with ID
205
214
  {
206
215
  match: /complete_criterion.*["']([a-z0-9-]+)["']/i,
207
- tools: [{ name: "complete_criterion", arguments: { id: "$1", reason: "Completed successfully" } }],
216
+ tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
217
+ response: "Marked criterion as complete."
218
+ },
219
+ // criterion with action "complete" for "id"
220
+ {
221
+ match: /criterion.*action\s*["']complete["'].*["']([a-z0-9-]+)["']/i,
222
+ tools: [{ name: "criterion", arguments: { action: "complete", id: "$1", reason: "Completed successfully" } }],
208
223
  response: "Marked criterion as complete."
209
224
  },
210
225
  // remove_criterion to remove "id"
211
226
  {
212
227
  match: /remove_criterion.*remove\s*["']([a-z0-9-]+)["']/i,
213
- tools: [{ name: "remove_criterion", arguments: { id: "$1" } }],
228
+ tools: [{ name: "criterion", arguments: { action: "remove", id: "$1" } }],
214
229
  response: "Removed the criterion."
215
230
  },
216
231
  // remove_criterion with ID
217
232
  {
218
233
  match: /remove_criterion.*["']([a-z0-9-]+)["']/i,
219
- tools: [{ name: "remove_criterion", arguments: { id: "$1" } }],
234
+ tools: [{ name: "criterion", arguments: { action: "remove", id: "$1" } }],
220
235
  response: "Removed the criterion."
221
236
  },
222
237
  // update_criterion to change "id" description to "new"
223
238
  {
224
239
  match: /update_criterion.*change\s*["']([a-z0-9-]+)["'].*to\s*["']([^"']+)["']/i,
225
- tools: [{ name: "update_criterion", arguments: { id: "$1", description: "$2" } }],
240
+ tools: [{ name: "criterion", arguments: { action: "update", id: "$1", description: "$2" } }],
226
241
  response: "Updated the criterion."
227
242
  },
228
243
  // update_criterion with ID and description
229
244
  {
230
245
  match: /update_criterion.*["']([a-z0-9-]+)["'].*["']([^"']+)["']/i,
231
- tools: [{ name: "update_criterion", arguments: { id: "$1", description: "$2" } }],
246
+ tools: [{ name: "criterion", arguments: { action: "update", id: "$1", description: "$2" } }],
232
247
  response: "Updated the criterion."
233
248
  },
234
249
  // get_criteria
235
250
  {
236
- match: /get_criteria/i,
237
- tools: [{ name: "get_criteria", arguments: {} }],
251
+ match: /get_criteria|show.*criteria|list.*criteria/i,
252
+ tools: [{ name: "criterion", arguments: { action: "get" } }],
238
253
  response: "Here are the current criteria."
239
254
  },
255
+ // pass_criterion
256
+ {
257
+ match: /pass_criterion.*["']([a-z0-9-]+)["']/i,
258
+ tools: [{ name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } }],
259
+ response: "Criterion passed."
260
+ },
261
+ // fail_criterion
262
+ {
263
+ match: /fail_criterion.*["']([a-z0-9-]+)["']/i,
264
+ tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
265
+ response: "Criterion failed."
266
+ },
240
267
  // Generic add criterion (fallback)
241
268
  {
242
269
  match: /add.*criterion/i,
243
- tools: [{ name: "add_criterion", arguments: { id: "$auto", description: "Test criterion" } }],
270
+ tools: [{ name: "criterion", arguments: { action: "add", description: "Test criterion" } }],
244
271
  response: "Added the criterion."
245
272
  },
273
+ // Verifier nudge with fail criterion (check for "fail" in ID) - MUST come before general pass rule
274
+ {
275
+ match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion.*:\s*([a-z0-9-]*fail[a-z0-9-]*)/i,
276
+ tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
277
+ response: "Criterion failed."
278
+ },
279
+ // Verifier nudge: Use criterion with action "pass" or "fail" for criterion-id-1, criterion-id-2
280
+ {
281
+ match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion:\s*([a-z0-9-]+)/i,
282
+ tools: [{ name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } }],
283
+ response: "Criterion passed."
284
+ },
285
+ // Verifier nudge with multiple criteria
286
+ {
287
+ match: /Use criterion with action ["']pass["'] or ["']fail["'].*criterion:\s*([a-z0-9-]+),\s*([a-z0-9-]+)/i,
288
+ tools: [
289
+ { name: "criterion", arguments: { action: "pass", id: "$1", reason: "Verified successfully" } },
290
+ { name: "criterion", arguments: { action: "pass", id: "$2", reason: "Verified successfully" } }
291
+ ],
292
+ response: "Criteria passed."
293
+ },
294
+ // Verifier should fail a criterion
295
+ {
296
+ match: /Verifier should fail.*criterion.*ID\s*["']([a-z0-9-]+)["']/i,
297
+ tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
298
+ response: "Criterion failed."
299
+ },
300
+ // Verifier fail_criterion command
301
+ {
302
+ match: /fail_criterion.*ID\s*["']([a-z0-9-]+)["']/i,
303
+ tools: [{ name: "criterion", arguments: { action: "fail", id: "$1", reason: "Verification failed" } }],
304
+ response: "Criterion failed."
305
+ },
246
306
  // -------------------------------------------------------------------------
247
307
  // File Read Tools
248
308
  // -------------------------------------------------------------------------
@@ -533,11 +593,12 @@ var RULES = [
533
593
  {
534
594
  match: /todo_write.*Read files.*Make changes/i,
535
595
  tools: [{
536
- name: "todo_write",
596
+ name: "todo",
537
597
  arguments: {
598
+ action: "write",
538
599
  todos: [
539
- { content: "Read files", status: "in_progress", priority: "high" },
540
- { content: "Make changes", status: "pending", priority: "medium" }
600
+ { content: "Read files", status: "in_progress" },
601
+ { content: "Make changes", status: "pending" }
541
602
  ]
542
603
  }
543
604
  }],
@@ -546,9 +607,10 @@ var RULES = [
546
607
  {
547
608
  match: /todo_write|todo.*list/i,
548
609
  tools: [{
549
- name: "todo_write",
610
+ name: "todo",
550
611
  arguments: {
551
- todos: [{ content: "Test task", status: "pending", priority: "medium" }]
612
+ action: "write",
613
+ todos: [{ content: "Test task", status: "pending" }]
552
614
  }
553
615
  }],
554
616
  response: "Created todo list."
@@ -567,6 +629,19 @@ var RULES = [
567
629
  response: "Here is a detailed explanation of the topic. TypeScript is a statically typed superset of JavaScript that adds optional type annotations. It provides better tooling, catches errors at compile time, and makes code more maintainable."
568
630
  },
569
631
  // -------------------------------------------------------------------------
632
+ // Ask User Tool
633
+ // -------------------------------------------------------------------------
634
+ {
635
+ match: /ask.*user|ask.*question|clarif/i,
636
+ tools: [{ name: "ask_user", arguments: { question: "What would you like me to do?" } }],
637
+ response: "I asked the user."
638
+ },
639
+ {
640
+ match: /confirm.*with.*user/i,
641
+ tools: [{ name: "ask_user", arguments: { question: "Should I proceed with this action?" } }],
642
+ response: "I asked for confirmation."
643
+ },
644
+ // -------------------------------------------------------------------------
570
645
  // Default Responses (no tools)
571
646
  // -------------------------------------------------------------------------
572
647
  {
@@ -737,11 +812,12 @@ function getPromptAwareToolResponse(prompt) {
737
812
  if (/Use the todo_write tool to create a todo list with 2 items/i.test(prompt)) {
738
813
  return {
739
814
  tools: [{
740
- name: "todo_write",
815
+ name: "todo",
741
816
  arguments: {
817
+ action: "write",
742
818
  todos: [
743
- { content: "Read files", status: "in_progress", priority: "high" },
744
- { content: "Make changes", status: "pending", priority: "medium" }
819
+ { content: "Read files", status: "in_progress" },
820
+ { content: "Make changes", status: "pending" }
745
821
  ]
746
822
  }
747
823
  }],
@@ -751,9 +827,9 @@ function getPromptAwareToolResponse(prompt) {
751
827
  if (/First call get_criteria to see what needs to be done, then create src\/test\.ts and call complete_criterion for ["']test-file["']\./i.test(prompt)) {
752
828
  return {
753
829
  tools: [
754
- { name: "get_criteria", arguments: {} },
830
+ { name: "criterion", arguments: { action: "get" } },
755
831
  { name: "write_file", arguments: { path: "src/test.ts", content: "export const created = true" } },
756
- { name: "complete_criterion", arguments: { id: "test-file", reason: "Created the requested file" } },
832
+ { name: "criterion", arguments: { action: "complete", id: "test-file", reason: "Created the requested file" } },
757
833
  { name: "step_done", arguments: {} }
758
834
  ],
759
835
  response: "Reviewed the criteria, created the file, and completed the criterion."
@@ -763,7 +839,7 @@ function getPromptAwareToolResponse(prompt) {
763
839
  return {
764
840
  tools: [
765
841
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
766
- { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } },
842
+ { name: "criterion", arguments: { action: "complete", id: "file-created", reason: "Created the requested file" } },
767
843
  { name: "step_done", arguments: {} }
768
844
  ],
769
845
  response: "Created the file and completed the criterion."
@@ -852,27 +928,27 @@ function getConversationAwareToolResponse(request) {
852
928
  if (conversationText.includes("inspect-src")) {
853
929
  tools.push(
854
930
  { name: "read_file", arguments: { path: "src" } },
855
- { name: "complete_criterion", arguments: { id: "inspect-src", reason: "Inspected the src directory and reported what exists" } }
931
+ { name: "criterion", arguments: { action: "complete", id: "inspect-src", reason: "Inspected the src directory and reported what exists" } }
856
932
  );
857
933
  completedCriteria.push("inspect-src");
858
934
  }
859
935
  if (conversationText.includes("trivial-pass")) {
860
- tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
936
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
861
937
  completedCriteria.push("trivial-pass");
862
938
  }
863
939
  if (conversationText.includes("verify-fail")) {
864
- tools.push({ name: "complete_criterion", arguments: { id: "verify-fail", reason: "Prepared criterion for verification" } });
940
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "verify-fail", reason: "Prepared criterion for verification" } });
865
941
  completedCriteria.push("verify-fail");
866
942
  }
867
943
  if (conversationText.includes("file-created")) {
868
944
  tools.push(
869
945
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
870
- { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
946
+ { name: "criterion", arguments: { action: "complete", id: "file-created", reason: "Created the requested file" } }
871
947
  );
872
948
  completedCriteria.push("file-created");
873
949
  }
874
950
  if (tools.length === 0) {
875
- tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
951
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" } });
876
952
  completedCriteria.push("mock-crit");
877
953
  }
878
954
  tools.push({ name: "step_done", arguments: {} });
@@ -884,7 +960,7 @@ function getConversationAwareToolResponse(request) {
884
960
  if (/fulfil the \d+ criteria/i.test(prompt)) {
885
961
  return {
886
962
  tools: [
887
- { name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } },
963
+ { name: "criterion", arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" } },
888
964
  { name: "step_done", arguments: {} }
889
965
  ],
890
966
  response: "Completed criterion and finished step."
@@ -895,17 +971,17 @@ function getConversationAwareToolResponse(request) {
895
971
  if (conversationText.includes("file-created")) {
896
972
  tools.push(
897
973
  { name: "write_file", arguments: { path: "src/utils.ts", content: "export const created = true" } },
898
- { name: "complete_criterion", arguments: { id: "file-created", reason: "Created the requested file" } }
974
+ { name: "criterion", arguments: { action: "complete", id: "file-created", reason: "Created the requested file" } }
899
975
  );
900
976
  } else if (conversationText.includes("trivial-pass")) {
901
- tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
977
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
902
978
  } else if (conversationText.includes("verify-fail")) {
903
- tools.push({ name: "complete_criterion", arguments: { id: "verify-fail", reason: "Prepared criterion for verification" } });
979
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "verify-fail", reason: "Prepared criterion for verification" } });
904
980
  } else {
905
981
  if (/1 criteria remaining/i.test(prompt)) {
906
- tools.push({ name: "complete_criterion", arguments: { id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
982
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "trivial-pass", reason: "Trivial criterion passes immediately" } });
907
983
  } else {
908
- tools.push({ name: "complete_criterion", arguments: { id: "mock-crit", reason: "Completed for testing" } });
984
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "mock-crit", reason: "Completed for testing" } });
909
985
  }
910
986
  }
911
987
  tools.push({ name: "step_done", arguments: {} });
@@ -917,7 +993,7 @@ function getConversationAwareToolResponse(request) {
917
993
  if (/Verify each criterion marked \[NEEDS VERIFICATION\]\./i.test(prompt)) {
918
994
  const alreadyVerified = request.messages.some(
919
995
  (m) => m.role === "assistant" && m.toolCalls?.some(
920
- (tc) => tc.name === "pass_criterion" || tc.name === "fail_criterion"
996
+ (tc) => tc.name === "criterion" && (tc.arguments["action"] === "pass" || tc.arguments["action"] === "fail")
921
997
  )
922
998
  );
923
999
  if (alreadyVerified) {
@@ -926,23 +1002,23 @@ function getConversationAwareToolResponse(request) {
926
1002
  const tools = [];
927
1003
  const terminalizedCriteria = [];
928
1004
  if (conversationText.includes("trivial-pass")) {
929
- tools.push({ name: "pass_criterion", arguments: { id: "trivial-pass", reason: "Verified successfully" } });
1005
+ tools.push({ name: "criterion", arguments: { action: "pass", id: "trivial-pass", reason: "Verified successfully" } });
930
1006
  terminalizedCriteria.push("trivial-pass");
931
1007
  }
932
1008
  if (conversationText.includes("inspect-src")) {
933
- tools.push({ name: "pass_criterion", arguments: { id: "inspect-src", reason: "Verified the src directory was inspected successfully" } });
1009
+ tools.push({ name: "criterion", arguments: { action: "pass", id: "inspect-src", reason: "Verified the src directory was inspected successfully" } });
934
1010
  terminalizedCriteria.push("inspect-src");
935
1011
  }
936
1012
  if (conversationText.includes("verify-fail")) {
937
- tools.push({ name: "fail_criterion", arguments: { id: "verify-fail", reason: "Verification fails intentionally for this criterion" } });
1013
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "verify-fail", reason: "Verification fails intentionally for this criterion" } });
938
1014
  terminalizedCriteria.push("verify-fail");
939
1015
  }
940
1016
  if (conversationText.includes("file-created")) {
941
- tools.push({ name: "pass_criterion", arguments: { id: "file-created", reason: "Verified the file was created successfully" } });
1017
+ tools.push({ name: "criterion", arguments: { action: "pass", id: "file-created", reason: "Verified the file was created successfully" } });
942
1018
  terminalizedCriteria.push("file-created");
943
1019
  }
944
1020
  if (conversationText.includes("impossible/path")) {
945
- tools.push({ name: "fail_criterion", arguments: { id: "auto-impossible", reason: "Impossible path does not exist" } });
1021
+ tools.push({ name: "criterion", arguments: { action: "complete", id: "auto-impossible", reason: "Impossible path does not exist" } });
946
1022
  terminalizedCriteria.push("auto-impossible");
947
1023
  }
948
1024
  if (tools.length > 0) {
@@ -1101,211 +1177,189 @@ function createMockLLMClient() {
1101
1177
  }
1102
1178
 
1103
1179
  // src/server/ws/server.ts
1104
- import { WebSocketServer, WebSocket } from "ws";
1180
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
1105
1181
 
1106
- // src/server/sub-agents/verifier-helpers.ts
1107
- var MAX_CONSECUTIVE_VERIFIER_NUDGES = 10;
1108
- var VERIFIER_STALL_REASON = "Verifier stopped repeatedly before terminalizing verification after repeated nudges.";
1109
- function getCriteriaAwaitingVerification(criteria) {
1110
- return criteria.filter((criterion) => criterion.status.type === "completed");
1111
- }
1112
- function buildVerifierNudgeContent(criteria) {
1113
- const ids = criteria.map((criterion) => criterion.id).join(", ");
1114
- return `You stopped before finalizing verification. ${criteria.length} criteria still need a terminal verification result. Use pass_criterion or fail_criterion for each remaining criterion: ${ids}.`;
1115
- }
1116
- function buildVerifierRestartContent(criteria) {
1117
- const ids = criteria.map((criterion) => criterion.id).join(", ");
1118
- return `${VERIFIER_STALL_REASON} Leaving remaining criteria unchanged so verification can restart in a fresh window: ${ids}.`;
1119
- }
1120
- function createVerifierNudgeConfig() {
1121
- return {
1122
- maxConsecutiveNudges: MAX_CONSECUTIVE_VERIFIER_NUDGES,
1123
- getCriteriaAwaiting: getCriteriaAwaitingVerification,
1124
- buildNudgeContent: buildVerifierNudgeContent,
1125
- buildRestartContent: buildVerifierRestartContent
1126
- };
1127
- }
1182
+ // src/server/ws/terminal.ts
1183
+ import WebSocket from "ws";
1128
1184
 
1129
- // src/server/chat/orchestrator.ts
1130
- function getCurrentWindowMessageOptions(sessionId) {
1131
- const contextWindowId = getCurrentContextWindowId(sessionId);
1132
- return contextWindowId ? { contextWindowId } : void 0;
1133
- }
1134
- function resolveStatsIdentity(options) {
1135
- const model = options.llmClient.getModel();
1136
- if (options.statsIdentity) {
1137
- return {
1138
- ...options.statsIdentity,
1139
- model
1185
+ // src/server/terminal/manager.ts
1186
+ import * as pty from "node-pty";
1187
+ import os from "os";
1188
+ var TerminalManager = class {
1189
+ sessions = /* @__PURE__ */ new Map();
1190
+ outputCallbacks = /* @__PURE__ */ new Set();
1191
+ outputHistory = /* @__PURE__ */ new Map();
1192
+ generateId() {
1193
+ return `term_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
1194
+ }
1195
+ getShell() {
1196
+ const shell = process.env["SHELL"] || "/bin/bash";
1197
+ return shell;
1198
+ }
1199
+ resolveWorkdir(workdir) {
1200
+ if (workdir && workdir.length > 0) {
1201
+ return workdir;
1202
+ }
1203
+ return os.homedir();
1204
+ }
1205
+ create(workdir) {
1206
+ const id = this.generateId();
1207
+ const shell = this.getShell();
1208
+ const cwd = this.resolveWorkdir(workdir);
1209
+ const ptyProcess = pty.spawn(shell, [], {
1210
+ name: "xterm-256color",
1211
+ cols: 80,
1212
+ rows: 24,
1213
+ cwd,
1214
+ env: process.env
1215
+ });
1216
+ const session = {
1217
+ id,
1218
+ pty: ptyProcess,
1219
+ workdir: cwd
1140
1220
  };
1221
+ ptyProcess.onData((data) => {
1222
+ const output = { sessionId: id, data };
1223
+ for (const cb of this.outputCallbacks) {
1224
+ cb(output);
1225
+ }
1226
+ const history = this.outputHistory.get(id) || [];
1227
+ history.push(data);
1228
+ if (history.length > 1e4) history.shift();
1229
+ this.outputHistory.set(id, history);
1230
+ });
1231
+ this.sessions.set(id, session);
1232
+ logger.info("Terminal session created", { id, shell, cwd });
1233
+ return session;
1141
1234
  }
1142
- return {
1143
- providerId: `provider:${model}`,
1144
- providerName: "Unknown Provider",
1145
- backend: "unknown",
1146
- model
1147
- };
1148
- }
1149
- async function runChatTurn(options) {
1150
- const { sessionManager: sessionManager2, sessionId } = options;
1151
- const eventStore = getEventStore();
1152
- const statsIdentity = resolveStatsIdentity(options);
1153
- const session = sessionManager2.requireSession(sessionId);
1154
- const mode = session.mode;
1155
- logger.debug("Starting chat turn", { sessionId, mode });
1156
- const turnMetrics = new TurnMetrics();
1157
- try {
1158
- if (mode === "builder") {
1159
- await runBuilderTurn(options, turnMetrics);
1160
- } else {
1161
- await runGenericAgentTurn(options, turnMetrics, mode);
1162
- }
1163
- const snapshot = buildSnapshot(sessionManager2, sessionId, turnMetrics.buildStats(statsIdentity, mode));
1164
- const snapshotEvent = eventStore.append(sessionId, { type: "turn.snapshot", data: snapshot });
1165
- const deletedCount = eventStore.cleanupOldEvents(sessionId);
1166
- if (deletedCount > 0) {
1167
- logger.debug("Cleaned up old events after snapshot", { sessionId, deletedCount, snapshotSeq: snapshotEvent.seq });
1235
+ write(sessionId, data) {
1236
+ const session = this.sessions.get(sessionId);
1237
+ if (!session) {
1238
+ return false;
1168
1239
  }
1169
- } catch (error) {
1170
- if (error instanceof AskUserInterrupt) {
1171
- const waitMsgId = crypto.randomUUID();
1172
- eventStore.append(sessionId, createMessageStartEvent(waitMsgId, "user", "Waiting for user input...", {
1173
- ...getCurrentWindowMessageOptions(sessionId) ?? {},
1174
- isSystemGenerated: true,
1175
- messageKind: "auto-prompt"
1176
- }));
1177
- eventStore.append(sessionId, createChatDoneEvent(waitMsgId, "waiting_for_user"));
1178
- return;
1240
+ session.pty.write(data);
1241
+ return true;
1242
+ }
1243
+ resize(sessionId, cols, rows) {
1244
+ const session = this.sessions.get(sessionId);
1245
+ if (!session) {
1246
+ return false;
1179
1247
  }
1180
- if (error instanceof PathAccessDeniedError) {
1181
- const errorMsgId2 = crypto.randomUUID();
1182
- const reasonText = error.reason === "sensitive_file" ? "sensitive files that may contain secrets" : error.reason === "both" ? "files outside the project and sensitive files" : "files outside the project directory";
1183
- eventStore.append(sessionId, {
1184
- type: "chat.error",
1185
- data: {
1186
- error: `User denied access to ${reasonText}.`,
1187
- recoverable: false
1188
- }
1189
- });
1190
- eventStore.append(sessionId, createMessageStartEvent(errorMsgId2, "user", `Access denied: ${error.paths.join(", ")}. If you need this file, explain why and ask the user for permission.`, {
1191
- ...getCurrentWindowMessageOptions(sessionId) ?? {},
1192
- isSystemGenerated: true,
1193
- messageKind: "correction"
1194
- }));
1195
- eventStore.append(sessionId, createChatDoneEvent(errorMsgId2, "error"));
1196
- return;
1248
+ session.pty.resize(cols, rows);
1249
+ return true;
1250
+ }
1251
+ kill(sessionId) {
1252
+ const session = this.sessions.get(sessionId);
1253
+ if (!session) {
1254
+ return false;
1197
1255
  }
1198
- if (error instanceof Error && error.message === "Aborted") {
1199
- return;
1256
+ session.pty.kill();
1257
+ this.sessions.delete(sessionId);
1258
+ logger.info("Terminal session killed", { id: sessionId });
1259
+ return true;
1260
+ }
1261
+ get(sessionId) {
1262
+ return this.sessions.get(sessionId);
1263
+ }
1264
+ getAll() {
1265
+ return Array.from(this.sessions.values());
1266
+ }
1267
+ getOutputHistory(sessionId) {
1268
+ return (this.outputHistory.get(sessionId) || []).join("");
1269
+ }
1270
+ onOutput(callback) {
1271
+ this.outputCallbacks.add(callback);
1272
+ return () => {
1273
+ this.outputCallbacks.delete(callback);
1274
+ };
1275
+ }
1276
+ killAll() {
1277
+ for (const session of this.sessions.values()) {
1278
+ session.pty.kill();
1200
1279
  }
1201
- logger.error("Chat turn error", { sessionId, mode, error });
1202
- const errorMsgId = crypto.randomUUID();
1203
- eventStore.append(sessionId, {
1204
- type: "chat.error",
1205
- data: {
1206
- error: error instanceof Error ? error.message : "Unknown error",
1207
- recoverable: false
1208
- }
1209
- });
1210
- eventStore.append(sessionId, createMessageStartEvent(errorMsgId, "user", `Error: ${error instanceof Error ? error.message : "Unknown error"}`, {
1211
- ...getCurrentWindowMessageOptions(sessionId) ?? {},
1212
- isSystemGenerated: true,
1213
- messageKind: "correction"
1214
- }));
1215
- eventStore.append(sessionId, createChatDoneEvent(errorMsgId, "error"));
1216
- } finally {
1217
- eventStore.append(sessionId, { type: "running.changed", data: { isRunning: false } });
1280
+ this.sessions.clear();
1281
+ logger.info("All terminal sessions killed");
1218
1282
  }
1283
+ };
1284
+ var terminalManager = new TerminalManager();
1285
+
1286
+ // src/server/ws/terminal.ts
1287
+ var subscriptions = /* @__PURE__ */ new Set();
1288
+ var outputHandlerRegistered = false;
1289
+ function registerOutputHandler() {
1290
+ if (outputHandlerRegistered) return;
1291
+ outputHandlerRegistered = true;
1292
+ terminalManager.onOutput((output) => {
1293
+ const subs = Array.from(subscriptions).filter(
1294
+ (s) => s.sessionId === output.sessionId && s.ws.readyState === WebSocket.OPEN
1295
+ );
1296
+ for (const sub of subs) {
1297
+ sub.ws.send(JSON.stringify({
1298
+ type: "terminal.output",
1299
+ payload: { sessionId: output.sessionId, data: output.data }
1300
+ }));
1301
+ }
1302
+ });
1219
1303
  }
1220
- async function runGenericAgentTurn(options, turnMetrics, agentId) {
1221
- const statsIdentity = resolveStatsIdentity(options);
1222
- const allAgents = await loadAllAgentsDefault();
1223
- const agentDef = findAgentById(agentId, allAgents) ?? findAgentById("planner", allAgents);
1224
- const subAgentDefs = getSubAgents(allAgents);
1225
- await runTopLevelAgentLoop({
1226
- mode: agentId,
1227
- sessionManager: options.sessionManager,
1228
- sessionId: options.sessionId,
1229
- llmClient: options.llmClient,
1230
- statsIdentity,
1231
- signal: options.signal,
1232
- onMessage: options.onMessage,
1233
- assembleRequest: (input) => assembleAgentRequest({ ...input, agentDef, subAgentDefs }),
1234
- getToolRegistry: () => getToolRegistryForAgent(agentDef)
1235
- }, turnMetrics);
1236
- }
1237
- async function runBuilderTurn(options, turnMetrics) {
1238
- const { sessionManager: sessionManager2, sessionId } = options;
1239
- const statsIdentity = resolveStatsIdentity(options);
1240
- const eventStore = getEventStore();
1241
- const allAgents = await loadAllAgentsDefault();
1242
- const builderDef = findAgentById("builder", allAgents);
1243
- const subAgentDefs = getSubAgents(allAgents);
1244
- let stepDoneCalled = false;
1245
- return {
1246
- ...await runTopLevelAgentLoop({
1247
- mode: "builder",
1248
- sessionManager: sessionManager2,
1249
- sessionId,
1250
- llmClient: options.llmClient,
1251
- statsIdentity,
1252
- signal: options.signal,
1253
- onMessage: options.onMessage,
1254
- assembleRequest: (input) => assembleAgentRequest({ ...input, agentDef: builderDef, subAgentDefs }),
1255
- getToolRegistry: () => {
1256
- const baseRegistry = getToolRegistryForAgent(builderDef);
1257
- if (options.injectStepDone === true) {
1258
- return baseRegistry;
1259
- }
1260
- return {
1261
- tools: baseRegistry.tools.filter((t) => t.name !== "step_done"),
1262
- definitions: baseRegistry.definitions.filter((d) => d.type === "function" && d.function.name !== "step_done"),
1263
- execute: baseRegistry.execute
1264
- };
1265
- },
1266
- onToolExecuted: (toolCall, toolResult) => {
1267
- if (toolCall.name === "step_done" && toolResult.success) {
1268
- stepDoneCalled = true;
1304
+ function handleTerminalMessage(ws, message) {
1305
+ switch (message.type) {
1306
+ case "terminal.subscribe": {
1307
+ if (message.payload.sessionId) {
1308
+ const exists = Array.from(subscriptions).some(
1309
+ (sub) => sub.ws === ws && sub.sessionId === message.payload.sessionId
1310
+ );
1311
+ if (!exists) {
1312
+ subscriptions.add({ ws, sessionId: message.payload.sessionId });
1269
1313
  }
1270
- if (toolResult.success && ["write_file", "edit_file"].includes(toolCall.name)) {
1271
- const path = toolCall.arguments["path"];
1272
- sessionManager2.addModifiedFile(sessionId, path);
1314
+ registerOutputHandler();
1315
+ const sessionId = message.payload.sessionId;
1316
+ const history = terminalManager.getOutputHistory(sessionId);
1317
+ if (history) {
1318
+ setTimeout(() => {
1319
+ ws.send(JSON.stringify({
1320
+ type: "terminal.output",
1321
+ payload: { sessionId, data: history }
1322
+ }));
1323
+ }, 100);
1273
1324
  }
1274
- },
1275
- injectKickoff: () => {
1276
- if (options.injectBuilderKickoff !== true) return;
1277
- const session = sessionManager2.requireSession(sessionId);
1278
- const events = eventStore.getEvents(sessionId);
1279
- const hasBuilderKickoff = events.some((e) => {
1280
- if (e.type !== "message.start") return false;
1281
- const data = e.data;
1282
- return data.messageKind === "auto-prompt" && data.content?.includes("fulfil the");
1283
- });
1284
- if (!hasBuilderKickoff) {
1285
- const kickoffMsgId = crypto.randomUUID();
1286
- const kickoffContent = BUILDER_KICKOFF_PROMPT(session.criteria.length);
1287
- eventStore.append(sessionId, createMessageStartEvent(kickoffMsgId, "user", kickoffContent, {
1288
- ...getCurrentWindowMessageOptions(sessionId) ?? {},
1289
- isSystemGenerated: true,
1290
- messageKind: "auto-prompt"
1291
- }));
1292
- eventStore.append(sessionId, { type: "message.done", data: { messageId: kickoffMsgId } });
1325
+ }
1326
+ break;
1327
+ }
1328
+ case "terminal.write": {
1329
+ if (message.payload.sessionId && message.payload.data) {
1330
+ terminalManager.write(message.payload.sessionId, message.payload.data);
1331
+ }
1332
+ break;
1333
+ }
1334
+ case "terminal.resize": {
1335
+ if (message.payload.sessionId && typeof message.payload.cols === "number" && typeof message.payload.rows === "number") {
1336
+ terminalManager.resize(message.payload.sessionId, message.payload.cols, message.payload.rows);
1337
+ }
1338
+ break;
1339
+ }
1340
+ case "terminal.kill": {
1341
+ if (message.payload.sessionId) {
1342
+ terminalManager.kill(message.payload.sessionId);
1343
+ for (const sub of subscriptions) {
1344
+ if (sub.sessionId === message.payload.sessionId) {
1345
+ subscriptions.delete(sub);
1346
+ }
1293
1347
  }
1348
+ ws.send(JSON.stringify({
1349
+ type: "terminal.killed",
1350
+ payload: { sessionId: message.payload.sessionId }
1351
+ }));
1294
1352
  }
1295
- }, turnMetrics),
1296
- stepDoneCalled
1297
- };
1353
+ break;
1354
+ }
1355
+ }
1298
1356
  }
1299
- function buildSnapshot(sessionManager2, sessionId, lastStats) {
1300
- const eventStore = getEventStore();
1301
- const session = sessionManager2.requireSession(sessionId);
1302
- const events = eventStore.getEvents(sessionId);
1303
- const latestSeq = eventStore.getLatestSeq(sessionId) ?? 0;
1304
- return buildSnapshotFromSessionState({
1305
- session,
1306
- events,
1307
- latestSeq
1308
- });
1357
+ function unsubscribeAllFromTerminal(ws) {
1358
+ for (const sub of subscriptions) {
1359
+ if (sub.ws === ws) {
1360
+ subscriptions.delete(sub);
1361
+ }
1362
+ }
1309
1363
  }
1310
1364
 
1311
1365
  // src/server/workflows/registry.ts
@@ -1465,7 +1519,7 @@ async function deleteWorkflow(configDir, workflowId) {
1465
1519
 
1466
1520
  // src/server/runner/types.ts
1467
1521
  var RUNNER_CONFIG = {
1468
- maxVerifyRetries: 4
1522
+ maxVerifyRetries: 10
1469
1523
  // Max times to retry a failing criterion
1470
1524
  };
1471
1525
 
@@ -1474,14 +1528,14 @@ var TERMINAL_DONE = "$done";
1474
1528
  var TERMINAL_BLOCKED = "$blocked";
1475
1529
 
1476
1530
  // src/server/workflows/shell.ts
1477
- import { spawn } from "child_process";
1531
+ import { spawn as spawn2 } from "child_process";
1478
1532
  function executeShellCommand(command, cwd, timeout, signal) {
1479
1533
  return new Promise((resolve2, reject) => {
1480
1534
  if (signal?.aborted) {
1481
1535
  reject(new Error("Aborted"));
1482
1536
  return;
1483
1537
  }
1484
- const proc = spawn("sh", ["-c", command], {
1538
+ const proc = spawn2("sh", ["-c", command], {
1485
1539
  cwd,
1486
1540
  env: { ...process.env, FORCE_COLOR: "0" },
1487
1541
  stdio: ["ignore", "pipe", "pipe"]
@@ -1588,7 +1642,7 @@ function evaluateTransitions(transitions, criteria, stepOutcome) {
1588
1642
  }
1589
1643
  return TERMINAL_BLOCKED;
1590
1644
  }
1591
- function getCurrentWindowMessageOptions2(sessionId) {
1645
+ function getCurrentWindowMessageOptions(sessionId) {
1592
1646
  const contextWindowId = getCurrentContextWindowId(sessionId);
1593
1647
  return contextWindowId ? { contextWindowId } : void 0;
1594
1648
  }
@@ -1597,21 +1651,21 @@ function buildReason(criteria) {
1597
1651
  return `${remaining.length} criteria remaining`;
1598
1652
  }
1599
1653
  async function executeWorkflow(workflow, options) {
1600
- const { sessionManager: sessionManager2, sessionId, llmClient, signal, onMessage } = options;
1654
+ const { sessionManager, sessionId, llmClient, signal, onMessage } = options;
1601
1655
  const eventStore = getEventStore();
1602
1656
  const startTime = performance.now();
1603
1657
  let iterations = 0;
1604
1658
  let currentStepId = workflow.entryStep;
1605
1659
  let lastStepOutput = {};
1606
1660
  let isFirstBuilderEntry = true;
1607
- const messagesBeforeWorkflow = sessionManager2.requireSession(sessionId).messages.length;
1661
+ const messagesBeforeWorkflow = sessionManager.requireSession(sessionId).messages.length;
1608
1662
  const stepsById = /* @__PURE__ */ new Map();
1609
1663
  for (const step of workflow.steps) {
1610
1664
  stepsById.set(step.id, step);
1611
1665
  }
1612
1666
  logger.debug("Workflow executor starting", { sessionId, workflow: workflow.metadata.id });
1613
1667
  if (workflow.startCondition && workflow.startCondition.type !== "always") {
1614
- const session = sessionManager2.requireSession(sessionId);
1668
+ const session = sessionManager.requireSession(sessionId);
1615
1669
  const criteria = session.criteria;
1616
1670
  const conditionMet = evaluateCondition(
1617
1671
  workflow.startCondition,
@@ -1628,7 +1682,7 @@ async function executeWorkflow(workflow, options) {
1628
1682
  }
1629
1683
  }
1630
1684
  const startMsgId = crypto.randomUUID();
1631
- const startWindowOpts = getCurrentWindowMessageOptions2(sessionId);
1685
+ const startWindowOpts = getCurrentWindowMessageOptions(sessionId);
1632
1686
  eventStore.append(sessionId, createMessageStartEvent(
1633
1687
  startMsgId,
1634
1688
  "user",
@@ -1637,7 +1691,7 @@ async function executeWorkflow(workflow, options) {
1637
1691
  ));
1638
1692
  eventStore.append(sessionId, { type: "message.done", data: { messageId: startMsgId } });
1639
1693
  if (options.userMessage) {
1640
- sessionManager2.addMessage(sessionId, {
1694
+ sessionManager.addMessage(sessionId, {
1641
1695
  role: "user",
1642
1696
  content: options.userMessage.content,
1643
1697
  ...options.userMessage.attachments ? { attachments: options.userMessage.attachments } : {}
@@ -1662,8 +1716,8 @@ async function executeWorkflow(workflow, options) {
1662
1716
  totalTime: (performance.now() - startTime) / 1e3
1663
1717
  };
1664
1718
  }
1665
- const session = sessionManager2.requireSession(sessionId);
1666
- const currentWindowMessageOptions = getCurrentWindowMessageOptions2(sessionId);
1719
+ const session = sessionManager.requireSession(sessionId);
1720
+ const currentWindowMessageOptions = getCurrentWindowMessageOptions(sessionId);
1667
1721
  const criteria = session.criteria;
1668
1722
  const templateCtx = {
1669
1723
  workdir: session.workdir,
@@ -1677,7 +1731,7 @@ async function executeWorkflow(workflow, options) {
1677
1731
  modifiedFiles: formatModifiedFiles(session),
1678
1732
  stepOutput: lastStepOutput
1679
1733
  };
1680
- sessionManager2.setPhase(sessionId, step.phase);
1734
+ sessionManager.setPhase(sessionId, step.phase);
1681
1735
  logger.debug("Workflow step executing", { sessionId, iteration: iterations, stepId: step.id, stepType: step.type });
1682
1736
  let stepOutcome = null;
1683
1737
  switch (step.type) {
@@ -1694,9 +1748,21 @@ async function executeWorkflow(workflow, options) {
1694
1748
  eventStore.append(sessionId, createMessageStartEvent(promptMsgId, "user", promptContent, {
1695
1749
  ...currentWindowMessageOptions ?? {},
1696
1750
  isSystemGenerated: true,
1697
- messageKind: "auto-prompt"
1751
+ messageKind: "auto-prompt",
1752
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1698
1753
  }));
1699
1754
  eventStore.append(sessionId, { type: "message.done", data: { messageId: promptMsgId } });
1755
+ if (onMessage) {
1756
+ onMessage(createChatMessageMessage({
1757
+ id: promptMsgId,
1758
+ role: "user",
1759
+ content: promptContent,
1760
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1761
+ isSystemGenerated: true,
1762
+ messageKind: "auto-prompt",
1763
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1764
+ }));
1765
+ }
1700
1766
  } else if (!isFirstBuilderEntry) {
1701
1767
  const parts = [];
1702
1768
  if (agentStep.nudgePrompt) {
@@ -1709,13 +1775,25 @@ async function executeWorkflow(workflow, options) {
1709
1775
  eventStore.append(sessionId, createMessageStartEvent(nudgeMsgId, "user", nudgeContent, {
1710
1776
  ...currentWindowMessageOptions ?? {},
1711
1777
  isSystemGenerated: true,
1712
- messageKind: "correction"
1778
+ messageKind: "correction",
1779
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1713
1780
  }));
1714
1781
  eventStore.append(sessionId, { type: "message.done", data: { messageId: nudgeMsgId } });
1782
+ if (onMessage) {
1783
+ onMessage(createChatMessageMessage({
1784
+ id: nudgeMsgId,
1785
+ role: "user",
1786
+ content: nudgeContent,
1787
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1788
+ isSystemGenerated: true,
1789
+ messageKind: "correction",
1790
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1791
+ }));
1792
+ }
1715
1793
  }
1716
1794
  const turnMetrics = new TurnMetrics();
1717
1795
  const agentResult = await runBuilderTurn({
1718
- sessionManager: sessionManager2,
1796
+ sessionManager,
1719
1797
  sessionId,
1720
1798
  llmClient,
1721
1799
  injectStepDone: true,
@@ -1757,7 +1835,7 @@ async function executeWorkflow(workflow, options) {
1757
1835
  const result = await executeSubAgent({
1758
1836
  subAgentType: subStep.subAgentType,
1759
1837
  prompt: resolvedPrompt,
1760
- sessionManager: sessionManager2,
1838
+ sessionManager,
1761
1839
  sessionId,
1762
1840
  llmClient,
1763
1841
  toolRegistry: filteredToolRegistry,
@@ -1780,8 +1858,20 @@ async function executeWorkflow(workflow, options) {
1780
1858
  eventStore.append(sessionId, createMessageStartEvent(shellMsgId, "user", `Running: \`${command}\``, {
1781
1859
  ...currentWindowMessageOptions ?? {},
1782
1860
  isSystemGenerated: true,
1783
- messageKind: "auto-prompt"
1861
+ messageKind: "auto-prompt",
1862
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1784
1863
  }));
1864
+ if (onMessage) {
1865
+ onMessage(createChatMessageMessage({
1866
+ id: shellMsgId,
1867
+ role: "user",
1868
+ content: `Running: \`${command}\``,
1869
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1870
+ isSystemGenerated: true,
1871
+ messageKind: "auto-prompt",
1872
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1873
+ }));
1874
+ }
1785
1875
  const result = await executeShellCommand(command, session.workdir, timeout, signal);
1786
1876
  const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
1787
1877
  lastStepOutput = { stdout: result.stdout ?? "", stderr: result.stderr ?? "", exitCode: String(result.exitCode) };
@@ -1797,20 +1887,30 @@ ${output.slice(0, 1e4)}
1797
1887
  messageKind: "correction"
1798
1888
  }));
1799
1889
  eventStore.append(sessionId, { type: "message.done", data: { messageId: outputMsgId } });
1890
+ if (onMessage) {
1891
+ onMessage(createChatMessageMessage({
1892
+ id: outputMsgId,
1893
+ role: "user",
1894
+ content: outputContent,
1895
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1896
+ isSystemGenerated: true,
1897
+ messageKind: "correction"
1898
+ }));
1899
+ }
1800
1900
  stepOutcome = { result: successCodes.includes(result.exitCode) ? "success" : "failure", output: lastStepOutput };
1801
1901
  break;
1802
1902
  }
1803
1903
  }
1804
- const refreshedSession = sessionManager2.requireSession(sessionId);
1904
+ const refreshedSession = sessionManager.requireSession(sessionId);
1805
1905
  const nextStepId = evaluateTransitions(
1806
1906
  step.transitions,
1807
1907
  refreshedSession.criteria,
1808
1908
  stepOutcome
1809
1909
  );
1810
1910
  if (nextStepId === TERMINAL_DONE) {
1811
- sessionManager2.setPhase(sessionId, "done");
1911
+ sessionManager.setPhase(sessionId, "done");
1812
1912
  const totalTimeSeconds = Math.round((performance.now() - startTime) / 100) / 10;
1813
- const completedSession = sessionManager2.requireSession(sessionId);
1913
+ const completedSession = sessionManager.requireSession(sessionId);
1814
1914
  const workflowMessages = completedSession.messages.slice(messagesBeforeWorkflow);
1815
1915
  const workflowStats = computeSessionStats(workflowMessages);
1816
1916
  const totalToolCalls = workflowMessages.reduce(
@@ -1848,16 +1948,28 @@ ${output.slice(0, 1e4)}
1848
1948
  };
1849
1949
  }
1850
1950
  if (nextStepId === TERMINAL_BLOCKED) {
1851
- sessionManager2.setPhase(sessionId, "blocked");
1951
+ sessionManager.setPhase(sessionId, "blocked");
1852
1952
  const blockedCriteria = refreshedSession.criteria.filter((c) => c.status.type === "failed" && c.attempts.filter((a) => a.status === "failed").length >= RUNNER_CONFIG.maxVerifyRetries).map((c) => c.id);
1853
1953
  const reason = blockedCriteria.length > 0 ? `Retry limit reached for: ${blockedCriteria.join(", ")}` : "No matching transition";
1854
1954
  const blockedMsgId = crypto.randomUUID();
1855
1955
  eventStore.append(sessionId, createMessageStartEvent(blockedMsgId, "user", `Runner blocked: ${reason}`, {
1856
1956
  ...currentWindowMessageOptions ?? {},
1857
1957
  isSystemGenerated: true,
1858
- messageKind: "correction"
1958
+ messageKind: "correction",
1959
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1859
1960
  }));
1860
1961
  eventStore.append(sessionId, { type: "message.done", data: { messageId: blockedMsgId } });
1962
+ if (onMessage) {
1963
+ onMessage(createChatMessageMessage({
1964
+ id: blockedMsgId,
1965
+ role: "user",
1966
+ content: `Runner blocked: ${reason}`,
1967
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1968
+ isSystemGenerated: true,
1969
+ messageKind: "correction",
1970
+ metadata: { type: "workflow", name: "Workflow", color: "#f59e0b" }
1971
+ }));
1972
+ }
1861
1973
  logger.warn("Workflow executor blocked", { sessionId, iterations, reason });
1862
1974
  const blockedAction = { type: "BLOCKED", reason, blockedCriteria };
1863
1975
  return {
@@ -1890,302 +2002,50 @@ async function runOrchestrator(options) {
1890
2002
  return executeWorkflow(workflow, options);
1891
2003
  }
1892
2004
 
1893
- // src/server/db/sessions.ts
1894
- function createSession(projectId, workdir, title, providerId, providerModel) {
1895
- const db = getDatabase();
1896
- const now = (/* @__PURE__ */ new Date()).toISOString();
1897
- const id = crypto.randomUUID();
1898
- db.prepare(`
1899
- INSERT INTO sessions (id, project_id, workdir, phase, mode, workflow_phase, is_running, created_at, updated_at, title, provider_id, provider_model)
1900
- VALUES (?, ?, ?, 'idle', 'planner', 'plan', 0, ?, ?, ?, ?, ?)
1901
- `).run(id, projectId, workdir, now, now, title ?? null, providerId ?? null, providerModel ?? null);
1902
- return {
1903
- id,
1904
- projectId,
1905
- workdir,
1906
- mode: "planner",
1907
- phase: "plan",
1908
- isRunning: false,
1909
- summary: null,
1910
- providerId: providerId ?? null,
1911
- providerModel: providerModel ?? null,
1912
- createdAt: now,
1913
- updatedAt: now,
1914
- messages: [],
1915
- criteria: [],
1916
- contextWindows: [],
1917
- executionState: null,
1918
- metadata: {
1919
- ...title ? { title } : {},
1920
- totalTokensUsed: 0,
1921
- totalToolCalls: 0,
1922
- iterationCount: 0
1923
- }
1924
- };
1925
- }
1926
- function getSession(id) {
1927
- const db = getDatabase();
1928
- const row = db.prepare(`
1929
- SELECT * FROM sessions WHERE id = ?
1930
- `).get(id);
1931
- if (!row) {
1932
- return null;
1933
- }
1934
- return {
1935
- id: row.id,
1936
- projectId: row.project_id,
1937
- workdir: row.workdir,
1938
- mode: row.mode ?? "planner",
1939
- phase: row.workflow_phase ?? "plan",
1940
- isRunning: Boolean(row.is_running),
1941
- summary: row.summary ?? null,
1942
- providerId: row.provider_id ?? null,
1943
- providerModel: row.provider_model ?? null,
1944
- createdAt: row.created_at,
1945
- updatedAt: row.updated_at,
1946
- messages: [],
1947
- criteria: [],
1948
- contextWindows: [],
1949
- executionState: null,
1950
- metadata: {
1951
- ...row.title ? { title: row.title } : {},
1952
- totalTokensUsed: row.total_tokens_used,
1953
- totalToolCalls: row.total_tool_calls,
1954
- iterationCount: row.iteration_count
1955
- }
1956
- };
1957
- }
1958
- function updateSessionProvider(id, providerId, providerModel) {
1959
- const db = getDatabase();
1960
- const now = (/* @__PURE__ */ new Date()).toISOString();
1961
- db.prepare(`
1962
- UPDATE sessions SET provider_id = ?, provider_model = ?, updated_at = ? WHERE id = ?
1963
- `).run(providerId, providerModel, now, id);
1964
- }
1965
- function updateSessionSummary(id, summary) {
1966
- const db = getDatabase();
1967
- const now = (/* @__PURE__ */ new Date()).toISOString();
1968
- db.prepare(`
1969
- UPDATE sessions SET summary = ?, updated_at = ? WHERE id = ?
1970
- `).run(summary, now, id);
1971
- }
1972
- function updateSessionMetadata(id, metadata) {
1973
- const db = getDatabase();
1974
- const now = (/* @__PURE__ */ new Date()).toISOString();
1975
- const updates = ["updated_at = ?"];
1976
- const values = [now];
1977
- if (metadata.title !== void 0) {
1978
- updates.push("title = ?");
1979
- values.push(metadata.title ?? "");
1980
- }
1981
- if (metadata.totalTokensUsed !== void 0) {
1982
- updates.push("total_tokens_used = ?");
1983
- values.push(metadata.totalTokensUsed);
1984
- }
1985
- if (metadata.totalToolCalls !== void 0) {
1986
- updates.push("total_tool_calls = ?");
1987
- values.push(metadata.totalToolCalls);
1988
- }
1989
- if (metadata.iterationCount !== void 0) {
1990
- updates.push("iteration_count = ?");
1991
- values.push(metadata.iterationCount);
1992
- }
1993
- values.push(id);
1994
- db.prepare(`
1995
- UPDATE sessions SET ${updates.join(", ")} WHERE id = ?
1996
- `).run(...values);
1997
- }
1998
- function listSessions() {
1999
- const db = getDatabase();
2000
- const rows = db.prepare(`
2001
- SELECT
2002
- s.id,
2003
- s.project_id,
2004
- s.workdir,
2005
- s.mode,
2006
- s.workflow_phase,
2007
- s.is_running,
2008
- s.created_at,
2009
- s.updated_at,
2010
- s.title,
2011
- s.provider_id,
2012
- s.provider_model,
2013
- COALESCE(
2014
- (SELECT COUNT(*) FROM events e
2015
- WHERE e.session_id = s.id
2016
- AND e.event_type = 'message.start'
2017
- AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
2018
- +
2019
- COALESCE(
2020
- (SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
2021
- WHERE e.session_id = s.id
2022
- AND e.event_type = 'turn.snapshot'
2023
- AND json_extract(e.payload, '$.messages') IS NOT NULL),
2024
- 0
2025
- ),
2026
- 0
2027
- ) as message_count
2028
- FROM sessions s
2029
- ORDER BY s.updated_at DESC
2030
- `).all();
2031
- return rows.map((row) => ({
2032
- id: row.id,
2033
- projectId: row.project_id,
2034
- ...row.title ? { title: row.title } : {},
2035
- workdir: row.workdir,
2036
- mode: row.mode ?? "planner",
2037
- phase: row.workflow_phase ?? "plan",
2038
- isRunning: Boolean(row.is_running),
2039
- providerId: row.provider_id ?? null,
2040
- providerModel: row.provider_model ?? null,
2041
- createdAt: row.created_at,
2042
- updatedAt: row.updated_at,
2043
- criteriaCount: 0,
2044
- // Derived from events
2045
- criteriaCompleted: 0,
2046
- // Derived from events
2047
- messageCount: row.message_count
2048
- }));
2049
- }
2050
- function listSessionsByProject(projectId) {
2051
- const db = getDatabase();
2052
- const rows = db.prepare(`
2053
- SELECT
2054
- s.id,
2055
- s.project_id,
2056
- s.workdir,
2057
- s.mode,
2058
- s.workflow_phase,
2059
- s.is_running,
2060
- s.created_at,
2061
- s.updated_at,
2062
- s.title,
2063
- s.provider_id,
2064
- s.provider_model,
2065
- COALESCE(
2066
- (SELECT COUNT(*) FROM events e
2067
- WHERE e.session_id = s.id
2068
- AND e.event_type = 'message.start'
2069
- AND json_extract(e.payload, '$.role') IN ('user', 'assistant'))
2070
- +
2071
- COALESCE(
2072
- (SELECT SUM(json_array_length(json_extract(e.payload, '$.messages'))) FROM events e
2073
- WHERE e.session_id = s.id
2074
- AND e.event_type = 'turn.snapshot'
2075
- AND json_extract(e.payload, '$.messages') IS NOT NULL),
2076
- 0
2077
- ),
2078
- 0
2079
- ) as message_count
2080
- FROM sessions s
2081
- WHERE s.project_id = ?
2082
- ORDER BY s.updated_at DESC
2083
- `).all(projectId);
2084
- return rows.map((row) => ({
2085
- id: row.id,
2086
- projectId: row.project_id,
2087
- ...row.title ? { title: row.title } : {},
2088
- workdir: row.workdir,
2089
- mode: row.mode ?? "planner",
2090
- phase: row.workflow_phase ?? "plan",
2091
- isRunning: Boolean(row.is_running),
2092
- providerId: row.provider_id ?? null,
2093
- providerModel: row.provider_model ?? null,
2094
- createdAt: row.created_at,
2095
- updatedAt: row.updated_at,
2096
- criteriaCount: 0,
2097
- // Derived from events
2098
- criteriaCompleted: 0,
2099
- // Derived from events
2100
- messageCount: row.message_count
2101
- }));
2102
- }
2103
- function deleteSession(id) {
2104
- const db = getDatabase();
2105
- db.prepare("DELETE FROM sessions WHERE id = ?").run(id);
2106
- }
2107
-
2108
- // src/server/session/name-generator.ts
2109
- var SESSION_NAME_PROMPT = `Generate a concise, descriptive session name (max 50 characters) based on the user's message.
2110
- Return ONLY the name, nothing else.
2111
-
2112
- Example inputs and outputs:
2113
- - "How do I set up React?" \u2192 "React setup"
2114
- - "Fix the authentication bug" \u2192 "Fix authentication bug"
2115
- - "Add unit tests for the API" \u2192 "Add API unit tests"
2116
-
2117
- User message: {message}`;
2118
- async function generateSessionName(options) {
2119
- const { userMessage, llmClient, signal } = options;
2120
- try {
2121
- const prompt = SESSION_NAME_PROMPT.replace("{message}", userMessage);
2122
- const messages = [
2123
- {
2124
- role: "user",
2125
- content: prompt
2126
- }
2127
- ];
2128
- const timeoutSignal = AbortSignal.timeout(6e4);
2129
- const composedSignal = signal ? AbortSignal.any([timeoutSignal, signal]) : timeoutSignal;
2130
- const response = await llmClient.complete({
2131
- messages,
2132
- tools: [],
2133
- signal: composedSignal,
2134
- disableThinking: true
2135
- });
2136
- let name = response.content.trim();
2137
- if (name.length > 50) {
2138
- name = name.substring(0, 47) + "...";
2139
- }
2140
- if (!name || name.length < 3) {
2141
- return {
2142
- success: false,
2143
- error: "Generated name is too short or empty"
2144
- };
2145
- }
2146
- return {
2147
- success: true,
2148
- name
2149
- };
2150
- } catch (error) {
2151
- return {
2152
- success: false,
2153
- error: error instanceof Error ? error.message : "Unknown error generating session name"
2154
- };
2155
- }
2156
- }
2157
- function needsNameGeneration(sessionTitle, messageCount) {
2158
- if (messageCount > 1) {
2159
- return false;
2160
- }
2161
- if (!sessionTitle || sessionTitle.trim() === "") {
2162
- return true;
2163
- }
2164
- if (/^Session \d+$/.test(sessionTitle)) {
2165
- return true;
2166
- }
2167
- return false;
2168
- }
2169
-
2170
2005
  // src/server/session/summary-generator.ts
2171
- var SESSION_SUMMARY_PROMPT = `Write a 2-3 sentence summary of what the user wants to accomplish. Focus on WHAT and WHY, not HOW. Output only the summary, no preamble.
2172
-
2173
- ## Conversation History
2174
- {messages}
2175
-
2176
- ## Summary
2177
- `;
2006
+ var SUMMARY_INSTRUCTION = "Write a 2-3 sentence summary of what the user wants to accomplish. Focus on WHAT and WHY, not HOW. Output only the summary, no preamble.";
2178
2007
  async function generateSessionSummary(options) {
2179
- const { messages, llmClient } = options;
2008
+ const { messages, llmClient, workdir, customInstructions, skills } = options;
2180
2009
  try {
2181
2010
  const messagesText = messages.map((m) => `${m.role}: ${m.content}`).join("\n");
2182
- const prompt = SESSION_SUMMARY_PROMPT.replace("{messages}", messagesText);
2183
- const llmMessages = [
2011
+ const userPrompt = `${SUMMARY_INSTRUCTION}
2012
+
2013
+ ## Conversation History
2014
+ ${messagesText}
2015
+
2016
+ ## Summary`;
2017
+ const contextMessages = [
2184
2018
  {
2185
2019
  role: "user",
2186
- content: prompt
2020
+ content: userPrompt,
2021
+ source: "history"
2187
2022
  }
2188
2023
  ];
2024
+ const mockAgentDef = {
2025
+ metadata: {
2026
+ id: "summary",
2027
+ name: "Summary",
2028
+ description: "Session summary generator",
2029
+ subagent: false,
2030
+ allowedTools: []
2031
+ },
2032
+ prompt: SUMMARY_INSTRUCTION
2033
+ };
2034
+ const assembledRequest = assembleAgentRequest({
2035
+ agentDef: mockAgentDef,
2036
+ workdir,
2037
+ messages: contextMessages,
2038
+ injectedFiles: [],
2039
+ ...customInstructions ? { customInstructions } : {},
2040
+ ...skills ? { skills } : {},
2041
+ promptTools: [],
2042
+ toolChoice: "none",
2043
+ disableThinking: true
2044
+ });
2045
+ const llmMessages = assembledRequest.messages.map((msg) => ({
2046
+ role: msg.role,
2047
+ content: msg.content
2048
+ }));
2189
2049
  const response = await llmClient.complete({
2190
2050
  messages: llmMessages,
2191
2051
  tools: [],
@@ -2218,7 +2078,7 @@ function needsSummaryGeneration(sessionSummary) {
2218
2078
  }
2219
2079
 
2220
2080
  // src/server/ws/server.ts
2221
- function getCurrentWindowMessageOptions3(sessionId) {
2081
+ function getCurrentWindowMessageOptions2(sessionId) {
2222
2082
  const contextWindowId = getCurrentContextWindowId(sessionId);
2223
2083
  return contextWindowId ? { contextWindowId } : void 0;
2224
2084
  }
@@ -2236,7 +2096,7 @@ function getSessionMessageCount(sessionId) {
2236
2096
  }
2237
2097
  return count;
2238
2098
  }
2239
- function resolveStatsIdentity2(llmClient, getActiveProvider) {
2099
+ function resolveStatsIdentity(llmClient, getActiveProvider) {
2240
2100
  const provider = getActiveProvider?.();
2241
2101
  const model = llmClient.getModel();
2242
2102
  const backend = provider?.backend ?? (llmClient.getBackend() === "unknown" ? "unknown" : llmClient.getBackend());
@@ -2248,12 +2108,12 @@ function resolveStatsIdentity2(llmClient, getActiveProvider) {
2248
2108
  };
2249
2109
  }
2250
2110
  var activeAgents = /* @__PURE__ */ new Map();
2251
- function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvider, toolRegistry, sessionManager2, providerManager) {
2111
+ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvider, sessionManager, providerManager) {
2252
2112
  const wss = new WebSocketServer({ server: httpServer, path: "/ws" });
2253
2113
  const clients = /* @__PURE__ */ new Map();
2254
2114
  const sessionLLMClients = /* @__PURE__ */ new Map();
2255
2115
  function getSessionLLMClient(sessionId) {
2256
- const session = sessionManager2.getSession(sessionId);
2116
+ const session = sessionManager.getSession(sessionId);
2257
2117
  if (!session?.providerId || !session?.providerModel || !providerManager) {
2258
2118
  return getLLMClient();
2259
2119
  }
@@ -2268,7 +2128,7 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2268
2128
  sessionId,
2269
2129
  providerId: session.providerId
2270
2130
  });
2271
- sessionManager2.setSessionProvider(sessionId, null, null);
2131
+ sessionManager.setSessionProvider(sessionId, null, null);
2272
2132
  sessionLLMClients.delete(sessionId);
2273
2133
  return getLLMClient();
2274
2134
  }
@@ -2286,9 +2146,9 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2286
2146
  return client;
2287
2147
  }
2288
2148
  function getSessionStatsIdentity(sessionId) {
2289
- const session = sessionManager2.getSession(sessionId);
2149
+ const session = sessionManager.getSession(sessionId);
2290
2150
  if (!session?.providerId || !providerManager) {
2291
- return resolveStatsIdentity2(getLLMClient(), getActiveProvider);
2151
+ return resolveStatsIdentity(getLLMClient(), getActiveProvider);
2292
2152
  }
2293
2153
  const provider = providerManager.getProviders().find((p) => p.id === session.providerId);
2294
2154
  const client = getSessionLLMClient(sessionId);
@@ -2307,23 +2167,128 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2307
2167
  };
2308
2168
  const broadcastForSession = (sessionId, msg) => {
2309
2169
  for (const [clientWs, client] of clients) {
2310
- if (isSubscribedToSession(client, sessionId) && clientWs.readyState === WebSocket.OPEN) {
2170
+ if (isSubscribedToSession(client, sessionId) && clientWs.readyState === WebSocket2.OPEN) {
2311
2171
  clientWs.send(serializeServerMessage({ ...msg, sessionId }));
2312
2172
  }
2313
2173
  }
2314
2174
  };
2315
- sessionManager2.subscribe((event) => {
2175
+ const llmForSession = (sessionId) => getSessionLLMClient?.(sessionId) ?? getLLMClient();
2176
+ const statsForSession = (sessionId) => getSessionStatsIdentity?.(sessionId) ?? resolveStatsIdentity(getLLMClient(), getActiveProvider);
2177
+ function startTurnWithCompletionChain(sessionId, controller) {
2178
+ runChatTurn({
2179
+ sessionManager,
2180
+ sessionId,
2181
+ llmClient: llmForSession(sessionId),
2182
+ statsIdentity: statsForSession(sessionId),
2183
+ signal: controller.signal,
2184
+ onMessage: (msg) => broadcastForSession(sessionId, msg)
2185
+ }).catch((error) => {
2186
+ if (error instanceof Error && error.message === "Aborted") {
2187
+ return;
2188
+ }
2189
+ logger.error("Chat turn error", { error });
2190
+ }).finally(() => {
2191
+ if (activeAgents.get(sessionId) !== controller) {
2192
+ return;
2193
+ }
2194
+ activeAgents.delete(sessionId);
2195
+ const asapMsgs = sessionManager.drainAsapMessages(sessionId);
2196
+ const nextAsap = asapMsgs[0];
2197
+ if (nextAsap) {
2198
+ for (const remaining of asapMsgs.slice(1)) {
2199
+ sessionManager.queueMessage(sessionId, "asap", remaining.content, remaining.attachments, remaining.messageKind);
2200
+ }
2201
+ broadcastForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2202
+ const userMessagePayload = {
2203
+ role: "user",
2204
+ content: nextAsap.content,
2205
+ ...nextAsap.attachments ? { attachments: nextAsap.attachments } : {}
2206
+ };
2207
+ const userMessage = sessionManager.addMessage(sessionId, userMessagePayload);
2208
+ broadcastForSession(sessionId, createChatMessageMessage(userMessage));
2209
+ const newController = new AbortController();
2210
+ activeAgents.set(sessionId, newController);
2211
+ startTurnWithCompletionChain(sessionId, newController);
2212
+ return;
2213
+ }
2214
+ const completionMsgs = sessionManager.drainCompletionMessages(sessionId);
2215
+ const next = completionMsgs[0];
2216
+ if (next) {
2217
+ for (const remaining of completionMsgs.slice(1)) {
2218
+ sessionManager.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
2219
+ }
2220
+ broadcastForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2221
+ const userMessage = sessionManager.addMessage(sessionId, {
2222
+ role: "user",
2223
+ content: next.content,
2224
+ ...next.attachments ? { attachments: next.attachments } : {}
2225
+ });
2226
+ broadcastForSession(sessionId, createChatMessageMessage(userMessage));
2227
+ const newController = new AbortController();
2228
+ activeAgents.set(sessionId, newController);
2229
+ startTurnWithCompletionChain(sessionId, newController);
2230
+ return;
2231
+ }
2232
+ sessionManager.clearMessageQueue(sessionId);
2233
+ sessionManager.setRunning(sessionId, false);
2234
+ const contextState = sessionManager.getContextState(sessionId);
2235
+ broadcastForSession(sessionId, createContextStateMessage(contextState));
2236
+ });
2237
+ }
2238
+ function triggerQueueProcessing(sessionId) {
2239
+ const currentSession = sessionManager.getSession(sessionId);
2240
+ if (!currentSession || currentSession.isRunning) {
2241
+ return false;
2242
+ }
2243
+ const queue = sessionManager.getQueueState(sessionId);
2244
+ if (queue.length === 0) {
2245
+ return false;
2246
+ }
2247
+ const controller = new AbortController();
2248
+ activeAgents.set(sessionId, controller);
2249
+ sessionManager.setRunning(sessionId, true);
2250
+ const eventStore = getEventStore();
2251
+ eventStore.append(sessionId, { type: "running.changed", data: { isRunning: true } });
2252
+ broadcastForSession(sessionId, createSessionRunningMessage(true));
2253
+ const nextAsap = queue.find((m) => m.mode === "asap") ?? queue[0];
2254
+ if (nextAsap) {
2255
+ sessionManager.cancelQueuedMessage(sessionId, nextAsap.queueId);
2256
+ const userMessage = sessionManager.addMessage(sessionId, {
2257
+ role: "user",
2258
+ content: nextAsap.content,
2259
+ ...nextAsap.attachments ? { attachments: nextAsap.attachments } : {},
2260
+ ...nextAsap.messageKind ? { messageKind: nextAsap.messageKind } : {}
2261
+ });
2262
+ broadcastForSession(sessionId, createChatMessageMessage(userMessage));
2263
+ }
2264
+ startTurnWithCompletionChain(sessionId, controller);
2265
+ return true;
2266
+ }
2267
+ sessionManager.subscribe((event) => {
2316
2268
  const sessionId = "sessionId" in event ? event.sessionId : "session" in event ? event.session.id : null;
2317
2269
  if (!sessionId) return;
2318
2270
  for (const [ws, client] of clients) {
2319
- if (isSubscribedToSession(client, sessionId) && ws.readyState === WebSocket.OPEN) {
2271
+ if (ws.readyState !== WebSocket2.OPEN) continue;
2272
+ if (event.type === "session_created") {
2273
+ const session = sessionManager.getSession(sessionId);
2274
+ if (session) {
2275
+ const eventStore = getEventStore();
2276
+ const events = eventStore.getEvents(sessionId);
2277
+ const messages = events.length > 0 ? buildMessagesFromStoredEvents(events) : [];
2278
+ const pendingConfirmations = foldPendingConfirmations(events);
2279
+ ws.send(serializeServerMessage({ ...createSessionStateMessage(session, messages, pendingConfirmations), sessionId }));
2280
+ }
2281
+ continue;
2282
+ }
2283
+ if (isSubscribedToSession(client, sessionId)) {
2320
2284
  if (event.type === "session_updated") {
2321
- const session = sessionManager2.getSession(sessionId);
2285
+ const session = sessionManager.getSession(sessionId);
2322
2286
  if (session) {
2323
2287
  const eventStore = getEventStore();
2324
2288
  const events = eventStore.getEvents(sessionId);
2325
2289
  const messages = events.length > 0 ? buildMessagesFromStoredEvents(events) : [];
2326
- ws.send(serializeServerMessage({ ...createSessionStateMessage(session, messages), sessionId }));
2290
+ const pendingConfirmations = foldPendingConfirmations(events);
2291
+ ws.send(serializeServerMessage({ ...createSessionStateMessage(session, messages, pendingConfirmations), sessionId }));
2327
2292
  }
2328
2293
  }
2329
2294
  if (event.type === "running_changed") {
@@ -2335,7 +2300,7 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2335
2300
  const broadcastAll = (msg) => {
2336
2301
  const serialized = serializeServerMessage(msg);
2337
2302
  for (const [clientWs] of clients) {
2338
- if (clientWs.readyState === WebSocket.OPEN) {
2303
+ if (clientWs.readyState === WebSocket2.OPEN) {
2339
2304
  clientWs.send(serialized);
2340
2305
  }
2341
2306
  }
@@ -2363,9 +2328,13 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2363
2328
  ws.send(serializeServerMessage(createErrorMessage("INVALID_MESSAGE", "Invalid message format")));
2364
2329
  return;
2365
2330
  }
2331
+ if (message.type.startsWith("terminal.")) {
2332
+ handleTerminalMessage(ws, message);
2333
+ return;
2334
+ }
2366
2335
  const client = clients.get(ws);
2367
2336
  try {
2368
- await handleClientMessage(ws, client, message, config, getLLMClient, getActiveProvider, toolRegistry, sessionManager2, broadcastForSession, providerManager, getSessionLLMClient, getSessionStatsIdentity, invalidateSessionLLMClient);
2337
+ await handleClientMessage(ws, client, message, getLLMClient, getActiveProvider, sessionManager, broadcastForSession, providerManager, getSessionLLMClient, getSessionStatsIdentity, llmForSession, statsForSession, startTurnWithCompletionChain);
2369
2338
  } catch (error) {
2370
2339
  logger.error("Error handling client message", { error, type: message.type });
2371
2340
  ws.send(serializeServerMessage(
@@ -2388,17 +2357,31 @@ function createWebSocketServer(httpServer, config, getLLMClient, getActiveProvid
2388
2357
  unsubscribe();
2389
2358
  }
2390
2359
  }
2360
+ unsubscribeAllFromTerminal(ws);
2391
2361
  clients.delete(ws);
2392
2362
  });
2393
2363
  ws.on("error", (error) => {
2394
2364
  logger.error("WebSocket error", { error });
2395
2365
  });
2396
2366
  });
2397
- return wss;
2367
+ return {
2368
+ wss,
2369
+ abortSession: (sessionId) => {
2370
+ const controller = activeAgents.get(sessionId);
2371
+ if (controller) {
2372
+ activeAgents.delete(sessionId);
2373
+ controller.abort();
2374
+ return true;
2375
+ }
2376
+ return false;
2377
+ },
2378
+ close: (cb) => wss.close(cb),
2379
+ broadcastForSession
2380
+ };
2398
2381
  }
2399
- async function handleClientMessage(ws, client, message, config, getLLMClient, getActiveProvider, toolRegistry, sessionManager2, broadcastForSession, providerManager, getSessionLLMClient, getSessionStatsIdentity, invalidateSessionLLMClient) {
2382
+ async function handleClientMessage(ws, client, message, getLLMClient, getActiveProvider, sessionManager, broadcastForSession, providerManager, getSessionLLMClient, getSessionStatsIdentity, llmForSession, statsForSession, startTurnWithCompletionChain) {
2400
2383
  const send = (msg) => {
2401
- if (ws.readyState === WebSocket.OPEN) {
2384
+ if (ws.readyState === WebSocket2.OPEN) {
2402
2385
  ws.send(serializeServerMessage(msg));
2403
2386
  }
2404
2387
  };
@@ -2414,57 +2397,13 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2414
2397
  controller.abort();
2415
2398
  cancelQuestionsForSession(sessionId, reason);
2416
2399
  cancelPathConfirmationsForSession(sessionId, reason);
2417
- sessionManager2.clearMessageQueue(sessionId);
2418
- sessionManager2.setRunning(sessionId, false);
2400
+ sessionManager.clearMessageQueue(sessionId);
2401
+ sessionManager.setRunning(sessionId, false);
2419
2402
  sendForSession(sessionId, createSessionRunningMessage(false));
2420
- const contextState = sessionManager2.getContextState(sessionId);
2403
+ const contextState = sessionManager.getContextState(sessionId);
2421
2404
  sendForSession(sessionId, createContextStateMessage(contextState));
2422
2405
  return true;
2423
2406
  };
2424
- const llmForSession = (sessionId) => getSessionLLMClient?.(sessionId) ?? getLLMClient();
2425
- const statsForSession = (sessionId) => getSessionStatsIdentity?.(sessionId) ?? resolveStatsIdentity2(getLLMClient(), getActiveProvider);
2426
- const startTurnWithCompletionChain = (sessionId, controller) => {
2427
- runChatTurn({
2428
- sessionManager: sessionManager2,
2429
- sessionId,
2430
- llmClient: llmForSession(sessionId),
2431
- statsIdentity: statsForSession(sessionId),
2432
- signal: controller.signal,
2433
- onMessage: (msg) => sendForSession(sessionId, msg)
2434
- }).catch((error) => {
2435
- if (error instanceof Error && error.message === "Aborted") {
2436
- return;
2437
- }
2438
- logger.error("Chat turn error", { error });
2439
- }).finally(() => {
2440
- if (activeAgents.get(sessionId) !== controller) {
2441
- return;
2442
- }
2443
- activeAgents.delete(sessionId);
2444
- const completionMsgs = sessionManager2.drainCompletionMessages(sessionId);
2445
- const next = completionMsgs[0];
2446
- if (next) {
2447
- for (const remaining of completionMsgs.slice(1)) {
2448
- sessionManager2.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
2449
- }
2450
- sendForSession(sessionId, createQueueStateMessage(sessionManager2.getQueueState(sessionId)));
2451
- const userMessage = sessionManager2.addMessage(sessionId, {
2452
- role: "user",
2453
- content: next.content,
2454
- ...next.attachments ? { attachments: next.attachments } : {}
2455
- });
2456
- sendForSession(sessionId, createChatMessageMessage(userMessage));
2457
- const newController = new AbortController();
2458
- activeAgents.set(sessionId, newController);
2459
- startTurnWithCompletionChain(sessionId, newController);
2460
- return;
2461
- }
2462
- sessionManager2.clearMessageQueue(sessionId);
2463
- sessionManager2.setRunning(sessionId, false);
2464
- const contextState = sessionManager2.getContextState(sessionId);
2465
- sendForSession(sessionId, createContextStateMessage(contextState));
2466
- });
2467
- };
2468
2407
  const ensureEventStoreSubscription = (sessionId) => {
2469
2408
  if (client.eventStoreSubscriptions.has(sessionId)) {
2470
2409
  return;
@@ -2477,7 +2416,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2477
2416
  try {
2478
2417
  for await (const storedEvent of iterator) {
2479
2418
  const serverMsg = storedEventToServerMessage(storedEvent);
2480
- if (serverMsg && ws.readyState === WebSocket.OPEN) {
2419
+ if (serverMsg && ws.readyState === WebSocket2.OPEN) {
2481
2420
  ws.send(serializeServerMessage({ ...serverMsg, seq: storedEvent.seq, sessionId: sid }));
2482
2421
  }
2483
2422
  }
@@ -2489,138 +2428,36 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2489
2428
  };
2490
2429
  switch (message.type) {
2491
2430
  // =========================================================================
2492
- // Project Management
2493
- // =========================================================================
2494
- case "project.create": {
2495
- if (!isProjectCreatePayload(message.payload)) {
2496
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid project.create payload", message.id));
2497
- return;
2498
- }
2499
- const project = createProject(message.payload.name, message.payload.workdir);
2500
- send(createProjectStateMessage(project, message.id));
2501
- break;
2502
- }
2503
- case "project.create-with-dir": {
2504
- logger.debug("WS project.create-with-dir received", { payload: message.payload });
2505
- if (!isProjectCreateWithDirPayload(message.payload)) {
2506
- logger.error("WS project.create-with-dir invalid payload");
2507
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid project.create-with-dir payload", message.id));
2508
- return;
2509
- }
2510
- try {
2511
- const workdir = config.workdir;
2512
- logger.debug("WS creating project directory", { name: message.payload.name, workdir });
2513
- const { createDirectoryWithGit } = await import("./project-creator-CWPKRIKI.js");
2514
- const project = await createDirectoryWithGit(message.payload.name, workdir);
2515
- logger.debug("WS project created", { id: project.id, name: project.name });
2516
- send(createProjectStateMessage(project, message.id));
2517
- logger.debug("WS sent project.state", { messageId: message.id });
2518
- } catch (error) {
2519
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
2520
- logger.error("WS project creation failed", { error: errorMessage });
2521
- send(createErrorMessage("PROJECT_CREATION_FAILED", errorMessage, message.id));
2522
- }
2523
- break;
2524
- }
2525
- case "project.list": {
2526
- logger.debug("WS project.list received", { messageId: message.id });
2527
- const projects = listProjects();
2528
- logger.debug("WS projects found", { count: projects.length, projects: projects.map((p) => ({ id: p.id, name: p.name })) });
2529
- send(createProjectListMessage(projects, message.id));
2530
- logger.debug("WS sent project.list", { messageId: message.id });
2531
- break;
2532
- }
2533
- case "project.load": {
2534
- if (!isProjectLoadPayload(message.payload)) {
2535
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid project.load payload", message.id));
2536
- return;
2537
- }
2538
- const project = getProject(message.payload.projectId);
2539
- if (!project) {
2540
- send(createErrorMessage("NOT_FOUND", "Project not found", message.id));
2541
- return;
2542
- }
2543
- send(createProjectStateMessage(project, message.id));
2544
- break;
2545
- }
2546
- case "project.update": {
2547
- if (!isProjectUpdatePayload(message.payload)) {
2548
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid project.update payload", message.id));
2549
- return;
2550
- }
2551
- const updates = {};
2552
- if (message.payload.name !== void 0) {
2553
- updates.name = message.payload.name;
2554
- }
2555
- if (message.payload.customInstructions !== void 0) {
2556
- updates.customInstructions = message.payload.customInstructions;
2557
- }
2558
- const updated = updateProject(message.payload.projectId, updates);
2559
- if (!updated) {
2560
- send(createErrorMessage("NOT_FOUND", "Project not found", message.id));
2561
- return;
2562
- }
2563
- send(createProjectStateMessage(updated, message.id));
2564
- break;
2565
- }
2566
- case "project.delete": {
2567
- if (!isProjectDeletePayload(message.payload)) {
2568
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid project.delete payload", message.id));
2569
- return;
2570
- }
2571
- deleteProject(message.payload.projectId);
2572
- send({ type: "project.deleted", payload: { projectId: message.payload.projectId }, id: message.id });
2573
- break;
2574
- }
2575
- // =========================================================================
2576
- // Settings Management
2431
+ // DEPRECATED: All CRUD operations moved to REST API
2432
+ // If you see this error, update your code to use REST endpoints instead.
2433
+ // See docs/REST-API.md for details.
2577
2434
  // =========================================================================
2578
- case "settings.get": {
2579
- if (!isSettingsGetPayload(message.payload)) {
2580
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid settings.get payload", message.id));
2581
- return;
2582
- }
2583
- const value = getSetting(message.payload.key);
2584
- send(createSettingsValueMessage(message.payload.key, value, message.id));
2585
- break;
2586
- }
2587
- case "settings.set": {
2588
- if (!isSettingsSetPayload(message.payload)) {
2589
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid settings.set payload", message.id));
2590
- return;
2591
- }
2592
- setSetting(message.payload.key, message.payload.value);
2593
- send(createSettingsValueMessage(message.payload.key, message.payload.value, message.id));
2594
- break;
2595
- }
2435
+ case "project.create":
2436
+ case "project.create-with-dir":
2437
+ case "project.list":
2438
+ case "project.load":
2439
+ case "project.update":
2440
+ case "project.delete":
2441
+ case "settings.get":
2442
+ case "settings.set":
2443
+ case "session.create":
2444
+ case "session.list":
2445
+ case "session.delete":
2446
+ case "session.deleteAll":
2447
+ case "session.setProvider":
2448
+ send(createErrorMessage("DEPRECATED_MESSAGE_TYPE", `${message.type} removed. Use REST API instead. See docs/REST-API.md`, message.id));
2449
+ return;
2596
2450
  // =========================================================================
2597
- // Session Management
2451
+ // Session Load - Required for WS subscription mechanism
2452
+ // Note: This is kept ONLY to set activeSessionId for event routing.
2453
+ // For actual data loading, use REST API: GET /api/sessions/:id
2598
2454
  // =========================================================================
2599
- case "session.create": {
2600
- if (!isSessionCreatePayload(message.payload)) {
2601
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.create payload", message.id));
2602
- return;
2603
- }
2604
- const currentProvider = providerManager?.getActiveProvider();
2605
- const currentModel = currentProvider ? getLLMClient().getModel() : null;
2606
- const session = sessionManager2.createSession(
2607
- message.payload.projectId,
2608
- message.payload.title,
2609
- currentProvider?.id ?? null,
2610
- currentModel
2611
- );
2612
- client.activeSessionId = session.id;
2613
- sendForSession(session.id, createSessionStateMessage(session, [], message.id));
2614
- const contextState = sessionManager2.getContextState(session.id);
2615
- sendForSession(session.id, createContextStateMessage(contextState));
2616
- break;
2617
- }
2618
2455
  case "session.load": {
2619
2456
  if (!isSessionLoadPayload(message.payload)) {
2620
2457
  send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.load payload", message.id));
2621
2458
  return;
2622
2459
  }
2623
- const session = sessionManager2.getSession(message.payload.sessionId);
2460
+ const session = sessionManager.getSession(message.payload.sessionId);
2624
2461
  if (!session) {
2625
2462
  send(createErrorMessage("NOT_FOUND", "Session not found", message.id));
2626
2463
  return;
@@ -2630,77 +2467,15 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2630
2467
  const eventStore = getEventStore();
2631
2468
  const events = eventStore.getEvents(session.id);
2632
2469
  const messages = buildMessagesFromStoredEvents(events);
2633
- logger.debug("Loaded messages from EventStore", { sessionId: session.id, eventCount: events.length, messageCount: messages.length });
2634
- sendForSession(session.id, createSessionStateMessage(session, messages, message.id));
2635
- const contextState = sessionManager2.getContextState(session.id);
2470
+ const pendingConfirmations = foldPendingConfirmations(events);
2471
+ logger.debug("Loaded messages from EventStore", { sessionId: session.id, eventCount: events.length, messageCount: messages.length, pendingConfirmationsCount: pendingConfirmations.length });
2472
+ sendForSession(session.id, createSessionStateMessage(session, messages, pendingConfirmations, message.id));
2473
+ const contextState = sessionManager.getContextState(session.id);
2636
2474
  sendForSession(session.id, createContextStateMessage(contextState));
2637
2475
  break;
2638
2476
  }
2639
- case "session.list": {
2640
- const sessions = sessionManager2.listSessions();
2641
- const sessionsWithPrompts = sessions.map((session) => ({
2642
- ...session,
2643
- recentUserPrompts: getRecentUserPromptsForSession(session.id, 10)
2644
- }));
2645
- send(createSessionListMessage(sessionsWithPrompts, message.id));
2646
- break;
2647
- }
2648
- case "session.delete": {
2649
- if (!isSessionLoadPayload(message.payload)) {
2650
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.delete payload", message.id));
2651
- return;
2652
- }
2653
- sessionManager2.deleteSession(message.payload.sessionId);
2654
- send({ type: "session.deleted", payload: { sessionId: message.payload.sessionId }, id: message.id });
2655
- break;
2656
- }
2657
- case "session.deleteAll": {
2658
- if (!message.payload || typeof message.payload !== "object" || !("projectId" in message.payload)) {
2659
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.deleteAll payload", message.id));
2660
- return;
2661
- }
2662
- const payload = message.payload;
2663
- const project = sessionManager2.getProject(payload.projectId);
2664
- if (!project) {
2665
- send(createErrorMessage("PROJECT_NOT_FOUND", "Project not found", message.id));
2666
- return;
2667
- }
2668
- sessionManager2.deleteAllSessions(payload.projectId, project.workdir);
2669
- send({ type: "session.deletedAll", payload: { projectId: payload.projectId }, id: message.id });
2670
- break;
2671
- }
2672
- case "session.setProvider": {
2673
- if (!client.activeSessionId) {
2674
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2675
- return;
2676
- }
2677
- if (!isSessionSetProviderPayload(message.payload)) {
2678
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid session.setProvider payload", message.id));
2679
- return;
2680
- }
2681
- if (!providerManager) {
2682
- send(createErrorMessage("NO_PROVIDERS", "Provider management not available", message.id));
2683
- return;
2684
- }
2685
- const { providerId, model: requestedModel } = message.payload;
2686
- const provider = providerManager.getProviders().find((p) => p.id === providerId);
2687
- if (!provider) {
2688
- send(createErrorMessage("PROVIDER_NOT_FOUND", "Provider not found", message.id));
2689
- return;
2690
- }
2691
- const resolvedModel = requestedModel ?? "auto";
2692
- const sessionId = client.activeSessionId;
2693
- sessionManager2.setSessionProvider(sessionId, providerId, resolvedModel);
2694
- invalidateSessionLLMClient?.(sessionId);
2695
- const eventStore = getEventStore();
2696
- const updatedSession = sessionManager2.requireSession(sessionId);
2697
- const events = eventStore.getEvents(sessionId);
2698
- const messages = buildMessagesFromStoredEvents(events);
2699
- sendForSession(sessionId, createSessionStateMessage(updatedSession, messages, message.id));
2700
- break;
2701
- }
2702
2477
  // =========================================================================
2703
- // Unified Chat (replaces plan.message, agent.start, etc.)
2478
+ // Unified Chat (replaces plan.message, agent.start, etc.) - WS REQUIRED
2704
2479
  // =========================================================================
2705
2480
  case "chat.send": {
2706
2481
  if (!client.activeSessionId) {
@@ -2711,15 +2486,15 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2711
2486
  send(createErrorMessage("INVALID_PAYLOAD", "Invalid chat.send payload", message.id));
2712
2487
  return;
2713
2488
  }
2714
- const currentSession = sessionManager2.requireSession(client.activeSessionId);
2489
+ const currentSession = sessionManager.requireSession(client.activeSessionId);
2715
2490
  if (currentSession.isRunning) {
2716
2491
  send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
2717
2492
  return;
2718
2493
  }
2719
2494
  if (currentSession.phase === "blocked") {
2720
2495
  logger.info("User intervention - resetting blocked state", { sessionId: client.activeSessionId });
2721
- sessionManager2.setPhase(client.activeSessionId, "build");
2722
- sessionManager2.resetAllCriteriaAttempts(client.activeSessionId);
2496
+ sessionManager.setPhase(client.activeSessionId, "build");
2497
+ sessionManager.resetAllCriteriaAttempts(client.activeSessionId);
2723
2498
  sendForSession(client.activeSessionId, createPhaseChangedMessage("build"));
2724
2499
  }
2725
2500
  const sessionId = client.activeSessionId;
@@ -2732,12 +2507,12 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2732
2507
  existingController.abort();
2733
2508
  }
2734
2509
  activeAgents.set(sessionId, controller);
2735
- sessionManager2.setRunning(sessionId, true);
2510
+ sessionManager.setRunning(sessionId, true);
2736
2511
  sendForSession(sessionId, createSessionRunningMessage(true));
2737
2512
  send({ type: "ack", payload: {}, id: message.id });
2738
2513
  try {
2739
2514
  await maybeAutoCompactContext({
2740
- sessionManager: sessionManager2,
2515
+ sessionManager,
2741
2516
  sessionId,
2742
2517
  llmClient: llmForSession(sessionId),
2743
2518
  statsIdentity: statsForSession(sessionId),
@@ -2746,7 +2521,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2746
2521
  if (controller.signal.aborted) {
2747
2522
  break;
2748
2523
  }
2749
- const userMessage = sessionManager2.addMessage(sessionId, {
2524
+ const userMessage = sessionManager.addMessage(sessionId, {
2750
2525
  role: "user",
2751
2526
  content: message.payload.content,
2752
2527
  ...message.payload.attachments && { attachments: message.payload.attachments },
@@ -2767,11 +2542,12 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2767
2542
  type: "session.name_generated",
2768
2543
  data: { name: result.name }
2769
2544
  });
2770
- const updatedSession = sessionManager2.getSession(sessionId);
2545
+ const updatedSession = sessionManager.getSession(sessionId);
2771
2546
  if (updatedSession) {
2772
2547
  const events = eventStore.getEvents(sessionId);
2773
2548
  const messages = buildMessagesFromStoredEvents(events);
2774
- broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
2549
+ const pendingConfirmations = foldPendingConfirmations(events);
2550
+ broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages, pendingConfirmations));
2775
2551
  }
2776
2552
  logger.info("Session name generated", { sessionId, name: result.name });
2777
2553
  }
@@ -2784,9 +2560,9 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2784
2560
  if (activeAgents.get(sessionId) === controller) {
2785
2561
  activeAgents.delete(sessionId);
2786
2562
  }
2787
- sessionManager2.setRunning(sessionId, false);
2563
+ sessionManager.setRunning(sessionId, false);
2788
2564
  sendForSession(sessionId, createSessionRunningMessage(false));
2789
- const contextState = sessionManager2.getContextState(sessionId);
2565
+ const contextState = sessionManager.getContextState(sessionId);
2790
2566
  sendForSession(sessionId, createContextStateMessage(contextState));
2791
2567
  if (!(error instanceof Error && error.message === "Aborted")) {
2792
2568
  logger.error("Chat send pre-turn error", { sessionId, error });
@@ -2795,134 +2571,33 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2795
2571
  break;
2796
2572
  }
2797
2573
  case "chat.stop": {
2798
- if (!client.activeSessionId) {
2799
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2800
- return;
2801
- }
2802
- abortSessionExecution(client.activeSessionId, "Session aborted by user");
2803
- send({ type: "ack", payload: {}, id: message.id });
2574
+ send(createErrorMessage("DEPRECATED", "chat.stop removed. Use REST API: POST /api/sessions/:id/stop", message.id));
2804
2575
  break;
2805
2576
  }
2806
2577
  // =========================================================================
2807
2578
  // Message Queue
2808
- // =========================================================================
2809
- case "queue.asap": {
2810
- if (!client.activeSessionId) {
2811
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2812
- return;
2813
- }
2814
- if (!isQueueAsapPayload(message.payload)) {
2815
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid queue.asap payload", message.id));
2816
- return;
2817
- }
2818
- const { content, attachments } = message.payload;
2819
- sessionManager2.queueMessage(client.activeSessionId, "asap", content, attachments);
2820
- sendForSession(client.activeSessionId, createQueueStateMessage(sessionManager2.getQueueState(client.activeSessionId)));
2821
- send({ type: "ack", payload: {}, id: message.id });
2579
+ // =========================================================================
2580
+ case "queue.asap": {
2581
+ send(createErrorMessage("DEPRECATED", "queue.asap removed. Use REST API: POST /api/sessions/:id/queue/asap", message.id));
2822
2582
  break;
2823
2583
  }
2824
2584
  case "queue.completion": {
2825
- if (!client.activeSessionId) {
2826
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2827
- return;
2828
- }
2829
- if (!isQueueCompletionPayload(message.payload)) {
2830
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid queue.completion payload", message.id));
2831
- return;
2832
- }
2833
- const { content, attachments } = message.payload;
2834
- sessionManager2.queueMessage(client.activeSessionId, "completion", content, attachments);
2835
- sendForSession(client.activeSessionId, createQueueStateMessage(sessionManager2.getQueueState(client.activeSessionId)));
2836
- send({ type: "ack", payload: {}, id: message.id });
2585
+ send(createErrorMessage("DEPRECATED", "queue.completion removed. Use REST API: POST /api/sessions/:id/queue/completion", message.id));
2837
2586
  break;
2838
2587
  }
2839
2588
  case "queue.cancel": {
2840
- if (!client.activeSessionId) {
2841
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2842
- return;
2843
- }
2844
- if (!isQueueCancelPayload(message.payload)) {
2845
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid queue.cancel payload", message.id));
2846
- return;
2847
- }
2848
- sessionManager2.cancelQueuedMessage(client.activeSessionId, message.payload.queueId);
2849
- sendForSession(client.activeSessionId, createQueueStateMessage(sessionManager2.getQueueState(client.activeSessionId)));
2850
- send({ type: "ack", payload: {}, id: message.id });
2589
+ send(createErrorMessage("DEPRECATED", "queue.cancel removed. Use REST API: DELETE /api/sessions/:id/queue/:queueId", message.id));
2851
2590
  break;
2852
2591
  }
2853
2592
  case "chat.continue": {
2854
- if (!client.activeSessionId) {
2855
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2856
- return;
2857
- }
2858
- const session = sessionManager2.requireSession(client.activeSessionId);
2859
- if (session.isRunning) {
2860
- send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
2861
- return;
2862
- }
2863
- const continueEventStore = getEventStore();
2864
- const events = continueEventStore.getEvents(session.id);
2865
- const messages = buildMessagesFromStoredEvents(events);
2866
- const lastAssistantMessage = [...messages].reverse().find((msg) => msg.role === "assistant");
2867
- const fallbackMessageId = lastAssistantMessage?.id ?? [...messages].reverse().find((msg) => msg.role === "user")?.id ?? crypto.randomUUID();
2868
- send({ type: "ack", payload: {}, id: message.id });
2869
- sendForSession(session.id, createChatDoneMessage(fallbackMessageId, "complete", lastAssistantMessage?.stats));
2593
+ send(createErrorMessage("DEPRECATED", "chat.continue removed. Use REST API: POST /api/sessions/:id/continue", message.id));
2870
2594
  break;
2871
2595
  }
2872
2596
  // =========================================================================
2873
2597
  // Mode Switching
2874
2598
  // =========================================================================
2875
2599
  case "mode.switch": {
2876
- if (!client.activeSessionId) {
2877
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
2878
- return;
2879
- }
2880
- if (!isModeSwitchPayload(message.payload)) {
2881
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid mode.switch payload", message.id));
2882
- return;
2883
- }
2884
- const allAgents = await loadAllAgentsDefault();
2885
- const targetAgent = findAgentById(message.payload.mode, allAgents);
2886
- if (!targetAgent || targetAgent.metadata.subagent) {
2887
- send(createErrorMessage("INVALID_AGENT", `Agent '${message.payload.mode}' not found or is a sub-agent`, message.id));
2888
- return;
2889
- }
2890
- const sessionId = client.activeSessionId;
2891
- const session = sessionManager2.requireSession(sessionId);
2892
- const eventStore = getEventStore();
2893
- if (message.payload.mode === "builder" && needsSummaryGeneration(session.summary)) {
2894
- const events = eventStore.getEvents(sessionId);
2895
- const nonSystemEvents = events.filter((event) => {
2896
- if (event.type !== "message.start") return true;
2897
- return event.data.isSystemGenerated !== true;
2898
- });
2899
- const contextMessages = buildContextMessagesFromEventHistory(nonSystemEvents);
2900
- const summaryMessages = contextMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: m.content }));
2901
- if (summaryMessages.length > 0) {
2902
- generateSessionSummary({
2903
- messages: summaryMessages,
2904
- llmClient: llmForSession(sessionId)
2905
- }).then(async (result) => {
2906
- if (result.success && result.summary) {
2907
- sessionManager2.setSummary(sessionId, result.summary);
2908
- const updatedSession = sessionManager2.getSession(sessionId);
2909
- if (updatedSession) {
2910
- const events2 = eventStore.getEvents(sessionId);
2911
- const messages = buildMessagesFromStoredEvents(events2);
2912
- broadcastForSession(sessionId, createSessionStateMessage(updatedSession, messages));
2913
- }
2914
- logger.info("Session summary generated", { sessionId, summaryLength: result.summary.length });
2915
- }
2916
- }).catch((error) => {
2917
- logger.warn("Session summary generation failed", { sessionId, error: error instanceof Error ? error.message : error });
2918
- });
2919
- }
2920
- }
2921
- sessionManager2.setMode(sessionId, message.payload.mode);
2922
- sendForSession(sessionId, createModeChangedMessage(message.payload.mode, false));
2923
- const modeEvents = eventStore.getEvents(sessionId);
2924
- const modeMessages = buildMessagesFromStoredEvents(modeEvents);
2925
- sendForSession(sessionId, createSessionStateMessage(sessionManager2.getSession(sessionId), modeMessages, message.id));
2600
+ send(createErrorMessage("DEPRECATED", "mode.switch removed. Use REST API: PUT /api/sessions/:id/mode", message.id));
2926
2601
  break;
2927
2602
  }
2928
2603
  case "mode.accept": {
@@ -2930,9 +2605,26 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2930
2605
  send(createErrorMessage("NO_SESSION", "No active session", message.id));
2931
2606
  return;
2932
2607
  }
2933
- const session = sessionManager2.requireSession(client.activeSessionId);
2608
+ const session = sessionManager.requireSession(client.activeSessionId);
2609
+ const acceptPayload = message.payload;
2934
2610
  if (session.isRunning) {
2935
- send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
2611
+ const content = acceptPayload?.content ?? "";
2612
+ const attachments = acceptPayload?.attachments;
2613
+ const workflowId = acceptPayload?.workflowId;
2614
+ let fullContent = content;
2615
+ if (workflowId) {
2616
+ const workflowInfo = `// Workflow: ${workflowId}`;
2617
+ fullContent = content ? `${workflowInfo}
2618
+
2619
+ ${content}` : workflowInfo;
2620
+ }
2621
+ sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-accept");
2622
+ const queueState = sessionManager.getQueueState(client.activeSessionId);
2623
+ send({
2624
+ type: "queue.state",
2625
+ payload: { success: true, queueState },
2626
+ id: message.id
2627
+ });
2936
2628
  return;
2937
2629
  }
2938
2630
  const acceptPayloadEarly = message.payload;
@@ -2951,7 +2643,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2951
2643
  try {
2952
2644
  for await (const storedEvent of iterator) {
2953
2645
  const serverMsg = storedEventToServerMessage(storedEvent);
2954
- if (serverMsg && ws.readyState === WebSocket.OPEN) {
2646
+ if (serverMsg && ws.readyState === WebSocket2.OPEN) {
2955
2647
  ws.send(serializeServerMessage({ ...serverMsg, seq: storedEvent.seq, sessionId: sid }));
2956
2648
  }
2957
2649
  }
@@ -2960,10 +2652,10 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2960
2652
  }
2961
2653
  })();
2962
2654
  }
2963
- sessionManager2.setRunning(sessionId, true);
2655
+ sessionManager.setRunning(sessionId, true);
2964
2656
  eventStore.append(sessionId, { type: "running.changed", data: { isRunning: true } });
2965
2657
  sendForSession(sessionId, createSessionRunningMessage(true));
2966
- const currentSession = sessionManager2.requireSession(sessionId);
2658
+ const currentSession = sessionManager.requireSession(sessionId);
2967
2659
  if (needsSummaryGeneration(currentSession.summary)) {
2968
2660
  const events = eventStore.getEvents(sessionId);
2969
2661
  const nonSystemEvents = events.filter((event) => {
@@ -2973,12 +2665,19 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2973
2665
  const contextMessages = buildContextMessagesFromEventHistory(nonSystemEvents);
2974
2666
  const summaryMessages = contextMessages.filter((m) => m.role === "user" || m.role === "assistant").map((m) => ({ role: m.role, content: m.content }));
2975
2667
  if (summaryMessages.length > 0) {
2668
+ const config = getRuntimeConfig();
2669
+ const configDir = getGlobalConfigDir(config.mode ?? "production");
2670
+ const skills = await getEnabledSkillMetadata(configDir);
2671
+ const { content: instructions } = await getAllInstructions(currentSession.workdir, currentSession.projectId);
2976
2672
  generateSessionSummary({
2977
2673
  messages: summaryMessages,
2978
- llmClient: llmForSession(sessionId)
2674
+ llmClient: llmForSession(sessionId),
2675
+ workdir: currentSession.workdir,
2676
+ customInstructions: instructions || void 0,
2677
+ skills
2979
2678
  }).then(async (result) => {
2980
2679
  if (result.success && result.summary) {
2981
- sessionManager2.setSummary(sessionId, result.summary);
2680
+ sessionManager.setSummary(sessionId, result.summary);
2982
2681
  }
2983
2682
  }).catch((error) => {
2984
2683
  logger.warn("Session summary generation failed", { sessionId, error: error instanceof Error ? error.message : error });
@@ -2989,8 +2688,8 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
2989
2688
  (async () => {
2990
2689
  let controller = null;
2991
2690
  try {
2992
- sessionManager2.setMode(sessionId, "builder");
2993
- sessionManager2.setPhase(sessionId, "build");
2691
+ sessionManager.setMode(sessionId, "builder");
2692
+ sessionManager.setPhase(sessionId, "build");
2994
2693
  eventStore.append(sessionId, { type: "mode.changed", data: { mode: "builder", auto: false, reason: "Criteria accepted" } });
2995
2694
  eventStore.append(sessionId, { type: "phase.changed", data: { phase: "build" } });
2996
2695
  controller = new AbortController();
@@ -3000,19 +2699,19 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3000
2699
  existingController.abort();
3001
2700
  }
3002
2701
  activeAgents.set(sessionId, controller);
3003
- const acceptPayload = message.payload;
3004
- const acceptAttachments = acceptPayload?.attachments;
3005
- const hasAcceptContent = acceptPayload?.content && typeof acceptPayload.content === "string" && acceptPayload.content.trim();
2702
+ const acceptPayload2 = message.payload;
2703
+ const acceptAttachments = acceptPayload2?.attachments;
2704
+ const hasAcceptContent = acceptPayload2?.content && typeof acceptPayload2.content === "string" && acceptPayload2.content.trim();
3006
2705
  const hasAcceptAttachments = acceptAttachments && acceptAttachments.length > 0;
3007
2706
  const hasAcceptMessage = hasAcceptContent || hasAcceptAttachments;
3008
2707
  await runOrchestrator({
3009
- sessionManager: sessionManager2,
2708
+ sessionManager,
3010
2709
  sessionId,
3011
2710
  llmClient: llmForSession(sessionId),
3012
2711
  statsIdentity: statsForSession(sessionId),
3013
2712
  injectBuilderKickoff: !hasAcceptMessage,
3014
- ...acceptPayload?.workflowId ? { workflowId: acceptPayload.workflowId } : {},
3015
- ...hasAcceptMessage ? { userMessage: { content: hasAcceptContent ? acceptPayload.content : "", ...hasAcceptAttachments ? { attachments: acceptAttachments } : {} } } : {},
2713
+ ...acceptPayload2?.workflowId ? { workflowId: acceptPayload2.workflowId } : {},
2714
+ ...hasAcceptMessage ? { userMessage: { content: hasAcceptContent ? acceptPayload2.content : "", ...hasAcceptAttachments ? { attachments: acceptAttachments } : {} } } : {},
3016
2715
  signal: controller.signal,
3017
2716
  onMessage: (msg) => sendForSession(sessionId, msg)
3018
2717
  // For path confirmation dialogs
@@ -3031,7 +2730,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3031
2730
  });
3032
2731
  const errorMsgId = crypto.randomUUID();
3033
2732
  eventStore.append(sessionId, createMessageStartEvent(errorMsgId, "user", `Error: ${error instanceof Error ? error.message : "Unknown error"}`, {
3034
- ...getCurrentWindowMessageOptions3(sessionId) ?? {},
2733
+ ...getCurrentWindowMessageOptions2(sessionId) ?? {},
3035
2734
  isSystemGenerated: true,
3036
2735
  messageKind: "correction"
3037
2736
  }));
@@ -3042,14 +2741,14 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3042
2741
  return;
3043
2742
  }
3044
2743
  activeAgents.delete(sessionId);
3045
- const completionMsgs = sessionManager2.drainCompletionMessages(sessionId);
2744
+ const completionMsgs = sessionManager.drainCompletionMessages(sessionId);
3046
2745
  const nextCompletion = completionMsgs[0];
3047
2746
  if (nextCompletion) {
3048
2747
  for (const remaining of completionMsgs.slice(1)) {
3049
- sessionManager2.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
2748
+ sessionManager.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
3050
2749
  }
3051
- sendForSession(sessionId, createQueueStateMessage(sessionManager2.getQueueState(sessionId)));
3052
- const userMessage = sessionManager2.addMessage(sessionId, {
2750
+ sendForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2751
+ const userMessage = sessionManager.addMessage(sessionId, {
3053
2752
  role: "user",
3054
2753
  content: nextCompletion.content,
3055
2754
  ...nextCompletion.attachments ? { attachments: nextCompletion.attachments } : {}
@@ -3060,10 +2759,28 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3060
2759
  startTurnWithCompletionChain(sessionId, newController);
3061
2760
  return;
3062
2761
  }
3063
- sessionManager2.clearMessageQueue(sessionId);
3064
- sessionManager2.setRunning(sessionId, false);
2762
+ const asapMsgs = sessionManager.drainAsapMessages(sessionId);
2763
+ const nextAsap = asapMsgs[0];
2764
+ if (nextAsap) {
2765
+ for (const remaining of asapMsgs.slice(1)) {
2766
+ sessionManager.queueMessage(sessionId, "asap", remaining.content, remaining.attachments, remaining.messageKind);
2767
+ }
2768
+ sendForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2769
+ const userMessage = sessionManager.addMessage(sessionId, {
2770
+ role: "user",
2771
+ content: nextAsap.content,
2772
+ ...nextAsap.attachments ? { attachments: nextAsap.attachments } : {}
2773
+ });
2774
+ sendForSession(sessionId, createChatMessageMessage(userMessage));
2775
+ const newerController = new AbortController();
2776
+ activeAgents.set(sessionId, newerController);
2777
+ startTurnWithCompletionChain(sessionId, newerController);
2778
+ return;
2779
+ }
2780
+ sessionManager.clearMessageQueue(sessionId);
2781
+ sessionManager.setRunning(sessionId, false);
3065
2782
  eventStore.append(sessionId, { type: "running.changed", data: { isRunning: false } });
3066
- const contextState = sessionManager2.getContextState(sessionId);
2783
+ const contextState = sessionManager.getContextState(sessionId);
3067
2784
  sendForSession(sessionId, createContextStateMessage(contextState));
3068
2785
  }
3069
2786
  })();
@@ -3073,17 +2790,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3073
2790
  // Criteria Editing
3074
2791
  // =========================================================================
3075
2792
  case "criteria.edit": {
3076
- if (!client.activeSessionId) {
3077
- send(createErrorMessage("NO_SESSION", "No active session", message.id));
3078
- return;
3079
- }
3080
- if (!isCriteriaEditPayload(message.payload)) {
3081
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid criteria.edit payload", message.id));
3082
- return;
3083
- }
3084
- sessionManager2.setCriteria(client.activeSessionId, message.payload.criteria);
3085
- sendForSession(client.activeSessionId, createCriteriaUpdatedMessage(message.payload.criteria));
3086
- send({ type: "ack", payload: {}, id: message.id });
2793
+ send(createErrorMessage("DEPRECATED", "criteria.edit removed. Use REST API: PUT /api/sessions/:id/criteria", message.id));
3087
2794
  break;
3088
2795
  }
3089
2796
  // =========================================================================
@@ -3094,31 +2801,32 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3094
2801
  send(createErrorMessage("NO_SESSION", "No active session", message.id));
3095
2802
  return;
3096
2803
  }
3097
- const session = sessionManager2.requireSession(client.activeSessionId);
2804
+ const session = sessionManager.requireSession(client.activeSessionId);
3098
2805
  const sessionId = client.activeSessionId;
3099
2806
  if (session.isRunning) {
3100
2807
  send(createErrorMessage("SESSION_RUNNING", "Cannot compact while session is running", message.id));
3101
2808
  return;
3102
2809
  }
3103
- const contextState = sessionManager2.getContextState(sessionId);
2810
+ const contextState = sessionManager.getContextState(sessionId);
3104
2811
  const tokensBefore = contextState.currentTokens;
3105
2812
  send({ type: "ack", payload: {}, id: message.id });
3106
2813
  (async () => {
3107
2814
  try {
3108
2815
  await performManualContextCompaction({
3109
- sessionManager: sessionManager2,
2816
+ sessionManager,
3110
2817
  sessionId,
3111
2818
  llmClient: llmForSession(sessionId),
3112
2819
  statsIdentity: statsForSession(sessionId),
3113
2820
  tokenCountAtClose: tokensBefore
3114
2821
  });
3115
- const newContextState = sessionManager2.getContextState(sessionId);
2822
+ const newContextState = sessionManager.getContextState(sessionId);
3116
2823
  sendForSession(sessionId, createContextStateMessage(newContextState));
3117
- const updatedSession = sessionManager2.requireSession(sessionId);
2824
+ const updatedSession = sessionManager.requireSession(sessionId);
3118
2825
  const compactEventStore = getEventStore();
3119
2826
  const compactEvents = compactEventStore.getEvents(sessionId);
3120
2827
  const compactMessages = buildMessagesFromStoredEvents(compactEvents);
3121
- sendForSession(sessionId, createSessionStateMessage(updatedSession, compactMessages));
2828
+ const pendingConfirmations = foldPendingConfirmations(compactEvents);
2829
+ sendForSession(sessionId, createSessionStateMessage(updatedSession, compactMessages, pendingConfirmations));
3122
2830
  } catch (error) {
3123
2831
  logger.error("Compaction failed", { error, sessionId });
3124
2832
  sendForSession(sessionId, createChatErrorMessage(
@@ -3137,9 +2845,26 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3137
2845
  send(createErrorMessage("NO_SESSION", "No active session", message.id));
3138
2846
  return;
3139
2847
  }
3140
- const session = sessionManager2.requireSession(client.activeSessionId);
2848
+ const session = sessionManager.requireSession(client.activeSessionId);
3141
2849
  if (session.isRunning) {
3142
- send(createErrorMessage("ALREADY_RUNNING", "Session is already running", message.id));
2850
+ const launchPayload2 = message.payload;
2851
+ const content = launchPayload2?.content ?? "";
2852
+ const attachments = launchPayload2?.attachments;
2853
+ const workflowId = launchPayload2?.workflowId;
2854
+ let fullContent = content;
2855
+ if (workflowId) {
2856
+ const workflowInfo = `// Workflow: ${workflowId}`;
2857
+ fullContent = content ? `${workflowInfo}
2858
+
2859
+ ${content}` : workflowInfo;
2860
+ }
2861
+ sessionManager.queueMessage(client.activeSessionId, "asap", fullContent, attachments, "workflow-launch");
2862
+ const queueState = sessionManager.getQueueState(client.activeSessionId);
2863
+ send({
2864
+ type: "queue.state",
2865
+ payload: { success: true, queueState },
2866
+ id: message.id
2867
+ });
3143
2868
  return;
3144
2869
  }
3145
2870
  if (session.mode !== "builder") {
@@ -3155,15 +2880,15 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3155
2880
  const sessionId = client.activeSessionId;
3156
2881
  if (session.phase === "blocked") {
3157
2882
  logger.info("User launched runner - resetting blocked state", { sessionId });
3158
- sessionManager2.setPhase(sessionId, "build");
3159
- sessionManager2.resetAllCriteriaAttempts(sessionId);
2883
+ sessionManager.setPhase(sessionId, "build");
2884
+ sessionManager.resetAllCriteriaAttempts(sessionId);
3160
2885
  }
3161
2886
  const launchPayload = message.payload;
3162
2887
  const launchAttachments = launchPayload?.attachments;
3163
2888
  const hasUserContent = launchPayload?.content && typeof launchPayload.content === "string" && launchPayload.content.trim();
3164
2889
  const hasUserAttachments = launchAttachments && launchAttachments.length > 0;
3165
2890
  const hasUserMessage = hasUserContent || hasUserAttachments;
3166
- sessionManager2.setRunning(sessionId, true);
2891
+ sessionManager.setRunning(sessionId, true);
3167
2892
  sendForSession(sessionId, createSessionRunningMessage(true));
3168
2893
  const controller = new AbortController();
3169
2894
  const existingController = activeAgents.get(sessionId);
@@ -3176,7 +2901,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3176
2901
  ensureEventStoreSubscription(sessionId);
3177
2902
  logger.info("Runner launching", { sessionId, pendingCriteria: pendingCriteria.length });
3178
2903
  runOrchestrator({
3179
- sessionManager: sessionManager2,
2904
+ sessionManager,
3180
2905
  sessionId,
3181
2906
  llmClient: llmForSession(sessionId),
3182
2907
  statsIdentity: statsForSession(sessionId),
@@ -3196,14 +2921,32 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3196
2921
  return;
3197
2922
  }
3198
2923
  activeAgents.delete(sessionId);
3199
- const completionMsgs = sessionManager2.drainCompletionMessages(sessionId);
2924
+ const asapMsgs = sessionManager.drainAsapMessages(sessionId);
2925
+ const nextAsap = asapMsgs[0];
2926
+ if (nextAsap) {
2927
+ for (const remaining of asapMsgs.slice(1)) {
2928
+ sessionManager.queueMessage(sessionId, "asap", remaining.content, remaining.attachments, remaining.messageKind);
2929
+ }
2930
+ sendForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2931
+ const userMessage = sessionManager.addMessage(sessionId, {
2932
+ role: "user",
2933
+ content: nextAsap.content,
2934
+ ...nextAsap.attachments ? { attachments: nextAsap.attachments } : {}
2935
+ });
2936
+ sendForSession(sessionId, createChatMessageMessage(userMessage));
2937
+ const newerController = new AbortController();
2938
+ activeAgents.set(sessionId, newerController);
2939
+ startTurnWithCompletionChain(sessionId, newerController);
2940
+ return;
2941
+ }
2942
+ const completionMsgs = sessionManager.drainCompletionMessages(sessionId);
3200
2943
  const nextCompletion = completionMsgs[0];
3201
2944
  if (nextCompletion) {
3202
2945
  for (const remaining of completionMsgs.slice(1)) {
3203
- sessionManager2.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
2946
+ sessionManager.queueMessage(sessionId, "completion", remaining.content, remaining.attachments);
3204
2947
  }
3205
- sendForSession(sessionId, createQueueStateMessage(sessionManager2.getQueueState(sessionId)));
3206
- const userMessage = sessionManager2.addMessage(sessionId, {
2948
+ sendForSession(sessionId, createQueueStateMessage(sessionManager.getQueueState(sessionId)));
2949
+ const userMessage = sessionManager.addMessage(sessionId, {
3207
2950
  role: "user",
3208
2951
  content: nextCompletion.content,
3209
2952
  ...nextCompletion.attachments ? { attachments: nextCompletion.attachments } : {}
@@ -3214,8 +2957,8 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3214
2957
  startTurnWithCompletionChain(sessionId, newController);
3215
2958
  return;
3216
2959
  }
3217
- sessionManager2.clearMessageQueue(sessionId);
3218
- sessionManager2.setRunning(sessionId, false);
2960
+ sessionManager.clearMessageQueue(sessionId);
2961
+ sessionManager.setRunning(sessionId, false);
3219
2962
  });
3220
2963
  break;
3221
2964
  }
@@ -3223,24 +2966,31 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3223
2966
  // Path Confirmation
3224
2967
  // =========================================================================
3225
2968
  case "path.confirm": {
2969
+ send(createErrorMessage("DEPRECATED", "path.confirm removed. Use REST API: POST /api/sessions/:id/confirm-path", message.id));
2970
+ break;
2971
+ }
2972
+ // =========================================================================
2973
+ // Ask User
2974
+ // =========================================================================
2975
+ case "ask.answer": {
3226
2976
  if (!client.activeSessionId) {
3227
2977
  send(createErrorMessage("NO_SESSION", "No active session", message.id));
3228
2978
  return;
3229
2979
  }
3230
- if (!isPathConfirmPayload(message.payload)) {
3231
- send(createErrorMessage("INVALID_PAYLOAD", "Invalid path.confirm payload", message.id));
2980
+ if (!isAskAnswerPayload(message.payload)) {
2981
+ send(createErrorMessage("INVALID_PAYLOAD", "Invalid ask.answer payload", message.id));
3232
2982
  return;
3233
2983
  }
3234
- const { callId, approved } = message.payload;
3235
- const result = providePathConfirmation(callId, approved);
3236
- if (!result.found) {
3237
- send(createErrorMessage("NOT_FOUND", "No pending path confirmation with that ID", message.id));
2984
+ const { callId, answer } = message.payload;
2985
+ const found = provideAnswer(callId, answer);
2986
+ if (!found) {
2987
+ send(createErrorMessage("NOT_FOUND", "No pending question with that ID", message.id));
3238
2988
  return;
3239
2989
  }
3240
- logger.debug("Path confirmation response", {
2990
+ logger.debug("Ask user answer received", {
3241
2991
  sessionId: client.activeSessionId,
3242
2992
  callId,
3243
- approved
2993
+ answerLength: answer.length
3244
2994
  });
3245
2995
  send({ type: "ack", payload: {}, id: message.id });
3246
2996
  break;
@@ -3252,7 +3002,7 @@ async function handleClientMessage(ws, client, message, config, getLLMClient, ge
3252
3002
  }
3253
3003
 
3254
3004
  // src/server/lsp/server.ts
3255
- import { spawn as spawn2 } from "child_process";
3005
+ import { spawn as spawn3 } from "child_process";
3256
3006
  import { extname } from "path";
3257
3007
  import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node.js";
3258
3008
  var LSP = {
@@ -3314,7 +3064,7 @@ var LspServer = class {
3314
3064
  }
3315
3065
  this.state = "starting";
3316
3066
  try {
3317
- this.process = spawn2(this.commandPath, this.config.serverArgs, {
3067
+ this.process = spawn3(this.commandPath, this.config.serverArgs, {
3318
3068
  cwd: this.workdir,
3319
3069
  stdio: ["pipe", "pipe", "pipe"],
3320
3070
  env: {
@@ -4299,11 +4049,16 @@ function canCompact(currentTokens, maxTokens) {
4299
4049
  var SessionManager = class {
4300
4050
  events = new EventEmitter();
4301
4051
  activeSessionId = null;
4052
+ providerManager;
4053
+ constructor(providerManager) {
4054
+ this.providerManager = providerManager;
4055
+ }
4302
4056
  // ============================================================================
4303
4057
  // Session Lifecycle
4304
4058
  // ============================================================================
4305
4059
  /**
4306
4060
  * Create a new session. Emits session.initialized event.
4061
+ * Note: maxTokens is no longer stored in the session - it comes from the current model config
4307
4062
  */
4308
4063
  createSession(projectId, title, providerId, providerModel) {
4309
4064
  const project = getProject(projectId);
@@ -4312,8 +4067,8 @@ var SessionManager = class {
4312
4067
  }
4313
4068
  let sessionTitle = title;
4314
4069
  if (!sessionTitle) {
4315
- const existingSessions = listSessionsByProject(projectId);
4316
- sessionTitle = `Session ${existingSessions.length + 1}`;
4070
+ const existingSessions = listSessionsByProject(projectId, 1e3, 0);
4071
+ sessionTitle = `Session ${existingSessions.sessions.length + 1}`;
4317
4072
  }
4318
4073
  logger.debug("Creating session", { projectId, workdir: project.workdir, title: sessionTitle });
4319
4074
  const dbSession = createSession(projectId, project.workdir, sessionTitle, providerId, providerModel);
@@ -4351,14 +4106,14 @@ var SessionManager = class {
4351
4106
  return listSessions();
4352
4107
  }
4353
4108
  /**
4354
- * List sessions for a project.
4109
+ * List sessions for a project with pagination.
4355
4110
  */
4356
- listSessionsByProject(projectId) {
4111
+ listSessionsByProject(projectId, limit = 20, offset = 0) {
4357
4112
  const project = getProject(projectId);
4358
4113
  if (!project) {
4359
- return [];
4114
+ return { sessions: [], hasMore: false };
4360
4115
  }
4361
- return listSessionsByProject(projectId);
4116
+ return listSessionsByProject(projectId, limit, offset);
4362
4117
  }
4363
4118
  /**
4364
4119
  * Delete a session and all its events.
@@ -4387,8 +4142,8 @@ var SessionManager = class {
4387
4142
  */
4388
4143
  deleteAllSessions(projectId, workdir) {
4389
4144
  logger.debug("Deleting all sessions for project", { projectId, workdir });
4390
- const sessions = listSessionsByProject(projectId);
4391
- sessions.forEach((session) => {
4145
+ const result = listSessionsByProject(projectId, 1e4, 0);
4146
+ result.sessions.forEach((session) => {
4392
4147
  this.deleteSession(session.id);
4393
4148
  });
4394
4149
  }
@@ -4425,6 +4180,16 @@ var SessionManager = class {
4425
4180
  this.emit({ type: "phase_changed", sessionId, phase });
4426
4181
  return updatedSession;
4427
4182
  }
4183
+ /**
4184
+ * Set danger level. Does NOT emit event - danger level is not part of session state.
4185
+ * Just updates DB and returns updated session.
4186
+ */
4187
+ setDangerLevel(sessionId, dangerLevel) {
4188
+ this.requireSession(sessionId);
4189
+ logger.debug("Setting danger level", { sessionId, dangerLevel });
4190
+ updateSessionDangerLevel(sessionId, dangerLevel);
4191
+ return this.requireSession(sessionId);
4192
+ }
4428
4193
  /**
4429
4194
  * Set session running state. Emits running.changed event.
4430
4195
  */
@@ -4598,17 +4363,18 @@ ${summary}`, {
4598
4363
  contextWindowId: newWindowId,
4599
4364
  isSystemGenerated: true,
4600
4365
  messageKind: "auto-prompt",
4601
- isCompactionSummary: true
4366
+ isCompactionSummary: true,
4367
+ metadata: { type: "compaction", name: "Compaction", color: "#64748b" }
4602
4368
  });
4603
4369
  }
4604
4370
  /**
4605
4371
  * Set current context size (for token tracking).
4606
4372
  * Emits a context.state event with the real promptTokens from the LLM.
4373
+ * maxTokens comes from providerManager.getCurrentModelContext() - the currently selected model's limit.
4607
4374
  */
4608
4375
  setCurrentContextSize(sessionId, promptTokens) {
4609
- const config = getRuntimeConfig();
4610
- const maxTokens = config.context.maxTokens;
4611
- const state = getSessionState(sessionId);
4376
+ const state = getSessionState(sessionId, this.providerManager.getCurrentModelContext());
4377
+ const maxTokens = this.providerManager.getCurrentModelContext();
4612
4378
  const compactionCount = state?.contextState.compactionCount ?? 0;
4613
4379
  emitContextState(
4614
4380
  sessionId,
@@ -4682,14 +4448,7 @@ ${summary}`, {
4682
4448
  if (!state) {
4683
4449
  return { error: "Session not found" };
4684
4450
  }
4685
- let actualId = criterion.id;
4686
- if (state.criteria.some((c) => c.id === criterion.id)) {
4687
- let suffix = 1;
4688
- while (state.criteria.some((c) => c.id === `${criterion.id}-${suffix}`)) {
4689
- suffix++;
4690
- }
4691
- actualId = `${criterion.id}-${suffix}`;
4692
- }
4451
+ const actualId = criterion.id || state.criteria.length.toString();
4693
4452
  const updatedCriteria = [...state.criteria, { ...criterion, id: actualId }];
4694
4453
  emitCriteriaSet(sessionId, updatedCriteria);
4695
4454
  return { criteria: updatedCriteria, actualId };
@@ -4730,17 +4489,19 @@ ${summary}`, {
4730
4489
  // Message Queue (runtime state, transient while agent is running)
4731
4490
  // ============================================================================
4732
4491
  messageQueues = /* @__PURE__ */ new Map();
4733
- queueMessage(sessionId, mode, content, attachments) {
4492
+ queueMessage(sessionId, mode, content, attachments, messageKind) {
4734
4493
  const queue = this.messageQueues.get(sessionId) ?? [];
4735
4494
  const msg = {
4736
4495
  queueId: crypto.randomUUID(),
4737
4496
  mode,
4738
4497
  content,
4739
4498
  ...attachments ? { attachments } : {},
4499
+ ...messageKind ? { messageKind } : {},
4740
4500
  queuedAt: (/* @__PURE__ */ new Date()).toISOString()
4741
4501
  };
4742
4502
  queue.push(msg);
4743
4503
  this.messageQueues.set(sessionId, queue);
4504
+ this.emit({ type: "queue_added", sessionId, queueId: msg.queueId, mode, content });
4744
4505
  return msg;
4745
4506
  }
4746
4507
  cancelQueuedMessage(sessionId, queueId) {
@@ -4749,6 +4510,7 @@ ${summary}`, {
4749
4510
  const idx = queue.findIndex((m) => m.queueId === queueId);
4750
4511
  if (idx === -1) return false;
4751
4512
  queue.splice(idx, 1);
4513
+ this.emit({ type: "queue_cancelled", sessionId, queueId });
4752
4514
  return true;
4753
4515
  }
4754
4516
  drainAsapMessages(sessionId) {
@@ -4756,6 +4518,9 @@ ${summary}`, {
4756
4518
  if (!queue) return [];
4757
4519
  const asap = queue.filter((m) => m.mode === "asap");
4758
4520
  this.messageQueues.set(sessionId, queue.filter((m) => m.mode !== "asap"));
4521
+ for (const msg of asap) {
4522
+ this.emit({ type: "queue_drained", sessionId, queueId: msg.queueId });
4523
+ }
4759
4524
  return asap;
4760
4525
  }
4761
4526
  drainCompletionMessages(sessionId) {
@@ -4763,11 +4528,18 @@ ${summary}`, {
4763
4528
  if (!queue) return [];
4764
4529
  const completion = queue.filter((m) => m.mode === "completion");
4765
4530
  this.messageQueues.set(sessionId, queue.filter((m) => m.mode !== "completion"));
4531
+ for (const msg of completion) {
4532
+ this.emit({ type: "queue_drained", sessionId, queueId: msg.queueId });
4533
+ }
4766
4534
  return completion;
4767
4535
  }
4768
4536
  getQueueState(sessionId) {
4769
4537
  return this.messageQueues.get(sessionId) ?? [];
4770
4538
  }
4539
+ hasQueuedMessages(sessionId) {
4540
+ const queue = this.messageQueues.get(sessionId);
4541
+ return queue !== void 0 && queue.length > 0;
4542
+ }
4771
4543
  clearMessageQueue(sessionId) {
4772
4544
  this.messageQueues.delete(sessionId);
4773
4545
  }
@@ -4849,12 +4621,34 @@ ${summary}`,
4849
4621
  * Get the current context state for a session.
4850
4622
  */
4851
4623
  getContextState(sessionId) {
4852
- const state = getSessionState(sessionId);
4624
+ const session = this.getSession(sessionId);
4625
+ const providerManager = this.providerManager;
4626
+ let maxTokens;
4627
+ if (session?.providerId && session.providerModel) {
4628
+ const providers = providerManager.getProviders();
4629
+ const provider = providers.find((p) => p.id === session.providerId);
4630
+ if (provider) {
4631
+ let modelConfig = provider.models.find((m) => m.id === session.providerModel);
4632
+ if (!modelConfig && session.providerModel) {
4633
+ const normalize = (s) => s.toLowerCase().replace(/[-_\s]+/g, "");
4634
+ const sessionModelNormalized = normalize(session.providerModel);
4635
+ modelConfig = provider.models.find((m) => {
4636
+ const modelIdNormalized = normalize(m.id);
4637
+ return modelIdNormalized === sessionModelNormalized || modelIdNormalized.includes(sessionModelNormalized) || sessionModelNormalized.includes(modelIdNormalized);
4638
+ });
4639
+ }
4640
+ maxTokens = modelConfig?.contextWindow ?? providerManager.getCurrentModelContext();
4641
+ } else {
4642
+ maxTokens = providerManager.getCurrentModelContext();
4643
+ }
4644
+ } else {
4645
+ maxTokens = providerManager.getCurrentModelContext();
4646
+ }
4647
+ const state = getSessionState(sessionId, maxTokens);
4853
4648
  if (!state) {
4854
- const config = getRuntimeConfig();
4855
4649
  return {
4856
4650
  currentTokens: 0,
4857
- maxTokens: config.context.maxTokens,
4651
+ maxTokens,
4858
4652
  compactionCount: 0,
4859
4653
  dangerZone: false,
4860
4654
  canCompact: false
@@ -4905,7 +4699,8 @@ ${summary}`,
4905
4699
  * Build a full Session object from DB session + EventStore state
4906
4700
  */
4907
4701
  buildSessionFromDb(dbSession) {
4908
- const eventState = getSessionState(dbSession.id);
4702
+ const maxTokens = this.providerManager.getCurrentModelContext();
4703
+ const eventState = getSessionState(dbSession.id, maxTokens);
4909
4704
  if (!eventState) {
4910
4705
  return {
4911
4706
  ...dbSession,
@@ -4949,12 +4744,10 @@ ${summary}`,
4949
4744
  criteria: eventState.criteria,
4950
4745
  contextWindows: [],
4951
4746
  // Derived from events, not stored separately
4952
- executionState: null
4953
- // No longer using execution state
4747
+ executionState: eventState.lastModeWithReminder ? { lastModeWithReminder: eventState.lastModeWithReminder } : null
4954
4748
  };
4955
4749
  }
4956
4750
  };
4957
- var sessionManager = new SessionManager();
4958
4751
 
4959
4752
  // src/server/commands/registry.ts
4960
4753
  import { readdir as readdir2, readFile as readFile2, writeFile as writeFile2, copyFile as copyFile2, mkdir as mkdir2, access as access3, unlink as unlink2 } from "fs/promises";
@@ -5509,8 +5302,10 @@ function createDevServerRoutes() {
5509
5302
  router.get("/logs", (req, res) => {
5510
5303
  const workdir = req.query["workdir"];
5511
5304
  if (!workdir) return res.status(400).json({ error: "workdir required" });
5512
- const logs = devServerManager.getLogs(workdir);
5513
- res.json({ logs });
5305
+ const offset = Math.max(0, parseInt(req.query["offset"]) || 0);
5306
+ const limit = Math.max(1, parseInt(req.query["limit"]) || Infinity);
5307
+ const result = devServerManager.getLogsSlice(workdir, offset, limit);
5308
+ res.json({ logs: result.logs, total: result.total, offset, limit });
5514
5309
  });
5515
5310
  router.get("/config", async (req, res) => {
5516
5311
  const workdir = req.query["workdir"];
@@ -5536,6 +5331,44 @@ function createDevServerRoutes() {
5536
5331
  return router;
5537
5332
  }
5538
5333
 
5334
+ // src/server/routes/terminals.ts
5335
+ import { Router as Router6 } from "express";
5336
+ function createTerminalRoutes() {
5337
+ const router = Router6();
5338
+ router.get("/", (_req, res) => {
5339
+ const sessions = terminalManager.getAll();
5340
+ const response = sessions.map((s) => ({
5341
+ id: s.id,
5342
+ workdir: s.workdir
5343
+ }));
5344
+ res.json(response);
5345
+ });
5346
+ router.post("/", (req, res) => {
5347
+ const workdir = req.body?.workdir;
5348
+ const session = terminalManager.create(workdir);
5349
+ res.status(201).json({ id: session.id, workdir: session.workdir });
5350
+ });
5351
+ router.delete("/:id", (req, res) => {
5352
+ const { id } = req.params;
5353
+ const success = terminalManager.kill(id);
5354
+ if (success) {
5355
+ res.status(204).send();
5356
+ } else {
5357
+ res.status(404).json({ error: "Terminal not found" });
5358
+ }
5359
+ });
5360
+ router.get("/:id", (req, res) => {
5361
+ const { id } = req.params;
5362
+ const session = terminalManager.get(id);
5363
+ if (session) {
5364
+ res.json({ id: session.id, workdir: session.workdir });
5365
+ } else {
5366
+ res.status(404).json({ error: "Terminal not found" });
5367
+ }
5368
+ });
5369
+ return router;
5370
+ }
5371
+
5539
5372
  // src/server/index.ts
5540
5373
  var __dirname2 = dirname4(fileURLToPath4(import.meta.url));
5541
5374
  async function createServerHandle(config) {
@@ -5548,8 +5381,8 @@ async function createServerHandle(config) {
5548
5381
  await ensureDefaultCommands(configDir);
5549
5382
  await ensureDefaultAgents(configDir);
5550
5383
  await ensureDefaultWorkflows(configDir);
5551
- const sessionManager2 = new SessionManager();
5552
5384
  const providerManager = createProviderManager(config);
5385
+ const sessionManager = new SessionManager(providerManager);
5553
5386
  const useMock = process.env["OPENFOX_MOCK_LLM"] === "true";
5554
5387
  const getMockClient = useMock ? createMockLLMClient : null;
5555
5388
  const getLLMClient = () => getMockClient ? getMockClient() : providerManager.getLLMClient();
@@ -5584,8 +5417,19 @@ async function createServerHandle(config) {
5584
5417
  logger.warn("Could not auto-detect model, using config", { model: config.llm.model });
5585
5418
  }
5586
5419
  }
5420
+ const activeProvider = providerManager.getActiveProvider();
5421
+ if (activeProvider) {
5422
+ await providerManager.refreshProviderModels(activeProvider.id).catch((err) => {
5423
+ logger.debug("Startup model refetch failed", {
5424
+ providerId: activeProvider.id,
5425
+ error: err instanceof Error ? err.message : String(err)
5426
+ });
5427
+ });
5428
+ }
5587
5429
  }
5588
- initLLM().catch((err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) }));
5430
+ initLLM().catch(
5431
+ (err) => logger.error("LLM initialization failed", { error: err instanceof Error ? err.message : String(err) })
5432
+ );
5589
5433
  const toolRegistry = createToolRegistry();
5590
5434
  const app = express();
5591
5435
  app.use(cors());
@@ -5594,39 +5438,317 @@ async function createServerHandle(config) {
5594
5438
  res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
5595
5439
  });
5596
5440
  app.get("/api/tools", (_req, res) => {
5597
- res.json({ tools: toolRegistry.tools.map((t) => t.name) });
5441
+ const tools = toolRegistry.tools.map((t) => ({
5442
+ name: t.name,
5443
+ actions: t.permittedActions || []
5444
+ }));
5445
+ res.json({ tools });
5446
+ });
5447
+ app.get("/api/projects", async (_req, res) => {
5448
+ const { listProjects } = await import("./projects-GSMMEVSO.js");
5449
+ const projects = listProjects();
5450
+ res.json({ projects });
5451
+ });
5452
+ app.post("/api/projects", async (req, res) => {
5453
+ const { name, workdir } = req.body;
5454
+ if (!name || !workdir) {
5455
+ return res.status(400).json({ error: "name and workdir are required" });
5456
+ }
5457
+ const { createDirectoryWithGit } = await import("./project-creator-KAQXMZCT.js");
5458
+ const project = await createDirectoryWithGit(name, workdir);
5459
+ res.status(201).json({ project });
5460
+ });
5461
+ app.get("/api/projects/:id", async (req, res) => {
5462
+ const { getProject: getProject2 } = await import("./projects-GSMMEVSO.js");
5463
+ const project = getProject2(req.params.id);
5464
+ if (!project) {
5465
+ return res.status(404).json({ error: "Project not found" });
5466
+ }
5467
+ res.json({ project });
5468
+ });
5469
+ app.put("/api/projects/:id", async (req, res) => {
5470
+ const { updateProject } = await import("./projects-GSMMEVSO.js");
5471
+ const { name, customInstructions } = req.body;
5472
+ const updates = {};
5473
+ if (name !== void 0) updates.name = name;
5474
+ if (customInstructions !== void 0) updates.customInstructions = customInstructions;
5475
+ const updated = updateProject(req.params.id, updates);
5476
+ if (!updated) {
5477
+ return res.status(404).json({ error: "Project not found" });
5478
+ }
5479
+ res.json({ project: updated });
5480
+ });
5481
+ app.delete("/api/projects/:id", async (req, res) => {
5482
+ const { getProject: getProject2, deleteProject } = await import("./projects-GSMMEVSO.js");
5483
+ const project = getProject2(req.params.id);
5484
+ if (!project) {
5485
+ return res.status(404).json({ error: "Project not found" });
5486
+ }
5487
+ deleteProject(req.params.id);
5488
+ res.json({ success: true });
5489
+ });
5490
+ app.get("/api/sessions", async (req, res) => {
5491
+ const { getRecentUserPromptsForSession } = await import("./events-G6CKHDNZ.js");
5492
+ const projectId = req.query["projectId"];
5493
+ const limit = Math.min(parseInt(req.query["limit"]) || 20, 100);
5494
+ const offset = parseInt(req.query["offset"]) || 0;
5495
+ let sessions;
5496
+ let hasMore = false;
5497
+ if (projectId) {
5498
+ const result = sessionManager.listSessionsByProject(projectId, limit, offset);
5499
+ sessions = result.sessions;
5500
+ hasMore = result.hasMore;
5501
+ } else {
5502
+ sessions = sessionManager.listSessions();
5503
+ }
5504
+ const sessionsWithPrompts = sessions.map((session) => ({
5505
+ ...session,
5506
+ recentUserPrompts: getRecentUserPromptsForSession(session.id, 10)
5507
+ }));
5508
+ res.json({ sessions: sessionsWithPrompts, hasMore });
5598
5509
  });
5599
5510
  app.post("/api/sessions", async (req, res) => {
5600
- const { workdir, title } = req.body;
5601
- const session = sessionManager2.createSession(workdir, title);
5511
+ const { projectId, title } = req.body;
5512
+ if (!projectId) {
5513
+ return res.status(400).json({ error: "projectId is required" });
5514
+ }
5515
+ const project = sessionManager.getProject(projectId);
5516
+ if (!project) {
5517
+ return res.status(404).json({ error: "Project not found" });
5518
+ }
5519
+ const { providerId, model } = parseDefaultModelSelection(config.defaultModelSelection);
5520
+ const session = sessionManager.createSession(projectId, title, providerId ?? null, model ?? null);
5602
5521
  res.status(201).json({ session });
5603
5522
  });
5604
- app.get("/api/sessions/:id", (req, res) => {
5605
- const session = sessionManager2.getSession(req.params.id);
5523
+ app.get("/api/sessions/:id", async (req, res) => {
5524
+ const { getEventStore: getEventStore2 } = await import("./events-G6CKHDNZ.js");
5525
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-O52L65VN.js");
5526
+ const session = sessionManager.getSession(req.params.id);
5606
5527
  if (!session) {
5607
5528
  return res.status(404).json({ error: "Session not found" });
5608
5529
  }
5609
- res.json({ session });
5530
+ const eventStore = getEventStore2();
5531
+ const events = eventStore.getEvents(req.params.id);
5532
+ const messages = buildMessagesFromStoredEvents2(events);
5533
+ const contextState = sessionManager.getContextState(req.params.id);
5534
+ const queueState = sessionManager.getQueueState(req.params.id);
5535
+ res.json({ session, messages, contextState, queueState });
5610
5536
  });
5611
5537
  app.delete("/api/sessions/:id", (req, res) => {
5612
- sessionManager2.deleteSession(req.params["id"]);
5538
+ const session = sessionManager.getSession(req.params["id"]);
5539
+ if (!session) {
5540
+ return res.status(404).json({ error: "Session not found" });
5541
+ }
5542
+ sessionManager.deleteSession(req.params["id"]);
5613
5543
  res.json({ success: true });
5614
5544
  });
5615
5545
  app.delete("/api/projects/:projectId/sessions", (req, res) => {
5616
5546
  const projectId = req.params["projectId"];
5617
- const project = sessionManager2.getProject(projectId);
5547
+ const project = sessionManager.getProject(projectId);
5618
5548
  if (!project) {
5619
5549
  return res.status(404).json({ error: "Project not found" });
5620
5550
  }
5621
- sessionManager2.deleteAllSessions(projectId, project.workdir);
5551
+ sessionManager.deleteAllSessions(projectId, project.workdir);
5552
+ res.json({ success: true });
5553
+ });
5554
+ app.post("/api/sessions/:id/provider", async (req, res) => {
5555
+ const { getEventStore: getEventStore2 } = await import("./events-G6CKHDNZ.js");
5556
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-O52L65VN.js");
5557
+ const sessionId = req.params.id;
5558
+ const session = sessionManager.getSession(sessionId);
5559
+ if (!session) {
5560
+ return res.status(404).json({ error: "Session not found" });
5561
+ }
5562
+ const { providerId, model } = req.body;
5563
+ if (!providerId) {
5564
+ return res.status(400).json({ error: "providerId is required" });
5565
+ }
5566
+ sessionManager.setSessionProvider(sessionId, providerId, model ?? "auto");
5567
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-GIFCEJDU.js");
5568
+ const globalConfig = await loadGlobalConfig(config.mode ?? "production");
5569
+ const updatedConfig = setDefaultModelSelection(globalConfig, providerId, model ?? "auto");
5570
+ await saveGlobalConfig(config.mode ?? "production", updatedConfig);
5571
+ config.defaultModelSelection = updatedConfig.defaultModelSelection;
5572
+ const contextState = sessionManager.getContextState(sessionId);
5573
+ const eventStore = getEventStore2();
5574
+ const events = eventStore.getEvents(sessionId);
5575
+ const messages = buildMessagesFromStoredEvents2(events);
5576
+ const updatedSession = sessionManager.getSession(sessionId);
5577
+ res.json({ session: updatedSession, messages, contextState });
5578
+ });
5579
+ app.put("/api/sessions/:id/criteria", async (req, res) => {
5580
+ const sessionId = req.params.id;
5581
+ const session = sessionManager.getSession(sessionId);
5582
+ if (!session) {
5583
+ return res.status(404).json({ error: "Session not found" });
5584
+ }
5585
+ const { criteria } = req.body;
5586
+ if (!Array.isArray(criteria)) {
5587
+ return res.status(400).json({ error: "criteria is required and must be an array" });
5588
+ }
5589
+ sessionManager.setCriteria(sessionId, criteria);
5590
+ res.json({ success: true });
5591
+ });
5592
+ app.put("/api/sessions/:id/mode", async (req, res) => {
5593
+ const { getEventStore: getEventStore2 } = await import("./events-G6CKHDNZ.js");
5594
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2 } = await import("./folding-O52L65VN.js");
5595
+ const sessionId = req.params.id;
5596
+ const session = sessionManager.getSession(sessionId);
5597
+ if (!session) {
5598
+ return res.status(404).json({ error: "Session not found" });
5599
+ }
5600
+ const { mode } = req.body;
5601
+ if (!mode || !["planner", "builder"].includes(mode)) {
5602
+ return res.status(400).json({ error: 'mode is required and must be "planner" or "builder"' });
5603
+ }
5604
+ sessionManager.setMode(sessionId, mode);
5605
+ const eventStore = getEventStore2();
5606
+ eventStore.append(sessionId, { type: "mode.changed", data: { mode, auto: false } });
5607
+ const events = eventStore.getEvents(sessionId);
5608
+ const messages = buildMessagesFromStoredEvents2(events);
5609
+ const updatedSession = sessionManager.getSession(sessionId);
5610
+ res.json({ session: updatedSession, messages });
5611
+ });
5612
+ app.put("/api/sessions/:id/danger-level", async (req, res) => {
5613
+ const sessionId = req.params.id;
5614
+ const session = sessionManager.getSession(sessionId);
5615
+ if (!session) {
5616
+ return res.status(404).json({ error: "Session not found" });
5617
+ }
5618
+ const { dangerLevel } = req.body;
5619
+ if (!dangerLevel || !["normal", "dangerous"].includes(dangerLevel)) {
5620
+ return res.status(400).json({ error: 'dangerLevel is required and must be "normal" or "dangerous"' });
5621
+ }
5622
+ sessionManager.setDangerLevel(sessionId, dangerLevel);
5623
+ const updatedSession = sessionManager.getSession(sessionId);
5624
+ res.json({ session: updatedSession });
5625
+ });
5626
+ app.post("/api/sessions/:id/confirm-path", async (req, res) => {
5627
+ const sessionId = req.params.id;
5628
+ const { callId, approved, alwaysAllow } = req.body;
5629
+ if (!callId || approved === void 0) {
5630
+ return res.status(400).json({ error: "callId and approved are required" });
5631
+ }
5632
+ const { providePathConfirmation: providePathConfirmation2 } = await import("./tools-SGBQE736.js");
5633
+ const result = providePathConfirmation2(callId, approved, alwaysAllow);
5634
+ if (!result.found) {
5635
+ return res.status(404).json({ error: "No pending path confirmation with that ID" });
5636
+ }
5637
+ const { getEventStore: getEventStore2 } = await import("./events-G6CKHDNZ.js");
5638
+ const { buildMessagesFromStoredEvents: buildMessagesFromStoredEvents2, foldPendingConfirmations: foldPendingConfirmations2 } = await import("./folding-O52L65VN.js");
5639
+ const { createSessionStateMessage: createSessionStateMessage2 } = await import("./protocol-WQENDK72.js");
5640
+ const eventStore = getEventStore2();
5641
+ const events = eventStore.getEvents(sessionId);
5642
+ const messages = buildMessagesFromStoredEvents2(events);
5643
+ const pendingConfirmations = foldPendingConfirmations2(events);
5644
+ const session = sessionManager.getSession(sessionId);
5645
+ if (session) {
5646
+ const stateMsg = createSessionStateMessage2(session, messages, pendingConfirmations);
5647
+ wssExports.broadcastForSession(sessionId, { ...stateMsg, sessionId });
5648
+ }
5649
+ res.json({ success: true });
5650
+ });
5651
+ app.post("/api/sessions/:id/answer", async (req, res) => {
5652
+ const sessionId = req.params.id;
5653
+ const { callId, answer } = req.body;
5654
+ if (!callId || !answer) {
5655
+ return res.status(400).json({ error: "callId and answer are required" });
5656
+ }
5657
+ const { provideAnswer: provideAnswer2 } = await import("./tools-SGBQE736.js");
5658
+ const found = provideAnswer2(callId, answer);
5659
+ if (!found) {
5660
+ return res.status(404).json({ error: "No pending question with that ID" });
5661
+ }
5662
+ res.json({ success: true });
5663
+ });
5664
+ app.post("/api/sessions/:id/message", (req, res) => {
5665
+ const sessionId = req.params.id;
5666
+ const session = sessionManager.getSession(sessionId);
5667
+ if (!session) {
5668
+ return res.status(404).json({ error: "Session not found" });
5669
+ }
5670
+ const { content, attachments, messageKind } = req.body;
5671
+ if (!content?.trim()) {
5672
+ return res.status(400).json({ error: "content is required" });
5673
+ }
5674
+ sessionManager.queueMessage(sessionId, "asap", content, attachments, messageKind);
5675
+ res.json({ success: true, queueState: sessionManager.getQueueState(sessionId) });
5676
+ });
5677
+ app.delete("/api/sessions/:id/queue/:queueId", (req, res) => {
5678
+ const sessionId = req.params.id;
5679
+ const { queueId } = req.params;
5680
+ const session = sessionManager.getSession(sessionId);
5681
+ if (!session) {
5682
+ return res.status(404).json({ error: "Session not found" });
5683
+ }
5684
+ sessionManager.cancelQueuedMessage(sessionId, queueId);
5685
+ res.json({ success: true, queueState: sessionManager.getQueueState(sessionId) });
5686
+ });
5687
+ app.post("/api/sessions/:id/stop", async (req, res) => {
5688
+ const sessionId = req.params.id;
5689
+ const session = sessionManager.getSession(sessionId);
5690
+ if (!session) {
5691
+ return res.status(404).json({ error: "Session not found" });
5692
+ }
5693
+ const { stopSessionExecution } = await import("./chat-handler-T7SLWZ4E.js");
5694
+ const { cancelQuestionsForSession: cancelQuestionsForSession2, cancelPathConfirmationsForSession: cancelPathConfirmationsForSession2 } = await import("./tools-SGBQE736.js");
5695
+ stopSessionExecution(sessionId, sessionManager);
5696
+ abortSession(sessionId);
5697
+ cancelQuestionsForSession2(sessionId, "Session stopped by user");
5698
+ cancelPathConfirmationsForSession2(sessionId, "Session stopped by user");
5699
+ sessionManager.clearMessageQueue(sessionId);
5700
+ const eventStore = (await import("./events-G6CKHDNZ.js")).getEventStore();
5701
+ eventStore.append(sessionId, { type: "running.changed", data: { isRunning: false } });
5622
5702
  res.json({ success: true });
5623
5703
  });
5704
+ app.post("/api/sessions/:id/chat", async (req, res) => {
5705
+ const sessionId = req.params.id;
5706
+ const session = sessionManager.getSession(sessionId);
5707
+ if (!session) {
5708
+ return res.status(404).json({ error: "Session not found" });
5709
+ }
5710
+ const { content, attachments, messageKind, isSystemGenerated } = req.body;
5711
+ if (!content) {
5712
+ return res.status(400).json({ error: "content is required" });
5713
+ }
5714
+ if (session.isRunning) {
5715
+ return res.status(409).json({ error: "Session is already running" });
5716
+ }
5717
+ res.json({ accepted: true, sessionId });
5718
+ });
5719
+ app.post("/api/sessions/:id/continue", async (req, res) => {
5720
+ const sessionId = req.params.id;
5721
+ const session = sessionManager.getSession(sessionId);
5722
+ if (!session) {
5723
+ return res.status(404).json({ error: "Session not found" });
5724
+ }
5725
+ if (session.isRunning) {
5726
+ return res.status(409).json({ error: "Session is already running" });
5727
+ }
5728
+ res.json({ accepted: true });
5729
+ });
5730
+ app.get("/api/settings/:key", async (req, res) => {
5731
+ const { getSetting } = await import("./settings-NCLN4JDZ.js");
5732
+ const key = req.params.key;
5733
+ const value = getSetting(key);
5734
+ res.json({ key, value });
5735
+ });
5736
+ app.put("/api/settings/:key", async (req, res) => {
5737
+ const { setSetting } = await import("./settings-NCLN4JDZ.js");
5738
+ const key = req.params.key;
5739
+ const { value } = req.body;
5740
+ if (value === void 0) {
5741
+ return res.status(400).json({ error: "value is required" });
5742
+ }
5743
+ setSetting(key, value);
5744
+ res.json({ key, value });
5745
+ });
5624
5746
  app.get("/api/config", (_req, res) => {
5625
5747
  const llmClient = getLLMClient();
5626
5748
  const activeProvider = providerManager.getActiveProvider();
5627
5749
  res.json({
5628
5750
  model: llmClient.getModel(),
5629
- maxContext: config.context.maxTokens,
5751
+ maxContext: providerManager.getCurrentModelContext(),
5630
5752
  llmUrl: activeProvider?.url ?? config.llm.baseUrl,
5631
5753
  llmStatus: getLlmStatus(),
5632
5754
  backend: llmClient.getBackend(),
@@ -5646,10 +5768,20 @@ async function createServerHandle(config) {
5646
5768
  const detected = await detectModel(baseUrl);
5647
5769
  if (detected) {
5648
5770
  llmClient.setModel(detected);
5649
- return res.json({ model: detected, source: "detected", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5771
+ return res.json({
5772
+ model: detected,
5773
+ source: "detected",
5774
+ llmStatus: getLlmStatus(),
5775
+ backend: llmClient.getBackend()
5776
+ });
5650
5777
  }
5651
5778
  }
5652
- res.json({ model: llmClient.getModel(), source: "cached", llmStatus: getLlmStatus(), backend: llmClient.getBackend() });
5779
+ res.json({
5780
+ model: llmClient.getModel(),
5781
+ source: "cached",
5782
+ llmStatus: getLlmStatus(),
5783
+ backend: llmClient.getBackend()
5784
+ });
5653
5785
  });
5654
5786
  app.get("/api/providers", (_req, res) => {
5655
5787
  const providers = providerManager.getProviders().map((p) => ({
@@ -5674,7 +5806,7 @@ async function createServerHandle(config) {
5674
5806
  return res.status(400).json({ error: result.error });
5675
5807
  }
5676
5808
  const llmClient = getLLMClient();
5677
- const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-BMXSDOZB.js");
5809
+ const { loadGlobalConfig, saveGlobalConfig, setDefaultModelSelection } = await import("./config-GIFCEJDU.js");
5678
5810
  const globalConfig = await loadGlobalConfig(config.mode ?? "production");
5679
5811
  const updatedConfig = setDefaultModelSelection(globalConfig, id, llmClient.getModel());
5680
5812
  await saveGlobalConfig(config.mode ?? "production", updatedConfig);
@@ -5685,12 +5817,65 @@ async function createServerHandle(config) {
5685
5817
  backend: llmClient.getBackend()
5686
5818
  });
5687
5819
  });
5820
+ app.post("/api/providers/:id/models/:modelId", async (req, res) => {
5821
+ const { id, modelId } = req.params;
5822
+ const body = req.body;
5823
+ if (!body.contextWindow) {
5824
+ return res.status(400).json({ error: "contextWindow is required" });
5825
+ }
5826
+ const result = await providerManager.updateModelContext(id, modelId, body.contextWindow);
5827
+ if (!result.success) {
5828
+ return res.status(400).json({ error: result.error });
5829
+ }
5830
+ const { loadGlobalConfig, saveGlobalConfig } = await import("./config-GIFCEJDU.js");
5831
+ const globalConfig = await loadGlobalConfig(config.mode ?? "production");
5832
+ const updatedProviders = providerManager.getProviders();
5833
+ const updatedConfig = {
5834
+ ...globalConfig,
5835
+ providers: updatedProviders,
5836
+ activeProviderId: providerManager.getActiveProviderId(),
5837
+ defaultModelSelection: providerManager.getActiveProviderId() ? `${providerManager.getActiveProviderId()}/${providerManager.getCurrentModel()}` : void 0
5838
+ };
5839
+ await saveGlobalConfig(config.mode ?? "production", updatedConfig);
5840
+ let contextState = null;
5841
+ const sessions = sessionManager.listSessions();
5842
+ if (sessions.length > 0) {
5843
+ for (const session of sessions) {
5844
+ const sessionProviderId = session.providerId || providerManager.getActiveProviderId();
5845
+ if (sessionProviderId === id) {
5846
+ contextState = sessionManager.getContextState(session.id);
5847
+ break;
5848
+ }
5849
+ }
5850
+ }
5851
+ res.json({
5852
+ success: true,
5853
+ providerId: id,
5854
+ modelId,
5855
+ contextWindow: body.contextWindow,
5856
+ contextState
5857
+ });
5858
+ });
5859
+ app.post("/api/providers/:id/refresh", async (req, res) => {
5860
+ const { id } = req.params;
5861
+ const result = await providerManager.refreshProviderModels(id);
5862
+ if (!result.success) {
5863
+ return res.status(400).json({ error: result.error });
5864
+ }
5865
+ const updatedProvider = providerManager.getProviders().find((p) => p.id === id);
5866
+ res.json({
5867
+ success: true,
5868
+ providerId: id,
5869
+ models: updatedProvider?.models ?? []
5870
+ });
5871
+ });
5688
5872
  app.use("/api/skills", createSkillRoutes(configDir));
5689
5873
  app.use("/api/commands", createCommandRoutes(configDir));
5690
5874
  app.use("/api/agents", createAgentRoutes(configDir));
5691
5875
  app.use("/api/workflows", createWorkflowRoutes(configDir, config));
5692
5876
  app.use("/api/dev-server", createDevServerRoutes());
5693
- const { getCurrentBranch } = await import("./branch.api-WDHKZJS6.js");
5877
+ app.use("/api/terminals", createTerminalRoutes());
5878
+ const { getCurrentBranch } = await import("./branch.api-XADFZZAA.js");
5694
5879
  app.get("/api/branch", async (req, res) => {
5695
5880
  await getCurrentBranch(req, res);
5696
5881
  });
@@ -5756,11 +5941,14 @@ async function createServerHandle(config) {
5756
5941
  res.status(404).send("Not found");
5757
5942
  });
5758
5943
  });
5759
- app.use("/sounds", express.static(join4(webDir, "public", "sounds"), {
5760
- setHeaders: (res) => {
5761
- res.set("Content-Type", "audio/mpeg");
5762
- }
5763
- }));
5944
+ app.use(
5945
+ "/sounds",
5946
+ express.static(join4(webDir, "public", "sounds"), {
5947
+ setHeaders: (res) => {
5948
+ res.set("Content-Type", "audio/mpeg");
5949
+ }
5950
+ })
5951
+ );
5764
5952
  app.get("/*path", (req, res) => {
5765
5953
  if (req.path.startsWith("/api/")) {
5766
5954
  return;
@@ -5774,13 +5962,16 @@ async function createServerHandle(config) {
5774
5962
  }
5775
5963
  if (!isDev) {
5776
5964
  const distWebDir = resolve(__dirname2, "web");
5777
- app.use("/assets", express.static(join4(distWebDir, "assets"), {
5778
- setHeaders: (res, filepath) => {
5779
- if (filepath.endsWith(".css")) {
5780
- res.set("Content-Type", "text/css");
5965
+ app.use(
5966
+ "/assets",
5967
+ express.static(join4(distWebDir, "assets"), {
5968
+ setHeaders: (res, filepath) => {
5969
+ if (filepath.endsWith(".css")) {
5970
+ res.set("Content-Type", "text/css");
5971
+ }
5781
5972
  }
5782
- }
5783
- }));
5973
+ })
5974
+ );
5784
5975
  app.get("/fox.svg", (_req, res) => {
5785
5976
  readFile3(join4(distWebDir, "fox.svg")).then((content) => {
5786
5977
  res.set("Content-Type", "image/svg+xml");
@@ -5789,11 +5980,14 @@ async function createServerHandle(config) {
5789
5980
  res.status(404).send("Not found");
5790
5981
  });
5791
5982
  });
5792
- app.use("/sounds", express.static(join4(distWebDir, "sounds"), {
5793
- setHeaders: (res) => {
5794
- res.set("Content-Type", "audio/mpeg");
5795
- }
5796
- }));
5983
+ app.use(
5984
+ "/sounds",
5985
+ express.static(join4(distWebDir, "sounds"), {
5986
+ setHeaders: (res) => {
5987
+ res.set("Content-Type", "audio/mpeg");
5988
+ }
5989
+ })
5990
+ );
5797
5991
  app.get("/", (_req, res) => {
5798
5992
  readFile3(join4(distWebDir, "index.html"), "utf-8").then((content) => res.send(content)).catch(() => {
5799
5993
  res.status(404).send("Web UI not built. Run `npm run build:web`");
@@ -5809,10 +6003,35 @@ async function createServerHandle(config) {
5809
6003
  });
5810
6004
  }
5811
6005
  const httpServer = createHttpServer(app);
5812
- const wss = createWebSocketServer(httpServer, config, getLLMClient, () => providerManager.getActiveProvider(), toolRegistry, sessionManager2, providerManager);
6006
+ const wssExports = createWebSocketServer(
6007
+ httpServer,
6008
+ config,
6009
+ getLLMClient,
6010
+ () => providerManager.getActiveProvider(),
6011
+ sessionManager,
6012
+ providerManager
6013
+ );
6014
+ const wss = wssExports.wss;
6015
+ const { QueueProcessor } = await import("./processor-KLHKDILM.js");
6016
+ const queueProcessor = new QueueProcessor({
6017
+ sessionManager,
6018
+ providerManager,
6019
+ getLLMClient,
6020
+ getActiveProvider: () => providerManager.getActiveProvider(),
6021
+ broadcastForSession: wssExports.broadcastForSession
6022
+ });
6023
+ queueProcessor.start();
6024
+ const abortSession = (sessionId) => {
6025
+ const aborted = wssExports.abortSession(sessionId) || queueProcessor.abortSession(sessionId);
6026
+ if (aborted) {
6027
+ sessionManager.setRunning(sessionId, false);
6028
+ wssExports.broadcastForSession(sessionId, { type: "session.running", payload: { isRunning: false } });
6029
+ }
6030
+ return aborted;
6031
+ };
5813
6032
  return {
5814
6033
  httpServer,
5815
- ctx: { config, sessionManager: sessionManager2, llmClient: getLLMClient(), toolRegistry, providerManager },
6034
+ ctx: { config, sessionManager, llmClient: getLLMClient(), toolRegistry, providerManager },
5816
6035
  start: (port) => new Promise((resolve2, reject) => {
5817
6036
  const listenPort = port ?? config.server.port;
5818
6037
  const host = config.server.host;
@@ -5863,4 +6082,4 @@ export {
5863
6082
  createServerHandle,
5864
6083
  createServer
5865
6084
  };
5866
- //# sourceMappingURL=chunk-3WOUSUW7.js.map
6085
+ //# sourceMappingURL=chunk-RAZQTQK2.js.map