tide-commander 1.32.2 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/assets/{BossLogsModal-2lqGyuCD.js → BossLogsModal-B_dgVF7L.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-CR-ofDeB.js → BossSpawnModal-CSO1bYxA.js} +1 -1
  3. package/dist/assets/{ControlsModal-DQ8QVshA.js → ControlsModal-WMTTqbca.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-D6yQTHy7.js → DockerLogsModal-THzhLHch.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-wechGxGl.js → EmbeddedEditor-DWLKJYav.js} +1 -1
  6. package/dist/assets/GmailOAuthSetup-DN9ceaS6.js +270 -0
  7. package/dist/assets/{GoogleOAuthSetup-BcpJEydM.js → GoogleOAuthSetup-bVST2EOB.js} +1 -1
  8. package/dist/assets/{IframeModal-E2E7NR08.js → IframeModal-BELsjvgi.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CrS2QOFR.js → IntegrationsPanel-DwDr4BRt.js} +2 -2
  10. package/dist/assets/{LogViewerModal-BpIPZeFr.js → LogViewerModal-CMe04PO5.js} +1 -1
  11. package/dist/assets/MonitoringModal-CqSalNeY.js +1 -0
  12. package/dist/assets/{PM2LogsModal-CDk_2mi1.js → PM2LogsModal-CCmCDxVt.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BmgBrk9J.js → RestoreArchivedAreaModal-IfzPidIv.js} +1 -1
  14. package/dist/assets/{SaveSnapshotModal-DWta9pcx.js → SaveSnapshotModal-DUhrVD5l.js} +1 -1
  15. package/dist/assets/{Scene2DCanvas-C3CcFsjU.js → Scene2DCanvas-Bl5DUC7w.js} +1 -1
  16. package/dist/assets/{SceneManager-mFUakRNl.js → SceneManager-BGO9tiaI.js} +1 -1
  17. package/dist/assets/{SkillsPanel-Br3h8GNh.js → SkillsPanel-CPFOI4Tl.js} +1 -1
  18. package/dist/assets/{SnapshotManager-CPYWfnPR.js → SnapshotManager-Cbu0tJBz.js} +1 -1
  19. package/dist/assets/{SpawnModal-DSDirR0j.js → SpawnModal-BqDbsYLY.js} +1 -1
  20. package/dist/assets/{SubordinateAssignmentModal-Daz67phV.js → SubordinateAssignmentModal-DOqkhL_L.js} +1 -1
  21. package/dist/assets/{SupervisorPanel-DoAl5e8W.js → SupervisorPanel-BvX-dlk_.js} +1 -1
  22. package/dist/assets/{TriggerManagerPanel-CVIHbqDt.js → TriggerManagerPanel-RUVFmKmf.js} +1 -1
  23. package/dist/assets/WorkflowEditorPanel-CwZpEqzM.js +42 -0
  24. package/dist/assets/browser-ponyfill-DZOWXZ4K.js +2 -0
  25. package/dist/assets/camera-D_KeL_pz.js +1 -0
  26. package/dist/assets/{index-I-I3pPPW.js → index-B-wV06cR.js} +1 -1
  27. package/dist/assets/index-BFguOWBW.js +2 -0
  28. package/dist/assets/{index-uXOqPsuU.js → index-C7gqY2AA.js} +1 -1
  29. package/dist/assets/{index-CE_GbjZ6.js → index-CiD1Rwaq.js} +1 -1
  30. package/dist/assets/index-D4nfDvz4.js +49 -0
  31. package/dist/assets/{index-CvMf5n2v.js → index-DDPUtz8-.js} +1 -1
  32. package/dist/assets/{index-Chrxgrys.js → index-EH8IBvSU.js} +1 -1
  33. package/dist/assets/{index-DWVQ48nQ.js → index-H0PzHVFw.js} +1 -1
  34. package/dist/assets/main-Cjm0d8dZ.js +152 -0
  35. package/dist/assets/main-DqC9_fF4.css +1 -0
  36. package/dist/assets/{prism-cpp-CcQnz8LL.js → prism-cpp-CK2Ly5dS.js} +1 -1
  37. package/dist/assets/{prism-csharp-DFIAaw4Y.js → prism-csharp-ByDDDiWW.js} +1 -1
  38. package/dist/assets/{prism-elixir-jP4m4T-8.js → prism-elixir-df27OMMQ.js} +1 -1
  39. package/dist/assets/{prism-haskell-BrMZM7_F.js → prism-haskell-Ce8aBmia.js} +1 -1
  40. package/dist/assets/{prism-java-BEsh8u4L.js → prism-java-CK6tws4L.js} +1 -1
  41. package/dist/assets/{prism-perl-ecHKp0bZ.js → prism-perl-UZfqnD51.js} +1 -1
  42. package/dist/assets/{prism-php-Ch-kk89U.js → prism-php-Dt9698bA.js} +1 -1
  43. package/dist/assets/{prism-ruby-zNpGDk6v.js → prism-ruby-CQBUuZIF.js} +1 -1
  44. package/dist/assets/{prism-scss-BeuXx0O0.js → prism-scss-CeN16CFC.js} +1 -1
  45. package/dist/assets/{vendor-react-uS-d4TUT.js → vendor-react--Eh9ivFN.js} +2 -2
  46. package/dist/assets/{web-IWJRtE3-.js → web-D1vWYL8u.js} +1 -1
  47. package/dist/assets/{web-DdRt5c0R.js → web-DUq3Undh.js} +1 -1
  48. package/dist/index.html +3 -3
  49. package/dist/src/packages/server/data/builtin-skills/boss-instructions.js +57 -66
  50. package/dist/src/packages/server/data/builtin-skills/index.js +2 -0
  51. package/dist/src/packages/server/data/builtin-skills/workflow-builder.js +253 -0
  52. package/dist/src/packages/server/data/builtin-skills/workflow-designer.js +157 -0
  53. package/dist/src/packages/server/data/event-queries.js +24 -0
  54. package/dist/src/packages/server/data/migrations/002_workflow_agent_binding.sql +14 -0
  55. package/dist/src/packages/server/data/migrations/003_matcher_executions.sql +19 -0
  56. package/dist/src/packages/server/data/migrations/004_matcher_message_source.sql +8 -0
  57. package/dist/src/packages/server/integrations/gmail/gmail-client.js +75 -15
  58. package/dist/src/packages/server/integrations/gmail/gmail-config.js +32 -2
  59. package/dist/src/packages/server/integrations/gmail/gmail-routes.js +5 -0
  60. package/dist/src/packages/server/integrations/gmail/index.js +23 -1
  61. package/dist/src/packages/server/integrations/jira/jira-client.js +11 -5
  62. package/dist/src/packages/server/integrations/jira/jira-routes.js +20 -3
  63. package/dist/src/packages/server/integrations/jira/jira-skill.js +110 -58
  64. package/dist/src/packages/server/routes/trigger-routes.js +22 -0
  65. package/dist/src/packages/server/routes/workflow-routes.js +86 -2
  66. package/dist/src/packages/server/services/boss-message-service.js +1 -1
  67. package/dist/src/packages/server/services/llm-matcher-service.js +50 -81
  68. package/dist/src/packages/server/services/trigger-service.js +195 -6
  69. package/dist/src/packages/server/services/workflow-executor.js +230 -0
  70. package/dist/src/packages/server/services/workflow-service.js +59 -13
  71. package/package.json +7 -7
  72. package/dist/assets/GmailOAuthSetup-B2GDjROU.js +0 -222
  73. package/dist/assets/MonitoringModal-CIF9MUm9.js +0 -1
  74. package/dist/assets/WorkflowEditorPanel-C4BRfmDM.js +0 -42
  75. package/dist/assets/browser-ponyfill-DIm4hKhx.js +0 -2
  76. package/dist/assets/camera-8crtHeRa.js +0 -1
  77. package/dist/assets/index--PCy0J0f.js +0 -49
  78. package/dist/assets/index-DOzx4Y9b.js +0 -2
  79. package/dist/assets/main-DQpuQfqS.css +0 -1
  80. package/dist/assets/main-DahZb6P4.js +0 -152
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { Router } from 'express';
7
7
  import * as workflowService from '../services/workflow-service.js';
