bosun 0.28.0 → 0.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/task-executor.mjs CHANGED
@@ -62,8 +62,13 @@ import {
62
62
  } from "./task-store.mjs";
63
63
  import { createErrorDetector } from "./error-detector.mjs";
64
64
  import { getSessionTracker } from "./session-tracker.mjs";
65
- import { getCompactDiffSummary, getRecentCommits } from "./diff-stats.mjs";
65
+ import {
66
+ getCompactDiffSummary,
67
+ getRecentCommits,
68
+ collectDiffStats,
69
+ } from "./diff-stats.mjs";
66
70
  import { createAnomalyDetector } from "./anomaly-detector.mjs";
71
+ import { normalizeDedupKey } from "./utils.mjs";
67
72
  import {
68
73
  resolveExecutorForTask,
69
74
  executorToSdk,
@@ -521,15 +526,46 @@ const AGENT_WORK_STREAM = resolve(AGENT_WORK_DIR, "agent-work-stream.jsonl");
521
526
  const AGENT_WORK_ERRORS = resolve(AGENT_WORK_DIR, "agent-errors.jsonl");
522
527
  const AGENT_WORK_METRICS = resolve(AGENT_WORK_DIR, "agent-metrics.jsonl");
523
528
  const AGENT_WORK_SESSIONS = resolve(AGENT_WORK_DIR, "agent-sessions");
529
+ const INTERNAL_ANOMALY_SIGNAL_PATH = resolve(
530
+ __dirname,
531
+ "..",
532
+ ".cache",
533
+ "anomaly-signals.json",
534
+ );
524
535
  const agentWorkSessionStarts = new Map();
536
+ const attemptTelemetry = new Map();
525
537
  const anomalyAbortTargets = new Map();
526
538
  const internalAnomalyEnabled = process.env.BOSUN_INTERNAL_ANOMALY !== "false";
539
+ const TELEMETRY_SAMPLE_RATE = (() => {
540
+ const raw = Number(process.env.BOSUN_TELEMETRY_SAMPLE_RATE || "1");
541
+ if (!Number.isFinite(raw)) return 1;
542
+ return Math.min(1, Math.max(0, raw));
543
+ })();
527
544
  const anomalyDetector = internalAnomalyEnabled
528
545
  ? createAnomalyDetector({
529
546
  onAnomaly: (anomaly) => {
530
547
  console.warn(
531
548
  `${TAG} anomaly ${anomaly.severity} ${anomaly.type} [${anomaly.shortId}]: ${anomaly.message}`,
532
549
  );
550
+ writeInternalAnomalySignal(anomaly);
551
+ const notifyFn = globalThis.__bosunNotifyAnomaly;
552
+ if (typeof notifyFn === "function") {
553
+ try {
554
+ notifyFn(anomaly);
555
+ } catch {
556
+ /* best effort */
557
+ }
558
+ }
559
+ if (anomaly?.processId && attemptTelemetry.has(anomaly.processId)) {
560
+ const telemetry = attemptTelemetry.get(anomaly.processId);
561
+ telemetry.anomaly_actions = telemetry.anomaly_actions || [];
562
+ telemetry.anomaly_actions.push({
563
+ type: anomaly.type,
564
+ action: anomaly.action,
565
+ severity: anomaly.severity,
566
+ timestamp: Date.now(),
567
+ });
568
+ }
533
569
  if (anomaly.action === "kill" || anomaly.action === "restart") {
534
570
  const target = anomalyAbortTargets.get(anomaly.processId);
535
571
  if (target && !target.signal.aborted) {
@@ -550,6 +586,14 @@ function ensureAgentWorkDirs() {
550
586
  }
551
587
 
552
588
  function writeAgentWorkEvent(entry) {
589
+ const eventType = String(entry?.event_type || "");
590
+ const shouldSample =
591
+ TELEMETRY_SAMPLE_RATE < 1 &&
592
+ ["agent_output", "tool_call", "tool_result", "usage"].includes(eventType);
593
+ if (shouldSample && Math.random() > TELEMETRY_SAMPLE_RATE) {
594
+ return;
595
+ }
596
+
553
597
  ensureAgentWorkDirs();
554
598
  const line = JSON.stringify(entry);
555
599
  appendFileSync(AGENT_WORK_STREAM, `${line}\n`, "utf8");
@@ -562,6 +606,113 @@ function writeAgentWorkEvent(entry) {
562
606
  }
563
607
  }
564
608
 
609
+ function getAttemptTelemetry(attemptId) {
610
+ if (!attemptTelemetry.has(attemptId)) {
611
+ attemptTelemetry.set(attemptId, {
612
+ tool_calls: 0,
613
+ tool_results: 0,
614
+ tool_failures: 0,
615
+ commands: 0,
616
+ command_failures: 0,
617
+ permission_requests: 0,
618
+ permission_denied: 0,
619
+ prompt_tokens: 0,
620
+ completion_tokens: 0,
621
+ total_tokens: 0,
622
+ cost_usd: 0,
623
+ model: null,
624
+ anomaly_actions: [],
625
+ auto_resume: false,
626
+ });
627
+ }
628
+ return attemptTelemetry.get(attemptId);
629
+ }
630
+
631
+ function applyUsage(telemetry, usage) {
632
+ if (!usage) return;
633
+ const prompt =
634
+ usage.prompt_tokens ??
635
+ usage.input_tokens ??
636
+ usage.tokens_in ??
637
+ usage.promptTokens ??
638
+ 0;
639
+ const completion =
640
+ usage.completion_tokens ??
641
+ usage.output_tokens ??
642
+ usage.tokens_out ??
643
+ usage.completionTokens ??
644
+ 0;
645
+ const total =
646
+ usage.total_tokens ??
647
+ usage.totalTokens ??
648
+ usage.tokens ??
649
+ (prompt + completion);
650
+ if (prompt) telemetry.prompt_tokens += Number(prompt) || 0;
651
+ if (completion) telemetry.completion_tokens += Number(completion) || 0;
652
+ if (total) telemetry.total_tokens += Number(total) || 0;
653
+ const cost =
654
+ usage.cost_usd ??
655
+ usage.cost ??
656
+ usage.total_cost ??
657
+ usage.usd ??
658
+ 0;
659
+ if (cost) telemetry.cost_usd += Number(cost) || 0;
660
+ }
661
+
662
+ function extractUsageFromEvent(event) {
663
+ return (
664
+ event?.usage ||
665
+ event?.data?.usage ||
666
+ event?.item?.usage ||
667
+ event?.message?.usage ||
668
+ event?.response?.usage ||
669
+ event?.metrics?.usage ||
670
+ event?.data?.metrics?.usage ||
671
+ event?.result?.usage ||
672
+ null
673
+ );
674
+ }
675
+
676
+ function classifyError(errorDetector, message, stderr) {
677
+ if (!errorDetector) return { category: "unknown", confidence: 0 };
678
+ const result = errorDetector.classify(message || "", stderr || "");
679
+ return {
680
+ category: result.pattern || "unknown",
681
+ confidence: result.confidence || 0,
682
+ details: result.details,
683
+ rawMatch: result.rawMatch,
684
+ };
685
+ }
686
+
687
+ function writeInternalAnomalySignal(anomaly) {
688
+ try {
689
+ const dir = resolve(__dirname, "..", ".cache");
690
+ mkdirSync(dir, { recursive: true });
691
+ let signals = [];
692
+ try {
693
+ const raw = readFileSync(INTERNAL_ANOMALY_SIGNAL_PATH, "utf8");
694
+ signals = JSON.parse(raw);
695
+ if (!Array.isArray(signals)) signals = [];
696
+ } catch {
697
+ /* ignore */
698
+ }
699
+ signals.push({
700
+ source: "internal",
701
+ type: anomaly.type,
702
+ severity: anomaly.severity,
703
+ action: anomaly.action,
704
+ shortId: anomaly.shortId,
705
+ processId: anomaly.processId,
706
+ message: anomaly.message,
707
+ timestamp: new Date().toISOString(),
708
+ });
709
+ if (signals.length > 50) signals = signals.slice(-50);
710
+ writeFileSync(INTERNAL_ANOMALY_SIGNAL_PATH, JSON.stringify(signals, null, 2));
711
+ } catch {
712
+ /* best effort */
713
+ }
714
+ }
715
+
565
716
  function startAgentWorkSession({ attemptId, taskMeta, executorInfo, gitContext, prompt }) {
566
717
  agentWorkSessionStarts.set(attemptId, Date.now());
567
718
  writeAgentWorkEvent({
@@ -575,9 +726,21 @@ function startAgentWorkSession({ attemptId, taskMeta, executorInfo, gitContext,
575
726
  });
576
727
  }
577
728
 
578
- function stopAgentWorkSession({ attemptId, status, executorInfo, taskMeta, gitContext, metrics }) {
729
+ function stopAgentWorkSession({
730
+ attemptId,
731
+ status,
732
+ executorInfo,
733
+ taskMeta,
734
+ gitContext,
735
+ metrics,
736
+ }) {
579
737
  const startedAt = agentWorkSessionStarts.get(attemptId);
580
738
  const durationMs = startedAt ? Date.now() - startedAt : null;
739
+ const telemetry = attemptTelemetry.get(attemptId) || {};
740
+ const mergedMetrics = {
741
+ ...telemetry,
742
+ ...metrics,
743
+ };
581
744
  writeAgentWorkEvent({
582
745
  timestamp: new Date().toISOString(),
583
746
  attempt_id: attemptId,
@@ -585,14 +748,14 @@ function stopAgentWorkSession({ attemptId, status, executorInfo, taskMeta, gitCo
585
748
  data: {
586
749
  completion_status: status,
587
750
  duration_ms: durationMs ?? undefined,
588
- ...metrics,
751
+ ...mergedMetrics,
589
752
  },
590
753
  ...taskMeta,
591
754
  ...executorInfo,
592
755
  ...gitContext,
593
756
  });
594
757
 
595
- if (metrics) {
758
+ if (mergedMetrics) {
596
759
  appendFileSync(
597
760
  AGENT_WORK_METRICS,
598
761
  `${JSON.stringify({
@@ -600,11 +763,12 @@ function stopAgentWorkSession({ attemptId, status, executorInfo, taskMeta, gitCo
600
763
  attempt_id: attemptId,
601
764
  ...taskMeta,
602
765
  ...executorInfo,
603
- metrics,
766
+ metrics: mergedMetrics,
604
767
  })}\n`,
605
768
  "utf8",
606
769
  );
607
770
  }
771
+ attemptTelemetry.delete(attemptId);
608
772
  }
609
773
 
610
774
  function synthesizeAnomalyLine(event) {
@@ -654,10 +818,12 @@ function createAgentLogStreamer({
654
818
  taskMeta,
655
819
  executorInfo,
656
820
  gitContext,
821
+ errorDetector,
657
822
  }) {
658
823
  const shortId = taskId.substring(0, 8);
659
824
  const logFile = resolve(AGENT_LOGS_DIR, `agent-${shortId}.log`);
660
825
  const tracker = getSessionTracker();
826
+ const telemetry = getAttemptTelemetry(attemptId);
661
827
 
662
828
  // Ensure log dir exists
663
829
  try {
@@ -699,24 +865,53 @@ function createAgentLogStreamer({
699
865
 
700
866
  try {
701
867
  const ts = new Date().toISOString();
868
+ if (event?.model && !telemetry.model) telemetry.model = event.model;
869
+ if (event?.data?.model && !telemetry.model)
870
+ telemetry.model = event.data.model;
871
+ if (event?.message?.model && !telemetry.model)
872
+ telemetry.model = event.message.model;
873
+
874
+ const usage = extractUsageFromEvent(event);
875
+ applyUsage(telemetry, usage);
876
+ if (usage) {
877
+ writeAgentWorkEvent({
878
+ timestamp: ts,
879
+ attempt_id: attemptId,
880
+ event_type: "usage",
881
+ data: usage,
882
+ ...taskMeta,
883
+ ...executorInfo,
884
+ ...gitContext,
885
+ });
886
+ }
887
+
888
+ if (String(event?.type || "").toLowerCase().includes("permission")) {
889
+ telemetry.permission_requests += 1;
890
+ if (String(event?.action || "").toLowerCase().includes("deny")) {
891
+ telemetry.permission_denied += 1;
892
+ }
893
+ }
894
+
702
895
  if (event.type === "item.completed" && event.item) {
703
896
  const item = event.item;
704
897
  if (item.type === "agent_message" && item.text) {
898
+ const text = item.text;
705
899
  writeAgentWorkEvent({
706
900
  timestamp: ts,
707
901
  attempt_id: attemptId,
708
902
  event_type: "agent_output",
709
- data: { output: item.text, stream_type: "stdout" },
903
+ data: { output: text, stream_type: "stdout" },
710
904
  ...taskMeta,
711
905
  ...executorInfo,
712
906
  ...gitContext,
713
907
  });
714
908
  appendFileSync(
715
909
  logFile,
716
- `[${ts}] AGENT: ${item.text.slice(0, 2000)}\n`,
910
+ `[${ts}] AGENT: ${text.slice(0, 2000)}\n`,
717
911
  "utf8",
718
912
  );
719
913
  } else if (item.type === "function_call") {
914
+ telemetry.tool_calls += 1;
720
915
  writeAgentWorkEvent({
721
916
  timestamp: ts,
722
917
  attempt_id: attemptId,
@@ -733,16 +928,29 @@ function createAgentLogStreamer({
733
928
  );
734
929
  } else if (item.type === "function_call_output") {
735
930
  const out = (item.output || "").slice(0, 500);
931
+ telemetry.tool_results += 1;
932
+ const failed =
933
+ item.status === "failed" ||
934
+ item.success === false ||
935
+ String(item.output || "").toLowerCase().includes("error");
936
+ if (failed) telemetry.tool_failures += 1;
736
937
  writeAgentWorkEvent({
737
938
  timestamp: ts,
738
939
  attempt_id: attemptId,
739
940
  event_type: "tool_result",
740
- data: { tool_name: item.name, result: item.output || "" },
941
+ data: {
942
+ tool_name: item.name,
943
+ result: item.output || "",
944
+ status: item.status || (failed ? "failed" : "completed"),
945
+ },
741
946
  ...taskMeta,
742
947
  ...executorInfo,
743
948
  ...gitContext,
744
949
  });
745
950
  appendFileSync(logFile, `[${ts}] RESULT: ${out}\n`, "utf8");
951
+ } else if (item.type === "commandExecution") {
952
+ telemetry.commands += 1;
953
+ if (item.status === "failed") telemetry.command_failures += 1;
746
954
  } else {
747
955
  appendFileSync(
748
956
  logFile,
@@ -750,6 +958,46 @@ function createAgentLogStreamer({
750
958
  "utf8",
751
959
  );
752
960
  }
961
+ } else if (event.type === "assistant.message") {
962
+ const content = event.data?.content || "";
963
+ if (content) {
964
+ writeAgentWorkEvent({
965
+ timestamp: ts,
966
+ attempt_id: attemptId,
967
+ event_type: "agent_output",
968
+ data: { output: content, stream_type: "stdout" },
969
+ ...taskMeta,
970
+ ...executorInfo,
971
+ ...gitContext,
972
+ });
973
+ }
974
+ } else if (event.type === "assistant.message_delta") {
975
+ const delta = event.data?.deltaContent || "";
976
+ if (delta && delta.length > 40) {
977
+ writeAgentWorkEvent({
978
+ timestamp: ts,
979
+ attempt_id: attemptId,
980
+ event_type: "agent_output",
981
+ data: { output: delta, stream_type: "stdout", delta: true },
982
+ ...taskMeta,
983
+ ...executorInfo,
984
+ ...gitContext,
985
+ });
986
+ }
987
+ } else if (event.type === "tool_call") {
988
+ telemetry.tool_calls += 1;
989
+ writeAgentWorkEvent({
990
+ timestamp: ts,
991
+ attempt_id: attemptId,
992
+ event_type: "tool_call",
993
+ data: {
994
+ tool_name: event.tool_name || event.data?.tool_name || "",
995
+ tool_params: event.tool_params || event.data?.tool_params || "",
996
+ },
997
+ ...taskMeta,
998
+ ...executorInfo,
999
+ ...gitContext,
1000
+ });
753
1001
  } else if (event.type === "item.created") {
754
1002
  const item = event.item || {};
755
1003
  appendFileSync(
@@ -763,12 +1011,44 @@ function createAgentLogStreamer({
763
1011
  }
764
1012
 
765
1013
  if (event.type === "error") {
1014
+ const errText = event.error || event.message || "unknown error";
1015
+ const classification = classifyError(errorDetector, errText);
1016
+ const fingerprint = normalizeDedupKey(errText).slice(0, 120);
1017
+ writeAgentWorkEvent({
1018
+ timestamp: ts,
1019
+ attempt_id: attemptId,
1020
+ event_type: "error",
1021
+ data: {
1022
+ error_message: errText,
1023
+ error_category: classification.category,
1024
+ error_fingerprint: fingerprint,
1025
+ error_confidence: classification.confidence,
1026
+ },
1027
+ ...taskMeta,
1028
+ ...executorInfo,
1029
+ ...gitContext,
1030
+ });
1031
+ } else if (
1032
+ event?.item?.type === "commandExecution" &&
1033
+ event.item?.status === "failed"
1034
+ ) {
1035
+ const errText =
1036
+ event.item?.error ||
1037
+ event.item?.stderr ||
1038
+ event.item?.message ||
1039
+ "command failed";
1040
+ const classification = classifyError(errorDetector, errText);
1041
+ const fingerprint = normalizeDedupKey(errText).slice(0, 120);
766
1042
  writeAgentWorkEvent({
767
1043
  timestamp: ts,
768
1044
  attempt_id: attemptId,
769
1045
  event_type: "error",
770
1046
  data: {
771
- error_message: event.error || event.message || "unknown error",
1047
+ error_message: errText,
1048
+ error_category: classification.category,
1049
+ error_fingerprint: fingerprint,
1050
+ error_confidence: classification.confidence,
1051
+ source: "commandExecution",
772
1052
  },
773
1053
  ...taskMeta,
774
1054
  ...executorInfo,
@@ -3448,6 +3728,7 @@ class TaskExecutor {
3448
3728
  taskMeta,
3449
3729
  executorInfo,
3450
3730
  gitContext,
3731
+ errorDetector: this._errorDetector,
3451
3732
  }),
3452
3733
  abortController: taskAbortController,
3453
3734
  // When AbortController is replaced after idle_continue, update our reference
@@ -3534,6 +3815,8 @@ class TaskExecutor {
3534
3815
  sessionMessages,
3535
3816
  );
3536
3817
  if (shouldAutoResume) {
3818
+ const telemetry = getAttemptTelemetry(attemptId);
3819
+ telemetry.auto_resume = true;
3537
3820
  console.log(
3538
3821
  `${TAG} completion validation: "${taskTitle}" reported success but appears incomplete — auto-resuming`,
3539
3822
  );
@@ -3569,6 +3852,7 @@ class TaskExecutor {
3569
3852
  taskMeta,
3570
3853
  executorInfo,
3571
3854
  gitContext,
3855
+ errorDetector: this._errorDetector,
3572
3856
  }),
3573
3857
  abortController: resumeAC,
3574
3858
  onAbortControllerReplaced: (newAC) => {
@@ -3612,6 +3896,13 @@ class TaskExecutor {
3612
3896
  }
3613
3897
  }
3614
3898
 
3899
+ let diffStats = null;
3900
+ try {
3901
+ diffStats = collectDiffStats(wt.path);
3902
+ } catch {
3903
+ diffStats = null;
3904
+ }
3905
+
3615
3906
  stopAgentWorkSession({
3616
3907
  attemptId,
3617
3908
  status: validatedResult?.success ? "success" : "failed",
@@ -3619,11 +3910,17 @@ class TaskExecutor {
3619
3910
  taskMeta,
3620
3911
  gitContext,
3621
3912
  metrics: {
3622
- tool_calls: Array.isArray(validatedResult?.items)
3623
- ? validatedResult.items.length
3624
- : undefined,
3625
3913
  success: !!validatedResult?.success,
3626
3914
  retry_count: Math.max(0, (validatedResult?.attempts || 1) - 1),
3915
+ attempts: validatedResult?.attempts || 1,
3916
+ continues: validatedResult?.continues || 0,
3917
+ first_shot_success:
3918
+ !!validatedResult?.success && (validatedResult?.attempts || 1) === 1,
3919
+ agent_made_commits: agentMadeNewCommits,
3920
+ diff_summary: diffStats?.formatted || null,
3921
+ diff_files_changed: diffStats?.totalFiles ?? null,
3922
+ diff_lines_added: diffStats?.totalAdditions ?? null,
3923
+ diff_lines_deleted: diffStats?.totalDeletions ?? null,
3627
3924
  },
3628
3925
  });
3629
3926
  anomalyAbortTargets.delete(attemptId);