funolio-agent 1.0.53 → 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/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- 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 +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- 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/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.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/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.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 +559 -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/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- 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 +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- 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 +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- 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 +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- 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 +428 -2
- 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 +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- 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 +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- 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/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- 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 +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- 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/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.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 +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- 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-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +81 -5
- 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 +204 -26
- 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/response-guard.js +1 -1
- package/dist/response-guard.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 +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.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 +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- 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 +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -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 +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
package/dist/local-data.js
CHANGED
|
@@ -36,14 +36,25 @@ 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;
|
|
47
|
+
exports.getCurrentOrchestratorSelection = getCurrentOrchestratorSelection;
|
|
48
|
+
exports.getResolvedClerkConfigInfo = getResolvedClerkConfigInfo;
|
|
43
49
|
exports.listAgentProfiles = listAgentProfiles;
|
|
44
50
|
exports.migrateAgentProfilesToDefaultToolProfile = migrateAgentProfilesToDefaultToolProfile;
|
|
45
51
|
exports.updateAgentProfile = updateAgentProfile;
|
|
46
52
|
exports.setOrchestratorBot = setOrchestratorBot;
|
|
53
|
+
exports.setDefaultAgentProfile = setDefaultAgentProfile;
|
|
54
|
+
exports.isClerkOrchestratorEnabled = isClerkOrchestratorEnabled;
|
|
55
|
+
exports.isOrchestrationToolDispatchEnabled = isOrchestrationToolDispatchEnabled;
|
|
56
|
+
exports.setOrchestrationToolDispatchEnabled = setOrchestrationToolDispatchEnabled;
|
|
57
|
+
exports.setClerkAsOrchestrator = setClerkAsOrchestrator;
|
|
47
58
|
exports.createProviderConnection = createProviderConnection;
|
|
48
59
|
exports.listProviderConnections = listProviderConnections;
|
|
49
60
|
exports.getProviderConnection = getProviderConnection;
|
|
@@ -55,26 +66,47 @@ exports.upsertOauthProviderConnection = upsertOauthProviderConnection;
|
|
|
55
66
|
exports.deleteProviderConnection = deleteProviderConnection;
|
|
56
67
|
exports.deleteAgentProfile = deleteAgentProfile;
|
|
57
68
|
exports.createConversation = createConversation;
|
|
69
|
+
exports.getConversationParticipantBotIds = getConversationParticipantBotIds;
|
|
70
|
+
exports.getConversationPrimaryBotId = getConversationPrimaryBotId;
|
|
58
71
|
exports.getConversation = getConversation;
|
|
59
72
|
exports.listConversations = listConversations;
|
|
60
73
|
exports.countConversations = countConversations;
|
|
61
74
|
exports.countMessagesForBot = countMessagesForBot;
|
|
75
|
+
exports.countMessagesForBotSince = countMessagesForBotSince;
|
|
76
|
+
exports.countDecisionsForBot = countDecisionsForBot;
|
|
77
|
+
exports.countBotActiveDays = countBotActiveDays;
|
|
78
|
+
exports.countBotConversationsUpdatedBetween = countBotConversationsUpdatedBetween;
|
|
79
|
+
exports.sumBotTokenUsageSince = sumBotTokenUsageSince;
|
|
62
80
|
exports.listBotConversationActivity = listBotConversationActivity;
|
|
63
81
|
exports.updateConversation = updateConversation;
|
|
82
|
+
exports.syncConversationParticipants = syncConversationParticipants;
|
|
64
83
|
exports.touchConversationActivity = touchConversationActivity;
|
|
84
|
+
exports.bumpConversationSyncRevision = bumpConversationSyncRevision;
|
|
65
85
|
exports.deleteConversation = deleteConversation;
|
|
86
|
+
exports.toWarmSessionBindingKey = toWarmSessionBindingKey;
|
|
87
|
+
exports.parseWarmSessionBindingKey = parseWarmSessionBindingKey;
|
|
88
|
+
exports.epochRowToWarmInfo = epochRowToWarmInfo;
|
|
66
89
|
exports.addMessage = addMessage;
|
|
67
90
|
exports.updateMessage = updateMessage;
|
|
68
91
|
exports.createChatJob = createChatJob;
|
|
69
92
|
exports.getCliSessionEpoch = getCliSessionEpoch;
|
|
70
93
|
exports.upsertCliSessionEpoch = upsertCliSessionEpoch;
|
|
71
94
|
exports.deleteCliSessionEpoch = deleteCliSessionEpoch;
|
|
95
|
+
exports.clearCliSessionEpochIfMatches = clearCliSessionEpochIfMatches;
|
|
96
|
+
exports.cleanupPoisonedClaudePrewarmSessionRows = cleanupPoisonedClaudePrewarmSessionRows;
|
|
97
|
+
exports.getCliSessionEpochsByConversation = getCliSessionEpochsByConversation;
|
|
98
|
+
exports.generateNextSessionId = generateNextSessionId;
|
|
72
99
|
exports.getChatJob = getChatJob;
|
|
73
100
|
exports.getLatestConversationChatJob = getLatestConversationChatJob;
|
|
101
|
+
exports.getLatestConversationBotChatJob = getLatestConversationBotChatJob;
|
|
74
102
|
exports.listQueuedChatJobs = listQueuedChatJobs;
|
|
103
|
+
exports.listRunningChatJobs = listRunningChatJobs;
|
|
104
|
+
exports.listActiveAssistantMessageIds = listActiveAssistantMessageIds;
|
|
75
105
|
exports.countRunningChatJobs = countRunningChatJobs;
|
|
76
106
|
exports.updateChatJob = updateChatJob;
|
|
77
107
|
exports.markRunningChatJobsInterrupted = markRunningChatJobsInterrupted;
|
|
108
|
+
exports.recoverInterruptedChatJobs = recoverInterruptedChatJobs;
|
|
109
|
+
exports.recoverInterruptedWorkflowRuns = recoverInterruptedWorkflowRuns;
|
|
78
110
|
exports.createMessageActivity = createMessageActivity;
|
|
79
111
|
exports.attachMessageActivitiesToMessage = attachMessageActivitiesToMessage;
|
|
80
112
|
exports.listMessageActivities = listMessageActivities;
|
|
@@ -109,6 +141,16 @@ exports.updateImportJob = updateImportJob;
|
|
|
109
141
|
exports.createImportJobItem = createImportJobItem;
|
|
110
142
|
exports.getQueuedImportItems = getQueuedImportItems;
|
|
111
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;
|
|
112
154
|
exports.createOutboxEvent = createOutboxEvent;
|
|
113
155
|
exports.getPendingOutboxEvents = getPendingOutboxEvents;
|
|
114
156
|
exports.markOutboxEventSent = markOutboxEventSent;
|
|
@@ -148,6 +190,7 @@ exports.completeTodoTaskByUser = completeTodoTaskByUser;
|
|
|
148
190
|
exports.listTodoArtifactsForTask = listTodoArtifactsForTask;
|
|
149
191
|
exports.completeTodoTaskByWorker = completeTodoTaskByWorker;
|
|
150
192
|
exports.blockTodoTaskByWorker = blockTodoTaskByWorker;
|
|
193
|
+
exports.markTodoTaskRuntimeBlocked = markTodoTaskRuntimeBlocked;
|
|
151
194
|
exports.deleteTodoTask = deleteTodoTask;
|
|
152
195
|
exports.reorderTodoTask = reorderTodoTask;
|
|
153
196
|
exports.listTodoAudit = listTodoAudit;
|
|
@@ -155,6 +198,7 @@ exports.moveNullTodoTasksToUnassigned = moveNullTodoTasksToUnassigned;
|
|
|
155
198
|
exports.getLatestActiveTodoForConversation = getLatestActiveTodoForConversation;
|
|
156
199
|
exports.getNextActiveTodoForConversation = getNextActiveTodoForConversation;
|
|
157
200
|
exports.listCompletedTodoTasksForConversation = listCompletedTodoTasksForConversation;
|
|
201
|
+
exports.listActiveTodoTasksForConversation = listActiveTodoTasksForConversation;
|
|
158
202
|
exports.createProject = createProject;
|
|
159
203
|
exports.upsertProject = upsertProject;
|
|
160
204
|
exports.getProject = getProject;
|
|
@@ -229,6 +273,7 @@ const crypto = __importStar(require("crypto"));
|
|
|
229
273
|
const default_tool_profile_1 = require("./default-tool-profile");
|
|
230
274
|
const approval_1 = require("./approval");
|
|
231
275
|
let _db = null;
|
|
276
|
+
const CLERK_IS_ORCHESTRATOR_KEY = 'clerk_is_orchestrator';
|
|
232
277
|
/** Get or open the shared database connection */
|
|
233
278
|
function getDb() {
|
|
234
279
|
if (!_db) {
|
|
@@ -249,24 +294,121 @@ function newId() {
|
|
|
249
294
|
function now() {
|
|
250
295
|
return new Date().toISOString().replace('T', ' ').replace('Z', '');
|
|
251
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
|
+
}
|
|
252
375
|
function createAgentProfile(p) {
|
|
253
376
|
const db = getDb();
|
|
254
|
-
const id = newId();
|
|
377
|
+
const id = p.id ?? newId();
|
|
255
378
|
const ts = now();
|
|
256
379
|
const insert = db.prepare(`
|
|
257
380
|
INSERT INTO agent_profile (
|
|
258
381
|
id, provider, model, name, soul_md, memory_md, tools_md, skills_md, api_key_enc,
|
|
259
|
-
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,
|
|
260
384
|
provider_connection_id, color, purpose_md, identity_summary, final_prompt,
|
|
261
|
-
enabled_builtin_tools_json, enabled_mcp_tools_json, show_thinking, is_orchestrator,
|
|
385
|
+
enabled_builtin_tools_json, enabled_mcp_tools_json, show_thinking, is_orchestrator,
|
|
386
|
+
codex_reasoning_effort, codex_reasoning_summary, codex_personality,
|
|
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,
|
|
390
|
+
created_at, updated_at
|
|
262
391
|
)
|
|
263
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
392
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
264
393
|
`);
|
|
265
394
|
const transaction = db.transaction(() => {
|
|
395
|
+
if (p.isDefault) {
|
|
396
|
+
db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1").run();
|
|
397
|
+
}
|
|
266
398
|
if (p.isOrchestrator) {
|
|
267
399
|
db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
|
|
400
|
+
setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
|
|
268
401
|
}
|
|
269
|
-
|
|
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);
|
|
270
412
|
});
|
|
271
413
|
transaction();
|
|
272
414
|
return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(id);
|
|
@@ -280,6 +422,47 @@ function getDefaultAgentProfile() {
|
|
|
280
422
|
function getOrchestratorBot() {
|
|
281
423
|
return getDb().prepare('SELECT * FROM agent_profile WHERE is_orchestrator = 1 ORDER BY updated_at DESC LIMIT 1').get();
|
|
282
424
|
}
|
|
425
|
+
function getCurrentOrchestratorSelection() {
|
|
426
|
+
if (isClerkOrchestratorEnabled()) {
|
|
427
|
+
const clerk = getResolvedClerkConfigInfo();
|
|
428
|
+
return {
|
|
429
|
+
type: 'clerk',
|
|
430
|
+
provider: clerk.provider,
|
|
431
|
+
model: clerk.model,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
const bot = getOrchestratorBot();
|
|
435
|
+
if (bot) {
|
|
436
|
+
return { type: 'bot', botId: bot.id, botName: bot.name };
|
|
437
|
+
}
|
|
438
|
+
return { type: 'none' };
|
|
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
|
+
}
|
|
283
466
|
function listAgentProfiles() {
|
|
284
467
|
return getDb().prepare('SELECT * FROM agent_profile ORDER BY is_default DESC, created_at ASC').all();
|
|
285
468
|
}
|
|
@@ -379,6 +562,18 @@ function updateAgentProfile(id, fields) {
|
|
|
379
562
|
sets.push('role_class = ?');
|
|
380
563
|
vals.push(fields.roleClass);
|
|
381
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
|
+
}
|
|
382
577
|
if (fields.isActive !== undefined) {
|
|
383
578
|
sets.push('is_active = ?');
|
|
384
579
|
vals.push(fields.isActive ? 1 : 0);
|
|
@@ -423,11 +618,72 @@ function updateAgentProfile(id, fields) {
|
|
|
423
618
|
sets.push('is_orchestrator = ?');
|
|
424
619
|
vals.push(fields.isOrchestrator ? 1 : 0);
|
|
425
620
|
}
|
|
621
|
+
if (fields.codexReasoningEffort !== undefined) {
|
|
622
|
+
sets.push('codex_reasoning_effort = ?');
|
|
623
|
+
vals.push(fields.codexReasoningEffort);
|
|
624
|
+
}
|
|
625
|
+
if (fields.codexReasoningSummary !== undefined) {
|
|
626
|
+
sets.push('codex_reasoning_summary = ?');
|
|
627
|
+
vals.push(normalizeCodexReasoningSummary(fields.codexReasoningSummary));
|
|
628
|
+
}
|
|
629
|
+
if (fields.codexPersonality !== undefined) {
|
|
630
|
+
sets.push('codex_personality = ?');
|
|
631
|
+
vals.push(fields.codexPersonality);
|
|
632
|
+
}
|
|
633
|
+
if (fields.codexServiceTier !== undefined) {
|
|
634
|
+
sets.push('codex_service_tier = ?');
|
|
635
|
+
vals.push(fields.codexServiceTier);
|
|
636
|
+
}
|
|
637
|
+
if (fields.codexSandboxPolicy !== undefined) {
|
|
638
|
+
sets.push('codex_sandbox_policy = ?');
|
|
639
|
+
vals.push(fields.codexSandboxPolicy);
|
|
640
|
+
}
|
|
641
|
+
if (fields.codexApprovalPolicy !== undefined) {
|
|
642
|
+
sets.push('codex_approval_policy = ?');
|
|
643
|
+
vals.push(fields.codexApprovalPolicy);
|
|
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
|
+
}
|
|
426
678
|
if (sets.length === 0)
|
|
427
679
|
return row;
|
|
428
680
|
const transaction = db.transaction(() => {
|
|
681
|
+
if (fields.isDefault === true) {
|
|
682
|
+
db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1 AND id <> ?").run(id);
|
|
683
|
+
}
|
|
429
684
|
if (fields.isOrchestrator === true) {
|
|
430
685
|
db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1 AND id <> ?").run(id);
|
|
686
|
+
setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
|
|
431
687
|
}
|
|
432
688
|
sets.push("updated_at = datetime('now')");
|
|
433
689
|
vals.push(id);
|
|
@@ -444,12 +700,57 @@ function setOrchestratorBot(botId) {
|
|
|
444
700
|
const transaction = db.transaction(() => {
|
|
445
701
|
db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
|
|
446
702
|
db.prepare("UPDATE agent_profile SET is_orchestrator = 1, updated_at = datetime('now') WHERE id = ?").run(botId);
|
|
703
|
+
setSetting(CLERK_IS_ORCHESTRATOR_KEY, 'false');
|
|
704
|
+
});
|
|
705
|
+
transaction();
|
|
706
|
+
return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
|
|
707
|
+
}
|
|
708
|
+
function setDefaultAgentProfile(botId) {
|
|
709
|
+
const db = getDb();
|
|
710
|
+
const row = db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
|
|
711
|
+
if (!row)
|
|
712
|
+
return undefined;
|
|
713
|
+
const transaction = db.transaction(() => {
|
|
714
|
+
db.prepare("UPDATE agent_profile SET is_default = 0, updated_at = datetime('now') WHERE is_default = 1").run();
|
|
715
|
+
db.prepare("UPDATE agent_profile SET is_default = 1, updated_at = datetime('now') WHERE id = ?").run(botId);
|
|
447
716
|
});
|
|
448
717
|
transaction();
|
|
449
718
|
return db.prepare('SELECT * FROM agent_profile WHERE id = ?').get(botId);
|
|
450
719
|
}
|
|
720
|
+
function isClerkOrchestratorEnabled() {
|
|
721
|
+
return getSetting(CLERK_IS_ORCHESTRATOR_KEY) === 'true';
|
|
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
|
+
}
|
|
742
|
+
function setClerkAsOrchestrator(enabled) {
|
|
743
|
+
const db = getDb();
|
|
744
|
+
const transaction = db.transaction(() => {
|
|
745
|
+
setSetting(CLERK_IS_ORCHESTRATOR_KEY, enabled ? 'true' : 'false');
|
|
746
|
+
if (enabled) {
|
|
747
|
+
db.prepare("UPDATE agent_profile SET is_orchestrator = 0, updated_at = datetime('now') WHERE is_orchestrator = 1").run();
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
transaction();
|
|
751
|
+
}
|
|
451
752
|
const CLI_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli']);
|
|
452
|
-
const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'cli']);
|
|
753
|
+
const SUPPORTED_PROVIDER_ACCESS_MODES = new Set(['api', 'apiKey', 'cli']);
|
|
453
754
|
function createProviderConnection(p) {
|
|
454
755
|
const db = getDb();
|
|
455
756
|
const id = newId();
|
|
@@ -650,12 +951,40 @@ function deleteAgentProfile(id) {
|
|
|
650
951
|
tx();
|
|
651
952
|
return true;
|
|
652
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
|
+
}
|
|
653
979
|
function createConversation(agentId, title, source, opts) {
|
|
654
980
|
const db = getDb();
|
|
655
981
|
const id = newId();
|
|
656
982
|
const ts = now();
|
|
657
983
|
let projectId = opts?.projectId ?? null;
|
|
658
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);
|
|
659
988
|
if (projectId) {
|
|
660
989
|
const project = getProject(projectId);
|
|
661
990
|
if (project) {
|
|
@@ -673,10 +1002,10 @@ function createConversation(agentId, title, source, opts) {
|
|
|
673
1002
|
projectName = unassigned.name;
|
|
674
1003
|
}
|
|
675
1004
|
db.prepare(`
|
|
676
|
-
INSERT INTO conversation (id, agent_id, title, source, project_id, project_name, created_at, updated_at)
|
|
677
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
678
|
-
`).run(id, agentId, title ?? null, source ?? 'local', projectId, projectName, ts, ts);
|
|
679
|
-
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);
|
|
680
1009
|
}
|
|
681
1010
|
function latestConversationSummarySql(conversationAlias) {
|
|
682
1011
|
return `(
|
|
@@ -770,14 +1099,42 @@ function conversationSelectSql(conversationAlias) {
|
|
|
770
1099
|
function hydrateConversationRow(row) {
|
|
771
1100
|
if (!row)
|
|
772
1101
|
return undefined;
|
|
1102
|
+
const activeBotIds = parseConversationBotIds(row.active_bot_ids_json);
|
|
773
1103
|
return {
|
|
774
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,
|
|
775
1108
|
title: row.resolved_title ?? row.title,
|
|
776
1109
|
summary: row.resolved_summary ?? row.summary,
|
|
777
1110
|
created_at: row.effective_created_at ?? row.created_at,
|
|
778
1111
|
updated_at: row.effective_updated_at ?? row.updated_at,
|
|
779
1112
|
};
|
|
780
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
|
+
}
|
|
781
1138
|
function getConversation(id) {
|
|
782
1139
|
return hydrateConversationRow(getDb().prepare(`${conversationSelectSql('c')} WHERE c.id = ?`).get(id));
|
|
783
1140
|
}
|
|
@@ -790,15 +1147,18 @@ function listConversations(opts) {
|
|
|
790
1147
|
const wheres = [];
|
|
791
1148
|
if (opts?.agentId) {
|
|
792
1149
|
wheres.push(`(
|
|
793
|
-
c.
|
|
1150
|
+
coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
794
1151
|
OR EXISTS (
|
|
795
1152
|
SELECT 1
|
|
796
1153
|
FROM message m
|
|
797
1154
|
WHERE m.conversation_id = c.id
|
|
798
1155
|
AND m.bot_id = ?
|
|
799
1156
|
)
|
|
1157
|
+
OR c.initial_bot_id = ?
|
|
1158
|
+
OR c.agent_id = ?
|
|
800
1159
|
)`);
|
|
801
|
-
|
|
1160
|
+
const botNeedle = `%\"${opts.agentId}\"%`;
|
|
1161
|
+
params.push(botNeedle, opts.agentId, opts.agentId, opts.agentId);
|
|
802
1162
|
}
|
|
803
1163
|
if (opts?.search) {
|
|
804
1164
|
wheres.push(`(
|
|
@@ -828,14 +1188,16 @@ function countConversations(agentId) {
|
|
|
828
1188
|
return db.prepare(`
|
|
829
1189
|
SELECT count(DISTINCT c.id) as cnt
|
|
830
1190
|
FROM conversation c
|
|
831
|
-
WHERE c.
|
|
1191
|
+
WHERE coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
832
1192
|
OR EXISTS (
|
|
833
1193
|
SELECT 1
|
|
834
1194
|
FROM message m
|
|
835
1195
|
WHERE m.conversation_id = c.id
|
|
836
1196
|
AND m.bot_id = ?
|
|
837
1197
|
)
|
|
838
|
-
|
|
1198
|
+
OR c.initial_bot_id = ?
|
|
1199
|
+
OR c.agent_id = ?
|
|
1200
|
+
`).get(`%\"${agentId}\"%`, agentId, agentId, agentId).cnt;
|
|
839
1201
|
}
|
|
840
1202
|
return db.prepare('SELECT count(*) as cnt FROM conversation').get().cnt;
|
|
841
1203
|
}
|
|
@@ -846,6 +1208,74 @@ function countMessagesForBot(botId) {
|
|
|
846
1208
|
WHERE bot_id = ?
|
|
847
1209
|
`).get(botId).cnt;
|
|
848
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
|
+
}
|
|
849
1279
|
function listBotConversationActivity(botId, limit = 10) {
|
|
850
1280
|
const db = getDb();
|
|
851
1281
|
return db.prepare(`
|
|
@@ -866,18 +1296,20 @@ function listBotConversationActivity(botId, limit = 10) {
|
|
|
866
1296
|
FROM (
|
|
867
1297
|
${conversationSelectSql('c')}
|
|
868
1298
|
WHERE (
|
|
869
|
-
c.
|
|
1299
|
+
coalesce(c.active_bot_ids_json, '[]') LIKE ?
|
|
870
1300
|
OR EXISTS (
|
|
871
1301
|
SELECT 1
|
|
872
1302
|
FROM message m
|
|
873
1303
|
WHERE m.conversation_id = c.id
|
|
874
1304
|
AND m.bot_id = ?
|
|
875
1305
|
)
|
|
1306
|
+
OR c.initial_bot_id = ?
|
|
1307
|
+
OR c.agent_id = ?
|
|
876
1308
|
)
|
|
877
1309
|
) conv
|
|
878
1310
|
ORDER BY COALESCE(bot_last_message_at, conv.updated_at, conv.created_at) DESC
|
|
879
1311
|
LIMIT ?
|
|
880
|
-
`).all(botId, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
|
|
1312
|
+
`).all(botId, botId, `%\"${botId}\"%`, botId, botId, botId, limit).map((row) => hydrateConversationRow(row));
|
|
881
1313
|
}
|
|
882
1314
|
function updateConversation(id, fields) {
|
|
883
1315
|
const db = getDb();
|
|
@@ -919,6 +1351,8 @@ function updateConversation(id, fields) {
|
|
|
919
1351
|
processingError: ['processing_error', fields.processingError],
|
|
920
1352
|
botId: ['bot_id', fields.botId],
|
|
921
1353
|
forkedFromId: ['forked_from_id', fields.forkedFromId],
|
|
1354
|
+
initialBotId: ['initial_bot_id', fields.initialBotId],
|
|
1355
|
+
activeBotIdsJson: ['active_bot_ids_json', fields.activeBotIdsJson],
|
|
922
1356
|
createdAt: ['created_at', fields.createdAt],
|
|
923
1357
|
updatedAt: ['updated_at', fields.updatedAt],
|
|
924
1358
|
};
|
|
@@ -944,6 +1378,21 @@ function updateConversation(id, fields) {
|
|
|
944
1378
|
vals.push(id);
|
|
945
1379
|
db.prepare(`UPDATE conversation SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
946
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
|
+
}
|
|
947
1396
|
function touchConversationActivity(id, ts) {
|
|
948
1397
|
const stamp = ts?.trim() || now();
|
|
949
1398
|
getDb().prepare(`
|
|
@@ -955,10 +1404,46 @@ function touchConversationActivity(id, ts) {
|
|
|
955
1404
|
WHERE id = ?
|
|
956
1405
|
`).run(stamp, stamp, id);
|
|
957
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
|
+
}
|
|
958
1421
|
function deleteConversation(id) {
|
|
959
1422
|
return getDb().prepare('DELETE FROM conversation WHERE id = ?').run(id).changes > 0;
|
|
960
1423
|
}
|
|
961
|
-
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) {
|
|
962
1447
|
const db = getDb();
|
|
963
1448
|
const id = newId();
|
|
964
1449
|
const ts = createdAt?.trim() || now();
|
|
@@ -966,9 +1451,9 @@ function addMessage(conversationId, role, content, model, toolCallsJson, botId,
|
|
|
966
1451
|
const maxSeq = db.prepare('SELECT COALESCE(MAX(seq), 0) as m FROM message WHERE conversation_id = ?').get(conversationId);
|
|
967
1452
|
const seq = maxSeq.m + 1;
|
|
968
1453
|
db.prepare(`
|
|
969
|
-
INSERT INTO message (id, conversation_id, seq, role, content, model, tool_calls_json, bot_id, agent_name, created_at)
|
|
970
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
971
|
-
`).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);
|
|
972
1457
|
// Update conversation message_count and updated_at
|
|
973
1458
|
db.prepare(`
|
|
974
1459
|
UPDATE conversation
|
|
@@ -992,6 +1477,10 @@ function updateMessage(id, fields) {
|
|
|
992
1477
|
sets.push('content = ?');
|
|
993
1478
|
vals.push(fields.content);
|
|
994
1479
|
}
|
|
1480
|
+
if (fields.attachmentsJson !== undefined) {
|
|
1481
|
+
sets.push('attachments_json = ?');
|
|
1482
|
+
vals.push(fields.attachmentsJson);
|
|
1483
|
+
}
|
|
995
1484
|
if (fields.model !== undefined) {
|
|
996
1485
|
sets.push('model = ?');
|
|
997
1486
|
vals.push(fields.model);
|
|
@@ -1039,29 +1528,37 @@ function createChatJob(input) {
|
|
|
1039
1528
|
`).run(id, input.conversationId, input.userMessageId, input.assistantMessageId, input.botId, input.status ?? 'queued', input.requestJson ?? null, ts, ts);
|
|
1040
1529
|
return db.prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
|
|
1041
1530
|
}
|
|
1042
|
-
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);
|
|
1043
1537
|
return getDb().prepare(`
|
|
1044
1538
|
SELECT *
|
|
1045
1539
|
FROM cli_session_epoch
|
|
1046
|
-
WHERE
|
|
1540
|
+
WHERE session_scope_key = ? AND bot_id = ?
|
|
1047
1541
|
LIMIT 1
|
|
1048
|
-
`).get(
|
|
1542
|
+
`).get(scopeKey, botId);
|
|
1049
1543
|
}
|
|
1050
1544
|
function upsertCliSessionEpoch(input) {
|
|
1051
1545
|
const db = getDb();
|
|
1052
|
-
const
|
|
1546
|
+
const scopeKey = normalizeCliSessionScopeKey(input.conversationId, input.sessionScopeKey);
|
|
1547
|
+
const existing = getCliSessionEpoch(input.conversationId, input.botId, scopeKey);
|
|
1053
1548
|
const id = existing?.id || newId();
|
|
1054
1549
|
const epochStartedAt = input.epochStartedAt || existing?.epoch_started_at || now();
|
|
1055
1550
|
const lastUsedAt = input.lastUsedAt || now();
|
|
1551
|
+
const topicId = input.topicId !== undefined ? (input.topicId || null) : (existing?.topic_id ?? null);
|
|
1056
1552
|
db.prepare(`
|
|
1057
1553
|
INSERT INTO cli_session_epoch (
|
|
1058
|
-
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,
|
|
1059
1555
|
last_input_tokens, last_output_tokens, epoch_started_at, last_used_at,
|
|
1060
1556
|
reset_reason, created_at, updated_at
|
|
1061
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1062
|
-
ON CONFLICT(
|
|
1557
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1558
|
+
ON CONFLICT(session_scope_key, bot_id) DO UPDATE SET
|
|
1063
1559
|
provider = excluded.provider,
|
|
1064
1560
|
session_id = excluded.session_id,
|
|
1561
|
+
topic_id = excluded.topic_id,
|
|
1065
1562
|
epoch_turn_count = excluded.epoch_turn_count,
|
|
1066
1563
|
last_input_tokens = excluded.last_input_tokens,
|
|
1067
1564
|
last_output_tokens = excluded.last_output_tokens,
|
|
@@ -1069,14 +1566,54 @@ function upsertCliSessionEpoch(input) {
|
|
|
1069
1566
|
last_used_at = excluded.last_used_at,
|
|
1070
1567
|
reset_reason = excluded.reset_reason,
|
|
1071
1568
|
updated_at = excluded.updated_at
|
|
1072
|
-
`).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());
|
|
1073
|
-
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;
|
|
1074
1588
|
}
|
|
1075
|
-
function
|
|
1589
|
+
function cleanupPoisonedClaudePrewarmSessionRows() {
|
|
1076
1590
|
return getDb().prepare(`
|
|
1077
1591
|
DELETE FROM cli_session_epoch
|
|
1078
|
-
WHERE
|
|
1079
|
-
|
|
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);
|
|
1605
|
+
}
|
|
1606
|
+
/**
|
|
1607
|
+
* Generate the next sequential Funolio session ID.
|
|
1608
|
+
* Format: fff00000-0000-4000-8000-NNNNNNNNNNNN
|
|
1609
|
+
* Used by Pattern A providers (Claude CLI) that support --session-id.
|
|
1610
|
+
*/
|
|
1611
|
+
function generateNextSessionId() {
|
|
1612
|
+
getDb().prepare('UPDATE funolio_session_counter SET last_value = last_value + 1 WHERE id = 1').run();
|
|
1613
|
+
const row = getDb().prepare('SELECT last_value FROM funolio_session_counter WHERE id = 1').get();
|
|
1614
|
+
const counter = row?.last_value ?? 1;
|
|
1615
|
+
const hex = counter.toString(16).padStart(12, '0');
|
|
1616
|
+
return `fff00000-0000-4000-8000-${hex}`;
|
|
1080
1617
|
}
|
|
1081
1618
|
function getChatJob(id) {
|
|
1082
1619
|
return getDb().prepare('SELECT * FROM chat_job WHERE id = ?').get(id);
|
|
@@ -1090,6 +1627,16 @@ function getLatestConversationChatJob(conversationId) {
|
|
|
1090
1627
|
LIMIT 1
|
|
1091
1628
|
`).get(conversationId);
|
|
1092
1629
|
}
|
|
1630
|
+
function getLatestConversationBotChatJob(conversationId, botId) {
|
|
1631
|
+
return getDb().prepare(`
|
|
1632
|
+
SELECT *
|
|
1633
|
+
FROM chat_job
|
|
1634
|
+
WHERE conversation_id = ?
|
|
1635
|
+
AND bot_id = ?
|
|
1636
|
+
ORDER BY datetime(created_at) DESC, rowid DESC
|
|
1637
|
+
LIMIT 1
|
|
1638
|
+
`).get(conversationId, botId);
|
|
1639
|
+
}
|
|
1093
1640
|
function listQueuedChatJobs(limit = 50) {
|
|
1094
1641
|
return getDb().prepare(`
|
|
1095
1642
|
SELECT *
|
|
@@ -1099,6 +1646,25 @@ function listQueuedChatJobs(limit = 50) {
|
|
|
1099
1646
|
LIMIT ?
|
|
1100
1647
|
`).all(limit);
|
|
1101
1648
|
}
|
|
1649
|
+
function listRunningChatJobs(limit = 50) {
|
|
1650
|
+
return getDb().prepare(`
|
|
1651
|
+
SELECT *
|
|
1652
|
+
FROM chat_job
|
|
1653
|
+
WHERE status = 'running'
|
|
1654
|
+
ORDER BY datetime(started_at) ASC, datetime(created_at) ASC, rowid ASC
|
|
1655
|
+
LIMIT ?
|
|
1656
|
+
`).all(limit);
|
|
1657
|
+
}
|
|
1658
|
+
function listActiveAssistantMessageIds(conversationId) {
|
|
1659
|
+
return getDb().prepare(`
|
|
1660
|
+
SELECT assistant_message_id
|
|
1661
|
+
FROM chat_job
|
|
1662
|
+
WHERE conversation_id = ?
|
|
1663
|
+
AND status IN ('queued', 'running')
|
|
1664
|
+
AND assistant_message_id IS NOT NULL
|
|
1665
|
+
AND trim(assistant_message_id) <> ''
|
|
1666
|
+
`).all(conversationId).map((row) => String(row.assistant_message_id || '')).filter(Boolean);
|
|
1667
|
+
}
|
|
1102
1668
|
function countRunningChatJobs() {
|
|
1103
1669
|
return getDb().prepare(`
|
|
1104
1670
|
SELECT count(*) AS cnt
|
|
@@ -1161,6 +1727,119 @@ function markRunningChatJobsInterrupted(reason = 'Interrupted when app closed')
|
|
|
1161
1727
|
`).run(reason, ts, ts);
|
|
1162
1728
|
return result.changes;
|
|
1163
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
|
+
}
|
|
1164
1843
|
function createMessageActivity(activity) {
|
|
1165
1844
|
const db = getDb();
|
|
1166
1845
|
const id = newId();
|
|
@@ -1739,6 +2418,274 @@ function updateImportJobItem(id, fields) {
|
|
|
1739
2418
|
vals.push(id);
|
|
1740
2419
|
db.prepare(`UPDATE import_job_item SET ${sets.join(', ')} WHERE id = ?`).run(...vals);
|
|
1741
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
|
+
}
|
|
1742
2689
|
function createOutboxEvent(e) {
|
|
1743
2690
|
const db = getDb();
|
|
1744
2691
|
const id = newId();
|
|
@@ -1781,7 +2728,7 @@ function setJsonSetting(key, value) {
|
|
|
1781
2728
|
setSetting(key, JSON.stringify(value));
|
|
1782
2729
|
}
|
|
1783
2730
|
function getAllSettings() {
|
|
1784
|
-
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();
|
|
1785
2732
|
const result = {};
|
|
1786
2733
|
for (const r of rows)
|
|
1787
2734
|
result[r.key] = r.value;
|
|
@@ -2194,6 +3141,7 @@ function normalizeTodoRow(row, state) {
|
|
|
2194
3141
|
return {
|
|
2195
3142
|
id: Number(row.id),
|
|
2196
3143
|
project_id: row.project_id ? String(row.project_id) : null,
|
|
3144
|
+
topic_id: row.topic_id ? String(row.topic_id) : null,
|
|
2197
3145
|
conversation_id: row.conversation_id ? String(row.conversation_id) : null,
|
|
2198
3146
|
origin: row.origin ? String(row.origin) : null,
|
|
2199
3147
|
created_by_bot_id: row.created_by_bot_id ? String(row.created_by_bot_id) : null,
|
|
@@ -2233,9 +3181,9 @@ function roleClassToTaskType(roleClass) {
|
|
|
2233
3181
|
const normalized = String(roleClass || '').trim().toLowerCase();
|
|
2234
3182
|
if (!normalized)
|
|
2235
3183
|
return null;
|
|
2236
|
-
if (normalized === 'code' || normalized === 'coding' || normalized === 'developer')
|
|
3184
|
+
if (normalized === 'code' || normalized === 'coding' || normalized === 'developer' || normalized === 'builder' || normalized === 'engineer')
|
|
2237
3185
|
return 'coding';
|
|
2238
|
-
if (normalized === 'qa' || normalized === 'review')
|
|
3186
|
+
if (normalized === 'qa' || normalized === 'review' || normalized === 'reviewer')
|
|
2239
3187
|
return 'qa';
|
|
2240
3188
|
if (normalized === 'research' || normalized === 'brainstorm' || normalized === 'planning' || normalized === 'plan')
|
|
2241
3189
|
return 'research';
|
|
@@ -2255,7 +3203,11 @@ function inferTaskTypeForAgent(agent, requested) {
|
|
|
2255
3203
|
const explicit = String(requested || '').trim();
|
|
2256
3204
|
if (explicit)
|
|
2257
3205
|
return explicit;
|
|
2258
|
-
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;
|
|
2259
3211
|
}
|
|
2260
3212
|
function buildQueuedTaskContext(task) {
|
|
2261
3213
|
const lines = [
|
|
@@ -2268,21 +3220,21 @@ function buildQueuedTaskContext(task) {
|
|
|
2268
3220
|
return lines.join('\n');
|
|
2269
3221
|
}
|
|
2270
3222
|
function injectWorkerHandoffIntoPrompt(existingPrompt, handoffText, nextTask) {
|
|
2271
|
-
const
|
|
2272
|
-
|
|
3223
|
+
const safeHandoff = String(handoffText || '');
|
|
3224
|
+
const handoffSection = [
|
|
2273
3225
|
'PREVIOUS WORKER HANDOFF',
|
|
2274
|
-
|
|
3226
|
+
safeHandoff,
|
|
2275
3227
|
'',
|
|
2276
3228
|
buildQueuedTaskContext(nextTask),
|
|
2277
|
-
'',
|
|
2278
|
-
'RUNTIME CONTEXT',
|
|
2279
3229
|
].join('\n');
|
|
2280
3230
|
if (existingPrompt.includes('\nRUNTIME CONTEXT\n')) {
|
|
2281
|
-
return existingPrompt.replace(
|
|
3231
|
+
return existingPrompt.replace(/\nRUNTIME CONTEXT\n/, `\n${handoffSection}\n\nRUNTIME CONTEXT\n`);
|
|
2282
3232
|
}
|
|
2283
3233
|
return [
|
|
3234
|
+
existingPrompt,
|
|
3235
|
+
'',
|
|
2284
3236
|
'PREVIOUS WORKER HANDOFF',
|
|
2285
|
-
|
|
3237
|
+
safeHandoff,
|
|
2286
3238
|
'',
|
|
2287
3239
|
buildQueuedTaskContext(nextTask),
|
|
2288
3240
|
].join('\n');
|
|
@@ -2330,6 +3282,7 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
|
|
|
2330
3282
|
const row = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2331
3283
|
if (!row)
|
|
2332
3284
|
throw new Error(`Task ${taskId} not found`);
|
|
3285
|
+
const activeTask = normalizeTodoRow(row, 'active');
|
|
2333
3286
|
const completedAt = now();
|
|
2334
3287
|
db.prepare(`
|
|
2335
3288
|
INSERT INTO todo_completed (
|
|
@@ -2343,7 +3296,9 @@ function moveActiveTaskToCompletedTx(db, taskId, actor, reason) {
|
|
|
2343
3296
|
db.prepare('DELETE FROM todo_active WHERE id = ?').run(taskId);
|
|
2344
3297
|
logTodoAuditTx(db, taskId, 'completed', 'complete', actor, { reason });
|
|
2345
3298
|
const completed = db.prepare('SELECT * FROM todo_completed WHERE id = ?').get(taskId);
|
|
2346
|
-
|
|
3299
|
+
const completedTask = normalizeTodoRow(completed, 'completed');
|
|
3300
|
+
touchImportedPlanRunForTask(activeTask);
|
|
3301
|
+
return completedTask;
|
|
2347
3302
|
}
|
|
2348
3303
|
function determineInsertPositionTx(db, projectId, insertAfterTaskId) {
|
|
2349
3304
|
if (insertAfterTaskId) {
|
|
@@ -2417,43 +3372,35 @@ function resolveAllowedWorkerNamesForInsertion(currentTask) {
|
|
|
2417
3372
|
allowed.add(previous.owner_name.trim().toLowerCase());
|
|
2418
3373
|
return Array.from(allowed);
|
|
2419
3374
|
}
|
|
2420
|
-
function
|
|
3375
|
+
function resolvePreviousWorkerTask(currentTask) {
|
|
2421
3376
|
const db = getDb();
|
|
2422
3377
|
const previous = currentTask.project_id
|
|
2423
3378
|
? db.prepare(`
|
|
2424
|
-
SELECT owner_name
|
|
3379
|
+
SELECT owner_name, task_type
|
|
2425
3380
|
FROM (
|
|
2426
|
-
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 = ?
|
|
2427
3382
|
UNION ALL
|
|
2428
|
-
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 = ?
|
|
2429
3384
|
)
|
|
2430
3385
|
WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
|
|
2431
3386
|
ORDER BY created_order DESC
|
|
2432
3387
|
LIMIT 1
|
|
2433
3388
|
`).get(currentTask.project_id, currentTask.project_id, currentTask.position)
|
|
2434
3389
|
: db.prepare(`
|
|
2435
|
-
SELECT owner_name
|
|
3390
|
+
SELECT owner_name, task_type
|
|
2436
3391
|
FROM (
|
|
2437
|
-
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
|
|
2438
3393
|
UNION ALL
|
|
2439
|
-
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
|
|
2440
3395
|
)
|
|
2441
3396
|
WHERE created_order < ? AND COALESCE(internal_only, 0) = 0 AND owner_name IS NOT NULL
|
|
2442
3397
|
ORDER BY created_order DESC
|
|
2443
3398
|
LIMIT 1
|
|
2444
3399
|
`).get(currentTask.position);
|
|
2445
|
-
return previous
|
|
3400
|
+
return previous || null;
|
|
2446
3401
|
}
|
|
2447
|
-
function
|
|
2448
|
-
|
|
2449
|
-
return false;
|
|
2450
|
-
const combined = `${outputSummary || ''}\n${handoffPrompt || ''}`.trim().toLowerCase();
|
|
2451
|
-
if (!combined)
|
|
2452
|
-
return false;
|
|
2453
|
-
if (/(^|\b)(qa passed|all checks passed|no issues found|looks good to ship)(\b|$)/i.test(combined)) {
|
|
2454
|
-
return false;
|
|
2455
|
-
}
|
|
2456
|
-
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;
|
|
2457
3404
|
}
|
|
2458
3405
|
function normalizeArtifactInput(ref) {
|
|
2459
3406
|
if (typeof ref === 'string') {
|
|
@@ -2519,13 +3466,22 @@ function listTodoTasks(opts) {
|
|
|
2519
3466
|
const limit = opts?.limit ?? 200;
|
|
2520
3467
|
const offset = opts?.offset ?? 0;
|
|
2521
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
|
+
`;
|
|
2522
3478
|
const orderBy = state === 'active'
|
|
2523
|
-
? 'ORDER BY created_order ASC, id ASC'
|
|
2524
|
-
: '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';
|
|
2525
3481
|
const hasProjectFilter = Object.prototype.hasOwnProperty.call(opts || {}, 'projectId') && opts?.projectId !== undefined;
|
|
2526
3482
|
const sql = hasProjectFilter
|
|
2527
|
-
? `SELECT
|
|
2528
|
-
: `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 ?`;
|
|
2529
3485
|
const rows = hasProjectFilter
|
|
2530
3486
|
? (opts?.projectId
|
|
2531
3487
|
? db.prepare(sql).all(opts.projectId, limit, offset)
|
|
@@ -2698,7 +3654,9 @@ function editTodoTask(taskId, params) {
|
|
|
2698
3654
|
return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2699
3655
|
}
|
|
2700
3656
|
const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2701
|
-
|
|
3657
|
+
const updatedTask = normalizeTodoRow(updated, 'active');
|
|
3658
|
+
touchImportedPlanRunForTask(updatedTask);
|
|
3659
|
+
return updatedTask;
|
|
2702
3660
|
});
|
|
2703
3661
|
return tx();
|
|
2704
3662
|
}
|
|
@@ -2762,7 +3720,9 @@ function checkTodoTask(taskId, params) {
|
|
|
2762
3720
|
return moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2763
3721
|
}
|
|
2764
3722
|
const updated = db.prepare('SELECT * FROM todo_active WHERE id = ?').get(taskId);
|
|
2765
|
-
|
|
3723
|
+
const updatedTask = normalizeTodoRow(updated, 'active');
|
|
3724
|
+
touchImportedPlanRunForTask(updatedTask);
|
|
3725
|
+
return updatedTask;
|
|
2766
3726
|
});
|
|
2767
3727
|
return tx();
|
|
2768
3728
|
}
|
|
@@ -2817,6 +3777,23 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2817
3777
|
if (!isAssignedWorker && !isOrchestratorActor) {
|
|
2818
3778
|
throw new Error(`Actor "${actorName || 'unknown'}" is not the owner of task ${taskId}`);
|
|
2819
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
|
+
}
|
|
2820
3797
|
const existingArtifactRows = loadArtifactRowsByIdsTx(db, currentTask.artifact_ids);
|
|
2821
3798
|
const newArtifactRows = resolveArtifactRowsTx(db, params.artifactRefs || [], {
|
|
2822
3799
|
projectId: currentTask.project_id,
|
|
@@ -2843,6 +3820,9 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2843
3820
|
outputSummary,
|
|
2844
3821
|
artifactIds,
|
|
2845
3822
|
inserted: Boolean(params.insertTask),
|
|
3823
|
+
qaResult: params.qaResult ?? null,
|
|
3824
|
+
findings: params.findings ?? null,
|
|
3825
|
+
requiresFixTask: params.requiresFixTask ?? null,
|
|
2846
3826
|
});
|
|
2847
3827
|
const completedTask = moveActiveTaskToCompletedTx(db, taskId, params.actor, 'all_llms_complete');
|
|
2848
3828
|
let insertedTask;
|
|
@@ -2897,26 +3877,46 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2897
3877
|
}
|
|
2898
3878
|
}
|
|
2899
3879
|
else {
|
|
2900
|
-
|
|
2901
|
-
|
|
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();
|
|
2902
3885
|
const previousWorker = resolveAgentByName(previousWorkerName);
|
|
2903
|
-
|
|
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) {
|
|
2904
3902
|
const participants = [previousWorker.name];
|
|
2905
3903
|
insertedTask = insertTodoTaskTx(db, {
|
|
2906
3904
|
projectId: completedTask.project_id,
|
|
2907
3905
|
conversationId: completedTask.conversation_id,
|
|
2908
3906
|
title: 'Fix QA findings',
|
|
2909
|
-
details:
|
|
3907
|
+
details: findingsText
|
|
3908
|
+
? `QA findings:\n- ${findingsText}`
|
|
3909
|
+
: (handoffPrompt || outputSummary || 'Address the QA findings and update the implementation.'),
|
|
2910
3910
|
participants,
|
|
2911
3911
|
successCriteria: 'Address the QA findings and return the updated work for re-QA.',
|
|
2912
3912
|
ownerBotId: previousWorker.id,
|
|
2913
3913
|
ownerName: previousWorker.name,
|
|
2914
|
-
taskType: inferTaskTypeForAgent(previousWorker, null),
|
|
3914
|
+
taskType: previousWorkerTask?.task_type || inferTaskTypeForAgent(previousWorker, null),
|
|
2915
3915
|
profileName: completedTask.profile_name || 'Programming',
|
|
2916
3916
|
nextWorkerBotId: currentTask.owner_bot_id,
|
|
2917
3917
|
nextWorkerName: currentTask.owner_name,
|
|
2918
3918
|
nextWorkerRole: actorBot?.role_label || actorBot?.role_class || null,
|
|
2919
|
-
taskPrompt:
|
|
3919
|
+
taskPrompt: fixPrompt,
|
|
2920
3920
|
artifactIds,
|
|
2921
3921
|
insertAfterTaskId: completedTask.id,
|
|
2922
3922
|
actor: params.actor,
|
|
@@ -2924,13 +3924,54 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2924
3924
|
logTodoAuditTx(db, insertedTask.id, 'active', 'worker_insert', params.actor, {
|
|
2925
3925
|
insertedAfterTaskId: completedTask.id,
|
|
2926
3926
|
requestedWorkerName: previousWorker.name,
|
|
2927
|
-
|
|
3927
|
+
qaFailureFromStructuredField: true,
|
|
3928
|
+
findings: params.findings || null,
|
|
2928
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
|
+
}
|
|
2929
3968
|
}
|
|
2930
3969
|
else {
|
|
2931
3970
|
returnedToOrchestrator = true;
|
|
2932
3971
|
logTodoAuditTx(db, taskId, 'completed', 'return_to_orchestrator', params.actor, {
|
|
2933
3972
|
handoffPrompt,
|
|
3973
|
+
qaResult: 'fail',
|
|
3974
|
+
findings: params.findings || null,
|
|
2934
3975
|
missingPreviousWorkerForQaFix: true,
|
|
2935
3976
|
});
|
|
2936
3977
|
}
|
|
@@ -2997,6 +4038,11 @@ function completeTodoTaskByWorker(taskId, params) {
|
|
|
2997
4038
|
}
|
|
2998
4039
|
}
|
|
2999
4040
|
}
|
|
4041
|
+
touchImportedPlanRunForTask(completedTask);
|
|
4042
|
+
if (insertedTask)
|
|
4043
|
+
touchImportedPlanRunForTask(insertedTask);
|
|
4044
|
+
if (handedOffTask)
|
|
4045
|
+
touchImportedPlanRunForTask(handedOffTask);
|
|
3000
4046
|
return {
|
|
3001
4047
|
completedTask,
|
|
3002
4048
|
insertedTask,
|
|
@@ -3092,6 +4138,7 @@ function blockTodoTaskByWorker(taskId, params) {
|
|
|
3092
4138
|
checkedContext,
|
|
3093
4139
|
userQuestion,
|
|
3094
4140
|
});
|
|
4141
|
+
touchImportedPlanRunForTask(blockedTask);
|
|
3095
4142
|
return {
|
|
3096
4143
|
blockedTask,
|
|
3097
4144
|
artifacts: artifactRows,
|
|
@@ -3101,6 +4148,39 @@ function blockTodoTaskByWorker(taskId, params) {
|
|
|
3101
4148
|
});
|
|
3102
4149
|
return tx();
|
|
3103
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
|
+
}
|
|
3104
4184
|
function deleteTodoTask(taskId, params) {
|
|
3105
4185
|
if (params.actor.actorType !== 'user' && params.actor.actorType !== 'orchestrator')
|
|
3106
4186
|
throw new Error('Only user or orchestrator can delete tasks');
|
|
@@ -3154,7 +4234,11 @@ function reorderTodoTask(taskId, toPosition, actor) {
|
|
|
3154
4234
|
}
|
|
3155
4235
|
logTodoAuditTx(db, taskId, 'active', 'reorder', actor, { toPosition: clamped });
|
|
3156
4236
|
const updated = db.prepare('SELECT * FROM todo_active ORDER BY created_order ASC, id ASC').all();
|
|
3157
|
-
|
|
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;
|
|
3158
4242
|
});
|
|
3159
4243
|
return tx();
|
|
3160
4244
|
}
|
|
@@ -3191,6 +4275,15 @@ function listCompletedTodoTasksForConversation(conversationId) {
|
|
|
3191
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);
|
|
3192
4276
|
return rows.map((row) => normalizeTodoRow(row, 'completed'));
|
|
3193
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
|
+
}
|
|
3194
4287
|
exports.UNASSIGNED_PROJECT_NAME = 'unassigned';
|
|
3195
4288
|
function createProject(p) {
|
|
3196
4289
|
const db = getDb();
|
|
@@ -3265,12 +4358,22 @@ function getProjectOverview(projectId) {
|
|
|
3265
4358
|
const workflowTemplates = listWorkflowTemplates(projectId);
|
|
3266
4359
|
const roleAssignments = getJsonSetting(`project:${projectId}:role_assignments`)
|
|
3267
4360
|
|| (() => {
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
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
|
+
}
|
|
3273
4375
|
}
|
|
4376
|
+
return derived;
|
|
3274
4377
|
})();
|
|
3275
4378
|
let preferredWorkflow = null;
|
|
3276
4379
|
const rawWorkflow = projectSettings.orchestrator_preferred_workflow || projectSettings.preferred_workflow;
|
|
@@ -3290,6 +4393,7 @@ function getProjectOverview(projectId) {
|
|
|
3290
4393
|
defaultModel: connection.default_model || null,
|
|
3291
4394
|
hasSecret: connection.access_mode === 'cli' || Boolean(connection.api_key_enc),
|
|
3292
4395
|
}));
|
|
4396
|
+
const clerkConfig = getResolvedClerkConfigInfo();
|
|
3293
4397
|
const userPolicy = getUserOrchestrationPolicy() || null;
|
|
3294
4398
|
const projectPolicyOverride = getProjectOrchestrationPolicyOverride(projectId) || null;
|
|
3295
4399
|
return {
|
|
@@ -3305,9 +4409,9 @@ function getProjectOverview(projectId) {
|
|
|
3305
4409
|
preferred_workflow: preferredWorkflow,
|
|
3306
4410
|
provider_summary: providerSummary,
|
|
3307
4411
|
clerk: {
|
|
3308
|
-
provider:
|
|
3309
|
-
model:
|
|
3310
|
-
configured:
|
|
4412
|
+
provider: clerkConfig.provider,
|
|
4413
|
+
model: clerkConfig.model,
|
|
4414
|
+
configured: clerkConfig.configured,
|
|
3311
4415
|
},
|
|
3312
4416
|
user_policy: userPolicy,
|
|
3313
4417
|
project_policy_override: projectPolicyOverride,
|
|
@@ -3439,7 +4543,18 @@ function updateTopic(id, fields) {
|
|
|
3439
4543
|
return db.prepare('SELECT * FROM topic WHERE id = ?').get(id);
|
|
3440
4544
|
}
|
|
3441
4545
|
function deleteTopic(id) {
|
|
3442
|
-
|
|
4546
|
+
const db = getDb();
|
|
4547
|
+
const tx = db.transaction((topicId) => {
|
|
4548
|
+
const topicExists = db.prepare('SELECT id FROM topic WHERE id = ?').get(topicId);
|
|
4549
|
+
if (!topicExists)
|
|
4550
|
+
return false;
|
|
4551
|
+
const linkedConversationIds = db.prepare('SELECT DISTINCT conversation_id FROM topic_segment WHERE topic_id = ?').all(topicId).map((row) => row.conversation_id);
|
|
4552
|
+
for (const conversationId of linkedConversationIds) {
|
|
4553
|
+
db.prepare('DELETE FROM conversation WHERE id = ?').run(conversationId);
|
|
4554
|
+
}
|
|
4555
|
+
return db.prepare('DELETE FROM topic WHERE id = ?').run(topicId).changes > 0;
|
|
4556
|
+
});
|
|
4557
|
+
return tx(id);
|
|
3443
4558
|
}
|
|
3444
4559
|
function createTopicSegment(s) {
|
|
3445
4560
|
const db = getDb();
|
|
@@ -3580,7 +4695,7 @@ function getPreviousConversationInTopic(conversationId) {
|
|
|
3580
4695
|
ORDER BY ts.created_at DESC, c.updated_at DESC
|
|
3581
4696
|
LIMIT 1
|
|
3582
4697
|
`).get(topicId, conversationId);
|
|
3583
|
-
return row || null;
|
|
4698
|
+
return hydrateConversationRow(row) || null;
|
|
3584
4699
|
}
|
|
3585
4700
|
function getTopicMessages(topicId) {
|
|
3586
4701
|
const db = getDb();
|