sentinelayer-cli 0.4.5 → 0.8.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 (72) hide show
  1. package/README.md +16 -18
  2. package/package.json +7 -6
  3. package/src/agents/jules/config/definition.js +13 -62
  4. package/src/agents/jules/config/system-prompt.js +8 -1
  5. package/src/agents/jules/fix-cycle.js +12 -372
  6. package/src/agents/jules/loop.js +116 -26
  7. package/src/agents/jules/pulse.js +10 -327
  8. package/src/agents/jules/stream.js +13 -12
  9. package/src/agents/jules/swarm/orchestrator.js +3 -3
  10. package/src/agents/jules/swarm/sub-agent.js +6 -3
  11. package/src/agents/jules/tools/aidenid-email.js +189 -0
  12. package/src/agents/jules/tools/auth-audit.js +1187 -45
  13. package/src/agents/jules/tools/dispatch.js +25 -12
  14. package/src/agents/jules/tools/file-edit.js +2 -180
  15. package/src/agents/jules/tools/file-read.js +2 -100
  16. package/src/agents/jules/tools/glob.js +2 -168
  17. package/src/agents/jules/tools/grep.js +2 -228
  18. package/src/agents/jules/tools/path-guards.js +2 -161
  19. package/src/agents/jules/tools/runtime-audit.js +6 -2
  20. package/src/agents/jules/tools/shell.js +2 -383
  21. package/src/agents/persona-visuals.js +64 -0
  22. package/src/agents/shared-tools/dispatch-core.js +320 -0
  23. package/src/agents/shared-tools/file-edit.js +180 -0
  24. package/src/agents/shared-tools/file-read.js +100 -0
  25. package/src/agents/shared-tools/glob.js +168 -0
  26. package/src/agents/shared-tools/grep.js +228 -0
  27. package/src/agents/shared-tools/index.js +46 -0
  28. package/src/agents/shared-tools/path-guards.js +161 -0
  29. package/src/agents/shared-tools/shell.js +383 -0
  30. package/src/ai/aidenid.js +56 -7
  31. package/src/ai/client.js +45 -0
  32. package/src/ai/proxy.js +137 -0
  33. package/src/auth/gate.js +290 -16
  34. package/src/auth/http.js +450 -39
  35. package/src/auth/service.js +262 -47
  36. package/src/auth/session-store.js +475 -21
  37. package/src/cli.js +5 -0
  38. package/src/commands/audit.js +13 -8
  39. package/src/commands/auth.js +53 -9
  40. package/src/commands/omargate.js +10 -2
  41. package/src/commands/scan.js +10 -4
  42. package/src/commands/session.js +590 -0
  43. package/src/commands/spec.js +62 -0
  44. package/src/commands/watch.js +3 -2
  45. package/src/daemon/assignment-ledger.js +196 -0
  46. package/src/daemon/error-worker.js +599 -16
  47. package/src/daemon/fix-cycle.js +384 -0
  48. package/src/daemon/ingest-refresh.js +10 -9
  49. package/src/daemon/jira-lifecycle.js +135 -0
  50. package/src/daemon/pulse.js +327 -0
  51. package/src/daemon/scope-engine.js +1068 -0
  52. package/src/events/schema.js +190 -0
  53. package/src/interactive/index.js +18 -16
  54. package/src/legacy-cli.js +606 -37
  55. package/src/prompt/generator.js +19 -1
  56. package/src/review/ai-review.js +11 -1
  57. package/src/review/local-review.js +75 -19
  58. package/src/review/omargate-interactive.js +68 -0
  59. package/src/review/omargate-orchestrator.js +404 -0
  60. package/src/review/persona-prompts.js +296 -0
  61. package/src/review/scan-modes.js +48 -0
  62. package/src/scan/generator.js +1 -1
  63. package/src/session/agent-registry.js +352 -0
  64. package/src/session/daemon.js +801 -0
  65. package/src/session/paths.js +33 -0
  66. package/src/session/runtime-bridge.js +739 -0
  67. package/src/session/store.js +388 -0
  68. package/src/session/stream.js +325 -0
  69. package/src/spec/generator.js +100 -0
  70. package/src/telemetry/session-tracker.js +148 -32
  71. package/src/telemetry/sync.js +6 -2
  72. package/src/ui/command-hints.js +13 -0
@@ -3,13 +3,34 @@ import fsp from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
5
  import { resolveOutputRoot } from "../config/service.js";
6
+ import { createAgentEvent } from "../events/schema.js";
7
+ import { appendToStream } from "../session/stream.js";
6
8
 
7
9
  const QUEUE_SCHEMA_VERSION = "1.0.0";
8
10
  const STATE_SCHEMA_VERSION = "1.0.0";
9
11
  const DEFAULT_MAX_EVENTS = 200;
12
+ const DEFAULT_DAEMON_POLL_MS = 5000;
13
+ const DEFAULT_SWEEP_INTERVAL_MS = 24 * 60 * 60 * 1000;
14
+ const ERROR_DAEMON_AGENT_ID = "error-daemon";
15
+ const ACTIVE_DAEMONS = new Map();
10
16
 
