@triflux/core 10.51.0 → 10.52.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 (55) hide show
  1. package/hub/bridge.mjs +65 -1172
  2. package/hub/index.mjs +2 -18
  3. package/hub/lib/process-utils.mjs +1 -33
  4. package/hub/team/claude-daemon-control.mjs +1 -2
  5. package/hub/team/claude-session-projection.mjs +5 -73
  6. package/hub/team/nativeProxy.mjs +759 -0
  7. package/hub/team/session-context.mjs +1 -1
  8. package/hud/constants.mjs +2 -0
  9. package/hud/hud-qos-status.mjs +21 -5
  10. package/hud/providers/gemini.mjs +20 -0
  11. package/hud/renderers.mjs +79 -46
  12. package/hud/utils.mjs +9 -5
  13. package/package.json +2 -4
  14. package/scripts/lib/agent-route-policy.mjs +0 -67
  15. package/scripts/lib/cli-claude.mjs +0 -12
  16. package/scripts/lib/env-probe.mjs +0 -70
  17. package/scripts/lib/legacy-mcp-cleanup.mjs +283 -0
  18. package/scripts/lib/mcp-guard-engine.mjs +40 -98
  19. package/scripts/lib/session-presence.mjs +1 -99
  20. package/hub/adaptive-diagnostic.mjs +0 -350
  21. package/hub/adaptive-inject.mjs +0 -224
  22. package/hub/adaptive-memory.mjs +0 -277
  23. package/hub/adaptive.mjs +0 -172
  24. package/hub/assign-callbacks.mjs +0 -170
  25. package/hub/delegator/contracts.mjs +0 -38
  26. package/hub/delegator/index.mjs +0 -14
  27. package/hub/delegator/schema/delegator-tools.schema.json +0 -250
  28. package/hub/delegator/service.mjs +0 -336
  29. package/hub/delegator/tool-definitions.mjs +0 -35
  30. package/hub/hitl.mjs +0 -202
  31. package/hub/lib/known-errors.json +0 -72
  32. package/hub/lib/memory-store.mjs +0 -1001
  33. package/hub/lib/phase-manager.mjs +0 -493
  34. package/hub/lib/state-snapshot.mjs +0 -293
  35. package/hub/lib/trace-recorder.mjs +0 -153
  36. package/hub/lib/uuidv7.mjs +0 -45
  37. package/hub/memory-doctor.mjs +0 -640
  38. package/hub/middleware/quota-middleware.mjs +0 -70
  39. package/hub/middleware/request-logger.mjs +0 -191
  40. package/hub/paths.mjs +0 -24
  41. package/hub/pipeline/index.mjs +0 -163
  42. package/hub/pipeline/state.mjs +0 -191
  43. package/hub/pipeline/transitions.mjs +0 -131
  44. package/hub/reflexion.mjs +0 -290
  45. package/hub/role-contract.mjs +0 -312
  46. package/hub/router.mjs +0 -2026
  47. package/hub/session-fingerprint.mjs +0 -418
  48. package/hub/state.mjs +0 -300
  49. package/hub/team/synapse-http.mjs +0 -311
  50. package/hub/team-bridge.mjs +0 -27
  51. package/scripts/lib/claudemd-scanner.mjs +0 -26
  52. package/scripts/lib/context.mjs +0 -51
  53. package/scripts/lib/handoff.mjs +0 -187
  54. package/scripts/lib/logger.mjs +0 -122
  55. package/scripts/lib/stealth-fetch.mjs +0 -176
