@jingyi0605/codingns 0.3.6 → 0.4.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 (185) hide show
  1. package/README.md +3 -0
  2. package/bin/codingns.mjs +489 -1
  3. package/dist/public/assets/{TerminalPage-D00S4KM6.js → TerminalPage-6jHZV9Mh.js} +17 -17
  4. package/dist/public/assets/index-CSVhg7I8.js +123 -0
  5. package/dist/public/assets/index-Ce1VX19m.css +1 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/modules/assistant-capability/assistant-capability-controller.d.ts +173 -0
  8. package/dist/server/modules/assistant-capability/assistant-capability-controller.js +307 -0
  9. package/dist/server/modules/assistant-capability/assistant-capability-controller.js.map +1 -1
  10. package/dist/server/modules/assistant-capability/assistant-capability-service.d.ts +199 -2
  11. package/dist/server/modules/assistant-capability/assistant-capability-service.js +565 -3
  12. package/dist/server/modules/assistant-capability/assistant-capability-service.js.map +1 -1
  13. package/dist/server/modules/butler/assistant-automation-service.d.ts +110 -0
  14. package/dist/server/modules/butler/assistant-automation-service.js +786 -0
  15. package/dist/server/modules/butler/assistant-automation-service.js.map +1 -0
  16. package/dist/server/modules/butler/assistant-automation-trigger.d.ts +94 -0
  17. package/dist/server/modules/butler/assistant-automation-trigger.js +400 -0
  18. package/dist/server/modules/butler/assistant-automation-trigger.js.map +1 -0
  19. package/dist/server/modules/butler/assistant-sandbox-service.d.ts +55 -0
  20. package/dist/server/modules/butler/assistant-sandbox-service.js +266 -0
  21. package/dist/server/modules/butler/assistant-sandbox-service.js.map +1 -0
  22. package/dist/server/modules/butler/butler-action-context-service.d.ts +4 -1
  23. package/dist/server/modules/butler/butler-action-context-service.js +8 -2
  24. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -1
  25. package/dist/server/modules/butler/butler-control-session-service.d.ts +8 -1
  26. package/dist/server/modules/butler/butler-control-session-service.js +154 -40
  27. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -1
  28. package/dist/server/modules/butler/butler-control-timer-scheduler.d.ts +32 -0
  29. package/dist/server/modules/butler/butler-control-timer-scheduler.js +93 -0
  30. package/dist/server/modules/butler/butler-control-timer-scheduler.js.map +1 -0
  31. package/dist/server/modules/butler/butler-control-timer-service.d.ts +42 -0
  32. package/dist/server/modules/butler/butler-control-timer-service.js +132 -0
  33. package/dist/server/modules/butler/butler-control-timer-service.js.map +1 -0
  34. package/dist/server/modules/butler/butler-controller.d.ts +42 -2
  35. package/dist/server/modules/butler/butler-controller.js +79 -12
  36. package/dist/server/modules/butler/butler-controller.js.map +1 -1
  37. package/dist/server/modules/butler/butler-follow-up-service.d.ts +9 -1
  38. package/dist/server/modules/butler/butler-follow-up-service.js +273 -181
  39. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -1
  40. package/dist/server/modules/butler/butler-inbox-analysis-service.d.ts +4 -1
  41. package/dist/server/modules/butler/butler-inbox-analysis-service.js +18 -4
  42. package/dist/server/modules/butler/butler-inbox-analysis-service.js.map +1 -1
  43. package/dist/server/modules/butler/butler-profile-service.js +2 -5
  44. package/dist/server/modules/butler/butler-profile-service.js.map +1 -1
  45. package/dist/server/modules/butler/butler-project-service.d.ts +3 -1
  46. package/dist/server/modules/butler/butler-project-service.js +7 -1
  47. package/dist/server/modules/butler/butler-project-service.js.map +1 -1
  48. package/dist/server/modules/butler/butler-session-service.d.ts +3 -1
  49. package/dist/server/modules/butler/butler-session-service.js +12 -1
  50. package/dist/server/modules/butler/butler-session-service.js.map +1 -1
  51. package/dist/server/modules/butler/butler-session-summary-service.js +2 -1
  52. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -1
  53. package/dist/server/modules/butler/butler-workspace-context.d.ts +3 -0
  54. package/dist/server/modules/butler/butler-workspace-context.js +164 -44
  55. package/dist/server/modules/butler/butler-workspace-context.js.map +1 -1
  56. package/dist/server/modules/butler/patrol-execution-service.js +2 -1
  57. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -1
  58. package/dist/server/modules/butler/provider-adapter-registry.d.ts +3 -0
  59. package/dist/server/modules/butler/provider-adapter-registry.js +18 -1
  60. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -1
  61. package/dist/server/modules/butler/verification-run-service.d.ts +9 -2
  62. package/dist/server/modules/butler/verification-run-service.js +188 -34
  63. package/dist/server/modules/butler/verification-run-service.js.map +1 -1
  64. package/dist/server/modules/debug-target/debug-target-controller.js +1 -1
  65. package/dist/server/modules/debug-target/debug-target-controller.js.map +1 -1
  66. package/dist/server/modules/debug-target/debug-target-service.d.ts +7 -2
  67. package/dist/server/modules/debug-target/debug-target-service.js +563 -100
  68. package/dist/server/modules/debug-target/debug-target-service.js.map +1 -1
  69. package/dist/server/modules/git/git-command-helper-client.d.ts +1 -0
  70. package/dist/server/modules/git/git-command-helper-client.js +19 -26
  71. package/dist/server/modules/git/git-command-helper-client.js.map +1 -1
  72. package/dist/server/modules/git/git-command-runner.js +19 -1
  73. package/dist/server/modules/git/git-command-runner.js.map +1 -1
  74. package/dist/server/modules/preferences/profile-service.d.ts +3 -1
  75. package/dist/server/modules/preferences/profile-service.js +74 -3
  76. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  77. package/dist/server/modules/provider/provider-discovery-helper-client.d.ts +5 -3
  78. package/dist/server/modules/provider/provider-discovery-helper-client.js +129 -43
  79. package/dist/server/modules/provider/provider-discovery-helper-client.js.map +1 -1
  80. package/dist/server/modules/provider/provider-discovery-helper-process.js +44 -0
  81. package/dist/server/modules/provider/provider-discovery-helper-process.js.map +1 -1
  82. package/dist/server/modules/provider/provider-discovery-runtime.js +83 -3
  83. package/dist/server/modules/provider/provider-discovery-runtime.js.map +1 -1
  84. package/dist/server/modules/sessions/claude-runtime-helper-client.js +23 -1
  85. package/dist/server/modules/sessions/claude-runtime-helper-client.js.map +1 -1
  86. package/dist/server/modules/sessions/session-history-service.d.ts +7 -1
  87. package/dist/server/modules/sessions/session-history-service.js +251 -41
  88. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  89. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +6 -0
  90. package/dist/server/modules/sessions/session-live-runtime-service.js +97 -11
  91. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  92. package/dist/server/modules/sessions/session-message-origin-utils.d.ts +12 -0
  93. package/dist/server/modules/sessions/session-message-origin-utils.js +45 -0
  94. package/dist/server/modules/sessions/session-message-origin-utils.js.map +1 -0
  95. package/dist/server/modules/sessions/session-permission-request-service.js +167 -0
  96. package/dist/server/modules/sessions/session-permission-request-service.js.map +1 -1
  97. package/dist/server/modules/skills/builtin-skill-service.js +1 -1
  98. package/dist/server/modules/skills/builtin-skill-service.js.map +1 -1
  99. package/dist/server/modules/skills/builtin-skills/codingns-assistant/SKILL.md +19 -12
  100. package/dist/server/modules/skills/builtin-skills/codingns-assistant/references/cli-workflow.md +9 -3
  101. package/dist/server/modules/tasks/task-helper-client.d.ts +5 -2
  102. package/dist/server/modules/tasks/task-helper-client.js +118 -38
  103. package/dist/server/modules/tasks/task-helper-client.js.map +1 -1
  104. package/dist/server/modules/tasks/task-helper-process.js +94 -3
  105. package/dist/server/modules/tasks/task-helper-process.js.map +1 -1
  106. package/dist/server/modules/tasks/task-types.d.ts +3 -0
  107. package/dist/server/modules/tasks/task-types.js +4 -1
  108. package/dist/server/modules/tasks/task-types.js.map +1 -1
  109. package/dist/server/modules/terminal/command-template-service.d.ts +9 -0
  110. package/dist/server/modules/terminal/command-template-service.js +87 -5
  111. package/dist/server/modules/terminal/command-template-service.js.map +1 -1
  112. package/dist/server/modules/terminal/terminal-controller.d.ts +3 -0
  113. package/dist/server/modules/terminal/terminal-controller.js +41 -0
  114. package/dist/server/modules/terminal/terminal-controller.js.map +1 -1
  115. package/dist/server/modules/workbench/workbench-service.d.ts +3 -0
  116. package/dist/server/modules/workbench/workbench-service.js +4 -3
  117. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  118. package/dist/server/modules/workbench/workspace-file-watcher.d.ts +14 -6
  119. package/dist/server/modules/workbench/workspace-file-watcher.js +267 -57
  120. package/dist/server/modules/workbench/workspace-file-watcher.js.map +1 -1
  121. package/dist/server/modules/workbench/workspace-panel-snapshot-service.d.ts +2 -0
  122. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js +32 -3
  123. package/dist/server/modules/workbench/workspace-panel-snapshot-service.js.map +1 -1
  124. package/dist/server/modules/worktree/worktree-manager.d.ts +9 -1
  125. package/dist/server/modules/worktree/worktree-manager.js +9 -1
  126. package/dist/server/modules/worktree/worktree-manager.js.map +1 -1
  127. package/dist/server/routes/assistant.js +19 -0
  128. package/dist/server/routes/assistant.js.map +1 -1
  129. package/dist/server/routes/butler.js +5 -0
  130. package/dist/server/routes/butler.js.map +1 -1
  131. package/dist/server/server/create-server.d.ts +8 -0
  132. package/dist/server/server/create-server.js +36 -13
  133. package/dist/server/server/create-server.js.map +1 -1
  134. package/dist/server/storage/repositories/assistant-automation-run-repository.d.ts +12 -0
  135. package/dist/server/storage/repositories/assistant-automation-run-repository.js +139 -0
  136. package/dist/server/storage/repositories/assistant-automation-run-repository.js.map +1 -0
  137. package/dist/server/storage/repositories/assistant-automation-task-repository.d.ts +15 -0
  138. package/dist/server/storage/repositories/assistant-automation-task-repository.js +173 -0
  139. package/dist/server/storage/repositories/assistant-automation-task-repository.js.map +1 -0
  140. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.d.ts +17 -0
  141. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js +164 -0
  142. package/dist/server/storage/repositories/assistant-sandbox-workspace-repository.js.map +1 -0
  143. package/dist/server/storage/repositories/butler-control-session-repository.js +27 -3
  144. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -1
  145. package/dist/server/storage/repositories/butler-control-timer-repository.d.ts +15 -0
  146. package/dist/server/storage/repositories/butler-control-timer-repository.js +157 -0
  147. package/dist/server/storage/repositories/butler-control-timer-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/user-preference-profile-repository.js +6 -3
  149. package/dist/server/storage/repositories/user-preference-profile-repository.js.map +1 -1
  150. package/dist/server/storage/sqlite/client.js +239 -2
  151. package/dist/server/storage/sqlite/client.js.map +1 -1
  152. package/dist/server/storage/sqlite/schema.sql +107 -1
  153. package/dist/server/types/domain.d.ts +89 -2
  154. package/dist/server/ws/workbench-ws-hub.d.ts +14 -8
  155. package/dist/server/ws/workbench-ws-hub.js +299 -163
  156. package/dist/server/ws/workbench-ws-hub.js.map +1 -1
  157. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.d.ts +4 -1
  158. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js +111 -3
  159. package/node_modules/@codingns/session-sync-core/dist/providers/claude-code.js.map +1 -1
  160. package/node_modules/@codingns/session-sync-core/dist/providers/codex.d.ts +6 -1
  161. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +306 -31
  162. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  163. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +5 -1
  164. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +187 -26
  165. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -1
  166. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +4 -1
  167. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +98 -1
  168. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -1
  169. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.d.ts +2 -0
  170. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +71 -8
  171. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  172. package/node_modules/@codingns/session-sync-core/dist/providers/utils.d.ts +1 -0
  173. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +4 -1
  174. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  175. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js +44 -0
  176. package/node_modules/@codingns/session-sync-core/dist/runtime/claude-runtime.js.map +1 -1
  177. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +9 -3
  178. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  179. package/node_modules/@codingns/session-sync-core/dist/runtime/types.d.ts +1 -0
  180. package/node_modules/@codingns/session-sync-core/dist/services.js +17 -8
  181. package/node_modules/@codingns/session-sync-core/dist/services.js.map +1 -1
  182. package/node_modules/@codingns/session-sync-core/dist/types.d.ts +4 -0
  183. package/package.json +1 -1
  184. package/dist/public/assets/index-BlOinYqR.js +0 -122
  185. package/dist/public/assets/index-Dg_7g6lA.css +0 -1