11
17
  const TERMINAL_WORK_ITEM_STATUSES = new Set(["DONE", "SQUASHED"]);
12
18
 
19
+ export const DEDUP_KEY_FIELDS = Object.freeze([
20
+ "service",
21
+ "endpoint",
22
+ "error_code",
23
+ "stack_fingerprint",
24
+ "commit_sha",
25
+ ]);
26
+
27
+ export const WAKE_MODES = Object.freeze({
28
+ REALTIME: "realtime",
29
+ SCHEDULED: "scheduled",
30
+ });
31
+
32
+ const WAKE_MODE_SET = new Set(Object.values(WAKE_MODES));
33
+
13
34
  export const WORK_ITEM_STATUSES = Object.freeze([
14
35
  "QUEUED",
15
36
  "ASSIGNED",
@@ -90,6 +111,51 @@ function normalizeStackTrace(value) {
90
111
  return String(value || "").replace(/\r\n/g, "\n").trim();
91
112
  }
92
113
 
114
+ function normalizeWakeMode(value, fallbackValue = WAKE_MODES.REALTIME) {
115
+ const normalized = normalizeString(value).toLowerCase();
116
+ if (!normalized) {
117
+ return fallbackValue;
118
+ }
119
+ if (!WAKE_MODE_SET.has(normalized)) {
120
+ throw new Error(`wakeMode must be one of: ${Object.values(WAKE_MODES).join(", ")}.`);
121
+ }
122
+ return normalized;
123
+ }
124
+
125
+ function normalizeRequestIds(values) {
126
+ const input = Array.isArray(values) ? values : [];
127
+ const deduped = [];
128
+ const seen = new Set();
129
+ for (const value of input) {
130
+ const normalized = normalizeString(value);
131
+ if (!normalized || seen.has(normalized)) {
132
+ continue;
133
+ }
134
+ seen.add(normalized);
135
+ deduped.push(normalized);
136
+ }
137
+ return deduped;
138
+ }
139
+
140
+ function mergeRequestIds(existing = [], next = []) {
141
+ return normalizeRequestIds([...normalizeRequestIds(existing), ...normalizeRequestIds(next)]);
142
+ }
143
+
144
+ function buildDedupKey(event = {}) {
145
+ const materialized = [
146
+ normalizeString(event.service).toLowerCase(),
147
+ normalizeString(event.endpoint).toLowerCase(),
148
+ normalizeString(event.errorCode || event.error_code).toLowerCase(),
149
+ normalizeString(event.stackFingerprint || event.stack_fingerprint).toLowerCase(),
150
+ normalizeString(event.commitSha || event.commit_sha).toLowerCase() || "none",
151
+ ].join("|");
152
+ return materialized;
153
+ }
154
+
155
+ function buildDayKey(isoTimestamp = new Date().toISOString()) {
156
+ return normalizeIsoTimestamp(isoTimestamp, new Date().toISOString()).slice(0, 10);
157
+ }
158
+
93
159
  function computeStackFingerprint(stackTrace = "") {
94
160
  const normalized = normalizeStackTrace(stackTrace);
95
161
  if (!normalized) {
@@ -108,14 +174,7 @@ function computeStackFingerprint(stackTrace = "") {
108
174
  }
109
175
 
110
176
  function computeFingerprint(event = {}) {
111
- const materialized = [
112
- normalizeString(event.service).toLowerCase(),
113
- normalizeString(event.endpoint).toLowerCase(),
114
- normalizeString(event.errorCode).toLowerCase(),
115
- normalizeString(event.stackFingerprint).toLowerCase(),
116
- normalizeString(event.commitSha).toLowerCase(),
117
- ].join("|");
118
- return createHash("sha256").update(materialized).digest("hex");
177
+ return createHash("sha256").update(buildDedupKey(event)).digest("hex");
119
178
  }
120
179
 
121
180
  function chooseHigherSeverity(left, right) {
@@ -148,22 +207,40 @@ function isTerminalStatus(status = "") {
148
207
 
149
208
  function normalizeQueueItem(item = {}, fallbackNowIso = new Date().toISOString()) {
150
209
  const createdAt = normalizeIsoTimestamp(item.createdAt, fallbackNowIso);
210
+ const service = normalizeString(item.service) || "unknown-service";
211
+ const endpoint = normalizeString(item.endpoint) || "unknown-endpoint";
212
+ const errorCode = normalizeString(item.errorCode || item.error_code) || "UNKNOWN_ERROR";
213
+ const stackFingerprint =
214
+ normalizeString(item.stackFingerprint || item.stack_fingerprint) || "none";
215
+ const commitSha = normalizeString(item.commitSha || item.commit_sha) || null;
216
+ const requestIds = normalizeRequestIds(item.requestIds || item.request_ids);
217
+ const dedupKey =
218
+ normalizeString(item.dedupKey || item.dedup_key) ||
219
+ buildDedupKey({
220
+ service,
221
+ endpoint,
222
+ errorCode,
223
+ stackFingerprint,
224
+ commitSha,
225
+ });
151
226
  return {
152
227
  workItemId: normalizeString(item.workItemId) || createWorkItemId(createdAt),
153
228
  fingerprint: normalizeString(item.fingerprint),
154
229
  source: normalizeString(item.source) || "admin_error_log",
155
- service: normalizeString(item.service) || "unknown-service",
156
- endpoint: normalizeString(item.endpoint) || "unknown-endpoint",
157
- errorCode: normalizeString(item.errorCode) || "UNKNOWN_ERROR",
230
+ service,
231
+ endpoint,
232
+ errorCode,
158
233
  severity: normalizeSeverity(item.severity),
159
234
  status: normalizeWorkItemStatus(item.status),
160
235
  message: normalizeString(item.message),
161
- stackFingerprint: normalizeString(item.stackFingerprint) || "none",
162
- commitSha: normalizeString(item.commitSha) || null,
236
+ stackFingerprint,
237
+ commitSha,
238
+ dedupKey,
163
239
  firstSeenAt: normalizeIsoTimestamp(item.firstSeenAt, createdAt),
164
240
  lastSeenAt: normalizeIsoTimestamp(item.lastSeenAt, createdAt),
165
241
  latestEventId: normalizeString(item.latestEventId) || null,
166
242
  occurrenceCount: Math.max(1, normalizeNonNegativeInteger(item.occurrenceCount, 1)),
243
+ requestIds,
167
244
  createdAt,
168
245
  updatedAt: normalizeIsoTimestamp(item.updatedAt, createdAt),
169
246
  metadata: normalizeMetadata(item.metadata),
@@ -286,6 +363,7 @@ function buildWorkItemFromEvent(event, nowIso) {
286
363
  {
287
364
  workItemId: createWorkItemId(nowIso),
288
365
  fingerprint: event.fingerprint,
366
+ dedupKey: event.dedupKey,
289
367
  source: event.source,
290
368
  service: event.service,
291
369
  endpoint: event.endpoint,
@@ -299,11 +377,13 @@ function buildWorkItemFromEvent(event, nowIso) {
299
377
  lastSeenAt: event.occurredAt,
300
378
  latestEventId: event.eventId,
301
379
  occurrenceCount: 1,
380
+ requestIds: event.requestId ? [event.requestId] : [],
302
381
  createdAt: nowIso,
303
382
  updatedAt: nowIso,
304
383
  metadata: {
305
384
  ...event.metadata,
306
385
  sourceEventId: event.eventId,
386
+ requestId: event.requestId || null,
307
387
  },
308
388
  },
309
389
  nowIso
@@ -313,6 +393,10 @@ function buildWorkItemFromEvent(event, nowIso) {
313
393
  export function normalizeErrorEvent(event = {}, nowIso = new Date().toISOString()) {
314
394
  const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
315
395
  const stackTrace = normalizeStackTrace(event.stackTrace || event.stack || "");
396
+ const requestId =
397
+ normalizeString(event.requestId || event.request_id) ||
398
+ normalizeString(event?.metadata?.requestId || event?.metadata?.request_id) ||
399
+ null;
316
400
  const normalized = {
317
401
  eventId: normalizeString(event.eventId) || randomUUID(),
318
402
  occurredAt: normalizeIsoTimestamp(event.occurredAt, normalizedNow),
@@ -325,11 +409,14 @@ export function normalizeErrorEvent(event = {}, nowIso = new Date().toISOString(
325
409
  message: normalizeString(event.message) || "Unhandled runtime error",
326
410
  stackTrace,
327
411
  stackFingerprint: computeStackFingerprint(stackTrace),
328
- commitSha: normalizeString(event.commitSha) || null,
412
+ commitSha: normalizeString(event.commitSha || event.commit_sha) || null,
413
+ requestId,
329
414
  metadata: normalizeMetadata(event.metadata),
330
415
  };
416
+ const dedupKey = buildDedupKey(normalized);
331
417
  return {
332
418
  ...normalized,
419
+ dedupKey,
333
420
  fingerprint: computeFingerprint(normalized),
334
421
  };
335
422
  }
@@ -346,15 +433,18 @@ export async function resolveErrorDaemonStorage({
346
433
  env,
347
434
  homeDir,
348
435
  });
349
- const baseDir = path.join(outputRoot, "observability", "error-daemon");
436
+ const observabilityRoot = path.join(outputRoot, "observability");
437
+ const baseDir = path.join(observabilityRoot, "error-daemon");
350
438
  return {
351
439
  outputRoot,
440
+ observabilityRoot,
352
441
  baseDir,
353
442
  streamPath: path.join(baseDir, "admin-error-stream.ndjson"),
354
443
  queuePath: path.join(baseDir, "queue.json"),
355
444
  statePath: path.join(baseDir, "worker-state.json"),
356
445
  intakeDir: path.join(baseDir, "intake"),
357
446
  runsDir: path.join(baseDir, "runs"),
447
+ sweepsDir: path.join(baseDir, "sweeps"),
358
448
  };
359
449
  }
360
450
 
@@ -404,6 +494,86 @@ export async function appendAdminErrorEvent({
404
494
  };
405
495
  }
406
496
 
497
+ function resolveWorkItemIntakePath(storage, queueItem, nowIso = new Date().toISOString()) {
498
+ const dayKey = buildDayKey(queueItem.firstSeenAt || nowIso);
499
+ return path.join(storage.observabilityRoot, dayKey, queueItem.workItemId, "intake_event.json");
500
+ }
501
+
502
+ async function writeWorkItemIntakeArtifact(
503
+ storage,
504
+ queueItem,
505
+ {
506
+ nowIso = new Date().toISOString(),
507
+ } = {}
508
+ ) {
509
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
510
+ const intakePath = resolveWorkItemIntakePath(storage, queueItem, normalizedNow);
511
+ await fsp.mkdir(path.dirname(intakePath), { recursive: true });
512
+ const normalizedRequestIds = normalizeRequestIds(queueItem.requestIds || []);
513
+ const payload = {
514
+ schemaVersion: "1.0.0",
515
+ generated_at: normalizedNow,
516
+ work_item_id: queueItem.workItemId,
517
+ service: queueItem.service,
518
+ endpoint: queueItem.endpoint,
519
+ error_code: queueItem.errorCode,
520
+ stack_fingerprint: queueItem.stackFingerprint,
521
+ commit_sha: queueItem.commitSha,
522
+ first_seen_at: queueItem.firstSeenAt,
523
+ occurrence_count: Math.max(1, Number(queueItem.occurrenceCount || 1)),
524
+ last_seen_at: queueItem.lastSeenAt,
525
+ dedup_key:
526
+ normalizeString(queueItem.dedupKey) ||
527
+ buildDedupKey({
528
+ service: queueItem.service,
529
+ endpoint: queueItem.endpoint,
530
+ errorCode: queueItem.errorCode,
531
+ stackFingerprint: queueItem.stackFingerprint,
532
+ commitSha: queueItem.commitSha,
533
+ }),
534
+ request_ids: normalizedRequestIds,
535
+ fingerprint: queueItem.fingerprint,
536
+ severity: queueItem.severity,
537
+ status: queueItem.status,
538
+ message: queueItem.message,
539
+ };
540
+ await fsp.writeFile(intakePath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
541
+ return {
542
+ intakePath,
543
+ payload,
544
+ };
545
+ }
546
+
547
+ async function emitDaemonSessionEvent(
548
+ sessionId,
549
+ payload = {},
550
+ {
551
+ event = "agent_intake",
552
+ targetPath = ".",
553
+ nowIso = new Date().toISOString(),
554
+ } = {}
555
+ ) {
556
+ const normalizedSessionId = normalizeString(sessionId);
557
+ if (!normalizedSessionId) {
558
+ return null;
559
+ }
560
+ const normalizedPayload =
561
+ payload && typeof payload === "object" && !Array.isArray(payload) ? { ...payload } : {};
562
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
563
+ const envelope = createAgentEvent({
564
+ event: normalizeString(event) || "agent_intake",
565
+ agentId: ERROR_DAEMON_AGENT_ID,
566
+ sessionId: normalizedSessionId,
567
+ ts: normalizedNow,
568
+ workItemId: normalizedPayload.workItemId || normalizedPayload.work_item_id || undefined,
569
+ payload: normalizedPayload,
570
+ });
571
+ await appendToStream(normalizedSessionId, envelope, {
572
+ targetPath: path.resolve(String(targetPath || ".")),
573
+ });
574
+ return envelope;
575
+ }
576
+
407
577
  export async function getErrorDaemonState({
408
578
  targetPath = ".",
409
579
  outputDir = "",
@@ -427,14 +597,19 @@ export async function runErrorDaemonWorker({
427
597
  targetPath = ".",
428
598
  outputDir = "",
429
599
  maxEvents = DEFAULT_MAX_EVENTS,
600
+ sessionId = "",
601
+ wakeMode = WAKE_MODES.REALTIME,
430
602
  env,
431
603
  homeDir,
432
604
  nowIso = new Date().toISOString(),
433
605
  } = {}) {
434
606
  const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
435
607
  const normalizedMaxEvents = normalizePositiveInteger(maxEvents, DEFAULT_MAX_EVENTS);
608
+ const normalizedWakeMode = normalizeWakeMode(wakeMode, WAKE_MODES.REALTIME);
609
+ const normalizedSessionId = normalizeString(sessionId);
610
+ const resolvedTargetPath = path.resolve(String(targetPath || "."));
436
611
  const storage = await resolveErrorDaemonStorage({
437
- targetPath,
612
+ targetPath: resolvedTargetPath,
438
613
  outputDir,
439
614
  env,
440
615
  homeDir,
@@ -463,6 +638,7 @@ export async function runErrorDaemonWorker({
463
638
  let queuedCount = 0;
464
639
  let dedupedCount = 0;
465
640
  let parseErrorCount = 0;
641
+ const intakeMutations = [];
466
642
  for (let index = startOffset; index < endOffset; index += 1) {
467
643
  const rawLine = lines[index];
468
644
  if (!rawLine) {
@@ -486,6 +662,11 @@ export async function runErrorDaemonWorker({
486
662
  existing.updatedAt = normalizedNow;
487
663
  existing.latestEventId = event.eventId;
488
664
  existing.severity = chooseHigherSeverity(existing.severity, event.severity);
665
+ existing.dedupKey =
666
+ normalizeString(existing.dedupKey) ||
667
+ normalizeString(event.dedupKey) ||
668
+ buildDedupKey(event);
669
+ existing.requestIds = mergeRequestIds(existing.requestIds, event.requestId ? [event.requestId] : []);
489
670
  if (event.message) {
490
671
  existing.message = event.message;
491
672
  }
@@ -493,7 +674,13 @@ export async function runErrorDaemonWorker({
493
674
  ...normalizeMetadata(existing.metadata),
494
675
  ...normalizeMetadata(event.metadata),
495
676
  sourceEventId: event.eventId,
677
+ requestId: event.requestId || null,
496
678
  };
679
+ intakeMutations.push({
680
+ action: "dedup",
681
+ queueItem: existing,
682
+ sourceEvent: event,
683
+ });
497
684
  continue;
498
685
  }
499
686
 
@@ -501,6 +688,11 @@ export async function runErrorDaemonWorker({
501
688
  queue.items.push(queued);
502
689
  openItemsByFingerprint.set(queued.fingerprint, queued);
503
690
  queuedCount += 1;
691
+ intakeMutations.push({
692
+ action: "new",
693
+ queueItem: queued,
694
+ sourceEvent: event,
695
+ });
504
696
  }
505
697
 
506
698
  queue.generatedAt = normalizedNow;
@@ -521,6 +713,52 @@ export async function runErrorDaemonWorker({
521
713
  normalizedNow
522
714
  );
523
715
 
716
+ const intakeArtifacts = [];
717
+ const emittedEvents = [];
718
+ for (const mutation of intakeMutations) {
719
+ const written = await writeWorkItemIntakeArtifact(storage, mutation.queueItem, {
720
+ nowIso: normalizedNow,
721
+ });
722
+ intakeArtifacts.push({
723
+ action: mutation.action,
724
+ workItemId: mutation.queueItem.workItemId,
725
+ intakePath: written.intakePath,
726
+ dedupKey: written.payload.dedup_key,
727
+ occurrenceCount: written.payload.occurrence_count,
728
+ });
729
+ if (normalizedSessionId) {
730
+ const emitted = await emitDaemonSessionEvent(
731
+ normalizedSessionId,
732
+ {
733
+ action: mutation.action,
734
+ wakeMode: normalizedWakeMode,
735
+ source: mutation.sourceEvent.source,
736
+ eventId: mutation.sourceEvent.eventId,
737
+ workItemId: mutation.queueItem.workItemId,
738
+ service: mutation.queueItem.service,
739
+ endpoint: mutation.queueItem.endpoint,
740
+ errorCode: mutation.queueItem.errorCode,
741
+ severity: mutation.queueItem.severity,
742
+ stackFingerprint: mutation.queueItem.stackFingerprint,
743
+ commitSha: mutation.queueItem.commitSha,
744
+ dedupKey:
745
+ normalizeString(mutation.queueItem.dedupKey) ||
746
+ buildDedupKey(mutation.queueItem),
747
+ occurrenceCount: Math.max(1, Number(mutation.queueItem.occurrenceCount || 1)),
748
+ requestId: mutation.sourceEvent.requestId || null,
749
+ },
750
+ {
751
+ event: "agent_intake",
752
+ targetPath: resolvedTargetPath,
753
+ nowIso: normalizedNow,
754
+ }
755
+ );
756
+ if (emitted) {
757
+ emittedEvents.push(emitted);
758
+ }
759
+ }
760
+ }
761
+
524
762
  await fsp.mkdir(storage.runsDir, { recursive: true });
525
763
  const runId = `error-daemon-run-${stableTimestampForFile(new Date(normalizedNow))}-${randomUUID().slice(0, 8)}`;
526
764
  const runPath = path.join(storage.runsDir, `${runId}.json`);
@@ -531,6 +769,8 @@ export async function runErrorDaemonWorker({
531
769
  schemaVersion: "1.0.0",
532
770
  generatedAt: normalizedNow,
533
771
  runId,
772
+ wakeMode: normalizedWakeMode,
773
+ sessionId: normalizedSessionId || null,
534
774
  streamPath: storage.streamPath,
535
775
  queuePath: storage.queuePath,
536
776
  statePath: storage.statePath,
@@ -543,6 +783,8 @@ export async function runErrorDaemonWorker({
543
783
  dedupedCount,
544
784
  parseErrorCount,
545
785
  queueDepth: savedQueue.items.length,
786
+ intakeArtifactCount: intakeArtifacts.length,
787
+ emittedEventCount: emittedEvents.length,
546
788
  },
547
789
  null,
548
790
  2
@@ -554,6 +796,8 @@ export async function runErrorDaemonWorker({
554
796
  ...storage,
555
797
  runId,
556
798
  runPath,
799
+ wakeMode: normalizedWakeMode,
800
+ sessionId: normalizedSessionId || null,
557
801
  maxEvents: normalizedMaxEvents,
558
802
  startOffset,
559
803
  endOffset,
@@ -563,11 +807,350 @@ export async function runErrorDaemonWorker({
563
807
  dedupedCount,
564
808
  parseErrorCount,
565
809
  queueDepth: savedQueue.items.length,
810
+ intakeArtifacts,
811
+ emittedEvents,
566
812
  queue: savedQueue,
567
813
  state: savedState,
568
814
  };
569
815
  }
570
816
 
817
+ function buildDaemonInstanceKey({
818
+ targetPath = ".",
819
+ sessionId = "",
820
+ wakeMode = WAKE_MODES.REALTIME,
821
+ } = {}) {
822
+ const resolvedTargetPath = path.resolve(String(targetPath || "."));
823
+ const normalizedSessionId = normalizeString(sessionId) || "no-session";
824
+ const normalizedWakeMode = normalizeWakeMode(wakeMode, WAKE_MODES.REALTIME);
825
+ return `${resolvedTargetPath}::${normalizedSessionId}::${normalizedWakeMode}`;
826
+ }
827
+
828
+ function buildSeverityCounts(items = []) {
829
+ const counts = {
830
+ UNKNOWN: 0,
831
+ P3: 0,
832
+ P2: 0,
833
+ P1: 0,
834
+ P0: 0,
835
+ };
836
+ for (const item of items) {
837
+ const normalizedSeverity = normalizeSeverity(item?.severity);
838
+ counts[normalizedSeverity] = (counts[normalizedSeverity] || 0) + 1;
839
+ }
840
+ return counts;
841
+ }
842
+
843
+ export async function scheduledErrorSweep({
844
+ targetPath = ".",
845
+ outputDir = "",
846
+ sessionId = "",
847
+ region = "global",
848
+ tz = "UTC",
849
+ env,
850
+ homeDir,
851
+ nowIso = new Date().toISOString(),
852
+ } = {}) {
853
+ const normalizedNow = normalizeIsoTimestamp(nowIso, new Date().toISOString());
854
+ const resolvedTargetPath = path.resolve(String(targetPath || "."));
855
+ const normalizedSessionId = normalizeString(sessionId);
856
+ const normalizedRegion = normalizeString(region) || "global";
857
+ const normalizedTz = normalizeString(tz) || "UTC";
858
+ const storage = await resolveErrorDaemonStorage({
859
+ targetPath: resolvedTargetPath,
860
+ outputDir,
861
+ env,
862
+ homeDir,
863
+ });
864
+ const queue = await loadQueueFile(storage.queuePath, normalizedNow);
865
+ const activeItems = queue.items.filter((item) => !isTerminalStatus(item.status));
866
+ const severityCounts = buildSeverityCounts(activeItems);
867
+ const totalOccurrences = activeItems.reduce(
868
+ (sum, item) => sum + Math.max(1, Number(item.occurrenceCount || 1)),
869
+ 0
870
+ );
871
+ const runId = `error-daemon-sweep-${stableTimestampForFile(new Date(normalizedNow))}-${randomUUID().slice(0, 8)}`;
872
+
873
+ const digest = {
874
+ schemaVersion: "1.0.0",
875
+ generatedAt: normalizedNow,
876
+ runId,
877
+ wakeMode: WAKE_MODES.SCHEDULED,
878
+ region: normalizedRegion,
879
+ tz: normalizedTz,
880
+ queueDepth: activeItems.length,
881
+ totalOccurrences,
882
+ severityCounts,
883
+ workItems: activeItems.map((item) => ({
884
+ workItemId: item.workItemId,
885
+ service: item.service,
886
+ endpoint: item.endpoint,
887
+ errorCode: item.errorCode,
888
+ severity: item.severity,
889
+ status: item.status,
890
+ occurrenceCount: item.occurrenceCount,
891
+ dedupKey: item.dedupKey,
892
+ lastSeenAt: item.lastSeenAt,
893
+ })),
894
+ };
895
+
896
+ await fsp.mkdir(storage.sweepsDir, { recursive: true });
897
+ const sweepPath = path.join(storage.sweepsDir, `${runId}.json`);
898
+ await fsp.writeFile(sweepPath, `${JSON.stringify(digest, null, 2)}\n`, "utf-8");
899
+
900
+ let sweepEvent = null;
901
+ if (normalizedSessionId) {
902
+ sweepEvent = await emitDaemonSessionEvent(
903
+ normalizedSessionId,
904
+ {
905
+ action: "scheduled_rollup",
906
+ wakeMode: WAKE_MODES.SCHEDULED,
907
+ region: normalizedRegion,
908
+ tz: normalizedTz,
909
+ queueDepth: activeItems.length,
910
+ totalOccurrences,
911
+ severityCounts,
912
+ runId,
913
+ },
914
+ {
915
+ event: "agent_intake",
916
+ targetPath: resolvedTargetPath,
917
+ nowIso: normalizedNow,
918
+ }
919
+ );
920
+ }
921
+
922
+ return {
923
+ ...storage,
924
+ runId,
925
+ sweepPath,
926
+ wakeMode: WAKE_MODES.SCHEDULED,
927
+ region: normalizedRegion,
928
+ tz: normalizedTz,
929
+ queueDepth: activeItems.length,
930
+ totalOccurrences,
931
+ severityCounts,
932
+ digest,
933
+ event: sweepEvent,
934
+ };
935
+ }
936
+
937
+ export async function startErrorEventDaemon({
938
+ sessionId = "",
939
+ wakeMode = WAKE_MODES.REALTIME,
940
+ targetPath = ".",
941
+ outputDir = "",
942
+ pollMs = DEFAULT_DAEMON_POLL_MS,
943
+ sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS,
944
+ maxEvents = DEFAULT_MAX_EVENTS,
945
+ autoStart = true,
946
+ region = "global",
947
+ tz = "UTC",
948
+ env,
949
+ homeDir,
950
+ } = {}) {
951
+ const normalizedWakeMode = normalizeWakeMode(wakeMode, WAKE_MODES.REALTIME);
952
+ const normalizedSessionId = normalizeString(sessionId);
953
+ const resolvedTargetPath = path.resolve(String(targetPath || "."));
954
+ const daemonKey = buildDaemonInstanceKey({
955
+ targetPath: resolvedTargetPath,
956
+ sessionId: normalizedSessionId,
957
+ wakeMode: normalizedWakeMode,
958
+ });
959
+ const existing = ACTIVE_DAEMONS.get(daemonKey);
960
+ if (existing) {
961
+ return existing.handle;
962
+ }
963
+
964
+ const controller = new AbortController();
965
+ const startedAt = new Date().toISOString();
966
+ const normalizedPollMs = normalizePositiveInteger(pollMs, DEFAULT_DAEMON_POLL_MS);
967
+ const normalizedSweepIntervalMs = normalizePositiveInteger(
968
+ sweepIntervalMs,
969
+ DEFAULT_SWEEP_INTERVAL_MS
970
+ );
971
+
972
+ const daemonState = {
973
+ daemonKey,
974
+ sessionId: normalizedSessionId || null,
975
+ wakeMode: normalizedWakeMode,
976
+ targetPath: resolvedTargetPath,
977
+ startedAt,
978
+ running: true,
979
+ timer: null,
980
+ inFlight: false,
981
+ lastTickAt: null,
982
+ lastTickResult: null,
983
+ stopReason: null,
984
+ };
985
+
986
+ const runTick = async () => {
987
+ if (!daemonState.running || controller.signal.aborted || daemonState.inFlight) {
988
+ return daemonState.lastTickResult;
989
+ }
990
+ daemonState.inFlight = true;
991
+ try {
992
+ const result =
993
+ normalizedWakeMode === WAKE_MODES.SCHEDULED
994
+ ? await scheduledErrorSweep({
995
+ targetPath: resolvedTargetPath,
996
+ outputDir,
997
+ sessionId: normalizedSessionId,
998
+ region,
999
+ tz,
1000
+ env,
1001
+ homeDir,
1002
+ })
1003
+ : await runErrorDaemonWorker({
1004
+ targetPath: resolvedTargetPath,
1005
+ outputDir,
1006
+ sessionId: normalizedSessionId,
1007
+ wakeMode: normalizedWakeMode,
1008
+ maxEvents,
1009
+ env,
1010
+ homeDir,
1011
+ });
1012
+ daemonState.lastTickAt = new Date().toISOString();
1013
+ daemonState.lastTickResult = result;
1014
+ return result;
1015
+ } finally {
1016
+ daemonState.inFlight = false;
1017
+ }
1018
+ };
1019
+
1020
+ const stop = async (reason = "manual") => {
1021
+ if (!daemonState.running) {
1022
+ return {
1023
+ stopped: false,
1024
+ daemonKey,
1025
+ reason: daemonState.stopReason || normalizeString(reason) || "manual",
1026
+ };
1027
+ }
1028
+ daemonState.running = false;
1029
+ daemonState.stopReason = normalizeString(reason) || "manual";
1030
+ controller.abort();
1031
+ if (daemonState.timer) {
1032
+ clearInterval(daemonState.timer);
1033
+ daemonState.timer = null;
1034
+ }
1035
+ ACTIVE_DAEMONS.delete(daemonKey);
1036
+ let event = null;
1037
+ if (normalizedSessionId) {
1038
+ event = await emitDaemonSessionEvent(
1039
+ normalizedSessionId,
1040
+ {
1041
+ target: ERROR_DAEMON_AGENT_ID,
1042
+ reason: daemonState.stopReason,
1043
+ wakeMode: normalizedWakeMode,
1044
+ },
1045
+ {
1046
+ event: "agent_killed",
1047
+ targetPath: resolvedTargetPath,
1048
+ nowIso: new Date().toISOString(),
1049
+ }
1050
+ );
1051
+ }
1052
+ return {
1053
+ stopped: true,
1054
+ daemonKey,
1055
+ sessionId: normalizedSessionId || null,
1056
+ wakeMode: normalizedWakeMode,
1057
+ stoppedAt: new Date().toISOString(),
1058
+ reason: daemonState.stopReason,
1059
+ event,
1060
+ };
1061
+ };
1062
+
1063
+ const handle = {
1064
+ daemonKey,
1065
+ sessionId: normalizedSessionId || null,
1066
+ wakeMode: normalizedWakeMode,
1067
+ targetPath: resolvedTargetPath,
1068
+ startedAt,
1069
+ signal: controller.signal,
1070
+ runTick,
1071
+ stop,
1072
+ isRunning: () => daemonState.running,
1073
+ getState: () => ({
1074
+ daemonKey,
1075
+ sessionId: normalizedSessionId || null,
1076
+ wakeMode: normalizedWakeMode,
1077
+ targetPath: resolvedTargetPath,
1078
+ startedAt,
1079
+ running: daemonState.running,
1080
+ lastTickAt: daemonState.lastTickAt,
1081
+ stopReason: daemonState.stopReason,
1082
+ }),
1083
+ };
1084
+ ACTIVE_DAEMONS.set(daemonKey, {
1085
+ ...daemonState,
1086
+ handle,
1087
+ });
1088
+
1089
+ if (autoStart) {
1090
+ await runTick();
1091
+ const intervalMs =
1092
+ normalizedWakeMode === WAKE_MODES.SCHEDULED ? normalizedSweepIntervalMs : normalizedPollMs;
1093
+ daemonState.timer = setInterval(() => {
1094
+ void runTick().catch(() => {});
1095
+ }, intervalMs);
1096
+ if (typeof daemonState.timer.unref === "function") {
1097
+ daemonState.timer.unref();
1098
+ }
1099
+ }
1100
+
1101
+ return handle;
1102
+ }
1103
+
1104
+ export async function stopErrorEventDaemon({
1105
+ targetPath = ".",
1106
+ sessionId = "",
1107
+ wakeMode = "",
1108
+ reason = "manual",
1109
+ } = {}) {
1110
+ const resolvedTargetPath = path.resolve(String(targetPath || "."));
1111
+ const normalizedSessionId = normalizeString(sessionId);
1112
+ const normalizedReason = normalizeString(reason) || "manual";
1113
+ const normalizedWakeMode = normalizeString(wakeMode)
1114
+ ? normalizeWakeMode(wakeMode, WAKE_MODES.REALTIME)
1115
+ : "";
1116
+
1117
+ const matching = [];
1118
+ for (const [key, value] of ACTIVE_DAEMONS.entries()) {
1119
+ const sameTargetPath = value.targetPath === resolvedTargetPath;
1120
+ const sameSession =
1121
+ !normalizedSessionId || normalizeString(value.sessionId) === normalizedSessionId;
1122
+ const sameWakeMode = !normalizedWakeMode || value.wakeMode === normalizedWakeMode;
1123
+ if (sameTargetPath && sameSession && sameWakeMode) {
1124
+ matching.push({ key, value });
1125
+ }
1126
+ }
1127
+
1128
+ if (matching.length === 0) {
1129
+ return {
1130
+ stopped: false,
1131
+ count: 0,
1132
+ targetPath: resolvedTargetPath,
1133
+ sessionId: normalizedSessionId || null,
1134
+ wakeMode: normalizedWakeMode || null,
1135
+ };
1136
+ }
1137
+
1138
+ const stopped = [];
1139
+ for (const match of matching) {
1140
+ const result = await match.value.handle.stop(normalizedReason);
1141
+ stopped.push(result);
1142
+ }
1143
+
1144
+ return {
1145
+ stopped: true,
1146
+ count: stopped.length,
1147
+ targetPath: resolvedTargetPath,
1148
+ sessionId: normalizedSessionId || null,
1149
+ wakeMode: normalizedWakeMode || null,
1150
+ daemons: stopped,
1151
+ };
1152
+ }
1153
+
571
1154
  function parseStatusFilter(statuses = []) {
572
1155
  if (!Array.isArray(statuses)) {
573
1156
  return null;