@@ -1,1001 +0,0 @@
1
- // hub/lib/memory-store.mjs — In-memory store (SQLite-free fallback for @triflux/core)
2
-
3
- import { recalcConfidence } from "../reflexion.mjs";
4
- import {
5
- clampDurationMs,
6
- DEFAULT_ASSIGN_TIMEOUT_MS,
7
- } from "./timeout-defaults.mjs";
8
- import { uuidv7 } from "./uuidv7.mjs";
9
-
10
- export function clone(value) {
11
- return value == null ? value : JSON.parse(JSON.stringify(value));
12
- }
13
-
14
- function clampMaxMessages(value, fallback = 20) {
15
- const num = Number(value);
16
- if (!Number.isFinite(num)) return fallback;
17
- return Math.max(1, Math.min(Math.trunc(num), 100));
18
- }
19
-
20
- function clampPriority(value, fallback = 5) {
21
- const num = Number(value);
22
- if (!Number.isFinite(num)) return fallback;
23
- return Math.max(1, Math.min(Math.trunc(num), 9));
24
- }
25
-
26
- export function clampConfidence(value, fallback = 0.5) {
27
- const num = Number(value);
28
- if (!Number.isFinite(num)) return fallback;
29
- return Math.max(0, Math.min(num, 1));
30
- }
31
-
32
- function clampHitCount(value, fallback = 1) {
33
- const num = Number(value);
34
- if (!Number.isFinite(num)) return fallback;
35
- return Math.max(1, Math.trunc(num));
36
- }
37
-
38
- export function clampHitIncrement(value, fallback = 1) {
39
- const num = Number(value);
40
- if (!Number.isFinite(num)) return fallback;
41
- return Math.max(0, Math.trunc(num));
42
- }
43
-
44
- export function coerceTimestamp(value, fallback = Date.now()) {
45
- const num = Number(value);
46
- if (!Number.isFinite(num)) return fallback;
47
- return Math.trunc(num);
48
- }
49
-
50
- export function clampRetentionMs(value, fallback = 30 * 24 * 3600 * 1000) {
51
- const num = Number(value);
52
- if (!Number.isFinite(num)) return fallback;
53
- return Math.max(1, Math.trunc(num));
54
- }
55
-
56
- export function normalizeAdaptiveRuleIdentity(projectSlug, pattern) {
57
- const normalizedProject = String(projectSlug || "");
58
- const normalizedPattern = String(pattern || "");
59
- if (!normalizedProject || !normalizedPattern) return null;
60
- return { project_slug: normalizedProject, pattern: normalizedPattern };
61
- }
62
-
63
- export function buildAdaptiveRuleRow({
64
- project_slug,
65
- pattern,
66
- confidence = 0.5,
67
- hit_count = 1,
68
- last_seen_ms,
69
- created_ms,
70
- error_message,
71
- solution,
72
- context,
73
- } = {}) {
74
- const identity = normalizeAdaptiveRuleIdentity(project_slug, pattern);
75
- if (!identity) return null;
76
- const createdAt = coerceTimestamp(created_ms);
77
- const lastSeenAt = Math.max(
78
- createdAt,
79
- coerceTimestamp(last_seen_ms, createdAt),
80
- );
81
- return {
82
- ...identity,
83
- confidence: clampConfidence(confidence, 0.5),
84
- hit_count: clampHitCount(hit_count, 1),
85
- last_seen_ms: lastSeenAt,
86
- created_ms: createdAt,
87
- error_message: typeof error_message === "string" ? error_message : null,
88
- solution: typeof solution === "string" ? solution : null,
89
- context: typeof context === "string" ? context : null,
90
- };
91
- }
92
-
93
- function buildAssignCallbackEvent(row) {
94
- return {
95
- job_id: row.job_id,
96
- status: row.status,
97
- result: row.result ?? row.error ?? null,
98
- timestamp: new Date(row.updated_at_ms || Date.now()).toISOString(),
99
- };
100
- }
101
-
102
- /**
103
- * 인메모리 방식의 데이터 저장소를 생성합니다.
104
- * SQLite를 사용할 수 없는 환경에서 폴백(fallback)으로 사용됩니다.
105
- *
106
- * @returns {object} 인메모리 스토어 객체
107
- */
108
- export function createMemoryStore() {
109
- const agents = new Map();
110
- const messages = new Map();
111
- const humanRequests = new Map();
112
- const deadLetters = new Map();
113
- const assignJobs = new Map();
114
- const reflexionEntries = new Map();
115
- const adaptiveRules = new Map();
116
- const assignStatusListeners = new Set();
117
-
118
- function getAdaptiveRuleKey(projectSlug, pattern) {
119
- return `${projectSlug}\u0000${pattern}`;
120
- }
121
-
122
- function notifyAssignStatusListeners(row) {
123
- const event = buildAssignCallbackEvent(row);
124
- for (const listener of Array.from(assignStatusListeners)) {
125
- try {
126
- listener(event, clone(row));
127
- } catch {}
128
- }
129
- }
130
-
131
- function getRecentMessages() {
132
- return Array.from(messages.values()).sort(
133
- (left, right) => right.created_at_ms - left.created_at_ms,
134
- );
135
- }
136
-
137
- function upsertMessage(message) {
138
- messages.set(message.id, clone(message));
139
- return clone(message);
140
- }
141
-
142
- const store = {
143
- type: "memory",
144
- db: null,
145
- fingerprint: "memory:ephemeral",
146
- uuidv7,
147
-
148
- close() {},
149
-
150
- registerAgent({
151
- agent_id,
152
- cli,
153
- pid,
154
- capabilities = [],
155
- topics = [],
156
- heartbeat_ttl_ms = 30000,
157
- metadata = {},
158
- }) {
159
- const now = Date.now();
160
- const current = agents.get(agent_id) || {};
161
- const next = {
162
- ...current,
163
- agent_id,
164
- cli,
165
- pid: pid ?? null,
166
- capabilities: clone(capabilities),
167
- topics: clone(topics),
168
- last_seen_ms: now,
169
- lease_expires_ms: now + heartbeat_ttl_ms,
170
- presence_generation: Number(current.presence_generation || 0) + 1,
171
- status: "online",
172
- metadata: clone(metadata),
173
- };
174
- agents.set(agent_id, next);
175
- return {
176
- agent_id,
177
- lease_id: uuidv7(),
178
- presence_generation: next.presence_generation,
179
- lease_expires_ms: next.lease_expires_ms,
180
- server_time_ms: now,
181
- effective: {
182
- updated: true,
183
- online: next.status === "online",
184
- status: next.status,
185
- last_seen_ms: next.last_seen_ms,
186
- lease_expires_ms: next.lease_expires_ms,
187
- store_type: "memory",
188
- db_path: null,
189
- store_fingerprint: "memory:ephemeral",
190
- },
191
- };
192
- },
193
-
194
- getAgent(id) {
195
- return clone(agents.get(id) || null);
196
- },
197
-
198
- refreshLease(agentId, ttlMs = 30000, presenceGeneration) {
199
- const current = agents.get(agentId);
200
- if (
201
- !current ||
202
- (presenceGeneration != null &&
203
- current.presence_generation !== presenceGeneration)
204
- )
205
- return {
206
- agent_id: agentId,
207
- lease_expires_ms: Date.now() + ttlMs,
208
- server_time_ms: Date.now(),
209
- effective: {
210
- updated: false,
211
- store_type: "memory",
212
- db_path: null,
213
- store_fingerprint: "memory:ephemeral",
214
- },
215
- };
216
- const now = Date.now();
217
- current.last_seen_ms = now;
218
- current.lease_expires_ms = now + ttlMs;
219
- current.status = "online";
220
- return {
221
- agent_id: agentId,
222
- lease_expires_ms: current.lease_expires_ms,
223
- server_time_ms: now,
224
- effective: {
225
- updated: true,
226
- store_type: "memory",
227
- db_path: null,
228
- store_fingerprint: "memory:ephemeral",
229
- },
230
- };
231
- },
232
-
233
- updateAgentTopics(agentId, topics = []) {
234
- const current = agents.get(agentId);
235
- if (!current) return false;
236
- current.topics = clone(topics);
237
- current.last_seen_ms = Date.now();
238
- return true;
239
- },
240
-
241
- listOnlineAgents() {
242
- return Array.from(agents.values())
243
- .filter((agent) => agent.status !== "offline")
244
- .map((agent) => clone(agent));
245
- },
246
-
247
- listAllAgents() {
248
- return Array.from(agents.values()).map((agent) => clone(agent));
249
- },
250
-
251
- getAgentsByTopic(topic) {
252
- return Array.from(agents.values())
253
- .filter(
254
- (agent) =>
255
- agent.status !== "offline" &&
256
- Array.isArray(agent.topics) &&
257
- agent.topics.includes(topic),
258
- )
259
- .map((agent) => clone(agent));
260
- },
261
-
262
- sweepStaleAgents() {
263
- const now = Date.now();
264
- const candidates = Array.from(agents.values(), (agent) => ({
265
- agent_id: agent.agent_id,
266
- presence_generation: agent.presence_generation,
267
- }));
268
- let stale = 0;
269
- let offline = 0;
270
- for (const candidate of candidates) {
271
- const agent = agents.get(candidate.agent_id);
272
- if (
273
- !agent ||
274
- agent.presence_generation !== candidate.presence_generation
275
- )
276
- continue;
277
- if (agent.status === "online" && agent.lease_expires_ms < now) {
278
- agent.status = "stale";
279
- stale += 1;
280
- } else if (
281
- agent.status === "stale" &&
282
- agent.lease_expires_ms < now - 300000
283
- ) {
284
- agent.status = "offline";
285
- offline += 1;
286
- }
287
- }
288
- return { stale, offline };
289
- },
290
-
291
- updateAgentStatus(agentId, status, presenceGeneration) {
292
- const current = agents.get(agentId);
293
- if (
294
- !current ||
295
- (presenceGeneration != null &&
296
- current.presence_generation !== presenceGeneration)
297
- )
298
- return false;
299
- current.status = status;
300
- return true;
301
- },
302
-
303
- auditLog({
304
- type,
305
- from,
306
- to,
307
- topic,
308
- priority = 5,
309
- ttl_ms = 300000,
310
- payload = {},
311
- trace_id,
312
- correlation_id,
313
- status = "queued",
314
- }) {
315
- const now = Date.now();
316
- const row = {
317
- id: uuidv7(),
318
- type,
319
- from_agent: from,
320
- to_agent: to,
321
- topic,
322
- priority,
323
- ttl_ms,
324
- created_at_ms: now,
325
- expires_at_ms: now + ttl_ms,
326
- correlation_id: correlation_id || uuidv7(),
327
- trace_id: trace_id || uuidv7(),
328
- payload: clone(payload || {}),
329
- status,
330
- };
331
- return upsertMessage(row);
332
- },
333
-
334
- enqueueMessage(args) {
335
- return store.auditLog(args);
336
- },
337
-
338
- getMessage(id) {
339
- return clone(messages.get(id) || null);
340
- },
341
-
342
- getResponseByCorrelation(cid) {
343
- return (
344
- getRecentMessages().find(
345
- (message) =>
346
- message.correlation_id === cid && message.type === "response",
347
- ) || null
348
- );
349
- },
350
-
351
- getMessagesByTrace(tid) {
352
- return Array.from(messages.values())
353
- .filter((message) => message.trace_id === tid)
354
- .sort((left, right) => left.created_at_ms - right.created_at_ms)
355
- .map((message) => clone(message));
356
- },
357
-
358
- updateMessageStatus(id, status) {
359
- const current = messages.get(id);
360
- if (!current) return false;
361
- current.status = status;
362
- return true;
363
- },
364
-
365
- getAuditMessagesForAgent(
366
- agentId,
367
- { max_messages = 20, include_topics = null } = {},
368
- ) {
369
- const limit = clampMaxMessages(max_messages);
370
- const topics =
371
- Array.isArray(include_topics) && include_topics.length
372
- ? include_topics
373
- : agents.get(agentId)?.topics || [];
374
- const topicSet = new Set(topics);
375
- return getRecentMessages()
376
- .filter(
377
- (message) =>
378
- message.to_agent === agentId ||
379
- (String(message.to_agent || "").startsWith("topic:") &&
380
- topicSet.has(message.topic)),
381
- )
382
- .slice(0, limit)
383
- .map((message) => clone(message));
384
- },
385
-
386
- deliverToAgent(messageId, agentId) {
387
- return Boolean(messages.get(messageId) && agentId);
388
- },
389
-
390
- deliverToTopic(messageId, topic) {
391
- void messageId;
392
- return store.getAgentsByTopic(topic).length;
393
- },
394
-
395
- pollForAgent(agentId, { max_messages = 20, include_topics = null } = {}) {
396
- return store.getAuditMessagesForAgent(agentId, {
397
- max_messages,
398
- include_topics,
399
- });
400
- },
401
-
402
- ackMessages() {
403
- return 0;
404
- },
405
-
406
- insertHumanRequest({
407
- requester_agent,
408
- kind,
409
- prompt,
410
- requested_schema = {},
411
- deadline_ms,
412
- default_action,
413
- correlation_id,
414
- trace_id,
415
- }) {
416
- const requestId = uuidv7();
417
- const now = Date.now();
418
- const row = {
419
- request_id: requestId,
420
- requester_agent,
421
- kind,
422
- prompt,
423
- schema: clone(requested_schema),
424
- state: "pending",
425
- deadline_ms: now + deadline_ms,
426
- default_action,
427
- correlation_id: correlation_id || uuidv7(),
428
- trace_id: trace_id || uuidv7(),
429
- response: null,
430
- };
431
- humanRequests.set(requestId, row);
432
- return {
433
- request_id: requestId,
434
- state: "pending",
435
- deadline_ms: row.deadline_ms,
436
- };
437
- },
438
-
439
- getHumanRequest(id) {
440
- return clone(humanRequests.get(id) || null);
441
- },
442
-
443
- updateHumanRequest(id, state, resp = null) {
444
- const current = humanRequests.get(id);
445
- if (!current) return false;
446
- current.state = state;
447
- current.response = resp == null ? null : clone(resp);
448
- return true;
449
- },
450
-
451
- getPendingHumanRequests() {
452
- return Array.from(humanRequests.values())
453
- .filter((request) => request.state === "pending")
454
- .map((request) => clone(request));
455
- },
456
-
457
- moveToDeadLetter(messageId, reason, lastError = null) {
458
- const current = messages.get(messageId);
459
- if (!current || current.status === "dead_letter") return false;
460
- current.status = "dead_letter";
461
- if (deadLetters.has(messageId)) return true;
462
- deadLetters.set(messageId, {
463
- message_id: messageId,
464
- reason,
465
- failed_at_ms: Date.now(),
466
- last_error: lastError,
467
- });
468
- return true;
469
- },
470
-
471
- getDeadLetters(limit = 50) {
472
- return Array.from(deadLetters.values())
473
- .sort((left, right) => right.failed_at_ms - left.failed_at_ms)
474
- .slice(0, limit)
475
- .map((entry) => clone(entry));
476
- },
477
-
478
- createAssign({
479
- job_id,
480
- supervisor_agent,
481
- worker_agent,
482
- topic = "assign.job",
483
- task = "",
484
- payload = {},
485
- status = "queued",
486
- attempt = 1,
487
- retry_count = 0,
488
- max_retries = 0,
489
- priority = 5,
490
- ttl_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
491
- timeout_ms = DEFAULT_ASSIGN_TIMEOUT_MS,
492
- deadline_ms,
493
- dispatched_at_ms = null,
494
- trace_id,
495
- correlation_id,
496
- last_message_id = null,
497
- result = null,
498
- error = null,
499
- }) {
500
- const now = Date.now();
501
- const normalizedTimeout = clampDurationMs(
502
- timeout_ms,
503
- DEFAULT_ASSIGN_TIMEOUT_MS,
504
- );
505
- const row = {
506
- job_id: job_id || uuidv7(),
507
- supervisor_agent,
508
- worker_agent,
509
- topic: String(topic || "assign.job"),
510
- task: String(task || ""),
511
- payload: clone(payload || {}),
512
- status,
513
- attempt: Math.max(1, Number(attempt) || 1),
514
- retry_count: Math.max(0, Number(retry_count) || 0),
515
- max_retries: Math.max(0, Number(max_retries) || 0),
516
- priority: clampPriority(priority, 5),
517
- ttl_ms: clampDurationMs(ttl_ms, normalizedTimeout),
518
- timeout_ms: normalizedTimeout,
519
- deadline_ms: Number.isFinite(Number(deadline_ms))
520
- ? Math.trunc(Number(deadline_ms))
521
- : now + normalizedTimeout,
522
- trace_id: trace_id || uuidv7(),
523
- correlation_id: correlation_id || uuidv7(),
524
- last_message_id,
525
- result: result == null ? null : clone(result),
526
- error: error == null ? null : clone(error),
527
- created_at_ms: now,
528
- updated_at_ms: now,
529
- started_at_ms: status === "running" ? now : null,
530
- dispatched_at_ms:
531
- dispatched_at_ms == null || !Number.isFinite(Number(dispatched_at_ms))
532
- ? null
533
- : Math.trunc(Number(dispatched_at_ms)),
534
- completed_at_ms: ["succeeded", "failed", "timed_out"].includes(status)
535
- ? now
536
- : null,
537
- last_retry_at_ms: retry_count > 0 ? now : null,
538
- };
539
- assignJobs.set(row.job_id, row);
540
- notifyAssignStatusListeners(row);
541
- return clone(row);
542
- },
543
-
544
- getAssign(jobId) {
545
- return clone(assignJobs.get(jobId) || null);
546
- },
547
-
548
- updateAssignStatus(jobId, status, patch = {}) {
549
- const current = assignJobs.get(jobId);
550
- if (!current) return null;
551
-
552
- const snapshot = clone(current);
553
- const now = Date.now();
554
- const nextStatus = status || current.status;
555
- const isTerminal = ["succeeded", "failed", "timed_out"].includes(
556
- nextStatus,
557
- );
558
- const nextTimeout = clampDurationMs(
559
- patch.timeout_ms ?? current.timeout_ms,
560
- current.timeout_ms,
561
- );
562
- const nextRow = {
563
- ...current,
564
- supervisor_agent: patch.supervisor_agent ?? current.supervisor_agent,
565
- worker_agent: patch.worker_agent ?? current.worker_agent,
566
- topic: patch.topic ?? current.topic,
567
- task: patch.task ?? current.task,
568
- payload: clone(patch.payload ?? current.payload ?? {}),
569
- status: nextStatus,
570
- attempt: Math.max(
571
- 1,
572
- Number(patch.attempt ?? current.attempt) || current.attempt || 1,
573
- ),
574
- retry_count: Math.max(
575
- 0,
576
- Number(patch.retry_count ?? current.retry_count) || 0,
577
- ),
578
- max_retries: Math.max(
579
- 0,
580
- Number(patch.max_retries ?? current.max_retries) || 0,
581
- ),
582
- priority: clampPriority(
583
- patch.priority ?? current.priority,
584
- current.priority || 5,
585
- ),
586
- ttl_ms: clampDurationMs(
587
- patch.ttl_ms ?? current.ttl_ms,
588
- current.ttl_ms || nextTimeout,
589
- ),
590
- timeout_ms: nextTimeout,
591
- deadline_ms: (() => {
592
- if (Object.hasOwn(patch, "deadline_ms")) {
593
- return patch.deadline_ms == null
594
- ? null
595
- : Math.trunc(Number(patch.deadline_ms));
596
- }
597
- if (isTerminal) return null;
598
- if (nextStatus === "running" && !current.deadline_ms)
599
- return now + nextTimeout;
600
- return current.deadline_ms;
601
- })(),
602
- trace_id: patch.trace_id ?? current.trace_id,
603
- correlation_id: patch.correlation_id ?? current.correlation_id,
604
- last_message_id: Object.hasOwn(patch, "last_message_id")
605
- ? patch.last_message_id
606
- : current.last_message_id,
607
- result: Object.hasOwn(patch, "result")
608
- ? patch.result == null
609
- ? null
610
- : clone(patch.result)
611
- : current.result,
612
- error: Object.hasOwn(patch, "error")
613
- ? patch.error == null
614
- ? null
615
- : clone(patch.error)
616
- : current.error,
617
- updated_at_ms: now,
618
- started_at_ms: Object.hasOwn(patch, "started_at_ms")
619
- ? patch.started_at_ms
620
- : nextStatus === "running"
621
- ? current.started_at_ms || now
622
- : current.started_at_ms,
623
- dispatched_at_ms: Object.hasOwn(patch, "dispatched_at_ms")
624
- ? patch.dispatched_at_ms
625
- : current.dispatched_at_ms,
626
- completed_at_ms: Object.hasOwn(patch, "completed_at_ms")
627
- ? patch.completed_at_ms
628
- : isTerminal
629
- ? current.completed_at_ms || now
630
- : current.completed_at_ms,
631
- last_retry_at_ms: Object.hasOwn(patch, "last_retry_at_ms")
632
- ? patch.last_retry_at_ms
633
- : current.last_retry_at_ms,
634
- };
635
- assignJobs.set(jobId, nextRow);
636
- if (snapshot.status !== nextRow.status) {
637
- notifyAssignStatusListeners(nextRow);
638
- }
639
- return clone(nextRow);
640
- },
641
-
642
- listAssigns({
643
- supervisor_agent,
644
- worker_agent,
645
- status,
646
- statuses,
647
- trace_id,
648
- correlation_id,
649
- active_before_ms,
650
- limit = 50,
651
- } = {}) {
652
- const statusList =
653
- Array.isArray(statuses) && statuses.length
654
- ? statuses
655
- : status
656
- ? [status]
657
- : [];
658
- return Array.from(assignJobs.values())
659
- .filter(
660
- (job) =>
661
- !supervisor_agent || job.supervisor_agent === supervisor_agent,
662
- )
663
- .filter((job) => !worker_agent || job.worker_agent === worker_agent)
664
- .filter((job) => !trace_id || job.trace_id === trace_id)
665
- .filter(
666
- (job) => !correlation_id || job.correlation_id === correlation_id,
667
- )
668
- .filter((job) => !statusList.length || statusList.includes(job.status))
669
- .filter(
670
- (job) =>
671
- !Number.isFinite(Number(active_before_ms)) ||
672
- (job.deadline_ms != null &&
673
- job.deadline_ms <= Number(active_before_ms)),
674
- )
675
- .sort((left, right) => right.updated_at_ms - left.updated_at_ms)
676
- .slice(0, clampMaxMessages(limit, 50))
677
- .map((job) => clone(job));
678
- },
679
-
680
- retryAssign(jobId, patch = {}) {
681
- const current = assignJobs.get(jobId);
682
- if (!current) return null;
683
- const nextRetryCount = Math.max(
684
- 0,
685
- Number(patch.retry_count ?? current.retry_count + 1) || 0,
686
- );
687
- const nextAttempt = Math.max(
688
- current.attempt + 1,
689
- Number(patch.attempt ?? current.attempt + 1) || 1,
690
- );
691
- const nextTimeout = clampDurationMs(
692
- patch.timeout_ms ?? current.timeout_ms,
693
- current.timeout_ms,
694
- );
695
- return store.updateAssignStatus(jobId, "queued", {
696
- retry_count: nextRetryCount,
697
- attempt: nextAttempt,
698
- timeout_ms: nextTimeout,
699
- ttl_ms: patch.ttl_ms ?? current.ttl_ms,
700
- deadline_ms: Date.now() + nextTimeout,
701
- completed_at_ms: null,
702
- started_at_ms: null,
703
- dispatched_at_ms: null,
704
- last_retry_at_ms: Date.now(),
705
- result: patch.result ?? null,
706
- error: Object.hasOwn(patch, "error") ? patch.error : current.error,
707
- last_message_id: null,
708
- });
709
- },
710
-
711
- sweepExpired({ now_ms = Date.now() } = {}) {
712
- const transitions = [];
713
- for (const message of messages.values()) {
714
- if (
715
- !["queued", "delivered"].includes(message.status) ||
716
- message.expires_at_ms > now_ms
717
- )
718
- continue;
719
- const snapshot = clone(message);
720
- const previousStatus = message.status;
721
- message.status = "dead_letter";
722
- const dlqInserted = !deadLetters.has(message.id);
723
- if (dlqInserted) {
724
- deadLetters.set(message.id, {
725
- message_id: message.id,
726
- reason: "ttl_expired",
727
- failed_at_ms: now_ms,
728
- last_error: null,
729
- });
730
- }
731
- transitions.push({
732
- message: snapshot,
733
- previous_status: previousStatus,
734
- reason: "ttl_expired",
735
- failed_at_ms: now_ms,
736
- dlq_inserted: dlqInserted,
737
- });
738
- }
739
- return {
740
- now_ms,
741
- messages: transitions.length,
742
- transitions,
743
- };
744
- },
745
-
746
- getQueueDepths() {
747
- const depths = { urgent: 0, normal: 0, dlq: deadLetters.size };
748
- const now = Date.now();
749
- for (const message of messages.values()) {
750
- if (message.status !== "queued" || message.expires_at_ms < now)
751
- continue;
752
- if (message.priority >= 7) depths.urgent += 1;
753
- else depths.normal += 1;
754
- }
755
- return depths;
756
- },
757
-
758
- onAssignStatusChange(listener) {
759
- if (typeof listener !== "function") {
760
- return () => {};
761
- }
762
- assignStatusListeners.add(listener);
763
- return () => assignStatusListeners.delete(listener);
764
- },
765
-
766
- getDeliveryStats() {
767
- const cutoff = Date.now() - 300000;
768
- const totalDeliveries = Array.from(messages.values()).filter(
769
- (message) =>
770
- message.status === "acked" && message.created_at_ms > cutoff,
771
- ).length;
772
- return { total_deliveries: totalDeliveries, avg_delivery_ms: 0 };
773
- },
774
-
775
- getHubStats() {
776
- return {
777
- online_agents: Array.from(agents.values()).filter(
778
- (agent) => agent.status === "online",
779
- ).length,
780
- total_messages: messages.size,
781
- active_assign_jobs: Array.from(assignJobs.values()).filter((job) =>
782
- ["queued", "running"].includes(job.status),
783
- ).length,
784
- ...store.getQueueDepths(),
785
- };
786
- },
787
-
788
- getAuditStats() {
789
- const countByStatus = (targetStatus) =>
790
- Array.from(assignJobs.values()).filter(
791
- (job) => job.status === targetStatus,
792
- ).length;
793
- return {
794
- online_agents: Array.from(agents.values()).filter(
795
- (agent) => agent.status === "online",
796
- ).length,
797
- total_messages: messages.size,
798
- dlq: deadLetters.size,
799
- assign_queued: countByStatus("queued"),
800
- assign_running: countByStatus("running"),
801
- assign_failed: countByStatus("failed"),
802
- assign_timed_out: countByStatus("timed_out"),
803
- };
804
- },
805
-
806
- addReflexion({
807
- type = "reflexion",
808
- error_pattern,
809
- error_message,
810
- context = {},
811
- solution,
812
- solution_code = null,
813
- adaptive_state = {},
814
- confidence = 0.5,
815
- hit_count = 1,
816
- success_count = 0,
817
- last_hit_ms,
818
- created_at_ms,
819
- updated_at_ms,
820
- }) {
821
- const now = Date.now();
822
- const entry = {
823
- id: uuidv7(),
824
- type,
825
- error_pattern,
826
- error_message,
827
- context: clone(context),
828
- solution,
829
- solution_code,
830
- adaptive_state: clone(adaptive_state),
831
- confidence,
832
- hit_count,
833
- success_count,
834
- last_hit_ms: last_hit_ms ?? now,
835
- created_at_ms: created_at_ms ?? now,
836
- updated_at_ms: updated_at_ms ?? now,
837
- };
838
- reflexionEntries.set(entry.id, entry);
839
- return clone(entry);
840
- },
841
-
842
- getReflexion(id) {
843
- return clone(reflexionEntries.get(id) || null);
844
- },
845
-
846
- findReflexion(errorPattern, context = {}) {
847
- const entries = Array.from(reflexionEntries.values())
848
- .filter(
849
- (entry) =>
850
- entry.error_pattern === errorPattern ||
851
- entry.error_pattern.includes(errorPattern) ||
852
- errorPattern.includes(entry.error_pattern),
853
- )
854
- .filter((entry) =>
855
- Object.entries(context).every(
856
- ([key, value]) => value == null || entry.context?.[key] === value,
857
- ),
858
- )
859
- .sort((left, right) => right.confidence - left.confidence);
860
- return entries.map((entry) => clone(entry));
861
- },
862
-
863
- updateReflexionHit(id, success = false) {
864
- const current = reflexionEntries.get(id);
865
- if (!current) return null;
866
- const now = Date.now();
867
- current.hit_count += 1;
868
- if (success) current.success_count += 1;
869
- current.last_hit_ms = now;
870
- current.updated_at_ms = now;
871
- current.confidence = Math.max(0, Math.min(1, recalcConfidence(current)));
872
- return clone(current);
873
- },
874
-
875
- listReflexion(filters = {}) {
876
- const {
877
- type,
878
- minConfidence = Number.NEGATIVE_INFINITY,
879
- projectSlug,
880
- } = filters;
881
- return Array.from(reflexionEntries.values())
882
- .filter((entry) => !type || entry.type === type)
883
- .filter((entry) => entry.confidence >= minConfidence)
884
- .filter(
885
- (entry) =>
886
- !projectSlug || entry.adaptive_state?.project_slug === projectSlug,
887
- )
888
- .sort((left, right) => right.confidence - left.confidence)
889
- .map((entry) => clone(entry));
890
- },
891
-
892
- patchReflexion(id, patch = {}) {
893
- const current = reflexionEntries.get(id);
894
- if (!current) return null;
895
- const next = {
896
- ...current,
897
- ...clone(patch),
898
- context: patch.context ? clone(patch.context) : current.context,
899
- adaptive_state: patch.adaptive_state
900
- ? clone(patch.adaptive_state)
901
- : current.adaptive_state,
902
- updated_at_ms: patch.updated_at_ms ?? Date.now(),
903
- };
904
- reflexionEntries.set(id, next);
905
- return clone(next);
906
- },
907
-
908
- deleteReflexion(id) {
909
- return reflexionEntries.delete(id);
910
- },
911
-
912
- pruneReflexion(maxAge_ms = 30 * 24 * 3600 * 1000, minConfidence = 0.2) {
913
- const cutoff = Date.now() - maxAge_ms;
914
- let removed = 0;
915
- for (const [id, entry] of Array.from(reflexionEntries.entries())) {
916
- if (entry.updated_at_ms < cutoff && entry.confidence < minConfidence) {
917
- reflexionEntries.delete(id);
918
- removed += 1;
919
- }
920
- }
921
- return removed;
922
- },
923
-
924
- addAdaptiveRule(rule) {
925
- const next = buildAdaptiveRuleRow(rule);
926
- if (!next) return null;
927
- const key = getAdaptiveRuleKey(next.project_slug, next.pattern);
928
- const current = adaptiveRules.get(key);
929
- adaptiveRules.set(key, {
930
- ...next,
931
- created_ms: current
932
- ? Math.min(current.created_ms, next.created_ms)
933
- : next.created_ms,
934
- });
935
- return clone(adaptiveRules.get(key));
936
- },
937
-
938
- findAdaptiveRule(projectSlug, pattern) {
939
- const identity = normalizeAdaptiveRuleIdentity(projectSlug, pattern);
940
- if (!identity) return null;
941
- return clone(
942
- adaptiveRules.get(
943
- getAdaptiveRuleKey(identity.project_slug, identity.pattern),
944
- ) || null,
945
- );
946
- },
947
-
948
- updateRuleConfidence(projectSlug, pattern, confidence, options = {}) {
949
- const current = store.findAdaptiveRule(projectSlug, pattern);
950
- if (!current) return null;
951
- const next = {
952
- ...current,
953
- confidence: clampConfidence(confidence, current.confidence),
954
- hit_count:
955
- current.hit_count + clampHitIncrement(options.hit_count_increment, 1),
956
- last_seen_ms: Math.max(
957
- current.last_seen_ms,
958
- coerceTimestamp(options.last_seen_ms, Date.now()),
959
- ),
960
- };
961
- adaptiveRules.set(
962
- getAdaptiveRuleKey(current.project_slug, current.pattern),
963
- next,
964
- );
965
- return clone(next);
966
- },
967
-
968
- pruneStaleRules(maxAge_ms = 30 * 24 * 3600 * 1000, minConfidence = 0.2) {
969
- const cutoff =
970
- Date.now() - clampRetentionMs(maxAge_ms, 30 * 24 * 3600 * 1000);
971
- let removed = 0;
972
- for (const [key, rule] of Array.from(adaptiveRules.entries())) {
973
- if (rule.last_seen_ms < cutoff && rule.confidence < minConfidence) {
974
- adaptiveRules.delete(key);
975
- removed += 1;
976
- }
977
- }
978
- return removed;
979
- },
980
-
981
- listAdaptiveRules(projectSlug) {
982
- const results = [];
983
- for (const rule of adaptiveRules.values()) {
984
- if (!projectSlug || rule.project_slug === projectSlug) {
985
- results.push(clone(rule));
986
- }
987
- }
988
- return results.sort((a, b) => b.confidence - a.confidence);
989
- },
990
-
991
- deleteAdaptiveRule(projectSlug, pattern) {
992
- const identity = normalizeAdaptiveRuleIdentity(projectSlug, pattern);
993
- if (!identity) return false;
994
- return adaptiveRules.delete(
995
- getAdaptiveRuleKey(identity.project_slug, identity.pattern),
996
- );
997
- },
998
- };
999
-
1000
- return store;
1001
- }