@@ -23,6 +23,12 @@ const FOLLOW_UP_AUTO_APPROVE_ACTION_PREFERENCE = [
23
23
  "once",
24
24
  "allow"
25
25
  ];
26
+ class FollowUpTaskCancelledError extends Error {
27
+ constructor() {
28
+ super("FOLLOW_UP_TASK_CANCELLED");
29
+ this.name = "FollowUpTaskCancelledError";
30
+ }
31
+ }
26
32
  export class ButlerFollowUpService {
27
33
  butlerProfileService;
28
34
  butlerProjectService;
@@ -38,6 +44,7 @@ export class ButlerFollowUpService {
38
44
  sourceCodexHomeDir;
39
45
  sessionMessageOriginRepository;
40
46
  permissionRequestSweepAtByTaskId = new Map();
47
+ activeExecutionStateByTaskId = new Map();
41
48
  constructor(butlerProfileService, butlerProjectService, butlerSessionService, butlerFollowUpTaskRepository, sessionHistoryService, sessionIndexRepository, sessionLiveRuntimeService, workspaceService, providerAdapterRegistry, instructionAdapter, followUpCodexHomeDir = null, sourceCodexHomeDir = null, sessionMessageOriginRepository = null) {
42
49
  this.butlerProfileService = butlerProfileService;
43
50
  this.butlerProjectService = butlerProjectService;
@@ -129,7 +136,7 @@ export class ButlerFollowUpService {
129
136
  const processed = await this.processTask(task.id);
130
137
  return mapTaskView(processed, project.workspaceId, project.name, session.title ?? null);
131
138
  }
132
- cancelTask(taskId, userId) {
139
+ async cancelTask(taskId, userId) {
133
140
  this.butlerProfileService.ensureInitialized();
134
141
  const task = this.butlerFollowUpTaskRepository.findById(taskId);
135
142
  if (!task) {
@@ -153,6 +160,7 @@ export class ButlerFollowUpService {
153
160
  detail: "当前跟进任务已经结束,不能再次停止"
154
161
  });
155
162
  }
163
+ const execution = this.markTaskExecutionCancelled(task.id);
156
164
  const timestamp = nowIso();
157
165
  const updated = this.persistWithRound({
158
166
  ...task,
@@ -173,6 +181,7 @@ export class ButlerFollowUpService {
173
181
  autoContinueCount: task.autoContinueCount,
174
182
  createdAt: timestamp
175
183
  });
184
+ await this.stopActiveTaskAutomation(execution);
176
185
  const project = this.butlerProjectService.getById(updated.projectId);
177
186
  const index = this.sessionIndexRepository.findIndexRecordBySessionId(updated.sessionId);
178
187
  return mapTaskView(updated, project.workspaceId, project.name, index?.title ?? null);
@@ -225,117 +234,101 @@ export class ButlerFollowUpService {
225
234
  if (task.status !== "active") {
226
235
  return task;
227
236
  }
228
- const profile = this.butlerProfileService.ensureInitialized();
229
- const project = this.butlerProjectService.getById(task.projectId);
230
- const inspection = await this.inspectTask(task);
231
- const runningState = normalizeRunningState(inspection.runningState);
232
- const baseUpdate = {
233
- ...task,
234
- lastCheckedAt: referenceAt,
235
- lastObservedRunningState: runningState,
236
- lastObservedMessageAt: inspection.messageAt,
237
- lastObservedMessageCount: inspection.messageCount,
238
- updatedAt: referenceAt
239
- };
240
- if (runningState === "starting" || runningState === "running") {
241
- return this.persist({
242
- ...baseUpdate,
243
- status: "active",
244
- waitingReason: null,
245
- nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
246
- lastAutomationSummary: hasReachedAutoContinueLimit(task)
247
- ? `会话仍在运行,但已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),本轮结束后将停止自动续接。`
248
- : "会话仍在运行,助手继续观察当前进度。"
249
- });
250
- }
251
- if (hasReachedAutoContinueLimit(task)) {
252
- const waitingReason = `已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),如需继续,请手动重新发起跟进。`;
253
- const summary = `自动跟进已按预设上限停止。结束条件:${task.completionCriteria}`;
254
- return this.persistWithRound({
255
- ...baseUpdate,
256
- status: "waiting_user",
257
- waitingReason,
258
- nextCheckAt: null,
259
- completedAt: null,
260
- lastAutomationAt: referenceAt,
261
- lastAutomationSummary: summary
262
- }, {
263
- kind: "limit_reached",
264
- status: "waiting_user",
265
- summary,
266
- waitingReason,
267
- continuePrompt: null,
268
- observedRunningState: runningState,
269
- autoContinueCount: task.autoContinueCount,
270
- createdAt: referenceAt
271
- });
272
- }
237
+ const execution = this.beginTaskExecution(task.id);
273
238
  try {
274
- const evaluation = await this.evaluateTask(profile, project, task, inspection, runningState);
275
- switch (evaluation.decision) {
276
- case "completed":
277
- return this.persistWithRound({
278
- ...baseUpdate,
279
- status: "completed",
280
- waitingReason: null,
281
- nextCheckAt: null,
282
- completedAt: referenceAt,
283
- lastAutomationAt: referenceAt,
284
- lastAutomationSummary: evaluation.summary
285
- }, {
286
- kind: "completed",
287
- status: "completed",
288
- summary: evaluation.summary,
289
- waitingReason: null,
290
- continuePrompt: null,
291
- observedRunningState: runningState,
292
- autoContinueCount: task.autoContinueCount,
293
- createdAt: referenceAt
294
- });
295
- case "waiting_user":
296
- return this.persistWithRound({
297
- ...baseUpdate,
298
- status: "waiting_user",
299
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
300
- nextCheckAt: null,
301
- completedAt: null,
302
- lastAutomationAt: referenceAt,
303
- lastAutomationSummary: evaluation.summary
304
- }, {
305
- kind: "waiting_user",
306
- status: "waiting_user",
307
- summary: evaluation.summary,
308
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
309
- continuePrompt: null,
310
- observedRunningState: runningState,
311
- autoContinueCount: task.autoContinueCount,
312
- createdAt: referenceAt
313
- });
314
- case "failed":
315
- return this.persistWithRound({
316
- ...baseUpdate,
317
- status: "failed",
318
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
319
- nextCheckAt: null,
320
- completedAt: null,
321
- lastAutomationAt: referenceAt,
322
- lastAutomationSummary: evaluation.summary
323
- }, {
324
- kind: "failed",
325
- status: "failed",
326
- summary: evaluation.summary,
327
- waitingReason: evaluation.waitingReason ?? evaluation.summary,
328
- continuePrompt: null,
329
- observedRunningState: runningState,
330
- autoContinueCount: task.autoContinueCount,
331
- createdAt: referenceAt
332
- });
333
- case "continue":
334
- if (!evaluation.continuePrompt) {
335
- return this.persistWithRound({
239
+ const profile = this.butlerProfileService.ensureInitialized();
240
+ const project = this.butlerProjectService.getById(task.projectId);
241
+ const inspection = await this.inspectTask(task);
242
+ this.ensureTaskExecutionActive(task.id, execution);
243
+ const runningState = normalizeRunningState(inspection.runningState);
244
+ const baseUpdate = {
245
+ ...task,
246
+ lastCheckedAt: referenceAt,
247
+ lastObservedRunningState: runningState,
248
+ lastObservedMessageAt: inspection.messageAt,
249
+ lastObservedMessageCount: inspection.messageCount,
250
+ updatedAt: referenceAt
251
+ };
252
+ if (runningState === "starting" || runningState === "running") {
253
+ return this.persistIfExecutionActive(task.id, execution, {
254
+ ...baseUpdate,
255
+ status: "active",
256
+ waitingReason: null,
257
+ nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
258
+ lastAutomationSummary: hasReachedAutoContinueLimit(task)
259
+ ? `会话仍在运行,但已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),本轮结束后将停止自动续接。`
260
+ : "会话仍在运行,助手继续观察当前进度。"
261
+ });
262
+ }
263
+ if (hasReachedAutoContinueLimit(task)) {
264
+ const waitingReason = `已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),如需继续,请手动重新发起跟进。`;
265
+ const summary = `自动跟进已按预设上限停止。结束条件:${task.completionCriteria}`;
266
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
267
+ ...baseUpdate,
268
+ status: "waiting_user",
269
+ waitingReason,
270
+ nextCheckAt: null,
271
+ completedAt: null,
272
+ lastAutomationAt: referenceAt,
273
+ lastAutomationSummary: summary
274
+ }, {
275
+ kind: "limit_reached",
276
+ status: "waiting_user",
277
+ summary,
278
+ waitingReason,
279
+ continuePrompt: null,
280
+ observedRunningState: runningState,
281
+ autoContinueCount: task.autoContinueCount,
282
+ createdAt: referenceAt
283
+ });
284
+ }
285
+ try {
286
+ const evaluation = await this.evaluateTask(profile, project, task, inspection, runningState, execution);
287
+ this.ensureTaskExecutionActive(task.id, execution);
288
+ switch (evaluation.decision) {
289
+ case "completed":
290
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
291
+ ...baseUpdate,
292
+ status: "completed",
293
+ waitingReason: null,
294
+ nextCheckAt: null,
295
+ completedAt: referenceAt,
296
+ lastAutomationAt: referenceAt,
297
+ lastAutomationSummary: evaluation.summary
298
+ }, {
299
+ kind: "completed",
300
+ status: "completed",
301
+ summary: evaluation.summary,
302
+ waitingReason: null,
303
+ continuePrompt: null,
304
+ observedRunningState: runningState,
305
+ autoContinueCount: task.autoContinueCount,
306
+ createdAt: referenceAt
307
+ });
308
+ case "waiting_user":
309
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
310
+ ...baseUpdate,
311
+ status: "waiting_user",
312
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
313
+ nextCheckAt: null,
314
+ completedAt: null,
315
+ lastAutomationAt: referenceAt,
316
+ lastAutomationSummary: evaluation.summary
317
+ }, {
318
+ kind: "waiting_user",
319
+ status: "waiting_user",
320
+ summary: evaluation.summary,
321
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
322
+ continuePrompt: null,
323
+ observedRunningState: runningState,
324
+ autoContinueCount: task.autoContinueCount,
325
+ createdAt: referenceAt
326
+ });
327
+ case "failed":
328
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
336
329
  ...baseUpdate,
337
330
  status: "failed",
338
- waitingReason: "后台评估助手没有返回可继续推进的指令。",
331
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
339
332
  nextCheckAt: null,
340
333
  completedAt: null,
341
334
  lastAutomationAt: referenceAt,
@@ -344,90 +337,121 @@ export class ButlerFollowUpService {
344
337
  kind: "failed",
345
338
  status: "failed",
346
339
  summary: evaluation.summary,
347
- waitingReason: "后台评估助手没有返回可继续推进的指令。",
340
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
341
+ continuePrompt: null,
342
+ observedRunningState: runningState,
343
+ autoContinueCount: task.autoContinueCount,
344
+ createdAt: referenceAt
345
+ });
346
+ case "continue":
347
+ if (!evaluation.continuePrompt) {
348
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
349
+ ...baseUpdate,
350
+ status: "failed",
351
+ waitingReason: "后台评估助手没有返回可继续推进的指令。",
352
+ nextCheckAt: null,
353
+ completedAt: null,
354
+ lastAutomationAt: referenceAt,
355
+ lastAutomationSummary: evaluation.summary
356
+ }, {
357
+ kind: "failed",
358
+ status: "failed",
359
+ summary: evaluation.summary,
360
+ waitingReason: "后台评估助手没有返回可继续推进的指令。",
361
+ continuePrompt: null,
362
+ observedRunningState: runningState,
363
+ autoContinueCount: task.autoContinueCount,
364
+ createdAt: referenceAt
365
+ });
366
+ }
367
+ this.ensureTaskExecutionActive(task.id, execution);
368
+ const sendResult = await this.sendContinuePrompt(task, evaluation.continuePrompt, referenceAt);
369
+ this.ensureTaskExecutionActive(task.id, execution);
370
+ this.butlerSessionService.captureSessionSnapshot(task.projectId, task.butlerSessionId, task.createdByUserId, { sourceKind: "manual" });
371
+ const nextAutoContinueCount = task.autoContinueCount + 1;
372
+ const nextSummary = sendResult.delivery === "queued"
373
+ ? buildQueuedFollowUpSummary(evaluation.summary, sendResult.queueItem)
374
+ : evaluation.summary;
375
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
376
+ ...baseUpdate,
377
+ status: "active",
378
+ waitingReason: null,
379
+ nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
380
+ lastAutomationAt: referenceAt,
381
+ autoContinueCount: nextAutoContinueCount,
382
+ lastAutomationSummary: nextSummary
383
+ }, {
384
+ kind: sendResult.delivery === "queued" ? "queued" : "continue",
385
+ status: "active",
386
+ summary: nextSummary,
387
+ waitingReason: null,
388
+ continuePrompt: evaluation.continuePrompt,
389
+ observedRunningState: runningState,
390
+ autoContinueCount: nextAutoContinueCount,
391
+ createdAt: referenceAt
392
+ });
393
+ default:
394
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
395
+ ...baseUpdate,
396
+ status: "failed",
397
+ waitingReason: "后台评估助手返回了不支持的决策。",
398
+ nextCheckAt: null,
399
+ completedAt: null,
400
+ lastAutomationAt: referenceAt,
401
+ lastAutomationSummary: "后台评估助手返回了不支持的决策。"
402
+ }, {
403
+ kind: "failed",
404
+ status: "failed",
405
+ summary: "后台评估助手返回了不支持的决策。",
406
+ waitingReason: "后台评估助手返回了不支持的决策。",
348
407
  continuePrompt: null,
349
408
  observedRunningState: runningState,
350
409
  autoContinueCount: task.autoContinueCount,
351
410
  createdAt: referenceAt
352
411
  });
