@jingyi0605/codingns 0.1.4 → 0.1.5

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 (209) hide show
  1. package/dist/public/assets/{TerminalPage-4ulgBhv9.js → TerminalPage-4p6EBqrR.js} +1 -1
  2. package/dist/public/assets/gemini-D4G1NbrE.png +0 -0
  3. package/dist/public/assets/index-CxeghocY.css +1 -0
  4. package/dist/public/assets/index-DXusStl0.js +108 -0
  5. package/dist/public/assets/kimi-BWNNSh7e.png +0 -0
  6. package/dist/public/index.html +2 -2
  7. package/dist/server/config/env.d.ts +6 -0
  8. package/dist/server/config/env.js +145 -0
  9. package/dist/server/config/env.js.map +1 -1
  10. package/dist/server/config/opencode-system-probe-helper-process.d.ts +24 -0
  11. package/dist/server/config/opencode-system-probe-helper-process.js +70 -5
  12. package/dist/server/config/opencode-system-probe-helper-process.js.map +1 -1
  13. package/dist/server/modules/butler/butler-action-context-service.d.ts +30 -0
  14. package/dist/server/modules/butler/butler-action-context-service.js +108 -0
  15. package/dist/server/modules/butler/butler-action-context-service.js.map +1 -0
  16. package/dist/server/modules/butler/butler-auth-service.d.ts +17 -0
  17. package/dist/server/modules/butler/butler-auth-service.js +91 -0
  18. package/dist/server/modules/butler/butler-auth-service.js.map +1 -0
  19. package/dist/server/modules/butler/butler-control-action-service.d.ts +65 -0
  20. package/dist/server/modules/butler/butler-control-action-service.js +296 -0
  21. package/dist/server/modules/butler/butler-control-action-service.js.map +1 -0
  22. package/dist/server/modules/butler/butler-control-session-service.d.ts +55 -0
  23. package/dist/server/modules/butler/butler-control-session-service.js +367 -0
  24. package/dist/server/modules/butler/butler-control-session-service.js.map +1 -0
  25. package/dist/server/modules/butler/butler-controller.d.ts +367 -0
  26. package/dist/server/modules/butler/butler-controller.js +475 -0
  27. package/dist/server/modules/butler/butler-controller.js.map +1 -0
  28. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.d.ts +34 -0
  29. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js +77 -0
  30. package/dist/server/modules/butler/butler-follow-up-evaluation-instruction-adapter.js.map +1 -0
  31. package/dist/server/modules/butler/butler-follow-up-scheduler.d.ts +23 -0
  32. package/dist/server/modules/butler/butler-follow-up-scheduler.js +57 -0
  33. package/dist/server/modules/butler/butler-follow-up-scheduler.js.map +1 -0
  34. package/dist/server/modules/butler/butler-follow-up-service.d.ts +86 -0
  35. package/dist/server/modules/butler/butler-follow-up-service.js +948 -0
  36. package/dist/server/modules/butler/butler-follow-up-service.js.map +1 -0
  37. package/dist/server/modules/butler/butler-inbox-service.d.ts +35 -0
  38. package/dist/server/modules/butler/butler-inbox-service.js +136 -0
  39. package/dist/server/modules/butler/butler-inbox-service.js.map +1 -0
  40. package/dist/server/modules/butler/butler-notification-service.d.ts +12 -0
  41. package/dist/server/modules/butler/butler-notification-service.js +45 -0
  42. package/dist/server/modules/butler/butler-notification-service.js.map +1 -0
  43. package/dist/server/modules/butler/butler-profile-service.d.ts +26 -0
  44. package/dist/server/modules/butler/butler-profile-service.js +529 -0
  45. package/dist/server/modules/butler/butler-profile-service.js.map +1 -0
  46. package/dist/server/modules/butler/butler-project-service.d.ts +48 -0
  47. package/dist/server/modules/butler/butler-project-service.js +253 -0
  48. package/dist/server/modules/butler/butler-project-service.js.map +1 -0
  49. package/dist/server/modules/butler/butler-session-service.d.ts +79 -0
  50. package/dist/server/modules/butler/butler-session-service.js +503 -0
  51. package/dist/server/modules/butler/butler-session-service.js.map +1 -0
  52. package/dist/server/modules/butler/butler-session-summary-service.d.ts +55 -0
  53. package/dist/server/modules/butler/butler-session-summary-service.js +382 -0
  54. package/dist/server/modules/butler/butler-session-summary-service.js.map +1 -0
  55. package/dist/server/modules/butler/context-aggregator.d.ts +187 -0
  56. package/dist/server/modules/butler/context-aggregator.js +807 -0
  57. package/dist/server/modules/butler/context-aggregator.js.map +1 -0
  58. package/dist/server/modules/butler/instruction-adapter.d.ts +28 -0
  59. package/dist/server/modules/butler/instruction-adapter.js +101 -0
  60. package/dist/server/modules/butler/instruction-adapter.js.map +1 -0
  61. package/dist/server/modules/butler/patrol-execution-service.d.ts +47 -0
  62. package/dist/server/modules/butler/patrol-execution-service.js +347 -0
  63. package/dist/server/modules/butler/patrol-execution-service.js.map +1 -0
  64. package/dist/server/modules/butler/patrol-plan-service.d.ts +54 -0
  65. package/dist/server/modules/butler/patrol-plan-service.js +272 -0
  66. package/dist/server/modules/butler/patrol-plan-service.js.map +1 -0
  67. package/dist/server/modules/butler/patrol-run-service.d.ts +60 -0
  68. package/dist/server/modules/butler/patrol-run-service.js +185 -0
  69. package/dist/server/modules/butler/patrol-run-service.js.map +1 -0
  70. package/dist/server/modules/butler/patrol-scheduler.d.ts +36 -0
  71. package/dist/server/modules/butler/patrol-scheduler.js +99 -0
  72. package/dist/server/modules/butler/patrol-scheduler.js.map +1 -0
  73. package/dist/server/modules/butler/project-memory-service.d.ts +30 -0
  74. package/dist/server/modules/butler/project-memory-service.js +103 -0
  75. package/dist/server/modules/butler/project-memory-service.js.map +1 -0
  76. package/dist/server/modules/butler/provider-adapter-registry.d.ts +61 -0
  77. package/dist/server/modules/butler/provider-adapter-registry.js +430 -0
  78. package/dist/server/modules/butler/provider-adapter-registry.js.map +1 -0
  79. package/dist/server/modules/butler/session-summary-instruction-adapter.d.ts +28 -0
  80. package/dist/server/modules/butler/session-summary-instruction-adapter.js +79 -0
  81. package/dist/server/modules/butler/session-summary-instruction-adapter.js.map +1 -0
  82. package/dist/server/modules/butler/session-summary-scheduler.d.ts +23 -0
  83. package/dist/server/modules/butler/session-summary-scheduler.js +57 -0
  84. package/dist/server/modules/butler/session-summary-scheduler.js.map +1 -0
  85. package/dist/server/modules/butler/verification-run-service.d.ts +73 -0
  86. package/dist/server/modules/butler/verification-run-service.js +633 -0
  87. package/dist/server/modules/butler/verification-run-service.js.map +1 -0
  88. package/dist/server/modules/preferences/profile-service.js +8 -2
  89. package/dist/server/modules/preferences/profile-service.js.map +1 -1
  90. package/dist/server/modules/sessions/claude-runtime-helper-process.js +1 -1
  91. package/dist/server/modules/sessions/claude-runtime-helper-process.js.map +1 -1
  92. package/dist/server/modules/sessions/codex-app-server-helper-client.d.ts +5 -1
  93. package/dist/server/modules/sessions/codex-app-server-helper-client.js +10 -2
  94. package/dist/server/modules/sessions/codex-app-server-helper-client.js.map +1 -1
  95. package/dist/server/modules/sessions/session-controller.d.ts +3 -1
  96. package/dist/server/modules/sessions/session-controller.js +11 -2
  97. package/dist/server/modules/sessions/session-controller.js.map +1 -1
  98. package/dist/server/modules/sessions/session-history-service.d.ts +14 -1
  99. package/dist/server/modules/sessions/session-history-service.js +291 -30
  100. package/dist/server/modules/sessions/session-history-service.js.map +1 -1
  101. package/dist/server/modules/sessions/session-live-runtime-service.d.ts +25 -2
  102. package/dist/server/modules/sessions/session-live-runtime-service.js +526 -158
  103. package/dist/server/modules/sessions/session-live-runtime-service.js.map +1 -1
  104. package/dist/server/modules/sessions/session-provider-error-mapper.js +28 -0
  105. package/dist/server/modules/sessions/session-provider-error-mapper.js.map +1 -1
  106. package/dist/server/modules/workbench/workbench-service.d.ts +7 -1
  107. package/dist/server/modules/workbench/workbench-service.js +31 -7
  108. package/dist/server/modules/workbench/workbench-service.js.map +1 -1
  109. package/dist/server/routes/butler.d.ts +3 -0
  110. package/dist/server/routes/butler.js +54 -0
  111. package/dist/server/routes/butler.js.map +1 -0
  112. package/dist/server/server/create-server.d.ts +61 -0
  113. package/dist/server/server/create-server.js +148 -4
  114. package/dist/server/server/create-server.js.map +1 -1
  115. package/dist/server/storage/repositories/butler-control-event-repository.d.ts +8 -0
  116. package/dist/server/storage/repositories/butler-control-event-repository.js +78 -0
  117. package/dist/server/storage/repositories/butler-control-event-repository.js.map +1 -0
  118. package/dist/server/storage/repositories/butler-control-session-repository.d.ts +11 -0
  119. package/dist/server/storage/repositories/butler-control-session-repository.js +86 -0
  120. package/dist/server/storage/repositories/butler-control-session-repository.js.map +1 -0
  121. package/dist/server/storage/repositories/butler-follow-up-task-repository.d.ts +16 -0
  122. package/dist/server/storage/repositories/butler-follow-up-task-repository.js +252 -0
  123. package/dist/server/storage/repositories/butler-follow-up-task-repository.js.map +1 -0
  124. package/dist/server/storage/repositories/butler-inbox-item-repository.d.ts +15 -0
  125. package/dist/server/storage/repositories/butler-inbox-item-repository.js +111 -0
  126. package/dist/server/storage/repositories/butler-inbox-item-repository.js.map +1 -0
  127. package/dist/server/storage/repositories/butler-notification-archive-repository.d.ts +9 -0
  128. package/dist/server/storage/repositories/butler-notification-archive-repository.js +48 -0
  129. package/dist/server/storage/repositories/butler-notification-archive-repository.js.map +1 -0
  130. package/dist/server/storage/repositories/butler-profile-repository.d.ts +9 -0
  131. package/dist/server/storage/repositories/butler-profile-repository.js +86 -0
  132. package/dist/server/storage/repositories/butler-profile-repository.js.map +1 -0
  133. package/dist/server/storage/repositories/butler-project-repository.d.ts +14 -0
  134. package/dist/server/storage/repositories/butler-project-repository.js +140 -0
  135. package/dist/server/storage/repositories/butler-project-repository.js.map +1 -0
  136. package/dist/server/storage/repositories/butler-session-repository.d.ts +11 -0
  137. package/dist/server/storage/repositories/butler-session-repository.js +106 -0
  138. package/dist/server/storage/repositories/butler-session-repository.js.map +1 -0
  139. package/dist/server/storage/repositories/butler-session-summary-state-repository.d.ts +8 -0
  140. package/dist/server/storage/repositories/butler-session-summary-state-repository.js +62 -0
  141. package/dist/server/storage/repositories/butler-session-summary-state-repository.js.map +1 -0
  142. package/dist/server/storage/repositories/patrol-plan-repository.d.ts +27 -0
  143. package/dist/server/storage/repositories/patrol-plan-repository.js +119 -0
  144. package/dist/server/storage/repositories/patrol-plan-repository.js.map +1 -0
  145. package/dist/server/storage/repositories/patrol-run-repository.d.ts +28 -0
  146. package/dist/server/storage/repositories/patrol-run-repository.js +121 -0
  147. package/dist/server/storage/repositories/patrol-run-repository.js.map +1 -0
  148. package/dist/server/storage/repositories/project-memory-repository.d.ts +15 -0
  149. package/dist/server/storage/repositories/project-memory-repository.js +150 -0
  150. package/dist/server/storage/repositories/project-memory-repository.js.map +1 -0
  151. package/dist/server/storage/repositories/session-checkpoint-repository.d.ts +9 -0
  152. package/dist/server/storage/repositories/session-checkpoint-repository.js +72 -0
  153. package/dist/server/storage/repositories/session-checkpoint-repository.js.map +1 -0
  154. package/dist/server/storage/repositories/session-message-origin-repository.d.ts +10 -0
  155. package/dist/server/storage/repositories/session-message-origin-repository.js +93 -0
  156. package/dist/server/storage/repositories/session-message-origin-repository.js.map +1 -0
  157. package/dist/server/storage/repositories/verification-run-repository.d.ts +29 -0
  158. package/dist/server/storage/repositories/verification-run-repository.js +125 -0
  159. package/dist/server/storage/repositories/verification-run-repository.js.map +1 -0
  160. package/dist/server/storage/sqlite/client.js +39 -0
  161. package/dist/server/storage/sqlite/client.js.map +1 -1
  162. package/dist/server/storage/sqlite/schema.sql +324 -0
  163. package/dist/server/types/domain.d.ts +261 -1
  164. package/dist/server/ws/ws-server.d.ts +2 -1
  165. package/dist/server/ws/ws-server.js +2 -1
  166. package/dist/server/ws/ws-server.js.map +1 -1
  167. package/node_modules/@codingns/session-sync-core/dist/index.d.ts +4 -0
  168. package/node_modules/@codingns/session-sync-core/dist/index.js +4 -0
  169. package/node_modules/@codingns/session-sync-core/dist/index.js.map +1 -1
  170. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.d.ts +18 -0
  171. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js +659 -0
  172. package/node_modules/@codingns/session-sync-core/dist/kimi-message-normalizer.js.map +1 -0
  173. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.d.ts +11 -0
  174. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js +72 -0
  175. package/node_modules/@codingns/session-sync-core/dist/kimi-shared.js.map +1 -0
  176. package/node_modules/@codingns/session-sync-core/dist/patch-builder.d.ts +8 -0
  177. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js +89 -0
  178. package/node_modules/@codingns/session-sync-core/dist/patch-builder.js.map +1 -1
  179. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js +4 -1
  180. package/node_modules/@codingns/session-sync-core/dist/providers/codex.js.map +1 -1
  181. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.d.ts +41 -0
  182. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js +1086 -0
  183. package/node_modules/@codingns/session-sync-core/dist/providers/gemini.js.map +1 -0
  184. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.d.ts +29 -0
  185. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js +578 -0
  186. package/node_modules/@codingns/session-sync-core/dist/providers/kimi.js.map +1 -0
  187. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js +2 -1
  188. package/node_modules/@codingns/session-sync-core/dist/providers/opencode.js.map +1 -1
  189. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js +30 -2
  190. package/node_modules/@codingns/session-sync-core/dist/providers/utils.js.map +1 -1
  191. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.d.ts +2 -0
  192. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js +43 -5
  193. package/node_modules/@codingns/session-sync-core/dist/runtime/active-run-registry.js.map +1 -1
  194. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.d.ts +2 -0
  195. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js +320 -69
  196. package/node_modules/@codingns/session-sync-core/dist/runtime/codex-runtime.js.map +1 -1
  197. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.d.ts +21 -0
  198. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js +537 -0
  199. package/node_modules/@codingns/session-sync-core/dist/runtime/gemini-runtime.js.map +1 -0
  200. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.d.ts +38 -0
  201. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js +911 -0
  202. package/node_modules/@codingns/session-sync-core/dist/runtime/kimi-runtime.js.map +1 -0
  203. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.d.ts +6 -0
  204. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js +9 -0
  205. package/node_modules/@codingns/session-sync-core/dist/sqlite/node-sqlite.js.map +1 -0
  206. package/node_modules/@codingns/session-sync-core/package.json +8 -0
  207. package/package.json +1 -1
  208. package/dist/public/assets/index-C5lu52cQ.css +0 -1
  209. package/dist/public/assets/index-WpdUo_Vs.js +0 -108
