funolio-agent 1.0.75 → 1.1.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +23 -14
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +376 -4
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +190 -17
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +13 -4
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/local-data.js
CHANGED
|
@@ -36,17 +36,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.UNASSIGNED_PROJECT_NAME = void 0;
|
|
37
37
|
exports.getDb = getDb;
|
|
38
38
|
exports.closeDb = closeDb;
|
|
39
|
+
exports.getAgentOrchestrationRoleClass = getAgentOrchestrationRoleClass;
|
|
40
|
+
exports.getAgentOrchestrationRoleLabel = getAgentOrchestrationRoleLabel;
|
|
41
|
+
exports.getAgentRolePriorities = getAgentRolePriorities;
|
|
42
|
+
exports.rolesMatch = rolesMatch;
|
|
39
43
|
exports.createAgentProfile = createAgentProfile;
|
|
40
44
|
exports.getAgentProfile = getAgentProfile;
|
|
41
45
|
exports.getDefaultAgentProfile = getDefaultAgentProfile;
|
|
42
46
|
exports.getOrchestratorBot = getOrchestratorBot;
|
|
43
47
|
exports.getCurrentOrchestratorSelection = getCurrentOrchestratorSelection;
|
|
48
|
+
exports.getResolvedClerkConfigInfo = getResolvedClerkConfigInfo;
|
|
44
49
|
exports.listAgentProfiles = listAgentProfiles;
|
|
45
50
|
exports.migrateAgentProfilesToDefaultToolProfile = migrateAgentProfilesToDefaultToolProfile;
|
|
46
51
|
exports.updateAgentProfile = updateAgentProfile;
|
|
47
52
|
exports.setOrchestratorBot = setOrchestratorBot;
|
|
48
53
|
exports.setDefaultAgentProfile = setDefaultAgentProfile;
|
|
49
54
|
exports.isClerkOrchestratorEnabled = isClerkOrchestratorEnabled;
|
|
55
|
+
exports.isOrchestrationToolDispatchEnabled = isOrchestrationToolDispatchEnabled;
|
|
56
|
+
exports.setOrchestrationToolDispatchEnabled = setOrchestrationToolDispatchEnabled;
|
|
50
57
|
exports.setClerkAsOrchestrator = setClerkAsOrchestrator;
|
|
51
58
|
exports.createProviderConnection = createProviderConnection;
|
|
52
59
|
exports.listProviderConnections = listProviderConnections;
|
|
@@ -59,20 +66,35 @@ exports.upsertOauthProviderConnection = upsertOauthProviderConnection;
|
|
|
59
66
|
exports.deleteProviderConnection = deleteProviderConnection;
|
|
60
67
|
exports.deleteAgentProfile = deleteAgentProfile;
|
|
61
68
|
exports.createConversation = createConversation;
|
|
69
|
+
exports.getConversationParticipantBotIds = getConversationParticipantBotIds;
|
|
70
|
+
exports.getConversationPrimaryBotId = getConversationPrimaryBotId;
|
|
62
71
|
exports.getConversation = getConversation;
|
|
63
72
|
exports.listConversations = listConversations;
|
|
64
73
|
exports.countConversations = countConversations;
|
|
65
74
|
exports.countMessagesForBot = countMessagesForBot;
|
|
75
|
+
exports.countMessagesForBotSince = countMessagesForBotSince;
|
|
76
|
+
exports.countDecisionsForBot = countDecisionsForBot;
|
|
77
|
+
exports.countBotActiveDays = countBotActiveDays;
|
|
78
|
+
exports.countBotConversationsUpdatedBetween = countBotConversationsUpdatedBetween;
|
|
79
|
+
exports.sumBotTokenUsageSince = sumBotTokenUsageSince;
|
|
66
80
|
exports.listBotConversationActivity = listBotConversationActivity;
|
|
67
81
|
exports.updateConversation = updateConversation;
|
|
82
|
+
exports.syncConversationParticipants = syncConversationParticipants;
|
|
68
83
|
exports.touchConversationActivity = touchConversationActivity;
|
|
84
|
+
exports.bumpConversationSyncRevision = bumpConversationSyncRevision;
|
|
69
85
|
exports.deleteConversation = deleteConversation;
|
|
86
|
+
exports.toWarmSessionBindingKey = toWarmSessionBindingKey;
|
|
87
|
+
exports.parseWarmSessionBindingKey = parseWarmSessionBindingKey;
|
|
88
|
+
exports.epochRowToWarmInfo = epochRowToWarmInfo;
|
|
70
89
|
exports.addMessage = addMessage;
|
|
71
90
|
exports.updateMessage = updateMessage;
|
|
72
91
|
exports.createChatJob = createChatJob;
|
|
73
92
|
exports.getCliSessionEpoch = getCliSessionEpoch;
|
|
74
93
|
exports.upsertCliSessionEpoch = upsertCliSessionEpoch;
|
|
75
94
|
exports.deleteCliSessionEpoch = deleteCliSessionEpoch;
|
|
95
|
+
exports.clearCliSessionEpochIfMatches = clearCliSessionEpochIfMatches;
|
|
96
|
+
exports.cleanupPoisonedClaudePrewarmSessionRows = cleanupPoisonedClaudePrewarmSessionRows;
|
|
97
|
+
exports.getCliSessionEpochsByConversation = getCliSessionEpochsByConversation;
|
|
76
98
|
exports.generateNextSessionId = generateNextSessionId;
|
|
77
99
|
exports.getChatJob = getChatJob;
|
|
78
100
|
exports.getLatestConversationChatJob = getLatestConversationChatJob;
|
|
@@ -83,6 +105,8 @@ exports.listActiveAssistantMessageIds = listActiveAssistantMessageIds;
|
|
|
83
105
|
exports.countRunningChatJobs = countRunningChatJobs;
|
|
84
106
|
exports.updateChatJob = updateChatJob;
|
|
85
107
|
exports.markRunningChatJobsInterrupted = markRunningChatJobsInterrupted;
|
|
108
|
+
exports.recoverInterruptedChatJobs = recoverInterruptedChatJobs;
|
|
109
|
+
exports.recoverInterruptedWorkflowRuns = recoverInterruptedWorkflowRuns;
|
|
86
110
|
exports.createMessageActivity = createMessageActivity;
|
|
87
111
|
exports.attachMessageActivitiesToMessage = attachMessageActivitiesToMessage;
|
|
88
112
|
exports.listMessageActivities = listMessageActivities;
|
|
@@ -117,6 +141,16 @@ exports.updateImportJob = updateImportJob;
|
|
|
117
141
|
exports.createImportJobItem = createImportJobItem;
|
|
118
142
|
exports.getQueuedImportItems = getQueuedImportItems;
|
|
119
143
|
exports.updateImportJobItem = updateImportJobItem;
|
|
144
|
+
exports.createImportedPlanRun = createImportedPlanRun;
|
|
145
|
+
exports.getImportedPlanRun = getImportedPlanRun;
|
|
146
|
+
exports.listImportedPlanRunsForConversation = listImportedPlanRunsForConversation;
|
|
147
|
+
exports.getLatestImportedPlanRunForConversation = getLatestImportedPlanRunForConversation;
|
|
148
|
+
exports.updateImportedPlanRun = updateImportedPlanRun;
|
|
149
|
+
exports.touchImportedPlanRun = touchImportedPlanRun;
|
|
150
|
+
exports.getImportedPlanRunTasks = getImportedPlanRunTasks;
|
|
151
|
+
exports.getImportedPlanRunRemainingTaskIds = getImportedPlanRunRemainingTaskIds;
|
|
152
|
+
exports.getImportedPlanRunCurrentTask = getImportedPlanRunCurrentTask;
|
|
153
|
+
exports.deleteImportedPlanRunSelectedTasks = deleteImportedPlanRunSelectedTasks;
|
|
120
154
|
exports.createOutboxEvent = createOutboxEvent;
|
|
121
155
|
exports.getPendingOutboxEvents = getPendingOutboxEvents;
|
|
122
156
|
exports.markOutboxEventSent = markOutboxEventSent;
|
|
@@ -156,6 +190,7 @@ exports.completeTodoTaskByUser = completeTodoTaskByUser;
|
|
|
156
190
|
exports.listTodoArtifactsForTask = listTodoArtifactsForTask;
|
|
157
191
|
exports.completeTodoTaskByWorker = completeTodoTaskByWorker;
|
|
158
192
|
exports.blockTodoTaskByWorker = blockTodoTaskByWorker;
|
|
193
|
+
exports.markTodoTaskRuntimeBlocked = markTodoTaskRuntimeBlocked;
|
|
159
194
|
exports.deleteTodoTask = deleteTodoTask;
|
|
160
195
|
exports.reorderTodoTask = reorderTodoTask;
|
|
161
196
|
exports.listTodoAudit = listTodoAudit;
|
|
@@ -163,6 +198,7 @@ exports.moveNullTodoTasksToUnassigned = moveNullTodoTasksToUnassigned;
|
|
|
163
198
|
exports.getLatestActiveTodoForConversation = getLatestActiveTodoForConversation;
|
|
164
199
|
exports.getNextActiveTodoForConversation = getNextActiveTodoForConversation;
|
|
165
200
|
exports.listCompletedTodoTasksForConversation = listCompletedTodoTasksForConversation;
|
|
201
|
+
exports.listActiveTodoTasksForConversation = listActiveTodoTasksForConversation;
|
|
166
202
|
exports.createProject = createProject;
|
|
167
203
|
exports.upsertProject = upsertProject;
|
|
168
204
|
exports.getProject = getProject;
|
|
@@ -258,21 +294,102 @@ function newId() {
|
|
|
258
294
|
function now() {
|
|
259
295
|
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
260
296
|
}
|
|
297
|
+
function defaultOrchestrationIncludeUserPrompt(_fields) {
|
|
298
|
+
// Every worker role gets the original user prompt by default.
|
|
299
|
+
// april19fixes.txt item 3 fix (IV): previously only verify/verification
|
|
300
|
+
// workers received it, which meant Ben/John/Brain flew blind on a
|
|
301
|
+
// generic "Complete X's workflow step" task whenever the handoff
|
|
302
|
+
// lacked detail. The orchestrator's task_instructions is still the
|
|
303
|
+
// primary context source; this default just ensures the original
|
|
304
|
+
// user message is always available as a fallback reference.
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
function getAgentOrchestrationRoleClass(agent) {
|
|
308
|
+
const roleClass = String(agent?.orchestration_role_class
|
|
309
|
+
|| agent?.orchestration_role_label
|
|
310
|
+
|| agent?.role_class
|
|
311
|
+
|| agent?.role_label
|
|
312
|
+
|| '').trim();
|
|
313
|
+
return roleClass || null;
|
|
314
|
+
}
|
|
315
|
+
function getAgentOrchestrationRoleLabel(agent) {
|
|
316
|
+
const roleLabel = String(agent?.orchestration_role_label
|
|
317
|
+
|| agent?.orchestration_role_class
|
|
318
|
+
|| agent?.role_label
|
|
319
|
+
|| agent?.role_class
|
|
320
|
+
|| '').trim();
|
|
321
|
+
return roleLabel || null;
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Read a bot's role priority list, dual-reading new and old columns.
|
|
325
|
+
* Used everywhere role-based dispatch or display is needed.
|
|
326
|
+
*
|
|
327
|
+
* Reads `orchestration_roles_json` first (authoritative). If empty/missing,
|
|
328
|
+
* falls back to parsing `orchestration_role_class` (comma-separated, left-
|
|
329
|
+
* to-right priority). Returns empty array if neither source has content.
|
|
330
|
+
*
|
|
331
|
+
* Normalization (per Decision 2): trim each entry, drop empties. Does NOT
|
|
332
|
+
* lower-case or canonicalize the role text — comparisons are the caller's
|
|
333
|
+
* responsibility.
|
|
334
|
+
*/
|
|
335
|
+
function getAgentRolePriorities(row) {
|
|
336
|
+
const raw = (row.orchestration_roles_json || '').trim();
|
|
337
|
+
if (raw) {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(raw);
|
|
340
|
+
if (Array.isArray(parsed)) {
|
|
341
|
+
return parsed.map((entry) => String(entry || '').trim()).filter(Boolean);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// fall through to legacy parse
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const legacy = (row.orchestration_role_class || '').trim();
|
|
349
|
+
if (!legacy)
|
|
350
|
+
return [];
|
|
351
|
+
return legacy.split(',').map((s) => s.trim()).filter(Boolean);
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Compare two role strings for equality per Decision 2 matching rules:
|
|
355
|
+
* case-insensitive + whitespace-normalized. NO alias table.
|
|
356
|
+
* "QA" == "qa" == " QA " == "qa ". "QA" != "Quality Assurance".
|
|
357
|
+
*/
|
|
358
|
+
function rolesMatch(a, b) {
|
|
359
|
+
const na = String(a || '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
360
|
+
const nb = String(b || '').trim().replace(/\s+/g, ' ').toLowerCase();
|
|
361
|
+
if (!na || !nb)
|
|
362
|
+
return false;
|
|
363
|
+
return na === nb;
|
|
364
|
+
}
|
|
365
|
+
const VALID_CODEX_REASONING_SUMMARIES = new Set(['auto', 'concise', 'detailed', 'none']);
|
|
366
|
+
const VALID_CLAUDE_OUTPUT_STYLES = new Set(['default', 'explanatory']);
|
|
367
|
+
function normalizeCodexReasoningSummary(value) {
|
|
368
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
369
|
+
return VALID_CODEX_REASONING_SUMMARIES.has(normalized) ? normalized : 'detailed';
|
|
370
|
+
}
|
|
371
|
+
function normalizeClaudeOutputStyle(value) {
|
|
372
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
373
|
+
return VALID_CLAUDE_OUTPUT_STYLES.has(normalized) ? normalized : 'default';
|
|
374
|
+
}
|
|
261
375
|
function createAgentProfile(p) {
|
|
262
376
|
const db = getDb();
|
|
263
|
-
const id = newId();
|
|
377
|
+
const id = p.id ?? newId();
|
|
264
378
|
const ts = now();
|
|
265
379
|
const insert = db.prepare(`
|
|
266
380
|
INSERT INTO agent_profile (
|
|
267
381
|
id, provider, model, name, soul_md, memory_md, tools_md, skills_md, api_key_enc,
|
|
268
|
-
permission_mode, is_default, role_label, role_class,
|
|
382
|
+
permission_mode, is_default, role_label, role_class, orchestration_role_label, orchestration_role_class,
|
|
383
|
+
orchestration_include_user_prompt, is_active, priority,
|
|
269
384
|
provider_connection_id, color, purpose_md, identity_summary, final_prompt,
|
|
270
385
|
enabled_builtin_tools_json, enabled_mcp_tools_json, show_thinking, is_orchestrator,
|
|
271
386
|
codex_reasoning_effort, codex_reasoning_summary, codex_personality,
|
|
272
387
|
codex_service_tier, codex_sandbox_policy, codex_approval_policy,
|
|
388
|
+
claude_effort_level, claude_output_style, claude_fast_mode, claude_permissions_json,
|
|
389
|
+
orchestration_roles_json,
|
|
273
390
|
created_at, updated_at
|
|
274
391
|
)
|
|
275
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
392
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
276
393
|
`);
|
|
277
394
|
const transaction = db.transaction(() => {
|
|
278
395
|
if (p.isDefault) {
|
|
@@ -282,7 +399,16 @@ function createAgentProfile(p) {
|
|
|
282
399
|
db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
|
|
283
400
|
setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
|
|
284
401
|
}
|
|
285
|
-
|
|
402
|
+
// Dual-write: if caller provided a role priority list, also derive the
|
|
403
|
+
// legacy comma-separated `orchestration_role_class` value for compat.
|
|
404
|
+
// Priority 0 item becomes the primary for downstream code that still
|
|
405
|
+
// reads the old column directly.
|
|
406
|
+
const rolesList = Array.isArray(p.orchestrationRolesJson)
|
|
407
|
+
? p.orchestrationRolesJson.map((s) => String(s || '').trim()).filter(Boolean)
|
|
408
|
+
: null;
|
|
409
|
+
const rolesJsonStored = rolesList && rolesList.length > 0 ? JSON.stringify(rolesList) : null;
|
|
410
|
+
const derivedRoleClass = rolesList && rolesList.length > 0 ? rolesList.join(', ') : null;
|
|
411
|
+
insert.run(id, p.provider, p.model, p.name, p.soulMd ?? null, p.memoryMd ?? null, p.toolsMd ?? null, p.skillsMd ?? null, p.apiKeyEnc ?? null, (0, approval_1.normalizePermissionMode)(p.permissionMode ?? default_tool_profile_1.DEFAULT_PERMISSION_MODE), p.isDefault ? 1 : 0, p.roleLabel ?? null, p.roleClass ?? null, p.orchestrationRoleLabel ?? p.roleLabel ?? null, p.orchestrationRoleClass ?? derivedRoleClass ?? p.roleClass ?? null, (p.orchestrationIncludeUserPrompt ?? defaultOrchestrationIncludeUserPrompt(p)) ? 1 : 0, p.isActive !== false ? 1 : 0, p.priority ?? 100, p.providerConnectionId ?? null, p.color ?? null, p.purposeMd ?? null, p.identitySummary ?? null, p.finalPrompt ?? null, p.enabledBuiltinToolsJson ?? null, p.enabledMcpToolsJson ?? null, 0, p.isOrchestrator ? 1 : 0, p.codexReasoningEffort ?? 'low', normalizeCodexReasoningSummary(p.codexReasoningSummary), p.codexPersonality ?? 'friendly', p.codexServiceTier ?? 'fast', p.codexSandboxPolicy ?? 'danger-full-access', p.codexApprovalPolicy ?? 'never', p.claudeEffortLevel ?? 'auto', normalizeClaudeOutputStyle(p.claudeOutputStyle), p.claudeFastMode ? 1 : 0, p.claudePermissionsJson ?? null, rolesJsonStored, ts, ts);
|
|
286
412
|
});
|
|
287
413
|
transaction();
|
|
288
414
|
return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(id);
|
|
@@ -298,10 +424,11 @@ function getOrchestratorBot() {
|
|
|
298
424
|
}
|
|
299
425
|
function getCurrentOrchestratorSelection() {
|
|
300
426
|
if (isClerkOrchestratorEnabled()) {
|
|
427
|
+
const clerk = getResolvedClerkConfigInfo();
|
|
301
428
|
return {
|
|
302
429
|
type: 'clerk',
|
|
303
|
-
provider:
|
|
304
|
-
model:
|
|
430
|
+
provider: clerk.provider,
|
|
431
|
+
model: clerk.model,
|
|
305
432
|
};
|
|
306
433
|
}
|
|
307
434
|
const bot = getOrchestratorBot();
|
|
@@ -310,6 +437,32 @@ function getCurrentOrchestratorSelection() {
|
|
|
310
437
|
}
|
|
311
438
|
return { type: 'none' };
|
|
312
439
|
}
|
|
440
|
+
function getResolvedClerkConfigInfo() {
|
|
441
|
+
const clerkConfig = getJsonSetting('clerk.config');
|
|
442
|
+
const providerConnectionId = clerkConfig?.providerConnectionId
|
|
443
|
+
? String(clerkConfig.providerConnectionId).trim()
|
|
444
|
+
: '';
|
|
445
|
+
const connection = providerConnectionId ? getProviderConnection(providerConnectionId) : undefined;
|
|
446
|
+
const provider = String(connection?.provider_id
|
|
447
|
+
|| clerkConfig?.providerId
|
|
448
|
+
|| getSetting('clerk_provider')
|
|
449
|
+
|| '').trim() || null;
|
|
450
|
+
const model = String(clerkConfig?.model
|
|
451
|
+
|| connection?.default_model
|
|
452
|
+
|| getSetting('clerk_model')
|
|
453
|
+
|| '').trim() || null;
|
|
454
|
+
const hasSecret = Boolean(connection?.access_mode === 'cli'
|
|
455
|
+
|| connection?.api_key_enc
|
|
456
|
+
|| connection?.oauth_token
|
|
457
|
+
|| getSetting('clerk_api_key'));
|
|
458
|
+
return {
|
|
459
|
+
provider,
|
|
460
|
+
model,
|
|
461
|
+
hasSecret,
|
|
462
|
+
configured: Boolean(provider && model && hasSecret),
|
|
463
|
+
providerConnectionId: providerConnectionId || null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
313
466
|
function listAgentProfiles() {
|
|
314
467
|
return getDb().prepare('SELECT * FROM agent_profile ORDER BY is_default DESC, created_at ASC').all();
|
|
315
468
|
}
|
|
@@ -409,6 +562,18 @@ function updateAgentProfile(id, fields) {
|
|
|
409
562
|
sets.push('role_class = ?');
|
|
410
563
|
vals.push(fields.roleClass);
|
|
411
564
|
}
|
|
565
|
+
if (fields.orchestrationRoleLabel !== undefined) {
|
|
566
|
+
sets.push('orchestration_role_label = ?');
|
|
567
|
+
vals.push(fields.orchestrationRoleLabel);
|
|
568
|
+
}
|
|
569
|
+
if (fields.orchestrationRoleClass !== undefined) {
|
|
570
|
+
sets.push('orchestration_role_class = ?');
|
|
571
|
+
vals.push(fields.orchestrationRoleClass);
|
|
572
|
+
}
|
|
573
|
+
if (fields.orchestrationIncludeUserPrompt !== undefined) {
|
|
574
|
+
sets.push('orchestration_include_user_prompt = ?');
|
|
575
|
+
vals.push(fields.orchestrationIncludeUserPrompt ? 1 : 0);
|
|
576
|
+
}
|
|
412
577
|
if (fields.isActive !== undefined) {
|
|
413
578
|
sets.push('is_active = ?');
|
|
414
579
|
vals.push(fields.isActive ? 1 : 0);
|
|
@@ -459,7 +624,7 @@ function updateAgentProfile(id, fields) {
|
|
|
459
624
|
}
|
|
460
625
|
if (fields.codexReasoningSummary !== undefined) {
|
|
461
626
|
sets.push('codex_reasoning_summary = ?');
|
|
462
|
-
vals.push(fields.codexReasoningSummary);
|
|
627
|
+
vals.push(normalizeCodexReasoningSummary(fields.codexReasoningSummary));
|
|
463
628
|
}
|
|
464
629
|
if (fields.codexPersonality !== undefined) {
|
|
465
630
|
sets.push('codex_personality = ?');
|
|
@@ -477,6 +642,39 @@ function updateAgentProfile(id, fields) {
|
|
|
477
642
|
sets.push('codex_approval_policy = ?');
|
|
478
643
|
vals.push(fields.codexApprovalPolicy);
|
|
479
644
|
}
|
|
645
|
+
if (fields.claudeEffortLevel !== undefined) {
|
|
646
|
+
sets.push('claude_effort_level = ?');
|
|
647
|
+
vals.push(fields.claudeEffortLevel);
|
|
648
|
+
}
|
|
649
|
+
if (fields.claudeOutputStyle !== undefined) {
|
|
650
|
+
sets.push('claude_output_style = ?');
|
|
651
|
+
vals.push(normalizeClaudeOutputStyle(fields.claudeOutputStyle));
|
|
652
|
+
}
|
|
653
|
+
if (fields.claudeFastMode !== undefined) {
|
|
654
|
+
sets.push('claude_fast_mode = ?');
|
|
655
|
+
vals.push(fields.claudeFastMode ? 1 : 0);
|
|
656
|
+
}
|
|
657
|
+
if (fields.claudePermissionsJson !== undefined) {
|
|
658
|
+
sets.push('claude_permissions_json = ?');
|
|
659
|
+
vals.push(fields.claudePermissionsJson);
|
|
660
|
+
}
|
|
661
|
+
if (fields.orchestrationRolesJson !== undefined) {
|
|
662
|
+
// Dual-write: update both the new JSON column and the legacy comma-separated
|
|
663
|
+
// orchestration_role_class so any code still reading the old column stays
|
|
664
|
+
// consistent until Phase B drops it.
|
|
665
|
+
const rolesList = Array.isArray(fields.orchestrationRolesJson)
|
|
666
|
+
? fields.orchestrationRolesJson.map((s) => String(s || '').trim()).filter(Boolean)
|
|
667
|
+
: null;
|
|
668
|
+
const rolesJsonStored = rolesList && rolesList.length > 0 ? JSON.stringify(rolesList) : null;
|
|
669
|
+
const derivedRoleClass = rolesList && rolesList.length > 0 ? rolesList.join(', ') : null;
|
|
670
|
+
sets.push('orchestration_roles_json = ?');
|
|
671
|
+
vals.push(rolesJsonStored);
|
|
672
|
+
// Only override orchestration_role_class if the caller didn't explicitly pass one this update.
|
|
673
|
+
if (fields.orchestrationRoleClass === undefined) {
|
|
674
|
+
sets.push('orchestration_role_class = ?');
|
|
675
|
+
vals.push(derivedRoleClass);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
480
678
|
if (sets.length === 0)
|
|
481
679
|
return row;
|
|
482
680
|
const transaction = db.transaction(() => {
|
|
@@ -522,6 +720,25 @@ function setDefaultAgentProfile(botId) {
|
|
|
522
720
|
function isClerkOrchestratorEnabled() {
|
|
523
721
|
return getSetting(CLERK_IS_ORCHESTRATOR_KEY) === 'true';
|
|
524
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* Phase B of orchestration-plan.txt: LLM-tool-dispatch is the only
|
|
725
|
+
* orchestration path in local_desktop mode. The feature flag introduced
|
|
726
|
+
* in Phase A has been retired — new path is always active, no user toggle.
|
|
727
|
+
* These helpers are kept for a brief deprecation window so any external
|
|
728
|
+
* callers (settings UI, HTTP endpoints) still compile while they are
|
|
729
|
+
* removed. Do not use in new code.
|
|
730
|
+
*
|
|
731
|
+
* @deprecated Phase B — tool dispatch is the only path.
|
|
732
|
+
*/
|
|
733
|
+
function isOrchestrationToolDispatchEnabled() {
|
|
734
|
+
return true;
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* @deprecated Phase B — no-op. Tool dispatch is the only path.
|
|
738
|
+
*/
|
|
739
|
+
function setOrchestrationToolDispatchEnabled(_enabled) {
|
|
740
|
+
// no-op after Phase B
|
|
741
|
+
}
|
|
525
742
|
function setClerkAsOrchestrator(enabled) {
|
|
526
743
|
const db = getDb();
|
|
527
744
|
const transaction = db.transaction(() => {
|
|
@@ -533,7 +750,7 @@ function setClerkAsOrchestrator(enabled) {
|
|
|
533
750
|
transaction();
|
|
534
751
|
}
|
|
535
752
|
const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli']);
|
|
536
|
-
const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'cli']);
|
|
753
|
+
const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'apiKey', 'cli']);
|
|
537
754
|
function createProviderConnection(p) {
|
|
538
755
|
const db = getDb();
|
|
539
756
|
const id = newId();
|
|
@@ -734,12 +951,40 @@ function deleteAgentProfile(id) {
|
|
|
734
951
|
tx();
|
|
735
952
|
return true;
|
|
736
953
|
}
|
|
954
|
+
function normalizeConversationBotIds(botIds, fallbackBotId) {
|
|
955
|
+
const next = new Set();
|
|
956
|
+
for (const botId of botIds) {
|
|
957
|
+
const trimmed = String(botId || '').trim();
|
|
958
|
+
if (trimmed)
|
|
959
|
+
next.add(trimmed);
|
|
960
|
+
}
|
|
961
|
+
const fallback = String(fallbackBotId || '').trim();
|
|
962
|
+
if (next.size === 0 && fallback)
|
|
963
|
+
next.add(fallback);
|
|
964
|
+
return Array.from(next);
|
|
965
|
+
}
|
|
966
|
+
function parseConversationBotIds(raw) {
|
|
967
|
+
if (!raw)
|
|
968
|
+
return [];
|
|
969
|
+
try {
|
|
970
|
+
const parsed = JSON.parse(raw);
|
|
971
|
+
if (!Array.isArray(parsed))
|
|
972
|
+
return [];
|
|
973
|
+
return normalizeConversationBotIds(parsed.map((value) => (typeof value === 'string' ? value : null)));
|
|
974
|
+
}
|
|
975
|
+
catch {
|
|
976
|
+
return [];
|
|
977
|
+
}
|
|
978
|
+
}
|
|
737
979
|
function createConversation(agentId, title, source, opts) {
|
|
738
980
|
const db = getDb();
|
|
739
981
|
const id = newId();
|
|
740
982
|
const ts = now();
|
|
741
983
|
let projectId = opts?.projectId ?? null;
|
|
742
984
|
let projectName = opts?.projectName ?? null;
|
|
985
|
+
const initialBotId = String(opts?.initialBotId || agentId || '').trim() || agentId;
|
|
986
|
+
const activeBotIds = normalizeConversationBotIds(opts?.botIds || [], initialBotId);
|
|
987
|
+
const activeBotIdsJson = JSON.stringify(activeBotIds);
|
|
743
988
|
if (projectId) {
|
|
744
989
|
const project = getProject(projectId);
|
|
745
990
|
if (project) {
|
|
@@ -757,10 +1002,10 @@ function createConversation(agentId, title, source, opts) {
|
|
|
757
1002
|
projectName = unassigned.name;
|
|
758
1003
|
}
|
|
759
1004
|
db.prepare(`
|
|
760
|
-
INSERT INTO conversation (id, agent_id, title, source, project_id, project_name, created_at, updated_at)
|
|
761
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
762
|
-
`).run(id, agentId, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
|
|
763
|
-
return
|
|
1005
|
+
INSERT INTO conversation (id, agent_id, initial_bot_id, active_bot_ids_json, title, source, project_id, project_name, sync_revision, created_at, updated_at)
|
|
1006
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
1007
|
+
`).run(id, agentId, initialBotId, activeBotIdsJson, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
|
|
1008
|
+
return getConversation(id);
|
|
764
1009
|
}
|
|
765
1010
|
function latestConversationSummarySql(conversationAlias) {
|
|
766
1011
|
return `(
|
|
@@ -854,14 +1099,42 @@ function conversationSelectSql(conversationAlias) {
|
|
|
854
1099
|
function hydrateConversationRow(row) {
|
|
855
1100
|
if (!row)
|
|
856
1101
|
return undefined;
|
|
1102
|
+
const activeBotIds = parseConversationBotIds(row.active_bot_ids_json);
|
|
857
1103
|
return {
|
|
858
1104
|
...row,
|
|
1105
|
+
initial_bot_id: row.initial_bot_id ?? null,
|
|
1106
|
+
active_bot_ids_json: row.active_bot_ids_json ?? null,
|
|
1107
|
+
active_bot_ids: activeBotIds,
|
|
859
1108
|
title: row.resolved_title ?? row.title,
|
|
860
1109
|
summary: row.resolved_summary ?? row.summary,
|
|
861
1110
|
created_at: row.effective_created_at ?? row.created_at,
|
|
862
1111
|
updated_at: row.effective_updated_at ?? row.updated_at,
|
|
863
1112
|
};
|
|
864
1113
|
}
|
|
1114
|
+
function getConversationParticipantBotIds(conversationId, conversation) {
|
|
1115
|
+
const conv = conversation || getConversation(conversationId);
|
|
1116
|
+
const fromConversation = normalizeConversationBotIds(conv?.active_bot_ids || []);
|
|
1117
|
+
const rows = getDb().prepare(`
|
|
1118
|
+
SELECT DISTINCT bot_id
|
|
1119
|
+
FROM message
|
|
1120
|
+
WHERE conversation_id = ?
|
|
1121
|
+
AND bot_id IS NOT NULL
|
|
1122
|
+
AND trim(bot_id) <> ''
|
|
1123
|
+
ORDER BY bot_id
|
|
1124
|
+
`).all(conversationId);
|
|
1125
|
+
return normalizeConversationBotIds([...fromConversation, ...rows.map((row) => row.bot_id)], conv?.initial_bot_id || conv?.agent_id || null);
|
|
1126
|
+
}
|
|
1127
|
+
function getConversationPrimaryBotId(conversationId, conversation) {
|
|
1128
|
+
const conv = conversation || getConversation(conversationId);
|
|
1129
|
+
const initialBotId = String(conv?.initial_bot_id || '').trim();
|
|
1130
|
+
if (initialBotId)
|
|
1131
|
+
return initialBotId;
|
|
1132
|
+
const participantIds = getConversationParticipantBotIds(conversationId, conv);
|
|
1133
|
+
if (participantIds.length > 0)
|
|
1134
|
+
return participantIds[0];
|
|
1135
|
+
const legacyBotId = String(conv?.agent_id || '').trim();
|
|
1136
|
+
return legacyBotId || null;
|
|
1137
|
+
}
|
|
865
1138
|
function getConversation(id) {
|
|
866
1139
|
return hydrateConversationRow(getDb().prepare(`${conversationSelectSql('c')} WHERE c.id = ?`).get(id));
|
|
867
1140
|
}
|
|
@@ -874,15 +1147,18 @@ function listConversations(opts) {
|
|
|
874
1147
|
const wheres = [];
|
|
875
1148
|
if (opts?.agentId) {
|
|
876
1149
|
wheres.push(`(
|
|
877
|
-
c.
|
|
1150
|
+
coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
878
1151
|
OR EXISTS (
|
|
879
1152
|
SELECT 1
|
|
880
1153
|
FROM message m
|
|
881
1154
|
WHERE m.conversation_id = c.id
|
|
882
1155
|
AND m.bot_id = ?
|
|
883
1156
|
)
|
|
1157
|
+
OR c.initial_bot_id = ?
|
|
1158
|
+
OR c.agent_id = ?
|
|
884
1159
|
)`);
|
|
885
|
-
|
|
1160
|
+
const botNeedle = `%\"${opts.agentId}\"%`;
|
|
1161
|
+
params.push(botNeedle, opts.agentId, opts.agentId, opts.agentId);
|
|
886
1162
|
}
|
|
887
1163
|
if (opts?.search) {
|
|
888
1164
|
wheres.push(`(
|
|
@@ -912,14 +1188,16 @@ function countConversations(agentId) {
|
|
|
912
1188
|
return db.prepare(`
|
|
913
1189
|
SELECT count(DISTINCT c.id) as cnt
|
|
914
1190
|
FROM conversation c
|
|
915
|
-
WHERE c.
|
|
1191
|
+
WHERE coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
916
1192
|
OR EXISTS (
|
|
917
1193
|
SELECT 1
|
|
918
1194
|
FROM message m
|
|
919
1195
|
WHERE m.conversation_id = c.id
|
|
920
1196
|
AND m.bot_id = ?
|
|
921
1197
|
)
|
|
922
|
-
|
|
1198
|
+
OR c.initial_bot_id = ?
|
|
1199
|
+
OR c.agent_id = ?
|
|
1200
|
+
`).get(`%\"${agentId}\"%`, agentId, agentId, agentId).cnt;
|
|
923
1201
|
}
|
|
924
1202
|
return db.prepare('SELECT count(*) as cnt FROM conversation').get().cnt;
|
|
925
1203
|
}
|
|
@@ -930,6 +1208,74 @@ function countMessagesForBot(botId) {
|
|
|
930
1208
|
WHERE bot_id = ?
|
|
931
1209
|
`).get(botId).cnt;
|
|
932
1210
|
}
|
|
1211
|
+
function botConversationMembershipSql(alias) {
|
|
1212
|
+
return `
|
|
1213
|
+
(
|
|
1214
|
+
coalesce(${alias}.active_bot_ids_json, '[]') LIKE ?
|
|
1215
|
+
OR EXISTS (
|
|
1216
|
+
SELECT 1
|
|
1217
|
+
FROM message m
|
|
1218
|
+
WHERE m.conversation_id = ${alias}.id
|
|
1219
|
+
AND m.bot_id = ?
|
|
1220
|
+
)
|
|
1221
|
+
OR ${alias}.initial_bot_id = ?
|
|
1222
|
+
OR ${alias}.agent_id = ?
|
|
1223
|
+
)
|
|
1224
|
+
`;
|
|
1225
|
+
}
|
|
1226
|
+
function botConversationMembershipParams(botId) {
|
|
1227
|
+
return [`%\"${botId}\"%`, botId, botId, botId];
|
|
1228
|
+
}
|
|
1229
|
+
function countMessagesForBotSince(botId, since) {
|
|
1230
|
+
return getDb().prepare(`
|
|
1231
|
+
SELECT count(*) as cnt
|
|
1232
|
+
FROM message
|
|
1233
|
+
WHERE bot_id = ?
|
|
1234
|
+
AND datetime(created_at) >= datetime(?)
|
|
1235
|
+
`).get(botId, since).cnt;
|
|
1236
|
+
}
|
|
1237
|
+
function countDecisionsForBot(botId) {
|
|
1238
|
+
const membershipSql = botConversationMembershipSql('c');
|
|
1239
|
+
const params = botConversationMembershipParams(botId);
|
|
1240
|
+
return getDb().prepare(`
|
|
1241
|
+
SELECT count(*) as cnt
|
|
1242
|
+
FROM decision d
|
|
1243
|
+
JOIN conversation c ON c.id = d.conversation_id
|
|
1244
|
+
WHERE ${membershipSql}
|
|
1245
|
+
`).get(...params).cnt;
|
|
1246
|
+
}
|
|
1247
|
+
function countBotActiveDays(botId) {
|
|
1248
|
+
const membershipSql = botConversationMembershipSql('c');
|
|
1249
|
+
const params = botConversationMembershipParams(botId);
|
|
1250
|
+
return getDb().prepare(`
|
|
1251
|
+
SELECT count(DISTINCT date(c.updated_at)) as cnt
|
|
1252
|
+
FROM conversation c
|
|
1253
|
+
WHERE ${membershipSql}
|
|
1254
|
+
`).get(...params).cnt;
|
|
1255
|
+
}
|
|
1256
|
+
function countBotConversationsUpdatedBetween(botId, from, to) {
|
|
1257
|
+
const membershipSql = botConversationMembershipSql('c');
|
|
1258
|
+
const params = [...botConversationMembershipParams(botId), from];
|
|
1259
|
+
let sql = `
|
|
1260
|
+
SELECT count(*) as cnt
|
|
1261
|
+
FROM conversation c
|
|
1262
|
+
WHERE ${membershipSql}
|
|
1263
|
+
AND datetime(c.updated_at) >= datetime(?)
|
|
1264
|
+
`;
|
|
1265
|
+
if (to) {
|
|
1266
|
+
sql += ` AND datetime(c.updated_at) < datetime(?)`;
|
|
1267
|
+
params.push(to);
|
|
1268
|
+
}
|
|
1269
|
+
return getDb().prepare(sql).get(...params).cnt;
|
|
1270
|
+
}
|
|
1271
|
+
function sumBotTokenUsageSince(botId, since) {
|
|
1272
|
+
return Number(getDb().prepare(`
|
|
1273
|
+
SELECT coalesce(sum(token_count), 0) as total
|
|
1274
|
+
FROM message
|
|
1275
|
+
WHERE bot_id = ?
|
|
1276
|
+
AND datetime(created_at) >= datetime(?)
|
|
1277
|
+
`).get(botId, since).total ?? 0);
|
|
1278
|
+
}
|
|
933
1279
|
function listBotConversationActivity(botId, limit = 10) {
|
|
934
1280
|
const db = getDb();
|
|
935
1281
|
return db.prepare(`
|
|
@@ -950,18 +1296,20 @@ function listBotConversationActivity(botId, limit = 10) {
|
|
|
950
1296
|
FROM (
|
|
951
1297
|
${conversationSelectSql('c')}
|
|
952
1298
|
WHERE (
|
|
953
|
-
c.
|
|
1299
|
+
coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
954
1300
|
OR EXISTS (
|
|
955
1301
|
SELECT 1
|
|
956
1302
|
FROM message m
|
|
957
1303
|
WHERE m.conversation_id = c.id
|
|
958
1304
|
AND m.bot_id = ?
|
|
959
1305
|
)
|
|
1306
|
+
OR c.initial_bot_id = ?
|
|
1307
|
+
OR c.agent_id = ?
|
|
960
1308
|
)
|
|
961
1309
|
) conv
|
|
962
1310
|
ORDER BY COALESCE(bot_last_message_at, conv.updated_at, conv.created_at) DESC
|
|
963
1311
|
LIMIT ?
|
|
964
|
-
`).all(botId, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
|
|
1312
|
+
`).all(botId, botId, `%\"${botId}\"%`, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
|
|
965
1313
|
}
|
|
966
1314
|
function updateConversation(id, fields) {
|
|
967
1315
|
const db = getDb();
|
|
@@ -1003,6 +1351,8 @@ function updateConversation(id, fields) {
|
|
|
1003
1351
|
processingError: ['processing_error', fields.processingError],
|
|
1004
1352
|
botId: ['bot_id', fields.botId],
|
|
1005
1353
|
forkedFromId: ['forked_from_id', fields.forkedFromId],
|
|
1354
|
+
initialBotId: ['initial_bot_id', fields.initialBotId],
|
|
1355
|
+
activeBotIdsJson: ['active_bot_ids_json', fields.activeBotIdsJson],
|
|
1006
1356
|
createdAt: ['created_at', fields.createdAt],
|
|
1007
1357
|
updatedAt: ['updated_at', fields.updatedAt],
|
|
1008
1358
|
};
|
|
@@ -1028,6 +1378,21 @@ function updateConversation(id, fields) {
|
|
|
1028
1378
|
vals.push(id);
|
|
1029
1379
|
db.prepare(`UPDATE conversation SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
1030
1380
|
}
|
|
1381
|
+
function syncConversationParticipants(conversationId, botIds, opts) {
|
|
1382
|
+
const conversation = getConversation(conversationId);
|
|
1383
|
+
if (!conversation)
|
|
1384
|
+
return;
|
|
1385
|
+
const normalized = normalizeConversationBotIds(botIds, opts?.initialBotId || conversation.initial_bot_id || conversation.agent_id || null);
|
|
1386
|
+
if (normalized.length === 0)
|
|
1387
|
+
return;
|
|
1388
|
+
const current = normalizeConversationBotIds(conversation.active_bot_ids || [], conversation.initial_bot_id || conversation.agent_id || null);
|
|
1389
|
+
const nextIds = opts?.replace ? normalized : Array.from(new Set([...current, ...normalized]));
|
|
1390
|
+
const nextInitialBotId = String(opts?.initialBotId || conversation.initial_bot_id || conversation.agent_id || nextIds[0] || '').trim() || null;
|
|
1391
|
+
updateConversation(conversationId, {
|
|
1392
|
+
activeBotIdsJson: JSON.stringify(nextIds),
|
|
1393
|
+
initialBotId: nextInitialBotId || undefined,
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1031
1396
|
function touchConversationActivity(id, ts) {
|
|
1032
1397
|
const stamp = ts?.trim() || now();
|
|
1033
1398
|
getDb().prepare(`
|
|
@@ -1039,10 +1404,46 @@ function touchConversationActivity(id, ts) {
|
|
|
1039
1404
|
WHERE id = ?
|
|
1040
1405
|
`).run(stamp, stamp, id);
|
|
1041
1406
|
}
|
|
1407
|
+
function bumpConversationSyncRevision(id, ts) {
|
|
1408
|
+
const stamp = ts?.trim() || now();
|
|
1409
|
+
getDb().prepare(`
|
|
1410
|
+
UPDATE conversation
|
|
1411
|
+
SET sync_revision = coalesce(sync_revision, 0) + 1,
|
|
1412
|
+
updated_at = CASE
|
|
1413
|
+
WHEN datetime(updated_at) > datetime(?) THEN updated_at
|
|
1414
|
+
ELSE ?
|
|
1415
|
+
END
|
|
1416
|
+
WHERE id = ?
|
|
1417
|
+
`).run(stamp, stamp, id);
|
|
1418
|
+
const row = getDb().prepare('SELECT sync_revision FROM conversation WHERE id = ?').get(id);
|
|
1419
|
+
return Number(row?.sync_revision ?? 0);
|
|
1420
|
+
}
|
|
1042
1421
|
function deleteConversation(id) {
|
|
1043
1422
|
return getDb().prepare('DELETE FROM conversation WHERE id = ?').run(id).changes > 0;
|
|
1044
1423
|
}
|
|
1045
|
-
function
|
|
1424
|
+
function toWarmSessionBindingKey(conversationId, botId) {
|
|
1425
|
+
return `${conversationId}::${botId}`;
|
|
1426
|
+
}
|
|
1427
|
+
function parseWarmSessionBindingKey(key) {
|
|
1428
|
+
const idx = key.indexOf('::');
|
|
1429
|
+
if (idx < 0)
|
|
1430
|
+
return null;
|
|
1431
|
+
return { conversationId: key.slice(0, idx), botId: key.slice(idx + 2) };
|
|
1432
|
+
}
|
|
1433
|
+
function epochRowToWarmInfo(row) {
|
|
1434
|
+
return {
|
|
1435
|
+
conversationId: row.conversation_id,
|
|
1436
|
+
sessionScopeKey: row.session_scope_key,
|
|
1437
|
+
botId: row.bot_id,
|
|
1438
|
+
provider: row.provider,
|
|
1439
|
+
sessionId: row.session_id,
|
|
1440
|
+
topicId: row.topic_id,
|
|
1441
|
+
epochTurnCount: row.epoch_turn_count,
|
|
1442
|
+
lastUsedAt: row.last_used_at,
|
|
1443
|
+
resetReason: row.reset_reason,
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
function addMessage(conversationId, role, content, model, toolCallsJson, botId, agentName, createdAt, attachmentsJson) {
|
|
1046
1447
|
const db = getDb();
|
|
1047
1448
|
const id = newId();
|
|
1048
1449
|
const ts = createdAt?.trim() || now();
|
|
@@ -1050,9 +1451,9 @@ function addMessage(conversationId, role, content, model, toolCallsJson, botId,
|
|
|
1050
1451
|
const maxSeq = db.prepare('SELECT COALESCE(MAX(seq), 0) as m FROM message WHERE conversation_id = ?').get(conversationId);
|
|
1051
1452
|
const seq = maxSeq.m + 1;
|
|
1052
1453
|
db.prepare(`
|
|
1053
|
-
INSERT INTO message (id, conversation_id, seq, role, content, model, tool_calls_json, bot_id, agent_name, created_at)
|
|
1054
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1055
|
-
`).run(id, conversationId, seq, role, content, model ?? null, toolCallsJson ?? null, botId ?? null, agentName ?? null, ts);
|
|
1454
|
+
INSERT INTO message (id, conversation_id, seq, role, content, attachments_json, model, tool_calls_json, bot_id, agent_name, created_at)
|
|
1455
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1456
|
+
`).run(id, conversationId, seq, role, content, attachmentsJson ?? null, model ?? null, toolCallsJson ?? null, botId ?? null, agentName ?? null, ts);
|
|
1056
1457
|
// Update conversation message_count and updated_at
|
|
1057
1458
|
db.prepare(`
|
|
1058
1459
|
UPDATE conversation
|
|
@@ -1076,6 +1477,10 @@ function updateMessage(id, fields) {
|
|
|
1076
1477
|
sets.push('content = ?');
|
|
1077
1478
|
vals.push(fields.content);
|
|
1078
1479
|
}
|
|
1480
|
+
if (fields.attachmentsJson !== undefined) {
|
|
1481
|
+
sets.push('attachments_json = ?');
|
|
1482
|
+
vals.push(fields.attachmentsJson);
|
|
1483
|
+
}
|
|
1079
1484
|
if (fields.model !== undefined) {
|
|
1080
1485
|
sets.push('model = ?');
|
|
1081
1486
|
vals.push(fields.model);
|
|
@@ -1123,29 +1528,37 @@ function createChatJob(input) {
|
|
|
1123
1528
|
`).run(id, input.conversationId, input.userMessageId, input.assistantMessageId, input.botId, input.status ?? 'queued', input.requestJson ?? null, ts, ts);
|
|
1124
1529
|
return db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
|
|
1125
1530
|
}
|
|
1126
|
-
function
|
|
1531
|
+
function normalizeCliSessionScopeKey(conversationId, sessionScopeKey) {
|
|
1532
|
+
const trimmed = String(sessionScopeKey || '').trim();
|
|
1533
|
+
return trimmed || conversationId;
|
|
1534
|
+
}
|
|
1535
|
+
function getCliSessionEpoch(conversationId, botId, sessionScopeKey) {
|
|
1536
|
+
const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
|
|
1127
1537
|
return getDb().prepare(`
|
|
1128
1538
|
SELECT *
|
|
1129
1539
|
FROM cli_session_epoch
|
|
1130
|
-
WHERE
|
|
1540
|
+
WHERE session_scope_key = ? AND bot_id = ?
|
|
1131
1541
|
LIMIT 1
|
|
1132
|
-
`).get(
|
|
1542
|
+
`).get(scopeKey, botId);
|
|
1133
1543
|
}
|
|
1134
1544
|
function upsertCliSessionEpoch(input) {
|
|
1135
1545
|
const db = getDb();
|
|
1136
|
-
const
|
|
1546
|
+
const scopeKey = normalizeCliSessionScopeKey(input.conversationId, input.sessionScopeKey);
|
|
1547
|
+
const existing = getCliSessionEpoch(input.conversationId, input.botId, scopeKey);
|
|
1137
1548
|
const id = existing?.id || newId();
|
|
1138
1549
|
const epochStartedAt = input.epochStartedAt || existing?.epoch_started_at || now();
|
|
1139
1550
|
const lastUsedAt = input.lastUsedAt || now();
|
|
1551
|
+
const topicId = input.topicId !== undefined ? (input.topicId || null) : (existing?.topic_id ?? null);
|
|
1140
1552
|
db.prepare(`
|
|
1141
1553
|
INSERT INTO cli_session_epoch (
|
|
1142
|
-
id, conversation_id, bot_id, provider, session_id, epoch_turn_count,
|
|
1554
|
+
id, conversation_id, session_scope_key, bot_id, provider, session_id, topic_id, epoch_turn_count,
|
|
1143
1555
|
last_input_tokens, last_output_tokens, epoch_started_at, last_used_at,
|
|
1144
1556
|
reset_reason, created_at, updated_at
|
|
1145
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1146
|
-
ON CONFLICT(
|
|
1557
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1558
|
+
ON CONFLICT(session_scope_key, bot_id) DO UPDATE SET
|
|
1147
1559
|
provider = excluded.provider,
|
|
1148
1560
|
session_id = excluded.session_id,
|
|
1561
|
+
topic_id = excluded.topic_id,
|
|
1149
1562
|
epoch_turn_count = excluded.epoch_turn_count,
|
|
1150
1563
|
last_input_tokens = excluded.last_input_tokens,
|
|
1151
1564
|
last_output_tokens = excluded.last_output_tokens,
|
|
@@ -1153,14 +1566,42 @@ function upsertCliSessionEpoch(input) {
|
|
|
1153
1566
|
last_used_at = excluded.last_used_at,
|
|
1154
1567
|
reset_reason = excluded.reset_reason,
|
|
1155
1568
|
updated_at = excluded.updated_at
|
|
1156
|
-
`).run(id, input.conversationId, input.botId, input.provider, input.sessionId, input.epochTurnCount, input.lastInputTokens ?? null, input.lastOutputTokens ?? null, epochStartedAt, lastUsedAt, input.resetReason ?? null, existing?.created_at || now(), now());
|
|
1157
|
-
return getCliSessionEpoch(input.conversationId, input.botId);
|
|
1569
|
+
`).run(id, input.conversationId, scopeKey, input.botId, input.provider, input.sessionId, topicId, input.epochTurnCount, input.lastInputTokens ?? null, input.lastOutputTokens ?? null, epochStartedAt, lastUsedAt, input.resetReason ?? null, existing?.created_at || now(), now());
|
|
1570
|
+
return getCliSessionEpoch(input.conversationId, input.botId, scopeKey);
|
|
1571
|
+
}
|
|
1572
|
+
function deleteCliSessionEpoch(conversationId, botId, sessionScopeKey) {
|
|
1573
|
+
const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
|
|
1574
|
+
return getDb().prepare(`
|
|
1575
|
+
DELETE FROM cli_session_epoch
|
|
1576
|
+
WHERE session_scope_key = ? AND bot_id = ?
|
|
1577
|
+
`).run(scopeKey, botId).changes > 0;
|
|
1578
|
+
}
|
|
1579
|
+
function clearCliSessionEpochIfMatches(conversationId, botId, sessionId, sessionScopeKey) {
|
|
1580
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
1581
|
+
if (!normalizedSessionId)
|
|
1582
|
+
return false;
|
|
1583
|
+
const scopeKey = normalizeCliSessionScopeKey(conversationId, sessionScopeKey);
|
|
1584
|
+
return getDb().prepare(`
|
|
1585
|
+
DELETE FROM cli_session_epoch
|
|
1586
|
+
WHERE session_scope_key = ? AND bot_id = ? AND session_id = ?
|
|
1587
|
+
`).run(scopeKey, botId, normalizedSessionId).changes > 0;
|
|
1158
1588
|
}
|
|
1159
|
-
function
|
|
1589
|
+
function cleanupPoisonedClaudePrewarmSessionRows() {
|
|
1160
1590
|
return getDb().prepare(`
|
|
1161
1591
|
DELETE FROM cli_session_epoch
|
|
1162
|
-
WHERE
|
|
1163
|
-
|
|
1592
|
+
WHERE provider = 'claude-cli'
|
|
1593
|
+
AND epoch_turn_count = 0
|
|
1594
|
+
AND reset_reason = 'prewarm'
|
|
1595
|
+
AND session_id LIKE 'fff00000-%'
|
|
1596
|
+
`).run().changes;
|
|
1597
|
+
}
|
|
1598
|
+
function getCliSessionEpochsByConversation(conversationId) {
|
|
1599
|
+
return getDb().prepare(`
|
|
1600
|
+
SELECT *
|
|
1601
|
+
FROM cli_session_epoch
|
|
1602
|
+
WHERE conversation_id = ?
|
|
1603
|
+
ORDER BY last_used_at DESC
|
|
1604
|
+
`).all(conversationId);
|
|
1164
1605
|
}
|
|
1165
1606
|
/**
|
|
1166
1607
|
* Generate the next sequential Funolio session ID.
|
|
@@ -1286,6 +1727,119 @@ function markRunningChatJobsInterrupted(reason = 'Interrupted when app closed')
|
|
|
1286
1727
|
`).run(reason, ts, ts);
|
|
1287
1728
|
return result.changes;
|
|
1288
1729
|
}
|
|
1730
|
+
function recoverInterruptedChatJobs() {
|
|
1731
|
+
const db = getDb();
|
|
1732
|
+
const ts = now();
|
|
1733
|
+
const INTERRUPTED_NOTE = '\n\n[Response interrupted - retry to continue]';
|
|
1734
|
+
const ERROR_MSG = 'Agent was restarted during processing';
|
|
1735
|
+
const orphanedJobs = db.prepare(`
|
|
1736
|
+
SELECT id, status, assistant_message_id
|
|
1737
|
+
FROM chat_job
|
|
1738
|
+
WHERE status IN ('running', 'pending')
|
|
1739
|
+
`).all();
|
|
1740
|
+
if (orphanedJobs.length === 0) {
|
|
1741
|
+
return { recoveredJobs: [], totalRecovered: 0 };
|
|
1742
|
+
}
|
|
1743
|
+
const updateJob = db.prepare(`
|
|
1744
|
+
UPDATE chat_job
|
|
1745
|
+
SET status = 'interrupted',
|
|
1746
|
+
error = ?,
|
|
1747
|
+
updated_at = ?
|
|
1748
|
+
WHERE id = ?
|
|
1749
|
+
`);
|
|
1750
|
+
const getMsg = db.prepare('SELECT id, content FROM message WHERE id = ?');
|
|
1751
|
+
const appendNote = db.prepare('UPDATE message SET content = content || ? WHERE id = ?');
|
|
1752
|
+
const recoveredJobs = [];
|
|
1753
|
+
const runAll = db.transaction(() => {
|
|
1754
|
+
for (const job of orphanedJobs) {
|
|
1755
|
+
updateJob.run(ERROR_MSG, ts, job.id);
|
|
1756
|
+
if (job.assistant_message_id) {
|
|
1757
|
+
const msg = getMsg.get(job.assistant_message_id);
|
|
1758
|
+
if (msg && msg.content && msg.content.trim().length > 0) {
|
|
1759
|
+
appendNote.run(INTERRUPTED_NOTE, msg.id);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
recoveredJobs.push({ jobId: job.id, previousStatus: job.status });
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
runAll();
|
|
1766
|
+
return { recoveredJobs, totalRecovered: recoveredJobs.length };
|
|
1767
|
+
}
|
|
1768
|
+
function recoverInterruptedWorkflowRuns(reason = 'Agent was restarted during workflow execution') {
|
|
1769
|
+
const db = getDb();
|
|
1770
|
+
const ts = now();
|
|
1771
|
+
const runningWorkflowRows = db.prepare(`
|
|
1772
|
+
SELECT id, conversation_id, status
|
|
1773
|
+
FROM workflow_execution
|
|
1774
|
+
WHERE status = 'running'
|
|
1775
|
+
`).all();
|
|
1776
|
+
const runningImportedRuns = db.prepare(`
|
|
1777
|
+
SELECT *
|
|
1778
|
+
FROM imported_plan_run
|
|
1779
|
+
WHERE status = 'running'
|
|
1780
|
+
`).all();
|
|
1781
|
+
const updateSteps = db.prepare(`
|
|
1782
|
+
UPDATE workflow_step_execution
|
|
1783
|
+
SET status = 'failed',
|
|
1784
|
+
error = CASE
|
|
1785
|
+
WHEN error IS NULL OR trim(error) = '' THEN ?
|
|
1786
|
+
ELSE error
|
|
1787
|
+
END,
|
|
1788
|
+
completed_at = COALESCE(completed_at, ?)
|
|
1789
|
+
WHERE workflow_execution_id = ?
|
|
1790
|
+
AND status = 'running'
|
|
1791
|
+
`);
|
|
1792
|
+
const updateWorkflow = db.prepare(`
|
|
1793
|
+
UPDATE workflow_execution
|
|
1794
|
+
SET status = 'failed',
|
|
1795
|
+
error = CASE
|
|
1796
|
+
WHEN error IS NULL OR trim(error) = '' THEN ?
|
|
1797
|
+
ELSE error
|
|
1798
|
+
END,
|
|
1799
|
+
completed_at = COALESCE(completed_at, ?)
|
|
1800
|
+
WHERE id = ?
|
|
1801
|
+
AND status = 'running'
|
|
1802
|
+
`);
|
|
1803
|
+
const updateImportedRun = db.prepare(`
|
|
1804
|
+
UPDATE imported_plan_run
|
|
1805
|
+
SET status = ?,
|
|
1806
|
+
completed_at = CASE WHEN ? = 'completed' THEN COALESCE(completed_at, ?) ELSE completed_at END,
|
|
1807
|
+
last_error = ?,
|
|
1808
|
+
updated_at = datetime('now')
|
|
1809
|
+
WHERE id = ?
|
|
1810
|
+
`);
|
|
1811
|
+
const recoveredWorkflowExecutions = [];
|
|
1812
|
+
const recoveredImportedPlanRuns = [];
|
|
1813
|
+
const runAll = db.transaction(() => {
|
|
1814
|
+
for (const workflow of runningWorkflowRows) {
|
|
1815
|
+
updateSteps.run(reason, ts, workflow.id);
|
|
1816
|
+
updateWorkflow.run(reason, ts, workflow.id);
|
|
1817
|
+
recoveredWorkflowExecutions.push({
|
|
1818
|
+
workflowId: workflow.id,
|
|
1819
|
+
conversationId: workflow.conversation_id,
|
|
1820
|
+
previousStatus: workflow.status,
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
for (const rawRun of runningImportedRuns) {
|
|
1824
|
+
const run = normalizeImportedPlanRunRow(rawRun);
|
|
1825
|
+
const remainingTaskCount = getImportedPlanRunRemainingTaskIds(run.id).length;
|
|
1826
|
+
const nextStatus = remainingTaskCount > 0 ? 'paused' : 'completed';
|
|
1827
|
+
updateImportedRun.run(nextStatus, nextStatus, ts, remainingTaskCount > 0 ? reason : null, run.id);
|
|
1828
|
+
recoveredImportedPlanRuns.push({
|
|
1829
|
+
runId: run.id,
|
|
1830
|
+
conversationId: run.conversation_id,
|
|
1831
|
+
previousStatus: run.status,
|
|
1832
|
+
remainingTaskCount,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
runAll();
|
|
1837
|
+
return {
|
|
1838
|
+
recoveredWorkflowExecutions,
|
|
1839
|
+
recoveredImportedPlanRuns,
|
|
1840
|
+
totalRecovered: recoveredWorkflowExecutions.length + recoveredImportedPlanRuns.length,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1289
1843
|
function createMessageActivity(activity) {
|
|
1290
1844
|
const db = getDb();
|
|
1291
1845
|
const id = newId();
|
|
@@ -1864,6 +2418,274 @@ function updateImportJobItem(id, fields) {
|
|
|
1864
2418
|
vals.push(id);
|
|
1865
2419
|
db.prepare(`UPDATE import_job_item SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
1866
2420
|
}
|
|
2421
|
+
function getImportedPlanFirstKnownOrder(run) {
|
|
2422
|
+
const knownTasks = run.task_ids
|
|
2423
|
+
.map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
|
|
2424
|
+
.filter((task) => !!task);
|
|
2425
|
+
if (knownTasks.length === 0)
|
|
2426
|
+
return null;
|
|
2427
|
+
return Math.min(...knownTasks.map((task) => task.created_order));
|
|
2428
|
+
}
|
|
2429
|
+
function doesTodoBelongToImportedPlanRun(run, task) {
|
|
2430
|
+
if (!task.conversation_id || task.conversation_id !== run.conversation_id)
|
|
2431
|
+
return false;
|
|
2432
|
+
if (run.task_ids.includes(task.id))
|
|
2433
|
+
return true;
|
|
2434
|
+
const firstImportedOrder = getImportedPlanFirstKnownOrder(run);
|
|
2435
|
+
if (firstImportedOrder == null)
|
|
2436
|
+
return false;
|
|
2437
|
+
return task.created_order >= firstImportedOrder;
|
|
2438
|
+
}
|
|
2439
|
+
function touchImportedPlanRunForTask(task) {
|
|
2440
|
+
if (!task.conversation_id)
|
|
2441
|
+
return undefined;
|
|
2442
|
+
const rows = getDb().prepare(`
|
|
2443
|
+
SELECT *
|
|
2444
|
+
FROM imported_plan_run
|
|
2445
|
+
WHERE conversation_id = ?
|
|
2446
|
+
AND status IN ('running', 'paused')
|
|
2447
|
+
ORDER BY datetime(updated_at) DESC, id DESC
|
|
2448
|
+
`).all(task.conversation_id);
|
|
2449
|
+
for (const row of rows) {
|
|
2450
|
+
const run = normalizeImportedPlanRunRow(row);
|
|
2451
|
+
if (!doesTodoBelongToImportedPlanRun(run, task))
|
|
2452
|
+
continue;
|
|
2453
|
+
getDb().prepare(`
|
|
2454
|
+
UPDATE imported_plan_run
|
|
2455
|
+
SET updated_at = datetime('now')
|
|
2456
|
+
WHERE id = ?
|
|
2457
|
+
`).run(run.id);
|
|
2458
|
+
return getImportedPlanRun(run.id);
|
|
2459
|
+
}
|
|
2460
|
+
return undefined;
|
|
2461
|
+
}
|
|
2462
|
+
function parseImportedPlanStageArray(raw) {
|
|
2463
|
+
try {
|
|
2464
|
+
const parsed = JSON.parse(String(raw || '[]'));
|
|
2465
|
+
if (!Array.isArray(parsed))
|
|
2466
|
+
return [];
|
|
2467
|
+
return parsed
|
|
2468
|
+
.map((entry) => ({
|
|
2469
|
+
role: String(entry?.role || '').trim(),
|
|
2470
|
+
botId: String(entry?.botId || '').trim(),
|
|
2471
|
+
}))
|
|
2472
|
+
.filter((entry) => entry.role && entry.botId);
|
|
2473
|
+
}
|
|
2474
|
+
catch {
|
|
2475
|
+
return [];
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function parseImportedPlanOptions(raw) {
|
|
2479
|
+
try {
|
|
2480
|
+
const parsed = JSON.parse(String(raw || '{}'));
|
|
2481
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
2482
|
+
return {};
|
|
2483
|
+
return {
|
|
2484
|
+
plannerInstructions: typeof parsed.plannerInstructions === 'string' ? parsed.plannerInstructions : null,
|
|
2485
|
+
executionOrder: parsed.executionOrder === 'batch_by_stage' ? 'batch_by_stage'
|
|
2486
|
+
: parsed.executionOrder === 'per_item_pipeline' ? 'per_item_pipeline'
|
|
2487
|
+
: null,
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
catch {
|
|
2491
|
+
return {};
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
function parseImportedPlanTaskIds(raw) {
|
|
2495
|
+
try {
|
|
2496
|
+
const parsed = JSON.parse(String(raw || '[]'));
|
|
2497
|
+
if (!Array.isArray(parsed))
|
|
2498
|
+
return [];
|
|
2499
|
+
return parsed
|
|
2500
|
+
.map((value) => Number(value))
|
|
2501
|
+
.filter((value) => Number.isFinite(value) && value > 0);
|
|
2502
|
+
}
|
|
2503
|
+
catch {
|
|
2504
|
+
return [];
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
function normalizeImportedPlanRunRow(row) {
|
|
2508
|
+
return {
|
|
2509
|
+
id: String(row.id),
|
|
2510
|
+
conversation_id: String(row.conversation_id),
|
|
2511
|
+
project_id: row.project_id ? String(row.project_id) : null,
|
|
2512
|
+
source_file_path: String(row.source_file_path || ''),
|
|
2513
|
+
source_file_name: String(row.source_file_name || ''),
|
|
2514
|
+
planner_bot_id: row.planner_bot_id ? String(row.planner_bot_id) : null,
|
|
2515
|
+
planner_agent_name: row.planner_agent_name ? String(row.planner_agent_name) : null,
|
|
2516
|
+
planner_provider: row.planner_provider ? String(row.planner_provider) : null,
|
|
2517
|
+
planner_model: row.planner_model ? String(row.planner_model) : null,
|
|
2518
|
+
requested_stages: parseImportedPlanStageArray(row.requested_stages_json),
|
|
2519
|
+
requested_options: parseImportedPlanOptions(row.requested_options_json),
|
|
2520
|
+
task_ids: parseImportedPlanTaskIds(row.task_ids_json),
|
|
2521
|
+
status: String(row.status || 'paused'),
|
|
2522
|
+
created_at: String(row.created_at || ''),
|
|
2523
|
+
updated_at: String(row.updated_at || ''),
|
|
2524
|
+
started_at: row.started_at ? String(row.started_at) : null,
|
|
2525
|
+
completed_at: row.completed_at ? String(row.completed_at) : null,
|
|
2526
|
+
discarded_at: row.discarded_at ? String(row.discarded_at) : null,
|
|
2527
|
+
last_error: row.last_error ? String(row.last_error) : null,
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
function createImportedPlanRun(params) {
|
|
2531
|
+
const db = getDb();
|
|
2532
|
+
const id = newId();
|
|
2533
|
+
const ts = now();
|
|
2534
|
+
db.prepare(`
|
|
2535
|
+
INSERT INTO imported_plan_run (
|
|
2536
|
+
id, conversation_id, project_id, source_file_path, source_file_name,
|
|
2537
|
+
planner_bot_id, planner_agent_name, planner_provider, planner_model,
|
|
2538
|
+
requested_stages_json, requested_options_json, task_ids_json, status,
|
|
2539
|
+
created_at, updated_at, started_at, completed_at, discarded_at, last_error
|
|
2540
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
2541
|
+
`).run(id, params.conversationId, params.projectId ?? null, params.sourceFilePath, params.sourceFileName, params.plannerBotId ?? null, params.plannerAgentName ?? null, params.plannerProvider ?? null, params.plannerModel ?? null, JSON.stringify(params.requestedStages || []), JSON.stringify(params.requestedOptions || {}), JSON.stringify((params.taskIds || []).filter((taskId) => Number.isFinite(taskId) && taskId > 0)), params.status || 'paused', ts, ts, params.status === 'running' ? ts : null, params.status === 'completed' ? ts : null, params.status === 'discarded' ? ts : null, null);
|
|
2542
|
+
return getImportedPlanRun(id);
|
|
2543
|
+
}
|
|
2544
|
+
function getImportedPlanRun(id) {
|
|
2545
|
+
const row = getDb().prepare('SELECT * FROM imported_plan_run WHERE id = ?').get(id);
|
|
2546
|
+
if (!row)
|
|
2547
|
+
return undefined;
|
|
2548
|
+
return normalizeImportedPlanRunRow(row);
|
|
2549
|
+
}
|
|
2550
|
+
function listImportedPlanRunsForConversation(conversationId) {
|
|
2551
|
+
const rows = getDb().prepare('SELECT * FROM imported_plan_run WHERE conversation_id = ? ORDER BY datetime(created_at) DESC, id DESC').all(conversationId);
|
|
2552
|
+
return rows.map(normalizeImportedPlanRunRow);
|
|
2553
|
+
}
|
|
2554
|
+
function getLatestImportedPlanRunForConversation(conversationId, opts) {
|
|
2555
|
+
const clauses = ['conversation_id = ?'];
|
|
2556
|
+
const params = [conversationId];
|
|
2557
|
+
if (!opts?.includeDiscarded)
|
|
2558
|
+
clauses.push("status <> 'discarded'");
|
|
2559
|
+
if (!opts?.includeCompleted)
|
|
2560
|
+
clauses.push("status <> 'completed'");
|
|
2561
|
+
const row = getDb().prepare(`SELECT * FROM imported_plan_run WHERE ${clauses.join(' AND ')} ORDER BY datetime(updated_at) DESC, id DESC LIMIT 1`).get(...params);
|
|
2562
|
+
if (!row)
|
|
2563
|
+
return undefined;
|
|
2564
|
+
return normalizeImportedPlanRunRow(row);
|
|
2565
|
+
}
|
|
2566
|
+
function updateImportedPlanRun(id, fields) {
|
|
2567
|
+
const db = getDb();
|
|
2568
|
+
const sets = [];
|
|
2569
|
+
const vals = [];
|
|
2570
|
+
if (fields.taskIds !== undefined) {
|
|
2571
|
+
sets.push('task_ids_json = ?');
|
|
2572
|
+
vals.push(JSON.stringify(fields.taskIds));
|
|
2573
|
+
}
|
|
2574
|
+
if (fields.status !== undefined) {
|
|
2575
|
+
sets.push('status = ?');
|
|
2576
|
+
vals.push(fields.status);
|
|
2577
|
+
}
|
|
2578
|
+
if (fields.startedAt !== undefined) {
|
|
2579
|
+
sets.push('started_at = ?');
|
|
2580
|
+
vals.push(fields.startedAt);
|
|
2581
|
+
}
|
|
2582
|
+
if (fields.completedAt !== undefined) {
|
|
2583
|
+
sets.push('completed_at = ?');
|
|
2584
|
+
vals.push(fields.completedAt);
|
|
2585
|
+
}
|
|
2586
|
+
if (fields.discardedAt !== undefined) {
|
|
2587
|
+
sets.push('discarded_at = ?');
|
|
2588
|
+
vals.push(fields.discardedAt);
|
|
2589
|
+
}
|
|
2590
|
+
if (fields.lastError !== undefined) {
|
|
2591
|
+
sets.push('last_error = ?');
|
|
2592
|
+
vals.push(fields.lastError);
|
|
2593
|
+
}
|
|
2594
|
+
if (sets.length === 0)
|
|
2595
|
+
return getImportedPlanRun(id);
|
|
2596
|
+
sets.push("updated_at = datetime('now')");
|
|
2597
|
+
vals.push(id);
|
|
2598
|
+
db.prepare(`UPDATE imported_plan_run SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
2599
|
+
return getImportedPlanRun(id);
|
|
2600
|
+
}
|
|
2601
|
+
function touchImportedPlanRun(id) {
|
|
2602
|
+
getDb().prepare(`
|
|
2603
|
+
UPDATE imported_plan_run
|
|
2604
|
+
SET updated_at = datetime('now')
|
|
2605
|
+
WHERE id = ?
|
|
2606
|
+
`).run(id);
|
|
2607
|
+
return getImportedPlanRun(id);
|
|
2608
|
+
}
|
|
2609
|
+
function getImportedPlanRunTasks(id) {
|
|
2610
|
+
const run = getImportedPlanRun(id);
|
|
2611
|
+
if (!run)
|
|
2612
|
+
return [];
|
|
2613
|
+
const knownTasks = run.task_ids
|
|
2614
|
+
.map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
|
|
2615
|
+
.filter((task) => !!task);
|
|
2616
|
+
if (knownTasks.length === 0) {
|
|
2617
|
+
return run.task_ids
|
|
2618
|
+
.map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
|
|
2619
|
+
.filter((task) => !!task);
|
|
2620
|
+
}
|
|
2621
|
+
const firstImportedOrder = Math.min(...knownTasks.map((task) => task.created_order));
|
|
2622
|
+
const db = getDb();
|
|
2623
|
+
const activeRows = db.prepare(`SELECT * FROM todo_active WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 AND created_order >= ? ORDER BY created_order ASC, id ASC`).all(run.conversation_id, firstImportedOrder);
|
|
2624
|
+
const completedRows = db.prepare(`SELECT * FROM todo_completed WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 AND created_order >= ? ORDER BY created_order ASC, id ASC`).all(run.conversation_id, firstImportedOrder);
|
|
2625
|
+
const allTasks = [
|
|
2626
|
+
...activeRows.map((row) => normalizeTodoRow(row, 'active')),
|
|
2627
|
+
...completedRows.map((row) => normalizeTodoRow(row, 'completed')),
|
|
2628
|
+
].sort((a, b) => (a.created_order - b.created_order) || (a.id - b.id));
|
|
2629
|
+
return allTasks;
|
|
2630
|
+
}
|
|
2631
|
+
function getImportedPlanRunRemainingTaskIds(id) {
|
|
2632
|
+
const run = getImportedPlanRun(id);
|
|
2633
|
+
if (!run)
|
|
2634
|
+
return [];
|
|
2635
|
+
const knownTasks = run.task_ids
|
|
2636
|
+
.map((taskId) => getTodoTask(taskId, 'active') || getTodoTask(taskId, 'completed'))
|
|
2637
|
+
.filter((task) => !!task);
|
|
2638
|
+
if (knownTasks.length === 0) {
|
|
2639
|
+
return run.task_ids.filter((taskId) => !!getTodoTask(taskId, 'active'));
|
|
2640
|
+
}
|
|
2641
|
+
// Resume must follow the current TODO queue, not just the original imported
|
|
2642
|
+
// IDs. QA-fail fix tasks and re-QA companions are inserted into the same
|
|
2643
|
+
// conversation/order after import; if the app resumes later, those inserted
|
|
2644
|
+
// active tasks must run before later original tasks.
|
|
2645
|
+
const firstImportedOrder = Math.min(...knownTasks.map((task) => task.created_order));
|
|
2646
|
+
const rows = getDb().prepare(`
|
|
2647
|
+
SELECT id
|
|
2648
|
+
FROM todo_active
|
|
2649
|
+
WHERE conversation_id = ?
|
|
2650
|
+
AND COALESCE(internal_only, 0) = 0
|
|
2651
|
+
AND created_order >= ?
|
|
2652
|
+
ORDER BY created_order ASC, id ASC
|
|
2653
|
+
`).all(run.conversation_id, firstImportedOrder);
|
|
2654
|
+
return rows.map((row) => Number(row.id)).filter((taskId) => Number.isFinite(taskId) && taskId > 0);
|
|
2655
|
+
}
|
|
2656
|
+
function getImportedPlanRunCurrentTask(id) {
|
|
2657
|
+
return getImportedPlanRunTasks(id).find((task) => task.state === 'active');
|
|
2658
|
+
}
|
|
2659
|
+
function deleteImportedPlanRunSelectedTasks(id, selectedTaskIds, actor) {
|
|
2660
|
+
const db = getDb();
|
|
2661
|
+
const tx = db.transaction(() => {
|
|
2662
|
+
const current = getImportedPlanRun(id);
|
|
2663
|
+
if (!current)
|
|
2664
|
+
throw new Error(`Imported plan run ${id} not found`);
|
|
2665
|
+
if (current.status !== 'paused') {
|
|
2666
|
+
throw new Error('Imported plan tasks can only be edited while the plan is paused.');
|
|
2667
|
+
}
|
|
2668
|
+
const selected = new Set((selectedTaskIds || [])
|
|
2669
|
+
.map((taskId) => Number(taskId))
|
|
2670
|
+
.filter((taskId) => Number.isFinite(taskId) && current.task_ids.includes(taskId)));
|
|
2671
|
+
if (selected.size === 0)
|
|
2672
|
+
return current;
|
|
2673
|
+
for (const taskId of selected) {
|
|
2674
|
+
if (getTodoTask(taskId, 'active')) {
|
|
2675
|
+
deleteTodoTask(taskId, { actor, state: 'active' });
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const remainingTaskIds = current.task_ids.filter((taskId) => !selected.has(taskId) && !!getTodoTask(taskId, 'active'));
|
|
2679
|
+
const nextStatus = remainingTaskIds.length > 0 ? 'paused' : 'discarded';
|
|
2680
|
+
return updateImportedPlanRun(id, {
|
|
2681
|
+
taskIds: remainingTaskIds,
|
|
2682
|
+
status: nextStatus,
|
|
2683
|
+
discardedAt: remainingTaskIds.length > 0 ? null : now(),
|
|
2684
|
+
lastError: null,
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
return tx();
|
|
2688
|
+
}
|
|
1867
2689
|
function createOutboxEvent(e) {
|
|
1868
2690
|
const db = getDb();
|
|
1869
2691
|
const id = newId();
|
|
@@ -1906,7 +2728,7 @@ function setJsonSetting(key, value) {
|
|
|
1906
2728
|
setSetting(key, JSON.stringify(value));
|
|
1907
2729
|
}
|
|
1908
2730
|
function getAllSettings() {
|
|
1909
|
-
const rows = getDb().prepare("SELECT key, value FROM settings WHERE key
|
|
2731
|
+
const rows = getDb().prepare("SELECT key, value FROM settings WHERE substr(key, 1, 1) != '_'").all();
|
|
1910
2732
|
const result = {};
|
|
1911
2733
|
for (const r of rows)
|
|
1912
2734
|
result[r.key] = r.value;
|
|
@@ -2319,6 +3141,7 @@ function normalizeTodoRow(row, state) {
|
|
|
2319
3141
|
return {
|
|
2320
3142
|
id: Number(row.id),
|
|
2321
3143
|
project_id: row.project_id ? String(row.project_id) : null,
|
|
3144
|
+
topic_id: row.topic_id ? String(row.topic_id) : null,
|
|
2322
3145
|
conversation_id: row.conversation_id ? String(row.conversation_id) : null,
|
|
2323
3146
|
origin: row.origin ? String(row.origin) : null,
|
|
2324
3147
|
created_by_bot_id: row.created_by_bot_id ? String(row.created_by_bot_id) : null,
|
|
@@ -2358,9 +3181,9 @@ function roleClassToTaskType(roleClass) {
|
|
|
2358
3181
|
const normalized = String(roleClass || '').trim().toLowerCase();
|
|
2359
3182
|
if (!normalized)
|
|
2360
3183
|
return null;
|
|
2361
|
-
if (normalized === 'code' || normalized === 'coding' || normalized === 'developer')
|
|
3184
|
+
if (normalized === 'code' || normalized === 'coding' || normalized === 'developer' || normalized === 'builder' || normalized === 'engineer')
|
|
2362
3185
|
return 'coding';
|
|
2363
|
-
if (normalized === 'qa' || normalized === 'review')
|
|
3186
|
+
if (normalized === 'qa' || normalized === 'review' || normalized === 'reviewer')
|
|
2364
3187
|
return 'qa';
|
|
2365
3188
|
if (normalized === 'research' || normalized === 'brainstorm' || normalized === 'planning' || normalized === 'plan')
|
|
2366
3189
|
return 'research';
|
|
@@ -2380,7 +3203,11 @@ function inferTaskTypeForAgent(agent, requested) {
|
|
|
2380
3203
|
const explicit = String(requested || '').trim();
|
|
2381
3204
|
if (explicit)
|
|
2382
3205
|
return explicit;
|
|
2383
|
-
return roleClassToTaskType(agent?.
|
|
3206
|
+
return roleClassToTaskType(agent?.orchestration_role_class)
|
|
3207
|
+
|| roleClassToTaskType(agent?.orchestration_role_label)
|
|
3208
|
+
|| roleClassToTaskType(agent?.role_class)
|
|
3209
|
+
|| roleClassToTaskType(agent?.role_label)
|
|
3210
|
+
|| null;
|
|
2384
3211
|
}
|
|
2385
3212
|
function buildQueuedTaskContext(task) {
|
|
2386
3213
|
const lines = [
|
|
@@ -2393,21 +3220,21 @@ function buildQueuedTaskContext(task) {
|
|
|
2393
3220
|
return lines.join('\n');
|
|
2394
3221
|
}
|
|
2395
3222
|
function injectWorkerHandoffIntoPrompt(existingPrompt, handoffText, nextTask) {
|
|
2396
|
-
const
|
|
2397
|
-
|
|
3223
|
+
const safeHandoff = String(handoffText || '');
|
|
3224
|
+
const handoffSection = [
|
|
2398
3225
|
'PREVIOUS WORKER HANDOFF',
|
|
2399
|
-
|
|
3226
|
+
safeHandoff,
|
|
2400
3227
|
'',
|
|
2401
3228
|
buildQueuedTaskContext(nextTask),
|
|
2402
|
-
'',
|
|
2403
|
-
'RUNTIME CONTEXT',
|
|
2404
3229
|
].join('\n');
|
|
2405
3230
|
if (existingPrompt.includes('\nRUNTIME CONTEXT\n')) {
|
|
2406
|
-
return existingPrompt.replace(
|
|
3231
|
+
return existingPrompt.replace(/\nRUNTIME CONTEXT\n/, `\n${handoffSection}\n\nRUNTIME CONTEXT\n`);
|
|
2407
3232
|
}
|
|
2408
3233
|
return [
|
|
3234
|
+
existingPrompt,
|
|
3235
|
+
'',
|
|
2409
3236
|
'PREVIOUS WORKER HANDOFF',
|
|
2410
|
-
|
|
3237
|
+
safeHandoff,
|
|
2411
3238
|
'',
|
|
2412
3239
|
buildQueuedTaskContext(nextTask),
|
|
2413
3240
|
].join('\n');
|
|
@@ -2455,6 +3282,7 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
|
|
|
2455
3282
|
const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2456
3283
|
if (!row)
|
|
2457
3284
|
throw new Error(`Task ${taskId} not found`);
|
|
3285
|
+
const activeTask = normalizeTodoRow(row, 'active');
|
|
2458
3286
|
const completedAt = now();
|
|
2459
3287
|
db.prepare(`
|
|
2460
3288
|
INSERT INTO todo_completed (
|
|
@@ -2468,7 +3296,9 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
|
|
|
2468
3296
|
db.prepare('DELETE FROM todo_active WHERE id = ?').run(taskId);
|
|
2469
3297
|
logTodoAuditTx(db, taskId, 'completed', 'complete', actor, { reason });
|
|
2470
3298
|
const completed = db.prepare('SELECT * FROM todo_completed WHERE id = ?').get(taskId);
|
|
2471
|
-
|
|
3299
|
+
const completedTask = normalizeTodoRow(completed, 'completed');
|
|
3300
|
+
touchImportedPlanRunForTask(activeTask);
|
|
3301
|
+
return completedTask;
|
|
2472
3302
|
}
|
|
2473
3303
|
function determineInsertPositionTx(db, projectId, insertAfterTaskId) {
|
|
2474
3304
|
if (insertAfterTaskId) {
|
|
@@ -2542,43 +3372,35 @@ function resolveAllowedWorkerNamesForInsertion(currentTask) {
|
|
|
2542
3372
|
allowed.add(previous.owner_name.trim().toLowerCase());
|
|
2543
3373
|
return Array.from(allowed);
|
|
2544
3374
|
}
|
|
2545
|
-
function
|
|
3375
|
+
function resolvePreviousWorkerTask(currentTask) {
|
|
2546
3376
|
const db = getDb();
|
|
2547
3377
|
const previous = currentTask.project_id
|
|
2548
3378
|
? db.prepare(`
|
|
2549
|
-
SELECT owner_name
|
|
3379
|
+
SELECT owner_name, task_type
|
|
2550
3380
|
FROM (
|
|
2551
|
-
SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id = ?
|
|
3381
|
+
SELECT owner_name, task_type, created_order, internal_only FROM todo_active WHERE project_id = ?
|
|
2552
3382
|
UNION ALL
|
|
2553
|
-
SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id = ?
|
|
3383
|
+
SELECT owner_name, task_type, created_order, internal_only FROM todo_completed WHERE project_id = ?
|
|
2554
3384
|
)
|
|
2555
3385
|
WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
|
|
2556
3386
|
ORDER BY created_order DESC
|
|
2557
3387
|
LIMIT 1
|
|
2558
3388
|
`).get(currentTask.project_id, currentTask.project_id, currentTask.position)
|
|
2559
3389
|
: db.prepare(`
|
|
2560
|
-
SELECT owner_name
|
|
3390
|
+
SELECT owner_name, task_type
|
|
2561
3391
|
FROM (
|
|
2562
|
-
SELECT owner_name, created_order, internal_only FROM todo_active WHERE project_id IS NULL
|
|
3392
|
+
SELECT owner_name, task_type, created_order, internal_only FROM todo_active WHERE project_id IS NULL
|
|
2563
3393
|
UNION ALL
|
|
2564
|
-
SELECT owner_name, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
|
|
3394
|
+
SELECT owner_name, task_type, created_order, internal_only FROM todo_completed WHERE project_id IS NULL
|
|
2565
3395
|
)
|
|
2566
3396
|
WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
|
|
2567
3397
|
ORDER BY created_order DESC
|
|
2568
3398
|
LIMIT 1
|
|
2569
3399
|
`).get(currentTask.position);
|
|
2570
|
-
return previous
|
|
3400
|
+
return previous || null;
|
|
2571
3401
|
}
|
|
2572
|
-
function
|
|
2573
|
-
|
|
2574
|
-
return false;
|
|
2575
|
-
const combined = `${outputSummary || ''}\n${handoffPrompt || ''}`.trim().toLowerCase();
|
|
2576
|
-
if (!combined)
|
|
2577
|
-
return false;
|
|
2578
|
-
if (/(^|\b)(qa passed|all checks passed|no issues found|looks good to ship)(\b|$)/i.test(combined)) {
|
|
2579
|
-
return false;
|
|
2580
|
-
}
|
|
2581
|
-
return /\b(fix|defect|issue|problem|error|not ready|fails?|failed|remaining problem|remaining issue|validator|re-qa)\b/i.test(combined);
|
|
3402
|
+
function resolvePreviousWorkerName(currentTask) {
|
|
3403
|
+
return resolvePreviousWorkerTask(currentTask)?.owner_name?.trim() || null;
|
|
2582
3404
|
}
|
|
2583
3405
|
function normalizeArtifactInput(ref) {
|
|
2584
3406
|
if (typeof ref === 'string') {
|
|
@@ -2644,13 +3466,22 @@ function listTodoTasks(opts) {
|
|
|
2644
3466
|
const limit = opts?.limit ?? 200;
|
|
2645
3467
|
const offset = opts?.offset ?? 0;
|
|
2646
3468
|
const table = state === 'active' ? 'todo_active' : 'todo_completed';
|
|
3469
|
+
const topicSelect = `
|
|
3470
|
+
(
|
|
3471
|
+
SELECT ts.topic_id
|
|
3472
|
+
FROM topic_segment ts
|
|
3473
|
+
WHERE ts.conversation_id = t.conversation_id
|
|
3474
|
+
ORDER BY datetime(ts.created_at) DESC, ts.id DESC
|
|
3475
|
+
LIMIT 1
|
|
3476
|
+
) AS topic_id
|
|
3477
|
+
`;
|
|
2647
3478
|
const orderBy = state === 'active'
|
|
2648
|
-
? 'ORDER BY created_order ASC, id ASC'
|
|
2649
|
-
: 'ORDER BY datetime(completed_at) DESC, id DESC';
|
|
3479
|
+
? 'ORDER BY t.created_order ASC, t.id ASC'
|
|
3480
|
+
: 'ORDER BY datetime(t.completed_at) DESC, t.id DESC';
|
|
2650
3481
|
const hasProjectFilter = Object.prototype.hasOwnProperty.call(opts || {}, 'projectId') && opts?.projectId !== undefined;
|
|
2651
3482
|
const sql = hasProjectFilter
|
|
2652
|
-
? `SELECT
|
|
2653
|
-
: `SELECT
|
|
3483
|
+
? `SELECT t.*, ${topicSelect} FROM ${table} t WHERE t.project_id ${opts?.projectId ? '= ?' : 'IS NULL'} ${orderBy} LIMIT ? OFFSET ?`
|
|
3484
|
+
: `SELECT t.*, ${topicSelect} FROM ${table} t ${orderBy} LIMIT ? OFFSET ?`;
|
|
2654
3485
|
const rows = hasProjectFilter
|
|
2655
3486
|
? (opts?.projectId
|
|
2656
3487
|
? db.prepare(sql).all(opts.projectId, limit, offset)
|
|
@@ -2823,7 +3654,9 @@ function editTodoTask(taskId, params) {
|
|
|
2823
3654
|
return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2824
3655
|
}
|
|
2825
3656
|
const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2826
|
-
|
|
3657
|
+
const updatedTask = normalizeTodoRow(updated, 'active');
|
|
3658
|
+
touchImportedPlanRunForTask(updatedTask);
|
|
3659
|
+
return updatedTask;
|
|
2827
3660
|
});
|
|
2828
3661
|
return tx();
|
|
2829
3662
|
}
|
|
@@ -2887,7 +3720,9 @@ function checkTodoTask(taskId, params) {
|
|
|
2887
3720
|
return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2888
3721
|
}
|
|
2889
3722
|
const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2890
|
-
|
|
3723
|
+
const updatedTask = normalizeTodoRow(updated, 'active');
|
|
3724
|
+
touchImportedPlanRunForTask(updatedTask);
|
|
3725
|
+
return updatedTask;
|
|
2891
3726
|
});
|
|
2892
3727
|
return tx();
|
|
2893
3728
|
}
|
|
@@ -2942,6 +3777,23 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2942
3777
|
if (!isAssignedWorker && !isOrchestratorActor) {
|
|
2943
3778
|
throw new Error(`Actor "${actorName || 'unknown'}" is not the owner of task ${taskId}`);
|
|
2944
3779
|
}
|
|
3780
|
+
// QA tasks MUST declare qa_result. Codex QA 2026-04-19 (HIGH): the
|
|
3781
|
+
// prompt says so, but the runtime was not enforcing it — a QA
|
|
3782
|
+
// worker that omitted qa_result was silently treated as "pass"
|
|
3783
|
+
// because control fell through to the next_worker handoff branch.
|
|
3784
|
+
// Now rejected at the edge. Orchestrator actors (admin override)
|
|
3785
|
+
// bypass this check — that's for cleanup operations only.
|
|
3786
|
+
const isQaTask = String(currentTask.task_type || '').trim().toLowerCase() === 'qa';
|
|
3787
|
+
if (isQaTask && !isOrchestratorActor) {
|
|
3788
|
+
const incomingQaResult = params.qaResult;
|
|
3789
|
+
if (incomingQaResult !== 'pass' && incomingQaResult !== 'fail') {
|
|
3790
|
+
throw new Error(`QA task ${taskId} must be completed with qa_result='pass' or qa_result='fail'. `
|
|
3791
|
+
+ `Got ${incomingQaResult === undefined ? 'no qa_result' : `qa_result='${String(incomingQaResult)}'`}. `
|
|
3792
|
+
+ `'not_applicable' is not valid on QA tasks. `
|
|
3793
|
+
+ `If QA passed with no defects, set qa_result='pass'. `
|
|
3794
|
+
+ `If defects were found, set qa_result='fail' and include findings.`);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
2945
3797
|
const existingArtifactRows = loadArtifactRowsByIdsTx(db, currentTask.artifact_ids);
|
|
2946
3798
|
const newArtifactRows = resolveArtifactRowsTx(db, params.artifactRefs || [], {
|
|
2947
3799
|
projectId: currentTask.project_id,
|
|
@@ -2968,6 +3820,9 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2968
3820
|
outputSummary,
|
|
2969
3821
|
artifactIds,
|
|
2970
3822
|
inserted: Boolean(params.insertTask),
|
|
3823
|
+
qaResult: params.qaResult ?? null,
|
|
3824
|
+
findings: params.findings ?? null,
|
|
3825
|
+
requiresFixTask: params.requiresFixTask ?? null,
|
|
2971
3826
|
});
|
|
2972
3827
|
const completedTask = moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2973
3828
|
let insertedTask;
|
|
@@ -3022,26 +3877,46 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
3022
3877
|
}
|
|
3023
3878
|
}
|
|
3024
3879
|
else {
|
|
3025
|
-
|
|
3026
|
-
|
|
3880
|
+
const wantsQaFixTask = params.qaResult === 'fail' && params.requiresFixTask !== false;
|
|
3881
|
+
if (wantsQaFixTask) {
|
|
3882
|
+
const previousWorkerTask = resolvePreviousWorkerTask(completedTask);
|
|
3883
|
+
const previousWorkerName = previousWorkerTask?.owner_name?.trim() || null;
|
|
3884
|
+
const previousTaskType = String(previousWorkerTask?.task_type || '').trim().toLowerCase();
|
|
3027
3885
|
const previousWorker = resolveAgentByName(previousWorkerName);
|
|
3028
|
-
|
|
3886
|
+
const findingsText = (params.findings || []).map((s) => String(s || '').trim()).filter(Boolean).join('\n- ');
|
|
3887
|
+
const fixPrompt = findingsText
|
|
3888
|
+
? `QA findings to address:\n- ${findingsText}\n\n${handoffPrompt ? handoffPrompt + '\n\n' : ''}Address the findings above and keep the approved parts unchanged.`
|
|
3889
|
+
: (handoffPrompt || outputSummary || 'Address the QA findings and keep the approved parts unchanged.');
|
|
3890
|
+
if (previousTaskType === 'qa') {
|
|
3891
|
+
returnedToOrchestrator = true;
|
|
3892
|
+
logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
|
|
3893
|
+
handoffPrompt,
|
|
3894
|
+
qaResult: 'fail',
|
|
3895
|
+
findings: params.findings || null,
|
|
3896
|
+
skippedQaFixLoop: true,
|
|
3897
|
+
previousWorkerName,
|
|
3898
|
+
previousTaskType,
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
else if (previousWorkerName && previousWorker) {
|
|
3029
3902
|
const participants = [previousWorker.name];
|
|
3030
3903
|
insertedTask = insertTodoTaskTx(db, {
|
|
3031
3904
|
projectId: completedTask.project_id,
|
|
3032
3905
|
conversationId: completedTask.conversation_id,
|
|
3033
3906
|
title: 'Fix QA findings',
|
|
3034
|
-
details:
|
|
3907
|
+
details: findingsText
|
|
3908
|
+
? `QA findings:\n- ${findingsText}`
|
|
3909
|
+
: (handoffPrompt || outputSummary || 'Address the QA findings and update the implementation.'),
|
|
3035
3910
|
participants,
|
|
3036
3911
|
successCriteria: 'Address the QA findings and return the updated work for re-QA.',
|
|
3037
3912
|
ownerBotId: previousWorker.id,
|
|
3038
3913
|
ownerName: previousWorker.name,
|
|
3039
|
-
taskType: inferTaskTypeForAgent(previousWorker, null),
|
|
3914
|
+
taskType: previousWorkerTask?.task_type || inferTaskTypeForAgent(previousWorker, null),
|
|
3040
3915
|
profileName: completedTask.profile_name || 'Programming',
|
|
3041
3916
|
nextWorkerBotId: currentTask.owner_bot_id,
|
|
3042
3917
|
nextWorkerName: currentTask.owner_name,
|
|
3043
3918
|
nextWorkerRole: actorBot?.role_label || actorBot?.role_class || null,
|
|
3044
|
-
taskPrompt:
|
|
3919
|
+
taskPrompt: fixPrompt,
|
|
3045
3920
|
artifactIds,
|
|
3046
3921
|
insertAfterTaskId: completedTask.id,
|
|
3047
3922
|
actor: params.actor,
|
|
@@ -3049,13 +3924,54 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
3049
3924
|
logTodoAuditTx(db, insertedTask.id, 'active', 'worker_insert', params.actor, {
|
|
3050
3925
|
insertedAfterTaskId: completedTask.id,
|
|
3051
3926
|
requestedWorkerName: previousWorker.name,
|
|
3052
|
-
|
|
3927
|
+
qaFailureFromStructuredField: true,
|
|
3928
|
+
findings: params.findings || null,
|
|
3053
3929
|
});
|
|
3930
|
+
// Companion re-QA TODO: after the previous worker addresses the
|
|
3931
|
+
// findings, the QA worker must re-review. Without this follow-up
|
|
3932
|
+
// task, the fix TODO's next_worker_bot_id points at a QA worker
|
|
3933
|
+
// with no active TODO, and the dispatcher silently skips re-QA
|
|
3934
|
+
// and runs whatever sits next in the queue (item 5 bug).
|
|
3935
|
+
// Chain: fix -> re-QA -> [QA's original next_worker].
|
|
3936
|
+
const currentOwnerId = currentTask.owner_bot_id;
|
|
3937
|
+
const currentOwnerName = currentTask.owner_name;
|
|
3938
|
+
if (currentOwnerId && currentOwnerName) {
|
|
3939
|
+
const reQaTask = insertTodoTaskTx(db, {
|
|
3940
|
+
projectId: completedTask.project_id,
|
|
3941
|
+
conversationId: completedTask.conversation_id,
|
|
3942
|
+
title: `Re-QA after fix`,
|
|
3943
|
+
details: findingsText
|
|
3944
|
+
? `Re-review the previous worker's fix for these findings:\n- ${findingsText}`
|
|
3945
|
+
: 'Re-review the previous worker\'s fix against the original handoff.',
|
|
3946
|
+
participants: [currentOwnerName],
|
|
3947
|
+
successCriteria: 'Re-QA confirms the findings were addressed and the work meets the original spec.',
|
|
3948
|
+
ownerBotId: currentOwnerId,
|
|
3949
|
+
ownerName: currentOwnerName,
|
|
3950
|
+
taskType: 'qa',
|
|
3951
|
+
profileName: completedTask.profile_name || 'Programming',
|
|
3952
|
+
nextWorkerBotId: completedTask.next_worker_bot_id,
|
|
3953
|
+
nextWorkerName: completedTask.next_worker_name,
|
|
3954
|
+
nextWorkerRole: completedTask.next_worker_role,
|
|
3955
|
+
taskPrompt: findingsText
|
|
3956
|
+
? `Re-review the previous worker's fix.\n\nOriginal findings:\n- ${findingsText}\n\nConfirm each finding has been addressed and the work meets the original success criteria.`
|
|
3957
|
+
: 'Re-review the previous worker\'s fix against the original handoff and success criteria.',
|
|
3958
|
+
artifactIds,
|
|
3959
|
+
insertAfterTaskId: insertedTask.id,
|
|
3960
|
+
actor: params.actor,
|
|
3961
|
+
});
|
|
3962
|
+
logTodoAuditTx(db, reQaTask.id, 'active', 'worker_insert', params.actor, {
|
|
3963
|
+
insertedAfterTaskId: insertedTask.id,
|
|
3964
|
+
requestedWorkerName: currentOwnerName,
|
|
3965
|
+
qaReQueueFromStructuredField: true,
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3054
3968
|
}
|
|
3055
3969
|
else {
|
|
3056
3970
|
returnedToOrchestrator = true;
|
|
3057
3971
|
logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
|
|
3058
3972
|
handoffPrompt,
|
|
3973
|
+
qaResult: 'fail',
|
|
3974
|
+
findings: params.findings || null,
|
|
3059
3975
|
missingPreviousWorkerForQaFix: true,
|
|
3060
3976
|
});
|
|
3061
3977
|
}
|
|
@@ -3122,6 +4038,11 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
3122
4038
|
}
|
|
3123
4039
|
}
|
|
3124
4040
|
}
|
|
4041
|
+
touchImportedPlanRunForTask(completedTask);
|
|
4042
|
+
if (insertedTask)
|
|
4043
|
+
touchImportedPlanRunForTask(insertedTask);
|
|
4044
|
+
if (handedOffTask)
|
|
4045
|
+
touchImportedPlanRunForTask(handedOffTask);
|
|
3125
4046
|
return {
|
|
3126
4047
|
completedTask,
|
|
3127
4048
|
insertedTask,
|
|
@@ -3217,6 +4138,7 @@ function blockTodoTaskByWorker(taskId, params) {
|
|
|
3217
4138
|
checkedContext,
|
|
3218
4139
|
userQuestion,
|
|
3219
4140
|
});
|
|
4141
|
+
touchImportedPlanRunForTask(blockedTask);
|
|
3220
4142
|
return {
|
|
3221
4143
|
blockedTask,
|
|
3222
4144
|
artifacts: artifactRows,
|
|
@@ -3226,6 +4148,39 @@ function blockTodoTaskByWorker(taskId, params) {
|
|
|
3226
4148
|
});
|
|
3227
4149
|
return tx();
|
|
3228
4150
|
}
|
|
4151
|
+
function markTodoTaskRuntimeBlocked(taskId, params) {
|
|
4152
|
+
const blockerSummary = String(params.blockerSummary || '').trim();
|
|
4153
|
+
if (!blockerSummary)
|
|
4154
|
+
throw new Error('Runtime blocked task requires blockerSummary');
|
|
4155
|
+
const checkedContext = String(params.checkedContext || 'Runtime/provider error was captured while executing the worker task.').trim();
|
|
4156
|
+
const userQuestion = String(params.userQuestion || 'Fix the provider/runtime issue, then resume the plan.').trim();
|
|
4157
|
+
const db = getDb();
|
|
4158
|
+
const tx = db.transaction(() => {
|
|
4159
|
+
const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
4160
|
+
if (!row)
|
|
4161
|
+
throw new Error(`Task ${taskId} not found`);
|
|
4162
|
+
db.prepare(`
|
|
4163
|
+
UPDATE todo_active
|
|
4164
|
+
SET blocker_summary = ?,
|
|
4165
|
+
blocker_checked = ?,
|
|
4166
|
+
blocker_question = ?,
|
|
4167
|
+
last_updated = ?,
|
|
4168
|
+
version = version + 1
|
|
4169
|
+
WHERE id = ?
|
|
4170
|
+
`).run(blockerSummary, checkedContext, userQuestion, now(), taskId);
|
|
4171
|
+
logTodoAuditTx(db, taskId, 'active', 'worker_blocked', params.actor, {
|
|
4172
|
+
blockerSummary,
|
|
4173
|
+
checkedContext,
|
|
4174
|
+
userQuestion,
|
|
4175
|
+
reasonCode: params.reasonCode || null,
|
|
4176
|
+
runtimeBlocked: true,
|
|
4177
|
+
});
|
|
4178
|
+
const blockedTask = normalizeTodoRow(db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId), 'active');
|
|
4179
|
+
touchImportedPlanRunForTask(blockedTask);
|
|
4180
|
+
return blockedTask;
|
|
4181
|
+
});
|
|
4182
|
+
return tx();
|
|
4183
|
+
}
|
|
3229
4184
|
function deleteTodoTask(taskId, params) {
|
|
3230
4185
|
if (params.actor.actorType !== 'user' && params.actor.actorType !== 'orchestrator')
|
|
3231
4186
|
throw new Error('Only user or orchestrator can delete tasks');
|
|
@@ -3279,7 +4234,11 @@ function reorderTodoTask(taskId, toPosition, actor) {
|
|
|
3279
4234
|
}
|
|
3280
4235
|
logTodoAuditTx(db, taskId, 'active', 'reorder', actor, { toPosition: clamped });
|
|
3281
4236
|
const updated = db.prepare('SELECT * FROM todo_active ORDER BY created_order ASC, id ASC').all();
|
|
3282
|
-
|
|
4237
|
+
const normalized = updated.map((r) => normalizeTodoRow(r, 'active'));
|
|
4238
|
+
const reorderedTask = normalized.find((task) => task.id === taskId);
|
|
4239
|
+
if (reorderedTask)
|
|
4240
|
+
touchImportedPlanRunForTask(reorderedTask);
|
|
4241
|
+
return normalized;
|
|
3283
4242
|
});
|
|
3284
4243
|
return tx();
|
|
3285
4244
|
}
|
|
@@ -3316,6 +4275,15 @@ function listCompletedTodoTasksForConversation(conversationId) {
|
|
|
3316
4275
|
const rows = getDb().prepare('SELECT * FROM todo_completed WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC').all(conversationId);
|
|
3317
4276
|
return rows.map((row) => normalizeTodoRow(row, 'completed'));
|
|
3318
4277
|
}
|
|
4278
|
+
/**
|
|
4279
|
+
* List all active (non-completed) TODOs for a conversation in creation order.
|
|
4280
|
+
* Used by the orchestration dispatch-runner to feed `existing_todos` into
|
|
4281
|
+
* the orchestrator hint so the LLM can see what's already queued.
|
|
4282
|
+
*/
|
|
4283
|
+
function listActiveTodoTasksForConversation(conversationId) {
|
|
4284
|
+
const rows = getDb().prepare('SELECT * FROM todo_active WHERE conversation_id = ? AND COALESCE(internal_only, 0) = 0 ORDER BY created_order ASC, id ASC').all(conversationId);
|
|
4285
|
+
return rows.map((row) => normalizeTodoRow(row, 'active'));
|
|
4286
|
+
}
|
|
3319
4287
|
exports.UNASSIGNED_PROJECT_NAME = 'unassigned';
|
|
3320
4288
|
function createProject(p) {
|
|
3321
4289
|
const db = getDb();
|
|
@@ -3390,12 +4358,22 @@ function getProjectOverview(projectId) {
|
|
|
3390
4358
|
const workflowTemplates = listWorkflowTemplates(projectId);
|
|
3391
4359
|
const roleAssignments = getJsonSetting(`project:${projectId}:role_assignments`)
|
|
3392
4360
|
|| (() => {
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
4361
|
+
const derived = {};
|
|
4362
|
+
for (const bot of bots) {
|
|
4363
|
+
const roleClass = (bot.role_class || '').toLowerCase();
|
|
4364
|
+
if (!roleClass)
|
|
4365
|
+
continue;
|
|
4366
|
+
if (!derived.coding && ['code', 'coding', 'coder', 'builder', 'developer', 'engineer'].includes(roleClass)) {
|
|
4367
|
+
derived.coding = bot.name;
|
|
4368
|
+
}
|
|
4369
|
+
else if (!derived.qa && ['qa', 'review', 'reviewer'].includes(roleClass)) {
|
|
4370
|
+
derived.qa = bot.name;
|
|
4371
|
+
}
|
|
4372
|
+
else if (!derived.research && ['research', 'researcher', 'analyst', 'advisor'].includes(roleClass)) {
|
|
4373
|
+
derived.research = bot.name;
|
|
4374
|
+
}
|
|
3398
4375
|
}
|
|
4376
|
+
return derived;
|
|
3399
4377
|
})();
|
|
3400
4378
|
let preferredWorkflow = null;
|
|
3401
4379
|
const rawWorkflow = projectSettings.orchestrator_preferred_workflow || projectSettings.preferred_workflow;
|
|
@@ -3415,6 +4393,7 @@ function getProjectOverview(projectId) {
|
|
|
3415
4393
|
defaultModel: connection.default_model || null,
|
|
3416
4394
|
hasSecret: connection.access_mode === 'cli' || Boolean(connection.api_key_enc),
|
|
3417
4395
|
}));
|
|
4396
|
+
const clerkConfig = getResolvedClerkConfigInfo();
|
|
3418
4397
|
const userPolicy = getUserOrchestrationPolicy() || null;
|
|
3419
4398
|
const projectPolicyOverride = getProjectOrchestrationPolicyOverride(projectId) || null;
|
|
3420
4399
|
return {
|
|
@@ -3430,9 +4409,9 @@ function getProjectOverview(projectId) {
|
|
|
3430
4409
|
preferred_workflow: preferredWorkflow,
|
|
3431
4410
|
provider_summary: providerSummary,
|
|
3432
4411
|
clerk: {
|
|
3433
|
-
provider:
|
|
3434
|
-
model:
|
|
3435
|
-
configured:
|
|
4412
|
+
provider: clerkConfig.provider,
|
|
4413
|
+
model: clerkConfig.model,
|
|
4414
|
+
configured: clerkConfig.configured,
|
|
3436
4415
|
},
|
|
3437
4416
|
user_policy: userPolicy,
|
|
3438
4417
|
project_policy_override: projectPolicyOverride,
|
|
@@ -3716,7 +4695,7 @@ function getPreviousConversationInTopic(conversationId) {
|
|
|
3716
4695
|
ORDER BY ts.created_at DESC, c.updated_at DESC
|
|
3717
4696
|
LIMIT 1
|
|
3718
4697
|
`).get(topicId, conversationId);
|
|
3719
|
-
return row || null;
|
|
4698
|
+
return hydrateConversationRow(row) || null;
|
|
3720
4699
|
}
|
|
3721
4700
|
function getTopicMessages(topicId) {
|
|
3722
4701
|
const db = getDb();
|