353
- }
354
- const sendResult = await this.sendContinuePrompt(task, evaluation.continuePrompt, referenceAt);
355
- this.butlerSessionService.captureSessionSnapshot(task.projectId, task.butlerSessionId, task.createdByUserId, { sourceKind: "manual" });
356
- const nextAutoContinueCount = task.autoContinueCount + 1;
357
- const nextSummary = sendResult.delivery === "queued"
358
- ? buildQueuedFollowUpSummary(evaluation.summary, sendResult.queueItem)
359
- : evaluation.summary;
360
- return this.persistWithRound({
412
+ }
413
+ }
414
+ catch (error) {
415
+ if (error instanceof FollowUpTaskCancelledError) {
416
+ return this.butlerFollowUpTaskRepository.findById(task.id) ?? task;
417
+ }
418
+ if (isDeferredFollowUpSendError(error)) {
419
+ return this.persistIfExecutionActive(task.id, execution, {
361
420
  ...baseUpdate,
362
421
  status: "active",
363
422
  waitingReason: null,
364
423
  nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
365
- lastAutomationAt: referenceAt,
366
- autoContinueCount: nextAutoContinueCount,
367
- lastAutomationSummary: nextSummary
368
- }, {
369
- kind: sendResult.delivery === "queued" ? "queued" : "continue",
370
- status: "active",
371
- summary: nextSummary,
372
- waitingReason: null,
373
- continuePrompt: evaluation.continuePrompt,
374
- observedRunningState: runningState,
375
- autoContinueCount: nextAutoContinueCount,
376
- createdAt: referenceAt
377
- });
378
- default:
379
- return this.persistWithRound({
380
- ...baseUpdate,
381
- status: "failed",
382
- waitingReason: "后台评估助手返回了不支持的决策。",
383
- nextCheckAt: null,
384
424
  completedAt: null,
385
425
  lastAutomationAt: referenceAt,
386
- lastAutomationSummary: "后台评估助手返回了不支持的决策。"
387
- }, {
388
- kind: "failed",
389
- status: "failed",
390
- summary: "后台评估助手返回了不支持的决策。",
391
- waitingReason: "后台评估助手返回了不支持的决策。",
392
- continuePrompt: null,
393
- observedRunningState: runningState,
394
- autoContinueCount: task.autoContinueCount,
395
- createdAt: referenceAt
426
+ lastAutomationSummary: "当前会话又进入运行态,本轮不插话,等待下一次检查。"
396
427
  });
397
- }
398
- }
399
- catch (error) {
400
- if (isDeferredFollowUpSendError(error)) {
401
- return this.persist({
428
+ }
429
+ const detail = error instanceof Error ? error.message : String(error);
430
+ const summary = `后台评估助手执行失败:${detail}`;
431
+ return this.persistWithRoundIfExecutionActive(task.id, execution, {
402
432
  ...baseUpdate,
403
- status: "active",
404
- waitingReason: null,
405
- nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
433
+ status: "failed",
434
+ waitingReason: detail,
435
+ nextCheckAt: null,
406
436
  completedAt: null,
407
437
  lastAutomationAt: referenceAt,
408
- lastAutomationSummary: "当前会话又进入运行态,本轮不插话,等待下一次检查。"
438
+ lastAutomationSummary: summary
439
+ }, {
440
+ kind: "failed",
441
+ status: "failed",
442
+ summary,
443
+ waitingReason: detail,
444
+ continuePrompt: null,
445
+ observedRunningState: runningState,
446
+ autoContinueCount: task.autoContinueCount,
447
+ createdAt: referenceAt
409
448
  });
410
449
  }
411
- const detail = error instanceof Error ? error.message : String(error);
412
- const summary = `后台评估助手执行失败:${detail}`;
413
- return this.persistWithRound({
414
- ...baseUpdate,
415
- status: "failed",
416
- waitingReason: detail,
417
- nextCheckAt: null,
418
- completedAt: null,
419
- lastAutomationAt: referenceAt,
420
- lastAutomationSummary: summary
421
- }, {
422
- kind: "failed",
423
- status: "failed",
424
- summary,
425
- waitingReason: detail,
426
- continuePrompt: null,
427
- observedRunningState: runningState,
428
- autoContinueCount: task.autoContinueCount,
429
- createdAt: referenceAt
430
- });
450
+ }
451
+ finally {
452
+ if (this.activeExecutionStateByTaskId.get(task.id) === execution) {
453
+ this.activeExecutionStateByTaskId.delete(task.id);
454
+ }
431
455
  }
432
456
  }