@@ -0,0 +1,948 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { AppError } from "../../shared/errors/app-error.js";
5
+ import { createId } from "../../shared/utils/id.js";
6
+ import { nowIso } from "../../shared/utils/time.js";
7
+ import { ensureButlerWorkspaceIsolation } from "./butler-profile-service.js";
8
+ const DEFAULT_CHECK_INTERVAL_SECONDS = 300;
9
+ const MIN_CHECK_INTERVAL_SECONDS = 60;
10
+ const MAX_CHECK_INTERVAL_SECONDS = 3600;
11
+ const DEFAULT_MAX_AUTO_CONTINUE_COUNT = 5;
12
+ const MIN_MAX_AUTO_CONTINUE_COUNT = 1;
13
+ const MAX_MAX_AUTO_CONTINUE_COUNT = 20;
14
+ const FOLLOW_UP_EVALUATOR_DIRNAME = ".butler-follow-up-evaluator";
15
+ const RECENT_HISTORY_LIMIT = 40;
16
+ const FOLLOW_UP_PERMISSION_CHECK_INTERVAL_MS = 10_000;
17
+ const FOLLOW_UP_AUTO_APPROVE_ACTION_PREFERENCE = [
18
+ "acceptForSession",
19
+ "allow_session",
20
+ "accept",
21
+ "allow_turn",
22
+ "once",
23
+ "allow"
24
+ ];
25
+ export class ButlerFollowUpService {
26
+ butlerProfileService;
27
+ butlerProjectService;
28
+ butlerSessionService;
29
+ butlerFollowUpTaskRepository;
30
+ sessionHistoryService;
31
+ sessionIndexRepository;
32
+ sessionLiveRuntimeService;
33
+ workspaceService;
34
+ providerAdapterRegistry;
35
+ instructionAdapter;
36
+ followUpCodexHomeDir;
37
+ sourceCodexHomeDir;
38
+ sessionMessageOriginRepository;
39
+ permissionRequestSweepAtByTaskId = new Map();
40
+ constructor(butlerProfileService, butlerProjectService, butlerSessionService, butlerFollowUpTaskRepository, sessionHistoryService, sessionIndexRepository, sessionLiveRuntimeService, workspaceService, providerAdapterRegistry, instructionAdapter, followUpCodexHomeDir = null, sourceCodexHomeDir = null, sessionMessageOriginRepository = null) {
41
+ this.butlerProfileService = butlerProfileService;
42
+ this.butlerProjectService = butlerProjectService;
43
+ this.butlerSessionService = butlerSessionService;
44
+ this.butlerFollowUpTaskRepository = butlerFollowUpTaskRepository;
45
+ this.sessionHistoryService = sessionHistoryService;
46
+ this.sessionIndexRepository = sessionIndexRepository;
47
+ this.sessionLiveRuntimeService = sessionLiveRuntimeService;
48
+ this.workspaceService = workspaceService;
49
+ this.providerAdapterRegistry = providerAdapterRegistry;
50
+ this.instructionAdapter = instructionAdapter;
51
+ this.followUpCodexHomeDir = followUpCodexHomeDir;
52
+ this.sourceCodexHomeDir = sourceCodexHomeDir;
53
+ this.sessionMessageOriginRepository = sessionMessageOriginRepository;
54
+ }
55
+ listTasks(filters = {}) {
56
+ this.butlerProfileService.ensureInitialized();
57
+ return this.butlerFollowUpTaskRepository.list(filters).flatMap((task) => {
58
+ const project = this.butlerProjectService.getById(task.projectId);
59
+ const index = this.sessionIndexRepository.findIndexRecordBySessionId(task.sessionId);
60
+ if (!project) {
61
+ return [];
62
+ }
63
+ return [mapTaskView(task, project.workspaceId, project.name, index?.title ?? null)];
64
+ });
65
+ }
66
+ getTask(taskId) {
67
+ this.butlerProfileService.ensureInitialized();
68
+ const task = this.butlerFollowUpTaskRepository.findById(taskId);
69
+ if (!task) {
70
+ throw new AppError({
71
+ statusCode: 404,
72
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_FOUND",
73
+ detail: "未找到对应的跟进任务"
74
+ });
75
+ }
76
+ const project = this.butlerProjectService.getById(task.projectId);
77
+ const index = this.sessionIndexRepository.findIndexRecordBySessionId(task.sessionId);
78
+ return mapTaskView(task, project.workspaceId, project.name, index?.title ?? null);
79
+ }
80
+ async createTask(input, userId) {
81
+ this.butlerProfileService.ensureInitialized();
82
+ const project = this.butlerProjectService.getById(input.projectId);
83
+ const objective = normalizeObjective(input.objective);
84
+ const completionCriteria = normalizeCompletionCriteria(input.completionCriteria, objective);
85
+ const maxAutoContinueCount = normalizeMaxAutoContinueCount(input.maxAutoContinueCount);
86
+ const checkIntervalSeconds = normalizeCheckInterval(input.checkIntervalSeconds);
87
+ const snapshot = this.butlerSessionService.captureSessionSnapshot(project.id, input.butlerSessionId, userId, { sourceKind: "manual" });
88
+ const existing = this.butlerFollowUpTaskRepository.findActiveByButlerSessionId(input.butlerSessionId);
89
+ if (existing) {
90
+ throw new AppError({
91
+ statusCode: 409,
92
+ errorCode: "BUTLER_FOLLOW_UP_TASK_EXISTS",
93
+ detail: "当前会话已经有一个进行中的跟进任务"
94
+ });
95
+ }
96
+ const session = this.sessionHistoryService.getSession(snapshot.sessionId, userId);
97
+ const timestamp = nowIso();
98
+ const initialSummary = snapshot.runningState === "starting" || snapshot.runningState === "running"
99
+ ? `已开始跟进,先等待当前运行结束,再由后台评估助手决定下一步。默认最多自动推进 ${maxAutoContinueCount} 轮。`
100
+ : `已开始跟进,准备由后台评估助手检查当前进展。默认最多自动推进 ${maxAutoContinueCount} 轮。`;
101
+ const task = this.butlerFollowUpTaskRepository.create({
102
+ id: createId(),
103
+ projectId: project.id,
104
+ butlerSessionId: input.butlerSessionId,
105
+ sessionId: snapshot.sessionId,
106
+ createdByUserId: userId,
107
+ objective,
108
+ completionCriteria,
109
+ maxAutoContinueCount,
110
+ status: "active",
111
+ checkIntervalSeconds,
112
+ lastCheckedAt: null,
113
+ nextCheckAt: snapshot.runningState === "starting" || snapshot.runningState === "running"
114
+ ? shiftSeconds(timestamp, checkIntervalSeconds)
115
+ : timestamp,
116
+ lastObservedRunningState: snapshot.runningState,
117
+ lastObservedMessageAt: session.lastMessageAt,
118
+ lastObservedMessageCount: session.messageCount,
119
+ lastAutomationSummary: initialSummary,
120
+ lastAutomationAt: null,
121
+ autoContinueCount: 0,
122
+ waitingReason: null,
123
+ rounds: [],
124
+ createdAt: timestamp,
125
+ updatedAt: timestamp,
126
+ completedAt: null
127
+ });
128
+ const processed = await this.processTask(task.id);
129
+ return mapTaskView(processed, project.workspaceId, project.name, session.title ?? null);
130
+ }
131
+ cancelTask(taskId, userId) {
132
+ this.butlerProfileService.ensureInitialized();
133
+ const task = this.butlerFollowUpTaskRepository.findById(taskId);
134
+ if (!task) {
135
+ throw new AppError({
136
+ statusCode: 404,
137
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_FOUND",
138
+ detail: "未找到对应的跟进任务"
139
+ });
140
+ }
141
+ if (task.createdByUserId !== userId) {
142
+ throw new AppError({
143
+ statusCode: 403,
144
+ errorCode: "BUTLER_FOLLOW_UP_TASK_FORBIDDEN",
145
+ detail: "你没有权限停止这个跟进任务"
146
+ });
147
+ }
148
+ if (task.status !== "active" && task.status !== "waiting_user") {
149
+ throw new AppError({
150
+ statusCode: 409,
151
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_STOPPABLE",
152
+ detail: "当前跟进任务已经结束,不能再次停止"
153
+ });
154
+ }
155
+ const timestamp = nowIso();
156
+ const updated = this.persistWithRound({
157
+ ...task,
158
+ status: "cancelled",
159
+ nextCheckAt: null,
160
+ waitingReason: null,
161
+ completedAt: timestamp,
162
+ updatedAt: timestamp,
163
+ lastAutomationAt: timestamp,
164
+ lastAutomationSummary: "已手动终止当前会话跟进任务,不再继续自动续接。"
165
+ }, {
166
+ kind: "cancelled",
167
+ status: "cancelled",
168
+ summary: "已手动终止当前会话跟进任务,不再继续自动续接。",
169
+ waitingReason: null,
170
+ continuePrompt: null,
171
+ observedRunningState: task.lastObservedRunningState,
172
+ autoContinueCount: task.autoContinueCount,
173
+ createdAt: timestamp
174
+ });
175
+ const project = this.butlerProjectService.getById(updated.projectId);
176
+ const index = this.sessionIndexRepository.findIndexRecordBySessionId(updated.sessionId);
177
+ return mapTaskView(updated, project.workspaceId, project.name, index?.title ?? null);
178
+ }
179
+ async runDueTasks(referenceAt = nowIso()) {
180
+ const tasks = this.butlerFollowUpTaskRepository.list({
181
+ statuses: ["active"],
182
+ limit: 100
183
+ });
184
+ for (const task of tasks) {
185
+ await this.autoApprovePendingPermissionRequestsIfDue(task, referenceAt);
186
+ if (task.nextCheckAt && task.nextCheckAt > referenceAt) {
187
+ continue;
188
+ }
189
+ await this.processTask(task.id, referenceAt);
190
+ }
191
+ }
192
+ async handleSessionTerminal(sessionId, referenceAt = nowIso()) {
193
+ const tasks = this.butlerFollowUpTaskRepository.list({
194
+ statuses: ["active"],
195
+ sessionId,
196
+ limit: 20
197
+ });
198
+ for (const task of tasks) {
199
+ if (this.shouldSkipImmediateTerminalRecheck(task)) {
200
+ continue;
201
+ }
202
+ await this.processTask(task.id, referenceAt);
203
+ }
204
+ }
205
+ async processTask(taskId, referenceAt = nowIso()) {
206
+ const task = this.butlerFollowUpTaskRepository.findById(taskId);
207
+ if (!task) {
208
+ throw new AppError({
209
+ statusCode: 404,
210
+ errorCode: "BUTLER_FOLLOW_UP_TASK_NOT_FOUND",
211
+ detail: "未找到对应的跟进任务"
212
+ });
213
+ }
214
+ if (task.status !== "active") {
215
+ return task;
216
+ }
217
+ const profile = this.butlerProfileService.ensureInitialized();
218
+ const project = this.butlerProjectService.getById(task.projectId);
219
+ const inspection = await this.inspectTask(task);
220
+ const runningState = normalizeRunningState(inspection.runningState);
221
+ const baseUpdate = {
222
+ ...task,
223
+ lastCheckedAt: referenceAt,
224
+ lastObservedRunningState: runningState,
225
+ lastObservedMessageAt: inspection.messageAt,
226
+ lastObservedMessageCount: inspection.messageCount,
227
+ updatedAt: referenceAt
228
+ };
229
+ if (runningState === "starting" || runningState === "running") {
230
+ return this.persist({
231
+ ...baseUpdate,
232
+ status: "active",
233
+ waitingReason: null,
234
+ nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
235
+ lastAutomationSummary: hasReachedAutoContinueLimit(task)
236
+ ? `会话仍在运行,但已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),本轮结束后将停止自动续接。`
237
+ : "会话仍在运行,助手继续观察当前进度。"
238
+ });
239
+ }
240
+ if (hasReachedAutoContinueLimit(task)) {
241
+ const waitingReason = `已达到预设的自动跟进轮数上限(${task.autoContinueCount}/${task.maxAutoContinueCount}),如需继续,请手动重新发起跟进。`;
242
+ const summary = `自动跟进已按预设上限停止。结束条件:${task.completionCriteria}`;
243
+ return this.persistWithRound({
244
+ ...baseUpdate,
245
+ status: "waiting_user",
246
+ waitingReason,
247
+ nextCheckAt: null,
248
+ completedAt: null,
249
+ lastAutomationAt: referenceAt,
250
+ lastAutomationSummary: summary
251
+ }, {
252
+ kind: "limit_reached",
253
+ status: "waiting_user",
254
+ summary,
255
+ waitingReason,
256
+ continuePrompt: null,
257
+ observedRunningState: runningState,
258
+ autoContinueCount: task.autoContinueCount,
259
+ createdAt: referenceAt
260
+ });
261
+ }
262
+ try {
263
+ const evaluation = await this.evaluateTask(profile, project, task, inspection, runningState);
264
+ switch (evaluation.decision) {
265
+ case "completed":
266
+ return this.persistWithRound({
267
+ ...baseUpdate,
268
+ status: "completed",
269
+ waitingReason: null,
270
+ nextCheckAt: null,
271
+ completedAt: referenceAt,
272
+ lastAutomationAt: referenceAt,
273
+ lastAutomationSummary: evaluation.summary
274
+ }, {
275
+ kind: "completed",
276
+ status: "completed",
277
+ summary: evaluation.summary,
278
+ waitingReason: null,
279
+ continuePrompt: null,
280
+ observedRunningState: runningState,
281
+ autoContinueCount: task.autoContinueCount,
282
+ createdAt: referenceAt
283
+ });
284
+ case "waiting_user":
285
+ return this.persistWithRound({
286
+ ...baseUpdate,
287
+ status: "waiting_user",
288
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
289
+ nextCheckAt: null,
290
+ completedAt: null,
291
+ lastAutomationAt: referenceAt,
292
+ lastAutomationSummary: evaluation.summary
293
+ }, {
294
+ kind: "waiting_user",
295
+ status: "waiting_user",
296
+ summary: evaluation.summary,
297
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
298
+ continuePrompt: null,
299
+ observedRunningState: runningState,
300
+ autoContinueCount: task.autoContinueCount,
301
+ createdAt: referenceAt
302
+ });
303
+ case "failed":
304
+ return this.persistWithRound({
305
+ ...baseUpdate,
306
+ status: "failed",
307
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
308
+ nextCheckAt: null,
309
+ completedAt: null,
310
+ lastAutomationAt: referenceAt,
311
+ lastAutomationSummary: evaluation.summary
312
+ }, {
313
+ kind: "failed",
314
+ status: "failed",
315
+ summary: evaluation.summary,
316
+ waitingReason: evaluation.waitingReason ?? evaluation.summary,
317
+ continuePrompt: null,
318
+ observedRunningState: runningState,
319
+ autoContinueCount: task.autoContinueCount,
320
+ createdAt: referenceAt
321
+ });
322
+ case "continue":
323
+ if (!evaluation.continuePrompt) {
324
+ return this.persistWithRound({
325
+ ...baseUpdate,
326
+ status: "failed",
327
+ waitingReason: "后台评估助手没有返回可继续推进的指令。",
328
+ nextCheckAt: null,
329
+ completedAt: null,
330
+ lastAutomationAt: referenceAt,
331
+ lastAutomationSummary: evaluation.summary
332
+ }, {
333
+ kind: "failed",
334
+ status: "failed",
335
+ summary: evaluation.summary,
336
+ waitingReason: "后台评估助手没有返回可继续推进的指令。",
337
+ continuePrompt: null,
338
+ observedRunningState: runningState,
339
+ autoContinueCount: task.autoContinueCount,
340
+ createdAt: referenceAt
341
+ });
342
+ }
343
+ const sendResult = await this.sendContinuePrompt(task, evaluation.continuePrompt, referenceAt);
344
+ this.butlerSessionService.captureSessionSnapshot(task.projectId, task.butlerSessionId, task.createdByUserId, { sourceKind: "manual" });
345
+ const nextAutoContinueCount = task.autoContinueCount + 1;
346
+ const nextSummary = sendResult.delivery === "queued"
347
+ ? buildQueuedFollowUpSummary(evaluation.summary, sendResult.queueItem)
348
+ : evaluation.summary;
349
+ return this.persistWithRound({
350
+ ...baseUpdate,
351
+ status: "active",
352
+ waitingReason: null,
353
+ nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
354
+ lastAutomationAt: referenceAt,
355
+ autoContinueCount: nextAutoContinueCount,
356
+ lastAutomationSummary: nextSummary
357
+ }, {
358
+ kind: sendResult.delivery === "queued" ? "queued" : "continue",
359
+ status: "active",
360
+ summary: nextSummary,
361
+ waitingReason: null,
362
+ continuePrompt: evaluation.continuePrompt,
363
+ observedRunningState: runningState,
364
+ autoContinueCount: nextAutoContinueCount,
365
+ createdAt: referenceAt
366
+ });
367
+ default:
368
+ return this.persistWithRound({
369
+ ...baseUpdate,
370
+ status: "failed",
371
+ waitingReason: "后台评估助手返回了不支持的决策。",
372
+ nextCheckAt: null,
373
+ completedAt: null,
374
+ lastAutomationAt: referenceAt,
375
+ lastAutomationSummary: "后台评估助手返回了不支持的决策。"
376
+ }, {
377
+ kind: "failed",
378
+ status: "failed",
379
+ summary: "后台评估助手返回了不支持的决策。",
380
+ waitingReason: "后台评估助手返回了不支持的决策。",
381
+ continuePrompt: null,
382
+ observedRunningState: runningState,
383
+ autoContinueCount: task.autoContinueCount,
384
+ createdAt: referenceAt
385
+ });
386
+ }
387
+ }
388
+ catch (error) {
389
+ if (isDeferredFollowUpSendError(error)) {
390
+ return this.persist({
391
+ ...baseUpdate,
392
+ status: "active",
393
+ waitingReason: null,
394
+ nextCheckAt: shiftSeconds(referenceAt, task.checkIntervalSeconds),
395
+ completedAt: null,
396
+ lastAutomationAt: referenceAt,
397
+ lastAutomationSummary: "当前会话又进入运行态,本轮不插话,等待下一次检查。"
398
+ });
399
+ }
400
+ const detail = error instanceof Error ? error.message : String(error);
401
+ const summary = `后台评估助手执行失败:${detail}`;
402
+ return this.persistWithRound({
403
+ ...baseUpdate,
404
+ status: "failed",
405
+ waitingReason: detail,
406
+ nextCheckAt: null,
407
+ completedAt: null,
408
+ lastAutomationAt: referenceAt,
409
+ lastAutomationSummary: summary
410
+ }, {
411
+ kind: "failed",
412
+ status: "failed",
413
+ summary,
414
+ waitingReason: detail,
415
+ continuePrompt: null,
416
+ observedRunningState: runningState,
417
+ autoContinueCount: task.autoContinueCount,
418
+ createdAt: referenceAt
419
+ });
420
+ }
421
+ }
422
+ persist(task) {
423
+ const normalizedTask = {
424
+ ...task,
425
+ rounds: normalizeFollowUpRounds(task.rounds)
426
+ };
427
+ if (normalizedTask.status !== "active") {
428
+ this.permissionRequestSweepAtByTaskId.delete(normalizedTask.id);
429
+ }
430
+ return this.butlerFollowUpTaskRepository.update(normalizedTask) ?? normalizedTask;
431
+ }
432
+ persistWithRound(task, round) {
433
+ const normalizedRounds = normalizeFollowUpRounds(task.rounds);
434
+ return this.persist({
435
+ ...task,
436
+ rounds: [...normalizedRounds, createFollowUpRound(normalizedRounds, round)]
437
+ });
438
+ }
439
+ async autoApprovePendingPermissionRequestsIfDue(task, referenceAt) {
440
+ const parsedReferenceAt = Date.parse(referenceAt);
441
+ const referenceAtMs = Number.isFinite(parsedReferenceAt) ? parsedReferenceAt : Date.now();
442
+ const lastSweepAtMs = this.permissionRequestSweepAtByTaskId.get(task.id) ?? 0;
443
+ if (lastSweepAtMs > 0
444
+ && referenceAtMs - lastSweepAtMs < FOLLOW_UP_PERMISSION_CHECK_INTERVAL_MS) {
445
+ return;
446
+ }
447
+ this.permissionRequestSweepAtByTaskId.set(task.id, referenceAtMs);
448
+ let requests;
449
+ try {
450
+ requests = await this.sessionLiveRuntimeService.listPermissionRequests(task.sessionId, task.createdByUserId);
451
+ }
452
+ catch (error) {
453
+ console.warn("[butler-follow-up] list permission requests failed", {
454
+ taskId: task.id,
455
+ sessionId: task.sessionId,
456
+ error: error instanceof Error ? error.message : String(error)
457
+ });
458
+ return;
459
+ }
460
+ for (const request of requests) {
461
+ const reply = buildAutoApprovePermissionReply(request);
462
+ if (!reply) {
463
+ continue;
464
+ }
465
+ try {
466
+ await this.sessionLiveRuntimeService.replyPermissionRequest(task.sessionId, task.createdByUserId, request.id, reply);
467
+ }
468
+ catch (error) {
469
+ if (isIgnorablePermissionReplyError(error)) {
470
+ continue;
471
+ }
472
+ console.warn("[butler-follow-up] auto approve permission request failed", {
473
+ taskId: task.id,
474
+ sessionId: task.sessionId,
475
+ requestId: request.id,
476
+ action: reply.action,
477
+ error: error instanceof Error ? error.message : String(error)
478
+ });
479
+ }
480
+ }
481
+ }
482
+ shouldSkipImmediateTerminalRecheck(task) {
483
+ const session = this.sessionHistoryService.getSession(task.sessionId, task.createdByUserId);
484
+ return (isTerminalFollowUpRunningState(task.lastObservedRunningState)
485
+ && normalizeRunningState(session.runningState) === task.lastObservedRunningState
486
+ && normalizeNullableIso(session.lastMessageAt) === normalizeNullableIso(task.lastObservedMessageAt)
487
+ && session.messageCount === task.lastObservedMessageCount);
488
+ }
489
+ async sendContinuePrompt(task, continuePrompt, referenceAt) {
490
+ const clientRequestId = buildFollowUpClientRequestId(task.id, referenceAt);
491
+ try {
492
+ const result = await this.sessionLiveRuntimeService.sendLiveMessage({
493
+ sessionId: task.sessionId,
494
+ userId: task.createdByUserId,
495
+ content: continuePrompt,
496
+ clientRequestId,
497
+ runtimeOptions: {
498
+ model: null,
499
+ reasoningLevel: null,
500
+ permissionMode: null,
501
+ attachments: []
502
+ }
503
+ });
504
+ this.recordMessageOrigin(task, clientRequestId, continuePrompt, result.acceptedAt, result.message.messageId);
505
+ return {
506
+ delivery: "sent"
507
+ };
508
+ }
509
+ catch (error) {
510
+ if (!isDeferredFollowUpSendError(error)) {
511
+ throw error;
512
+ }
513
+ const queueItem = await this.sessionLiveRuntimeService.enqueueLiveMessage({
514
+ sessionId: task.sessionId,
515
+ userId: task.createdByUserId,
516
+ content: continuePrompt,
517
+ clientRequestId,
518
+ runtimeOptions: {
519
+ model: null,
520
+ reasoningLevel: null,
521
+ permissionMode: null,
522
+ attachments: []
523
+ }
524
+ });
525
+ this.recordMessageOrigin(task, clientRequestId, continuePrompt, queueItem.createdAt, null);
526
+ return {
527
+ delivery: "queued",
528
+ queueItem
529
+ };
530
+ }
531
+ }
532
+ recordMessageOrigin(task, clientRequestId, content, timestamp, messageId) {
533
+ this.sessionMessageOriginRepository?.upsert({
534
+ sessionId: task.sessionId,
535
+ clientRequestId,
536
+ messageId: isSyntheticMessageId(messageId) ? null : messageId ?? null,
537
+ origin: "butler_proxy",
538
+ originRef: task.id,
539
+ content,
540
+ createdAt: timestamp,
541
+ updatedAt: timestamp
542
+ });
543
+ }
544
+ async inspectTask(task) {
545
+ const session = this.sessionHistoryService.getSession(task.sessionId, task.createdByUserId);
546
+ const runtime = await this.sessionLiveRuntimeService.getSessionRuntime(task.sessionId, task.createdByUserId);
547
+ const envelope = await this.sessionHistoryService.readRecentHistoryEnvelope(task.sessionId, RECENT_HISTORY_LIMIT);
548
+ const sortedMessages = (envelope?.messages ?? [])
549
+ .slice()
550
+ .sort((left, right) => left.sequence - right.sequence);
551
+ return {
552
+ runningState: normalizeRunningState(runtime.runningState),
553
+ messageAt: session.lastMessageAt,
554
+ messageCount: session.messageCount,
555
+ sessionTitle: session.title ?? null,
556
+ latestAssistantText: resolveLatestAssistantText(envelope),
557
+ transcriptLines: sortedMessages.map((message) => renderHistoryLine(message.sequence, message.role, message.kind ?? "text", message.timestamp, message.content))
558
+ };
559
+ }
560
+ async evaluateTask(profile, project, task, inspection, runningState) {
561
+ const evaluatorWorkspacePath = path.join(profile.workspacePath, FOLLOW_UP_EVALUATOR_DIRNAME);
562
+ ensureButlerWorkspaceIsolation(evaluatorWorkspacePath);
563
+ this.writeEvaluationInstructionFiles(evaluatorWorkspacePath, profile.providerId);
564
+ this.syncCodexInstructionConfig(profile.providerId, evaluatorWorkspacePath);
565
+ const workspace = this.workspaceService.importWorkspace(evaluatorWorkspacePath, "代码助手");
566
+ const instruction = this.instructionAdapter.buildInstruction({
567
+ providerId: profile.providerId,
568
+ project,
569
+ sessionId: task.sessionId,
570
+ butlerSessionId: task.butlerSessionId,
571
+ sessionTitle: inspection.sessionTitle,
572
+ objective: task.objective,
573
+ completionCriteria: task.completionCriteria,
574
+ runningState,
575
+ messageCount: inspection.messageCount,
576
+ lastMessageAt: inspection.messageAt,
577
+ autoContinueCount: task.autoContinueCount,
578
+ maxAutoContinueCount: task.maxAutoContinueCount,
579
+ lastAutomationSummary: task.lastAutomationSummary,
580
+ latestAssistantText: inspection.latestAssistantText,
581
+ transcriptLines: inspection.transcriptLines
582
+ });
583
+ const adapter = this.providerAdapterRegistry.get(profile.providerId);
584
+ const launch = await adapter.startPatrolSession({
585
+ workspaceId: workspace.id,
586
+ userId: task.createdByUserId,
587
+ providerId: profile.providerId,
588
+ prompt: instruction.prompt,
589
+ model: resolveFollowUpModel(profile.providerId),
590
+ reasoningLevel: "low",
591
+ permissionMode: "default"
592
+ });
593
+ await adapter.waitForSessionTerminal(launch.sessionId);
594
+ const result = await adapter.readPatrolResult(launch.sessionId);
595
+ return parseEvaluationResult(result);
596
+ }
597
+ writeEvaluationInstructionFiles(workspacePath, providerId) {
598
+ const content = [
599
+ "# 代码助手后台跟进评估规则",
600
+ "",
601
+ "你不是普通项目会话,也不是面向用户的聊天助手。",
602
+ "你的身份是后台跟进评估器,只负责判断某个开发会话现在该继续推进、等用户决定、还是已经完成。",
603
+ "如果目标或上下文里提到了 spec,完成标准只能按 spec 明确要求的必做项判断。",
604
+ "“建议下一步”“最佳实践”“可以顺手优化”这类内容默认都不是必做项,不能据此继续扩范围。",
605
+ "如果没有 spec,就先从目标和最近消息里归纳一句当前核心任务,后续只能围绕这个核心任务判断,不准无限扩展。",
606
+ "除非目标本身要求,否则不要把重构、补测试、补体验优化之类建议项升级成必须开发的工作。",
607
+ "禁止照搬最后一句回复做草率判断,必须结合用户目标、当前运行态和最近消息一起判断。",
608
+ "如果能继续推进,就直接给出下一条要发给开发会话的中文指令,不要空谈。",
609
+ "如果确实需要用户决定,要把缺口说清楚,但不要替用户做不存在的决定。",
610
+ "输出语言必须是中文,先给结论,再给结构化 JSON。"
611
+ ].join("\n");
612
+ writeFileIfChanged(path.join(workspacePath, "AGENTS.md"), `${content}\n`);
613
+ if (providerId === "claude-code") {
614
+ writeFileIfChanged(path.join(workspacePath, "CLAUDE.md"), `${content}\n`);
615
+ }
616
+ }
617
+ syncCodexInstructionConfig(providerId, workspacePath) {
618
+ if (providerId !== "codex" || !this.followUpCodexHomeDir?.trim()) {
619
+ return;
620
+ }
621
+ const targetHomeDir = path.resolve(this.followUpCodexHomeDir);
622
+ const sourceHomeDir = resolveSourceCodexHomeDir(this.sourceCodexHomeDir, targetHomeDir);
623
+ const sourceConfigPath = path.join(sourceHomeDir, "config.toml");
624
+ const sourceConfigContent = sourceHomeDir !== targetHomeDir && fs.existsSync(sourceConfigPath) && fs.statSync(sourceConfigPath).isFile()
625
+ ? fs.readFileSync(sourceConfigPath, "utf8")
626
+ : "";
627
+ const instructionFilePath = path.join(workspacePath, "AGENTS.md");
628
+ fs.mkdirSync(targetHomeDir, { recursive: true });
629
+ removeFileIfExists(path.join(targetHomeDir, "AGENTS.md"));
630
+ removeFileIfExists(path.join(targetHomeDir, "AGENTS.override.md"));
631
+ syncOptionalFile(path.join(sourceHomeDir, "auth.json"), path.join(targetHomeDir, "auth.json"));
632
+ writeFileIfChanged(path.join(targetHomeDir, "config.toml"), `${composeCodexConfigContent(sourceConfigContent, instructionFilePath)}\n`);
633
+ }
634
+ }
635
+ function mapTaskView(task, workspaceId, projectName, sessionTitle) {
636
+ const rounds = normalizeFollowUpRounds(task.rounds);
637
+ return {
638
+ id: task.id,
639
+ projectId: task.projectId,
640
+ projectName,
641
+ workspaceId,
642
+ butlerSessionId: task.butlerSessionId,
643
+ sessionId: task.sessionId,
644
+ sessionTitle,
645
+ objective: task.objective,
646
+ completionCriteria: task.completionCriteria,
647
+ maxAutoContinueCount: task.maxAutoContinueCount,
648
+ status: task.status,
649
+ checkIntervalSeconds: task.checkIntervalSeconds,
650
+ lastCheckedAt: task.lastCheckedAt,
651
+ nextCheckAt: task.nextCheckAt,
652
+ lastObservedRunningState: task.lastObservedRunningState,
653
+ lastObservedMessageAt: task.lastObservedMessageAt,
654
+ lastObservedMessageCount: task.lastObservedMessageCount,
655
+ lastAutomationSummary: task.lastAutomationSummary,
656
+ lastAutomationAt: task.lastAutomationAt,
657
+ autoContinueCount: task.autoContinueCount,
658
+ waitingReason: task.waitingReason,
659
+ rounds,
660
+ createdAt: task.createdAt,
661
+ updatedAt: task.updatedAt,
662
+ completedAt: task.completedAt
663
+ };
664
+ }
665
+ function createFollowUpRound(existingRounds, input) {
666
+ return {
667
+ roundNumber: existingRounds.length + 1,
668
+ kind: input.kind,
669
+ status: input.status,
670
+ summary: input.summary,
671
+ waitingReason: input.waitingReason,
672
+ continuePrompt: input.continuePrompt,
673
+ observedRunningState: input.observedRunningState,
674
+ autoContinueCount: input.autoContinueCount,
675
+ createdAt: input.createdAt
676
+ };
677
+ }
678
+ function normalizeFollowUpRounds(rounds) {
679
+ return rounds
680
+ .filter((round) => round.kind !== "started")
681
+ .map((round, index) => ({
682
+ ...round,
683
+ roundNumber: index + 1
684
+ }));
685
+ }
686
+ function buildAutoApprovePermissionReply(request) {
687
+ if (request.status !== "pending" || request.kind === "user_input") {
688
+ return null;
689
+ }
690
+ const availableActions = new Set(request.actions
691
+ .map((action) => action.value.trim())
692
+ .filter((action) => action.length > 0));
693
+ for (const action of FOLLOW_UP_AUTO_APPROVE_ACTION_PREFERENCE) {
694
+ if (availableActions.has(action)) {
695
+ return { action };
696
+ }
697
+ }
698
+ return null;
699
+ }
700
+ function isIgnorablePermissionReplyError(error) {
701
+ if (error instanceof AppError) {
702
+ return (error.errorCode === "PERMISSION_REQUEST_ALREADY_RESOLVED"
703
+ || error.errorCode === "PERMISSION_REQUEST_NOT_FOUND");
704
+ }
705
+ return (error instanceof Error
706
+ && (error.message === "PERMISSION_REQUEST_ALREADY_RESOLVED"
707
+ || error.message === "PERMISSION_REQUEST_NOT_FOUND"));
708
+ }
709
+ function normalizeObjective(value) {
710
+ const normalized = value?.trim();
711
+ if (!normalized) {
712
+ throw new AppError({
713
+ statusCode: 400,
714
+ errorCode: "INVALID_INPUT",
715
+ detail: "请先写清楚希望助手继续推动的目标",
716
+ field: "objective"
717
+ });
718
+ }
719
+ return normalized;
720
+ }
721
+ function normalizeCompletionCriteria(value, objective) {
722
+ const normalized = value?.trim();
723
+ return normalized && normalized.length > 0
724
+ ? normalized
725
+ : `仅当以下目标已经明确完成时,才允许结束本次自动跟进:${objective}`;
726
+ }
727
+ function normalizeMaxAutoContinueCount(value) {
728
+ const fallback = value ?? DEFAULT_MAX_AUTO_CONTINUE_COUNT;
729
+ const rounded = Math.round(fallback);
730
+ return Math.min(MAX_MAX_AUTO_CONTINUE_COUNT, Math.max(MIN_MAX_AUTO_CONTINUE_COUNT, rounded));
731
+ }
732
+ function normalizeCheckInterval(value) {
733
+ const fallback = value ?? DEFAULT_CHECK_INTERVAL_SECONDS;
734
+ const rounded = Math.round(fallback);
735
+ return Math.min(MAX_CHECK_INTERVAL_SECONDS, Math.max(MIN_CHECK_INTERVAL_SECONDS, rounded));
736
+ }
737
+ function hasReachedAutoContinueLimit(task) {
738
+ return task.autoContinueCount >= task.maxAutoContinueCount;
739
+ }
740
+ function buildFollowUpClientRequestId(taskId, referenceAt) {
741
+ return `butler-follow-up:${taskId}:${Date.parse(referenceAt) || Date.now()}`;
742
+ }
743
+ function buildQueuedFollowUpSummary(summary, queueItem) {
744
+ return `${summary} 已转入消息队列,等待当前会话空闲后自动补发(队列项 ${queueItem.orderIndex})。`;
745
+ }
746
+ function isDeferredFollowUpSendError(error) {
747
+ if (error instanceof AppError) {
748
+ return (error.errorCode === "ACTIVE_RUN_EXISTS"
749
+ || error.errorCode === "SESSION_NOT_RUNNING"
750
+ || error.errorCode === "IN_RUN_INPUT_NOT_SUPPORTED"
751
+ || error.errorCode === "SESSION_EXTERNAL_RUN_ACTIVE"
752
+ || error.errorCode === "PROVIDER_RUNTIME_UNAVAILABLE"
753
+ || error.errorCode === "PROVIDER_RUNTIME_TIMEOUT"
754
+ || error.statusCode >= 500);
755
+ }
756
+ if (!(error instanceof Error)) {
757
+ return false;
758
+ }
759
+ return (error.message === "ACTIVE_RUN_EXISTS"
760
+ || error.message === "SESSION_NOT_RUNNING"
761
+ || error.message === "IN_RUN_INPUT_NOT_SUPPORTED"
762
+ || error.message === "SESSION_EXTERNAL_RUN_ACTIVE"
763
+ || error.message === "SERVER_UNAVAILABLE"
764
+ || error.message === "SERVER_TIMEOUT"
765
+ || error.message.includes("当前会话正在运行"));
766
+ }
767
+ function isSyntheticMessageId(messageId) {
768
+ return typeof messageId === "string" && messageId.startsWith("synthetic-");
769
+ }
770
+ function shiftSeconds(referenceAt, seconds) {
771
+ const time = new Date(referenceAt).getTime();
772
+ return new Date(time + seconds * 1000).toISOString();
773
+ }
774
+ function normalizeRunningState(value) {
775
+ switch (value) {
776
+ case "idle":
777
+ case "starting":
778
+ case "running":
779
+ case "completed":
780
+ case "interrupted":
781
+ case "failed":
782
+ return value;
783
+ default:
784
+ return null;
785
+ }
786
+ }
787
+ function isTerminalFollowUpRunningState(value) {
788
+ return value === "completed" || value === "interrupted" || value === "failed";
789
+ }
790
+ function normalizeNullableIso(value) {
791
+ const normalized = value?.trim();
792
+ return normalized && normalized.length > 0 ? normalized : null;
793
+ }
794
+ function resolveLatestAssistantText(envelope) {
795
+ if (!envelope || envelope.messages.length === 0) {
796
+ return null;
797
+ }
798
+ const latestAssistant = [...envelope.messages]
799
+ .sort((left, right) => right.sequence - left.sequence)
800
+ .find((message) => message.role === "assistant" && message.content.trim().length > 0);
801
+ return latestAssistant?.content?.trim() || null;
802
+ }
803
+ function renderHistoryLine(sequence, role, kind, timestamp, content) {
804
+ const compactContent = truncateText(content
805
+ .replace(/\s+/g, " ")
806
+ .trim(), kind === "tool_call" ? 220 : 360);
807
+ return `#${sequence} [${timestamp}] ${role}/${kind}: ${compactContent || "(空内容)"}`;
808
+ }
809
+ function truncateText(value, maxLength) {
810
+ if (value.length <= maxLength) {
811
+ return value;
812
+ }
813
+ return `${value.slice(0, Math.max(0, maxLength - 1))}…`;
814
+ }
815
+ function resolveFollowUpModel(providerId) {
816
+ return providerId === "codex" ? "gpt-5.1-codex-mini" : "haiku";
817
+ }
818
+ function parseEvaluationResult(result) {
819
+ const rawJson = result.structured.rawJson ?? extractJsonFromText(result.latestAssistantMessage);
820
+ if (!rawJson) {
821
+ throw new Error("后台评估助手没有返回结构化 JSON");
822
+ }
823
+ let parsed;
824
+ try {
825
+ parsed = JSON.parse(rawJson);
826
+ }
827
+ catch (error) {
828
+ throw new Error(`后台评估助手返回的 JSON 无法解析:${error instanceof Error ? error.message : String(error)}`);
829
+ }
830
+ const decision = normalizeDecision(parsed.decision);
831
+ if (!decision) {
832
+ throw new Error("后台评估助手返回的 decision 不合法");
833
+ }
834
+ const summary = normalizeNonEmptyString(parsed.summary) ?? result.structured.summary ?? "后台评估助手未提供摘要";
835
+ const waitingReason = normalizeNullableString(parsed.waitingReason);
836
+ const continuePrompt = normalizeNullableString(parsed.continuePrompt);
837
+ const riskLevel = normalizeRiskLevel(parsed.riskLevel);
838
+ return {
839
+ decision,
840
+ summary,
841
+ waitingReason,
842
+ continuePrompt,
843
+ riskLevel
844
+ };
845
+ }
846
+ function normalizeDecision(value) {
847
+ switch (value) {
848
+ case "continue":
849
+ case "waiting_user":
850
+ case "completed":
851
+ case "failed":
852
+ return value;
853
+ default:
854
+ return null;
855
+ }
856
+ }
857
+ function normalizeRiskLevel(value) {
858
+ switch (value) {
859
+ case "low":
860
+ case "medium":
861
+ case "high":
862
+ return value;
863
+ default:
864
+ return null;
865
+ }
866
+ }
867
+ function normalizeNonEmptyString(value) {
868
+ if (typeof value !== "string") {
869
+ return null;
870
+ }
871
+ const normalized = value.trim();
872
+ return normalized || null;
873
+ }
874
+ function normalizeNullableString(value) {
875
+ if (value === null || value === undefined) {
876
+ return null;
877
+ }
878
+ return normalizeNonEmptyString(value);
879
+ }
880
+ function extractJsonFromText(value) {
881
+ if (!value) {
882
+ return null;
883
+ }
884
+ const matched = value.match(/```json\s*([\s\S]*?)```/i);
885
+ const raw = matched?.[1]?.trim();
886
+ return raw || null;
887
+ }
888
+ function resolveSourceCodexHomeDir(sourceCodexHomeDir, targetHomeDir) {
889
+ const configuredSource = sourceCodexHomeDir?.trim();
890
+ if (configuredSource) {
891
+ const resolvedConfiguredSource = path.resolve(configuredSource);
892
+ if (resolvedConfiguredSource !== targetHomeDir) {
893
+ return resolvedConfiguredSource;
894
+ }
895
+ }
896
+ const fallbackHomeDir = path.resolve(path.join(os.homedir(), ".codex"));
897
+ if (fallbackHomeDir !== targetHomeDir) {
898
+ return fallbackHomeDir;
899
+ }
900
+ return targetHomeDir;
901
+ }
902
+ function composeCodexConfigContent(sourceConfigContent, instructionFilePath) {
903
+ const normalizedSource = sourceConfigContent
904
+ .split(/\r?\n/)
905
+ .filter((line) => {
906
+ const trimmed = line.trim();
907
+ return trimmed.length > 0 && !trimmed.startsWith("model_instructions_file");
908
+ })
909
+ .join("\n")
910
+ .trim();
911
+ return [
912
+ "# 代码助手跟进评估专用 Codex 配置(系统自动生成)",
913
+ normalizedSource,
914
+ `model_instructions_file = ${toTomlString(path.resolve(instructionFilePath))}`
915
+ ]
916
+ .filter((part) => part.trim().length > 0)
917
+ .join("\n\n");
918
+ }
919
+ function toTomlString(value) {
920
+ return `"${value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"")}"`;
921
+ }
922
+ function writeFileIfChanged(filePath, content) {
923
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
924
+ if (fs.existsSync(filePath) && fs.readFileSync(filePath, "utf8") === content) {
925
+ return;
926
+ }
927
+ fs.writeFileSync(filePath, content, "utf8");
928
+ }
929
+ function removeFileIfExists(filePath) {
930
+ if (!fs.existsSync(filePath)) {
931
+ return;
932
+ }
933
+ if (fs.statSync(filePath).isFile()) {
934
+ fs.rmSync(filePath, { force: true });
935
+ }
936
+ }
937
+ function syncOptionalFile(sourcePath, targetPath) {
938
+ if (!fs.existsSync(sourcePath) || !fs.statSync(sourcePath).isFile()) {
939
+ removeFileIfExists(targetPath);
940
+ return;
941
+ }
942
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
943
+ if (fs.existsSync(targetPath) && fs.readFileSync(targetPath).equals(fs.readFileSync(sourcePath))) {
944
+ return;
945
+ }
946
+ fs.copyFileSync(sourcePath, targetPath);
947
+ }
948
+ //# sourceMappingURL=butler-follow-up-service.js.map