8
+ import * as workflowExecutor from '../services/workflow-executor.js';
8
9
  import * as workflowChatService from '../services/workflow-chat-service.js';
9
10
  const router = Router();
10
11
  // ─── Query Helpers (same pattern as event-routes.ts) ───
@@ -125,7 +126,33 @@ router.patch('/instances/:id/cancel', (req, res) => {
125
126
  }
126
127
  res.json(instance);
127
128
  });
128
- // ─── Manual Transition ───
129
+ // ─── Explicit Transition (agent-driven) ───
130
+ router.put('/instances/:id/transition', async (req, res) => {
131
+ try {
132
+ const { targetStateId, reason } = req.body;
133
+ if (!targetStateId) {
134
+ res.status(400).json({ error: 'targetStateId is required' });
135
+ return;
136
+ }
137
+ const instance = await workflowService.transitionTo(req.params.id, targetStateId, reason);
138
+ res.json(instance);
139
+ }
140
+ catch (err) {
141
+ const msg = err instanceof Error ? err.message : 'Transition failed';
142
+ res.status(400).json({ error: msg });
143
+ }
144
+ });
145
+ router.get('/instances/:id/available-transitions', (req, res) => {
146
+ try {
147
+ const transitions = workflowService.getAvailableTransitions(req.params.id);
148
+ res.json({ transitions });
149
+ }
150
+ catch (err) {
151
+ const msg = err instanceof Error ? err.message : 'Failed to get transitions';
152
+ res.status(400).json({ error: msg });
153
+ }
154
+ });
155
+ // ─── Legacy Manual Transition (event-based) ───
129
156
  router.post('/instances/:id/transition', async (req, res) => {
130
157
  try {
131
158
  await workflowService.notifyEvent({
@@ -172,6 +199,63 @@ router.post('/instances/:id/event', async (req, res) => {
172
199
  res.status(400).json({ error: msg });
173
200
  }
174
201
  });
202
+ // ─── Trigger Routing (workflow-executor) ───
203
+ router.post('/trigger', async (req, res) => {
204
+ try {
205
+ const { triggerId, triggerData, agentId } = req.body;
206
+ if (!triggerId) {
207
+ res.status(400).json({ error: 'triggerId is required' });
208
+ return;
209
+ }
210
+ await workflowExecutor.handleTrigger({
211
+ triggerId,
212
+ triggerData: triggerData ?? {},
213
+ agentId,
214
+ });
215
+ res.json({ routed: true, triggerId });
216
+ }
217
+ catch (err) {
218
+ const msg = err instanceof Error ? err.message : 'Trigger routing failed';
219
+ res.status(500).json({ error: msg });
220
+ }
221
+ });
222
+ // ─── Agent Complete with Reasoning (workflow-executor) ───
223
+ router.post('/instances/:id/agent-complete', async (req, res) => {
224
+ try {
225
+ const { agentResponse, agentReasoning, agentSummary, variables, triggerId, transitionId } = req.body;
226
+ const instance = await workflowExecutor.transitionInstance(req.params.id, 'agent_complete', { agentResponse, agentReasoning, agentSummary, variables }, { triggerId, transitionId });
227
+ if (!instance) {
228
+ res.status(404).json({ error: 'Instance not found' });
229
+ return;
230
+ }
231
+ res.json(instance);
232
+ }
233
+ catch (err) {
234
+ const msg = err instanceof Error ? err.message : 'Agent complete failed';
235
+ res.status(400).json({ error: msg });
236
+ }
237
+ });
238
+ // ─── Bind Agent to Instance (workflow-executor) ───
239
+ router.post('/instances/:id/bind-agent', (req, res) => {
240
+ try {
241
+ const { agentId, stateId } = req.body;
242
+ if (!agentId) {
243
+ res.status(400).json({ error: 'agentId is required' });
244
+ return;
245
+ }
246
+ const result = workflowExecutor.executeInstanceState(req.params.id, stateId || workflowService.getInstance(req.params.id)?.currentStateId || '', agentId);
247
+ res.json({
248
+ instanceId: result.instance.id,
249
+ stateId: result.state.id,
250
+ stateName: result.state.name,
251
+ currentStepId: result.currentStep?.id,
252
+ });
253
+ }
254
+ catch (err) {
255
+ const msg = err instanceof Error ? err.message : 'Bind agent failed';
256
+ res.status(400).json({ error: msg });
257
+ }
258
+ });
175
259
  // ============================================================================
176
260
  // History & Audit (reads from SQLite)
177
261
  // ============================================================================
@@ -193,7 +277,7 @@ router.get('/instances/:id/variables', (req, res) => {
193
277
  res.json({ changes });
194
278
  });
195
279
  router.get('/instances/:id/reasoning', (req, res) => {
196
- const steps = workflowService.getInstanceReasoning(req.params.id);
280
+ const steps = workflowExecutor.getReasoningChain(req.params.id);
197
281
  res.json({ steps });
198
282
  });
199
283
  // ============================================================================
@@ -12,7 +12,7 @@ import { buildBossContext } from './subordinate-context-service.js';
12
12
  export function buildBossSystemPrompt(bossName, bossId) {
13
13
  const agent = agentService.getAgent(bossId);
14
14
  const customInstructions = agent?.customInstructions;
15
- let prompt = `You are "${bossName}", a Boss Agent manager with ID \`${bossId}\`. You have access to all tools, but prefer delegating coding tasks to your subordinates when available. Use tools yourself only for quick lookups, exploration, or when you have no subordinates.
15
+ let prompt = `You are "${bossName}", a Boss Agent manager with ID \`${bossId}\`. Your default is to delegate tasks to your subordinates. Do small tasks yourself only when you already have the context to finish quickly (1-2 tool calls). For anything requiring exploration or substantial work, delegate to your team.
16
16
 
17
17
  Your agent ID for notifications: ${bossId}`;
18
18
  // Append agent-specific custom instructions
@@ -3,39 +3,57 @@
3
3
  * Evaluates whether events match triggers using LLM-powered semantic matching,
4
4
  * and extracts structured variables from unstructured event content.
5
5
  *
6
- * Uses Anthropic's API with Haiku by default (fast, cheap classification).
7
- * Temperature 0 for deterministic results.
8
- * 5-second timeout — match treated as false on timeout (fail-safe).
6
+ * Uses the Anthropic SDK to call Claude (requires TC_ANTHROPIC_API_KEY environment variable).
7
+ * Haiku by default (fast, cheap classification).
8
+ * 15-second timeout — match treated as false on timeout (fail-safe).
9
9
  */
10
10
  import Anthropic from '@anthropic-ai/sdk';
11
11
  import { createLogger } from '../utils/logger.js';
12
12
  const log = createLogger('LLMMatcher');
13
- // Model mapping: short names to full model IDs
13
+ const TIMEOUT_MS = 15_000;
14
+ // Model mapping — use latest available model IDs
14
15
  const MODEL_MAP = {
15
16
  haiku: 'claude-haiku-4-5-20251001',
16
- sonnet: 'claude-sonnet-4-6',
17
- opus: 'claude-opus-4-6',
17
+ sonnet: 'claude-sonnet-4-6-20250514',
18
+ opus: 'claude-opus-4-6-20250514',
18
19
  };
19
20
  function resolveModel(model) {
20
21
  if (!model)
21
22
  return MODEL_MAP.haiku;
22
23
  return MODEL_MAP[model] || model;
23
24
  }
24
- // Lazy-initialized client
25
- let client = null;
26
- function getClient() {
27
- if (!client) {
28
- client = new Anthropic();
25
+ /**
26
+ * Execute a prompt via the Anthropic SDK.
27
+ * Returns the raw text response.
28
+ */
29
+ async function callAnthropicAPI(prompt, model) {
30
+ const startTime = Date.now();
31
+ const client = new Anthropic({
32
+ apiKey: process.env.TC_ANTHROPIC_API_KEY,
33
+ timeout: TIMEOUT_MS,
34
+ });
35
+ try {
36
+ const message = await client.messages.create({
37
+ model,
38
+ max_tokens: 1024,
39
+ messages: [{ role: 'user', content: prompt }],
40
+ });
41
+ const durationMs = Date.now() - startTime;
42
+ const text = message.content
43
+ .filter(block => block.type === 'text')
44
+ .map(block => block.text)
45
+ .join('\n');
46
+ return { text, durationMs };
47
+ }
48
+ catch (err) {
49
+ throw err;
29
50
  }
30
- return client;
31
51
  }
32
52
  // ─── LLM Match ───
33
53
  export async function llmMatch(formattedEvent, config) {
34
54
  const startTime = Date.now();
35
55
  const model = resolveModel(config.model);
36
- const temperature = config.temperature ?? 0;
37
- const maxTokens = config.maxTokens ?? 150;
38
- const systemPrompt = `You are an event classifier. Your job is to decide whether an incoming event matches a given condition.
56
+ const prompt = `You are an event classifier. Your job is to decide whether an incoming event matches a given condition.
39
57
 
40
58
  EVENT:
41
59
  ---
@@ -55,18 +73,7 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
55
73
  "confidence": 0.0 to 1.0
56
74
  }`;
57
75
  try {
58
- const controller = new AbortController();
59
- const timeout = setTimeout(() => controller.abort(), 5000);
60
- const response = await getClient().messages.create({
61
- model,
62
- max_tokens: maxTokens,
63
- temperature,
64
- messages: [{ role: 'user', content: systemPrompt }],
65
- }, { signal: controller.signal });
66
- clearTimeout(timeout);
67
- const durationMs = Date.now() - startTime;
68
- const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
69
- const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0);
76
+ const { text, durationMs } = await callAnthropicAPI(prompt, model);
70
77
  try {
71
78
  // Extract JSON from response (handle potential markdown wrapping)
72
79
  const jsonMatch = text.match(/\{[\s\S]*\}/);
@@ -79,7 +86,7 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
79
86
  confidence: typeof parsed.confidence === 'number' ? parsed.confidence : (parsed.match ? 1.0 : 0.0),
80
87
  durationMs,
81
88
  model,
82
- tokensUsed,
89
+ tokensUsed: 0,
83
90
  };
84
91
  }
85
92
  catch (parseErr) {
@@ -90,32 +97,19 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
90
97
  confidence: 0,
91
98
  durationMs,
92
99
  model,
93
- tokensUsed,
100
+ tokensUsed: 0,
94
101
  };
95
102
  }
96
103
  }
97
104
  catch (err) {
98
105
  const durationMs = Date.now() - startTime;
99
- if (err instanceof Error && err.name === 'AbortError') {
100
- log.warn('LLM match timed out after 5s');
101
- return {
102
- match: false,
103
- reason: 'LLM call timed out (5s)',
104
- confidence: 0,
105
- durationMs,
106
- model,
107
- tokensUsed: 0,
108
- };
106
+ const reason = err instanceof Error ? err.message : 'unknown';
107
+ if (reason.includes('timed out') || reason.includes('timeout')) {
108
+ log.warn(`LLM match timed out after ${TIMEOUT_MS}ms`);
109
+ return { match: false, reason: `LLM call timed out (${TIMEOUT_MS}ms)`, confidence: 0, durationMs, model, tokensUsed: 0 };
109
110
  }
110
111
  log.error('LLM match error:', err);
111
- return {
112
- match: false,
113
- reason: `LLM error: ${err instanceof Error ? err.message : 'unknown'}`,
114
- confidence: 0,
115
- durationMs,
116
- model,
117
- tokensUsed: 0,
118
- };
112
+ return { match: false, reason: `LLM error: ${reason}`, confidence: 0, durationMs, model, tokensUsed: 0 };
119
113
  }
120
114
  }
121
115
  // ─── LLM Variable Extraction ───
@@ -123,7 +117,7 @@ export async function llmExtractVariables(formattedEvent, config) {
123
117
  const startTime = Date.now();
124
118
  const model = resolveModel(config.model);
125
119
  const variableList = config.variables.map(v => `- ${v}`).join('\n');
126
- const systemPrompt = `You are a data extractor. Extract specific variables from the event below.
120
+ const prompt = `You are a data extractor. Extract specific variables from the event below.
127
121
 
128
122
  EVENT:
129
123
  ---
@@ -147,18 +141,7 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
147
141
  "reason": "Brief explanation of how you extracted each value"
148
142
  }`;
149
143
  try {
150
- const controller = new AbortController();
151
- const timeout = setTimeout(() => controller.abort(), 5000);
152
- const response = await getClient().messages.create({
153
- model,
154
- max_tokens: 300,
155
- temperature: 0,
156
- messages: [{ role: 'user', content: systemPrompt }],
157
- }, { signal: controller.signal });
158
- clearTimeout(timeout);
159
- const durationMs = Date.now() - startTime;
160
- const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
161
- const tokensUsed = (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0);
144
+ const { text, durationMs } = await callAnthropicAPI(prompt, model);
162
145
  try {
163
146
  const jsonMatch = text.match(/\{[\s\S]*\}/);
164
147
  if (!jsonMatch)
@@ -169,17 +152,10 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
169
152
  for (const v of config.variables) {
170
153
  variables[v] = parsed.variables?.[v] ?? '';
171
154
  }
172
- return {
173
- variables,
174
- reason: parsed.reason || '',
175
- durationMs,
176
- model,
177
- tokensUsed,
178
- };
155
+ return { variables, reason: parsed.reason || '', durationMs, model, tokensUsed: 0 };
179
156
  }
180
157
  catch (parseErr) {
181
158
  log.error('Failed to parse LLM extract response:', text);
182
- // Return empty variables on parse failure
183
159
  const variables = {};
184
160
  for (const v of config.variables) {
185
161
  variables[v] = '';
@@ -187,9 +163,7 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
187
163
  return {
188
164
  variables,
189
165
  reason: `Parse error: ${parseErr instanceof Error ? parseErr.message : 'unknown'}`,
190
- durationMs,
191
- model,
192
- tokensUsed,
166
+ durationMs, model, tokensUsed: 0,
193
167
  };
194
168
  }
195
169
  }
@@ -199,17 +173,12 @@ Respond ONLY with valid JSON (no markdown, no explanation outside JSON):
199
173
  for (const v of config.variables) {
200
174
  variables[v] = '';
201
175
  }
202
- if (err instanceof Error && err.name === 'AbortError') {
203
- log.warn('LLM extract timed out after 5s');
204
- return { variables, reason: 'LLM call timed out (5s)', durationMs, model, tokensUsed: 0 };
176
+ const reason = err instanceof Error ? err.message : 'unknown';
177
+ if (reason.includes('timed out') || reason.includes('timeout')) {
178
+ log.warn(`LLM extract timed out after ${TIMEOUT_MS}ms`);
179
+ return { variables, reason: `LLM call timed out (${TIMEOUT_MS}ms)`, durationMs, model, tokensUsed: 0 };
205
180
  }
206
181
  log.error('LLM extract error:', err);
207
- return {
208
- variables,
209
- reason: `LLM error: ${err instanceof Error ? err.message : 'unknown'}`,
210
- durationMs,
211
- model,
212
- tokensUsed: 0,
213
- };
182
+ return { variables, reason: `LLM error: ${reason}`, durationMs, model, tokensUsed: 0 };
214
183
  }
215
184
  }
@@ -160,27 +160,54 @@ export function deleteTrigger(id) {
160
160
  // ─── Event Evaluation ───
161
161
  async function evaluateEvent(handler, event) {
162
162
  const triggersOfType = Array.from(triggers.values()).filter(t => t.type === handler.triggerType && t.enabled);
163
+ // Extract source info from the event for per-message debugging
164
+ const sourceType = event.source;
165
+ const sourceId = extractSourceId(event);
166
+ const sourceTimestamp = event.timestamp;
163
167
  for (const trigger of triggersOfType) {
164
168
  try {
165
169
  let matched = false;
170
+ let structuralPassed = true;
166
171
  let llmResult;
167
172
  let llmExtractResult;
173
+ const matcherExecutions = [];
174
+ const sourceInfo = { sourceType, sourceId, sourceTimestamp };
168
175
  // Step 1: Structural matching
169
176
  if (trigger.matchMode === 'structural' || trigger.matchMode === 'hybrid') {
170
177
  const structuralResult = handler.structuralMatch(trigger, event);
178
+ matcherExecutions.push({
179
+ matcherType: 'structural',
180
+ matcherName: `${trigger.type}_structural`,
181
+ executedAt: Date.now(),
182
+ matched: structuralResult,
183
+ reason: structuralResult ? 'Structural match passed' : 'Structural match failed',
184
+ resultJson: { triggerType: trigger.type, matchMode: trigger.matchMode },
185
+ ...sourceInfo,
186
+ });
171
187
  if (trigger.matchMode === 'structural') {
172
188
  matched = structuralResult;
173
189
  }
174
190
  else {
175
191
  // hybrid: structural must pass before LLM is called
192
+ structuralPassed = structuralResult;
176
193
  if (!structuralResult)
177
- continue;
194
+ matched = false;
178
195
  }
179
196
  }
180
- // Step 2: LLM matching
181
- if (trigger.matchMode === 'llm' || trigger.matchMode === 'hybrid') {
197
+ // Step 2: LLM matching (skip if hybrid structural failed)
198
+ if (structuralPassed && (trigger.matchMode === 'llm' || trigger.matchMode === 'hybrid')) {
182
199
  if (!trigger.llmMatch) {
183
200
  log.warn(`Trigger ${trigger.name} has matchMode=${trigger.matchMode} but no llmMatch config`);
201
+ matcherExecutions.push({
202
+ matcherType: 'llm',
203
+ matcherName: 'llm_match',
204
+ executedAt: Date.now(),
205
+ matched: false,
206
+ reason: 'No llmMatch config defined',
207
+ ...sourceInfo,
208
+ });
209
+ // Log non-match executions and skip to next trigger
210
+ logMatcherExecutions(null, trigger.id, matcherExecutions);
184
211
  continue;
185
212
  }
186
213
  const formatted = handler.formatEventForLLM(event);
@@ -188,10 +215,28 @@ async function evaluateEvent(handler, event) {
188
215
  // Check confidence threshold
189
216
  const minConfidence = trigger.llmMatch.minConfidence ?? 0.0;
190
217
  matched = llmResult.match && llmResult.confidence >= minConfidence;
218
+ matcherExecutions.push({
219
+ matcherType: 'llm',
220
+ matcherName: 'llm_match',
221
+ executedAt: Date.now(),
222
+ matched,
223
+ confidence: llmResult.confidence,
224
+ reason: llmResult.reason,
225
+ resultJson: {
226
+ model: llmResult.model,
227
+ tokensUsed: llmResult.tokensUsed,
228
+ durationMs: llmResult.durationMs,
229
+ minConfidence,
230
+ },
231
+ ...sourceInfo,
232
+ });
191
233
  }
192
- if (!matched)
234
+ // Log non-match executions directly (they won't go through fireTrigger)
235
+ if (!matched) {
236
+ logMatcherExecutions(null, trigger.id, matcherExecutions);
193
237
  continue;
194
- // Step 3: Variable extraction
238
+ }
239
+ // Step 3: Variable extraction (only on match)
195
240
  let variables;
196
241
  if (trigger.extractionMode === 'llm' && trigger.llmExtract) {
197
242
  const formatted = handler.formatEventForLLM(event);
@@ -199,15 +244,39 @@ async function evaluateEvent(handler, event) {
199
244
  // Merge with structural variables as fallback
200
245
  const structuralVars = handler.extractVariables(trigger, event);
201
246
  variables = { ...structuralVars, ...llmExtractResult.variables };
247
+ matcherExecutions.push({
248
+ matcherType: 'extraction',
249
+ matcherName: 'llm_extract',
250
+ executedAt: Date.now(),
251
+ matched: Object.keys(llmExtractResult.variables).length > 0,
252
+ reason: llmExtractResult.reason,
253
+ resultJson: {
254
+ extractedVariables: llmExtractResult.variables,
255
+ model: llmExtractResult.model,
256
+ tokensUsed: llmExtractResult.tokensUsed,
257
+ durationMs: llmExtractResult.durationMs,
258
+ },
259
+ ...sourceInfo,
260
+ });
202
261
  }
203
262
  else {
204
263
  variables = handler.extractVariables(trigger, event);
264
+ matcherExecutions.push({
265
+ matcherType: 'extraction',
266
+ matcherName: 'structural_extract',
267
+ executedAt: Date.now(),
268
+ matched: Object.keys(variables).length > 0,
269
+ reason: `Extracted ${Object.keys(variables).length} variables`,
270
+ resultJson: { extractedVariables: variables },
271
+ ...sourceInfo,
272
+ });
205
273
  }
206
- // Step 4: Fire the trigger
274
+ // Step 4: Fire the trigger (matcher executions linked after trigger_event created)
207
275
  await fireTrigger(trigger.id, variables, {
208
276
  rawPayload: event.data,
209
277
  llmMatchResult: llmResult,
210
278
  llmExtractResult: llmExtractResult,
279
+ matcherExecutions,
211
280
  });
212
281
  }
213
282
  catch (err) {
@@ -218,6 +287,33 @@ async function evaluateEvent(handler, event) {
218
287
  }
219
288
  }
220
289
  }
290
+ /**
291
+ * Extract a stable identifier from an external event's data.
292
+ * Each integration stores its message ID in a different field.
293
+ */
294
+ function extractSourceId(event) {
295
+ if (!event.data || typeof event.data !== 'object')
296
+ return undefined;
297
+ const data = event.data;
298
+ // Slack: ts or thread_ts
299
+ if (data.ts)
300
+ return String(data.ts);
301
+ if (data.event_ts)
302
+ return String(data.event_ts);
303
+ // Email: messageId or id
304
+ if (data.messageId)
305
+ return String(data.messageId);
306
+ // Jira: issue key or id
307
+ if (data.issue && typeof data.issue === 'object') {
308
+ const issue = data.issue;
309
+ if (issue.key)
310
+ return String(issue.key);
311
+ }
312
+ // Webhook: try id field
313
+ if (data.id)
314
+ return String(data.id);
315
+ return undefined;
316
+ }
221
317
  // ─── Test Match (dry run — no fire, no SQLite log) ───
222
318
  export async function testMatch(triggerId, event) {
223
319
  const trigger = triggers.get(triggerId);
@@ -229,9 +325,18 @@ export async function testMatch(triggerId, event) {
229
325
  let structuralResult;
230
326
  let llmResult;
231
327
  let wouldFire = false;
328
+ const matcherExecutions = [];
232
329
  // Structural matching
233
330
  if (trigger.matchMode === 'structural' || trigger.matchMode === 'hybrid') {
234
331
  structuralResult = effectiveHandler.structuralMatch(trigger, event);
332
+ matcherExecutions.push({
333
+ matcherType: 'structural',
334
+ matcherName: `${trigger.type}_structural`,
335
+ executedAt: Date.now(),
336
+ matched: structuralResult,
337
+ reason: structuralResult ? 'Structural match passed' : 'Structural match failed',
338
+ resultJson: { triggerType: trigger.type, matchMode: trigger.matchMode },
339
+ });
235
340
  if (trigger.matchMode === 'structural') {
236
341
  wouldFire = structuralResult;
237
342
  }
@@ -241,6 +346,7 @@ export async function testMatch(triggerId, event) {
241
346
  structuralMatch: structuralResult,
242
347
  extractedVariables: {},
243
348
  wouldFire: false,
349
+ matcherExecutions,
244
350
  };
245
351
  }
246
352
  }
@@ -251,6 +357,20 @@ export async function testMatch(triggerId, event) {
251
357
  llmResult = await llmMatch(formatted, trigger.llmMatch);
252
358
  const minConfidence = trigger.llmMatch.minConfidence ?? 0.0;
253
359
  wouldFire = llmResult.match && llmResult.confidence >= minConfidence;
360
+ matcherExecutions.push({
361
+ matcherType: 'llm',
362
+ matcherName: 'llm_match',
363
+ executedAt: Date.now(),
364
+ matched: wouldFire,
365
+ confidence: llmResult.confidence,
366
+ reason: llmResult.reason,
367
+ resultJson: {
368
+ model: llmResult.model,
369
+ tokensUsed: llmResult.tokensUsed,
370
+ durationMs: llmResult.durationMs,
371
+ minConfidence,
372
+ },
373
+ });
254
374
  }
255
375
  }
256
376
  // Variable extraction (always run for test)
@@ -260,15 +380,37 @@ export async function testMatch(triggerId, event) {
260
380
  const extractResult = await llmExtractVariables(formatted, trigger.llmExtract);
261
381
  const structuralVars = effectiveHandler.extractVariables(trigger, event);
262
382
  extractedVariables = { ...structuralVars, ...extractResult.variables };
383
+ matcherExecutions.push({
384
+ matcherType: 'extraction',
385
+ matcherName: 'llm_extract',
386
+ executedAt: Date.now(),
387
+ matched: Object.keys(extractResult.variables).length > 0,
388
+ reason: extractResult.reason,
389
+ resultJson: {
390
+ extractedVariables: extractResult.variables,
391
+ model: extractResult.model,
392
+ tokensUsed: extractResult.tokensUsed,
393
+ durationMs: extractResult.durationMs,
394
+ },
395
+ });
263
396
  }
264
397
  else {
265
398
  extractedVariables = effectiveHandler.extractVariables(trigger, event);
399
+ matcherExecutions.push({
400
+ matcherType: 'extraction',
401
+ matcherName: 'structural_extract',
402
+ executedAt: Date.now(),
403
+ matched: Object.keys(extractedVariables).length > 0,
404
+ reason: `Extracted ${Object.keys(extractedVariables).length} variables`,
405
+ resultJson: { extractedVariables },
406
+ });
266
407
  }
267
408
  return {
268
409
  structuralMatch: structuralResult,
269
410
  llmMatch: llmResult,
270
411
  extractedVariables,
271
412
  wouldFire,
413
+ matcherExecutions,
272
414
  };
273
415
  }
274
416
  // ─── Fire Trigger ───
@@ -305,6 +447,10 @@ export async function fireTrigger(id, variables, opts) {
305
447
  error: null,
306
448
  duration_ms: null,
307
449
  });
450
+ // Log matcher executions linked to this trigger event
451
+ if (opts?.matcherExecutions && eventId > 0) {
452
+ logMatcherExecutions(eventId, trigger.id, opts.matcherExecutions);
453
+ }
308
454
  }
309
455
  catch (err) {
310
456
  log.error('Failed to log trigger fire to SQLite:', err);
@@ -332,6 +478,16 @@ export async function fireTrigger(id, variables, opts) {
332
478
  }
333
479
  emit('trigger_fired', { triggerId: id, agentId: trigger.agentId, timestamp: startTime });
334
480
  log.log(`Fired trigger ${trigger.name} -> agent ${trigger.agentId}`);
481
+ // Route to workflow instances that are waiting for this trigger
482
+ try {
483
+ const { handleTrigger } = await import('./workflow-executor.js');
484
+ await handleTrigger({
485
+ triggerId: id,
486
+ triggerData: variables,
487
+ agentId: trigger.agentId,
488
+ });
489
+ }
490
+ catch { /* workflow routing is best-effort */ }
335
491
  }
336
492
  catch (err) {
337
493
  const errorMsg = err instanceof Error ? err.message : 'Unknown error';
@@ -434,6 +590,39 @@ export function getTriggerEvents(triggerId, limit = 50) {
434
590
  export function getAllTriggerEvents(limit = 100) {
435
591
  return queryMany('SELECT * FROM trigger_events ORDER BY fired_at DESC LIMIT ?', [limit]);
436
592
  }
593
+ // ─── Matcher Execution Logging (debugging) ───
594
+ function logMatcherExecutions(triggerEventId, triggerId, executions) {
595
+ for (const exec of executions) {
596
+ try {
597
+ insertOne('matcher_executions', {
598
+ trigger_event_id: triggerEventId,
599
+ trigger_id: triggerId,
600
+ matcher_type: exec.matcherType,
601
+ matcher_name: exec.matcherName,
602
+ executed_at: exec.executedAt,
603
+ matched: exec.matched ? 1 : 0,
604
+ confidence: exec.confidence ?? null,
605
+ reason: exec.reason ?? null,
606
+ result_json: exec.resultJson ? JSON.stringify(exec.resultJson) : null,
607
+ source_type: exec.sourceType ?? null,
608
+ source_id: exec.sourceId ?? null,
609
+ source_timestamp: exec.sourceTimestamp ?? null,
610
+ });
611
+ }
612
+ catch (err) {
613
+ log.error(`Failed to log matcher execution: ${err}`);
614
+ }
615
+ }
616
+ }
617
+ export function getMatchersByEvent(triggerEventId) {
618
+ return queryMany('SELECT * FROM matcher_executions WHERE trigger_event_id = ? ORDER BY executed_at ASC', [triggerEventId]);
619
+ }
620
+ export function getMatcherHistoryByTrigger(triggerId, limit = 100) {
621
+ return queryMany('SELECT * FROM matcher_executions WHERE trigger_id = ? ORDER BY executed_at DESC LIMIT ?', [triggerId, limit]);
622
+ }
623
+ export function getMatchersBySource(sourceType, sourceId) {
624
+ return queryMany('SELECT * FROM matcher_executions WHERE source_type = ? AND source_id = ? ORDER BY executed_at ASC', [sourceType, sourceId]);
625
+ }
437
626
  // ─── Webhook Handler (basic built-in handler for webhook triggers) ───
438
627
  function createWebhookHandler() {
439
628
  return {