433
457
  persist(task) {
@@ -447,6 +471,18 @@ export class ButlerFollowUpService {
447
471
  rounds: [...normalizedRounds, createFollowUpRound(normalizedRounds, round)]
448
472
  });
449
473
  }
474
+ persistIfExecutionActive(taskId, execution, task) {
475
+ if (!this.isTaskExecutionActive(taskId, execution)) {
476
+ return this.butlerFollowUpTaskRepository.findById(taskId) ?? task;
477
+ }
478
+ return this.persist(task);
479
+ }
480
+ persistWithRoundIfExecutionActive(taskId, execution, task, round) {
481
+ if (!this.isTaskExecutionActive(taskId, execution)) {
482
+ return this.butlerFollowUpTaskRepository.findById(taskId) ?? task;
483
+ }
484
+ return this.persistWithRound(task, round);
485
+ }
450
486
  async autoApprovePendingPermissionRequestsIfDue(task, referenceAt) {
451
487
  const parsedReferenceAt = Date.parse(referenceAt);
452
488
  const referenceAtMs = Number.isFinite(parsedReferenceAt) ? parsedReferenceAt : Date.now();
@@ -568,7 +604,7 @@ export class ButlerFollowUpService {
568
604
  transcriptLines: sortedMessages.map((message) => renderHistoryLine(message.sequence, message.role, message.kind ?? "text", message.timestamp, message.content))
569
605
  };
570
606
  }
571
- async evaluateTask(profile, project, task, inspection, runningState) {
607
+ async evaluateTask(profile, project, task, inspection, runningState, execution) {
572
608
  const evaluatorWorkspacePath = path.join(profile.workspacePath, FOLLOW_UP_EVALUATOR_DIRNAME);
573
609
  ensureButlerWorkspaceIsolation(evaluatorWorkspacePath);
574
610
  this.writeEvaluationInstructionFiles(evaluatorWorkspacePath, profile.providerId);
@@ -599,11 +635,64 @@ export class ButlerFollowUpService {
599
635
  prompt: instruction.prompt,
600
636
  model: resolveFollowUpModel(profile.providerId, this.sourceCodexHomeDir),
601
637
  reasoningLevel: "low",
602
- permissionMode: "default"
638
+ permissionMode: "default",
639
+ instructionFilePath: resolveFollowUpInstructionFilePath(profile.providerId, evaluatorWorkspacePath)
603
640
  });
604
- await adapter.waitForSessionTerminal(launch.sessionId);
605
- const result = await adapter.readPatrolResult(launch.sessionId);
606
- return parseEvaluationResult(result);
641
+ execution.evaluatorSessionId = launch.sessionId;
642
+ try {
643
+ await adapter.waitForSessionTerminal(launch.sessionId);
644
+ this.ensureTaskExecutionActive(task.id, execution);
645
+ const result = await adapter.readPatrolResult(launch.sessionId);
646
+ return parseEvaluationResult(result);
647
+ }
648
+ finally {
649
+ if (execution.evaluatorSessionId === launch.sessionId) {
650
+ execution.evaluatorSessionId = null;
651
+ }
652
+ }
653
+ }
654
+ beginTaskExecution(taskId) {
655
+ const execution = {
656
+ cancelled: false,
657
+ evaluatorSessionId: null
658
+ };
659
+ this.activeExecutionStateByTaskId.set(taskId, execution);
660
+ return execution;
661
+ }
662
+ markTaskExecutionCancelled(taskId) {
663
+ const execution = this.activeExecutionStateByTaskId.get(taskId) ?? null;
664
+ if (execution) {
665
+ execution.cancelled = true;
666
+ }
667
+ return execution;
668
+ }
669
+ ensureTaskExecutionActive(taskId, execution) {
670
+ if (!this.isTaskExecutionActive(taskId, execution)) {
671
+ throw new FollowUpTaskCancelledError();
672
+ }
673
+ }
674
+ isTaskExecutionActive(taskId, execution) {
675
+ const current = this.activeExecutionStateByTaskId.get(taskId);
676
+ return Boolean(current && current === execution && !execution.cancelled);
677
+ }
678
+ async stopActiveTaskAutomation(execution) {
679
+ if (!execution?.evaluatorSessionId) {
680
+ return;
681
+ }
682
+ const profile = this.butlerProfileService.ensureInitialized();
683
+ const adapter = this.providerAdapterRegistry.get(profile.providerId);
684
+ try {
685
+ await adapter.interruptPatrolSession(execution.evaluatorSessionId);
686
+ }
687
+ catch (error) {
688
+ console.warn("[butler-follow-up] interrupt evaluator session failed", {
689
+ sessionId: execution.evaluatorSessionId,
690
+ error: error instanceof Error ? error.message : String(error)
691
+ });
692
+ }
693
+ finally {
694
+ execution.evaluatorSessionId = null;
695
+ }
607
696
  }
608
697
  writeEvaluationInstructionFiles(workspacePath, providerId) {
609
698
  const content = [
@@ -829,6 +918,9 @@ function resolveFollowUpModel(providerId, sourceCodexHomeDir) {
829
918
  }
830
919
  return resolveButlerCodexBackgroundModel("gpt-5.1-codex-mini", sourceCodexHomeDir);
831
920
  }
921
+ function resolveFollowUpInstructionFilePath(providerId, workspacePath) {
922
+ return path.join(workspacePath, providerId === "claude-code" ? "CLAUDE.md" : "AGENTS.md");
923
+ }
832
924
  function parseEvaluationResult(result) {
833
925
  const rawJson = result.structured.rawJson ?? extractJsonFromText(result.latestAssistantMessage);
834
926
  if (!rawJson) {