@plainconceptsplatform/loop-task 2.6.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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +534 -0
  3. package/dist/app/App.js +132 -0
  4. package/dist/app/BoardLayout.js +5 -0
  5. package/dist/app/index.js +48 -0
  6. package/dist/app/providers/index.js +1 -0
  7. package/dist/app/router/index.js +16 -0
  8. package/dist/app/types.js +1 -0
  9. package/dist/cli/import-validator.js +152 -0
  10. package/dist/cli/import-writer.js +81 -0
  11. package/dist/cli.js +573 -0
  12. package/dist/client/cli-format.js +7 -0
  13. package/dist/client/commands.js +162 -0
  14. package/dist/client/ipc.js +96 -0
  15. package/dist/client/project-commands.js +164 -0
  16. package/dist/core/command/command-runner.js +300 -0
  17. package/dist/core/command/process-tree.js +73 -0
  18. package/dist/core/command/resolve-cwd.js +8 -0
  19. package/dist/core/command/stdout-capture-transform.js +73 -0
  20. package/dist/core/context/context-parser.js +65 -0
  21. package/dist/core/context/log-context.js +3 -0
  22. package/dist/core/context/template.js +20 -0
  23. package/dist/core/context/validate-context.js +19 -0
  24. package/dist/core/foreground/index.js +81 -0
  25. package/dist/core/logging/bounded-log-reader.js +368 -0
  26. package/dist/core/logging/log-follower.js +26 -0
  27. package/dist/core/logging/log-parser.js +29 -0
  28. package/dist/core/logging/log-rotator.js +34 -0
  29. package/dist/core/logging/rotating-log-stream.js +137 -0
  30. package/dist/core/loop/chain-executor.js +171 -0
  31. package/dist/core/loop/delay-utils.js +65 -0
  32. package/dist/core/loop/loop-controller.js +240 -0
  33. package/dist/core/loop/loop-runner.js +194 -0
  34. package/dist/core/loop/run-executor.js +144 -0
  35. package/dist/core/loop/types.js +1 -0
  36. package/dist/core/scheduling/index.js +16 -0
  37. package/dist/daemon/daemon-log.js +11 -0
  38. package/dist/daemon/diagnostics.js +85 -0
  39. package/dist/daemon/http/helpers.js +81 -0
  40. package/dist/daemon/http/openapi.js +119 -0
  41. package/dist/daemon/http/route-loops.js +311 -0
  42. package/dist/daemon/http/route-misc.js +60 -0
  43. package/dist/daemon/http/route-projects.js +55 -0
  44. package/dist/daemon/http/route-tasks.js +141 -0
  45. package/dist/daemon/http/routes.js +19 -0
  46. package/dist/daemon/http/server.js +82 -0
  47. package/dist/daemon/http/sse.js +36 -0
  48. package/dist/daemon/index.js +189 -0
  49. package/dist/daemon/ipc/send.js +5 -0
  50. package/dist/daemon/managers/loop-entry.js +27 -0
  51. package/dist/daemon/managers/loop-manager.js +346 -0
  52. package/dist/daemon/managers/loop-options.js +17 -0
  53. package/dist/daemon/managers/loop-serialization.js +53 -0
  54. package/dist/daemon/managers/project-manager.js +170 -0
  55. package/dist/daemon/managers/task-manager.js +94 -0
  56. package/dist/daemon/mcp/index.js +2 -0
  57. package/dist/daemon/mcp/openapi-sync.js +50 -0
  58. package/dist/daemon/mcp/server.js +167 -0
  59. package/dist/daemon/mcp/tools.js +382 -0
  60. package/dist/daemon/recipe/deferred-reload.js +32 -0
  61. package/dist/daemon/recipe/diagram-reader.js +26 -0
  62. package/dist/daemon/recipe/file-writer.js +52 -0
  63. package/dist/daemon/recipe/id-remapper.js +46 -0
  64. package/dist/daemon/recipe/runtime-state.js +23 -0
  65. package/dist/daemon/recipe/scanner.js +165 -0
  66. package/dist/daemon/recipe/task-store.js +33 -0
  67. package/dist/daemon/recipe/validator.js +35 -0
  68. package/dist/daemon/server/handlers/diagnostics-handlers.js +7 -0
  69. package/dist/daemon/server/handlers/index.js +101 -0
  70. package/dist/daemon/server/handlers/log-handlers.js +129 -0
  71. package/dist/daemon/server/handlers/loop-handlers.js +64 -0
  72. package/dist/daemon/server/handlers/project-handlers.js +42 -0
  73. package/dist/daemon/server/handlers/settings-handlers.js +8 -0
  74. package/dist/daemon/server/handlers/task-handlers.js +20 -0
  75. package/dist/daemon/server/handlers/telemetry-handlers.js +5 -0
  76. package/dist/daemon/server/index.js +117 -0
  77. package/dist/daemon/settings-manager.js +73 -0
  78. package/dist/daemon/spawner/index.js +204 -0
  79. package/dist/daemon/state/index.js +209 -0
  80. package/dist/daemon/telemetry/agent-integrations/agent-integration.js +1 -0
  81. package/dist/daemon/telemetry/agent-integrations/claude-code-integration.js +55 -0
  82. package/dist/daemon/telemetry/agent-integrations/detect-agent-integration.js +20 -0
  83. package/dist/daemon/telemetry/agent-integrations/index.js +3 -0
  84. package/dist/daemon/telemetry/agent-integrations/opencode-integration.js +72 -0
  85. package/dist/daemon/telemetry/index.js +6 -0
  86. package/dist/daemon/telemetry/noop-telemetry-adapter.js +53 -0
  87. package/dist/daemon/telemetry/open-telemetry-adapter.js +464 -0
  88. package/dist/daemon/telemetry/telemetry-manager.js +147 -0
  89. package/dist/daemon/telemetry/telemetry-redaction.js +67 -0
  90. package/dist/daemon/telemetry/telemetry-types.js +45 -0
  91. package/dist/daemon/telemetry/telemetry.js +1 -0
  92. package/dist/daemon/watcher/index.js +250 -0
  93. package/dist/duration.js +24 -0
  94. package/dist/entities/loops/filters.js +109 -0
  95. package/dist/entities/loops/index.js +1 -0
  96. package/dist/entities/projects/filters.js +69 -0
  97. package/dist/entities/projects/index.js +1 -0
  98. package/dist/entities/tasks/filters.js +41 -0
  99. package/dist/entities/tasks/index.js +1 -0
  100. package/dist/entry.js +16 -0
  101. package/dist/esm-loader.js +39 -0
  102. package/dist/features/chain-editor/ChainEditor.js +116 -0
  103. package/dist/features/chain-editor/mermaidToAscii.js +375 -0
  104. package/dist/features/chain-editor/renderChainDiagram.js +106 -0
  105. package/dist/features/code-editor/CodeEditorModal.js +84 -0
  106. package/dist/features/code-editor/CodeEditorPreview.js +14 -0
  107. package/dist/features/code-editor/useEditorKeyboard.js +150 -0
  108. package/dist/features/code-editor/useModalDimensions.js +9 -0
  109. package/dist/features/commands/commands.js +132 -0
  110. package/dist/features/commands/useCommandHandlers.js +280 -0
  111. package/dist/features/commands/useContextualActions.js +78 -0
  112. package/dist/features/commands/useGlobalShortcuts.js +154 -0
  113. package/dist/features/forms/FormRouter.js +46 -0
  114. package/dist/features/overlays/ContextHelpModal.js +8 -0
  115. package/dist/features/overlays/DiagramModal.js +45 -0
  116. package/dist/features/overlays/ExportModal.js +51 -0
  117. package/dist/features/overlays/HelpGuideModal.js +23 -0
  118. package/dist/features/overlays/HelpModal.js +59 -0
  119. package/dist/features/overlays/OverlayStack.js +10 -0
  120. package/dist/features/overlays/ProjectsModal.js +42 -0
  121. package/dist/features/overlays/TaskPickerModal.js +62 -0
  122. package/dist/features/overlays/WelcomeScreen.js +74 -0
  123. package/dist/features/overlays/useOverlayStack.js +55 -0
  124. package/dist/features/state/useAppState.js +151 -0
  125. package/dist/logger.js +25 -0
  126. package/dist/loop-config.js +152 -0
  127. package/dist/shared/clipboard.js +111 -0
  128. package/dist/shared/config/constants.js +104 -0
  129. package/dist/shared/config/paths.js +67 -0
  130. package/dist/shared/container/index.js +24 -0
  131. package/dist/shared/fs-utils.js +44 -0
  132. package/dist/shared/hooks/useBreakpoint.js +11 -0
  133. package/dist/shared/hooks/useDaemonSettings.js +41 -0
  134. package/dist/shared/hooks/useInject.js +6 -0
  135. package/dist/shared/hooks/useLogStream.js +37 -0
  136. package/dist/shared/hooks/useLoopFormValidation.js +131 -0
  137. package/dist/shared/hooks/useLoopPolling.js +30 -0
  138. package/dist/shared/hooks/useMouseScroll.js +56 -0
  139. package/dist/shared/hooks/useUndoRedo.js +77 -0
  140. package/dist/shared/i18n/en.json +709 -0
  141. package/dist/shared/i18n/index.js +11 -0
  142. package/dist/shared/providers/InversifyProvider.js +6 -0
  143. package/dist/shared/services/export-service.js +43 -0
  144. package/dist/shared/services/log-service.js +28 -0
  145. package/dist/shared/services/loop-service.js +78 -0
  146. package/dist/shared/services/project-service.js +39 -0
  147. package/dist/shared/services/settings-service.js +102 -0
  148. package/dist/shared/services/task-service.js +41 -0
  149. package/dist/shared/services/types.js +8 -0
  150. package/dist/shared/sleep.js +17 -0
  151. package/dist/shared/tail.js +4 -0
  152. package/dist/shared/ui/Button.js +18 -0
  153. package/dist/shared/ui/DebugPanel.js +9 -0
  154. package/dist/shared/ui/FocusableButton.js +18 -0
  155. package/dist/shared/ui/FocusableInput.js +72 -0
  156. package/dist/shared/ui/FocusableList.js +48 -0
  157. package/dist/shared/ui/Modal.js +13 -0
  158. package/dist/shared/ui/SelectModal.js +78 -0
  159. package/dist/shared/ui/Toast.js +37 -0
  160. package/dist/shared/ui/format.js +117 -0
  161. package/dist/shared/ui/hooks/useHoverState.js +11 -0
  162. package/dist/shared/ui/state.js +9 -0
  163. package/dist/shared/ui/theme.js +96 -0
  164. package/dist/shared/utils/log-lines.js +13 -0
  165. package/dist/shared/utils/paste.js +37 -0
  166. package/dist/shared/utils/syntax.js +128 -0
  167. package/dist/shared/utils/validation.js +56 -0
  168. package/dist/types.js +1 -0
  169. package/dist/visual-evidence/capture.js +51 -0
  170. package/dist/visual-evidence/cli.js +85 -0
  171. package/dist/visual-evidence/evidence-required.js +71 -0
  172. package/dist/visual-evidence/index.js +10 -0
  173. package/dist/visual-evidence/launch.js +61 -0
  174. package/dist/visual-evidence/manifest.js +32 -0
  175. package/dist/visual-evidence/openspec-resolver.js +45 -0
  176. package/dist/visual-evidence/publish.js +126 -0
  177. package/dist/visual-evidence/run.js +121 -0
  178. package/dist/visual-evidence/scenario-registry.js +132 -0
  179. package/dist/widgets/command-input/CommandDropdown.js +69 -0
  180. package/dist/widgets/command-input/CommandInput.js +133 -0
  181. package/dist/widgets/command-input/HintBar.js +9 -0
  182. package/dist/widgets/command-input/InputModes.js +72 -0
  183. package/dist/widgets/commands-browser/CommandsBrowserModal.js +85 -0
  184. package/dist/widgets/header/Header.js +54 -0
  185. package/dist/widgets/header/TabBar.js +20 -0
  186. package/dist/widgets/left-panel/LeftPanel.js +34 -0
  187. package/dist/widgets/left-panel/Navigator.js +107 -0
  188. package/dist/widgets/left-panel/ProjectsPage.js +76 -0
  189. package/dist/widgets/left-panel/ProjectsPageParts.js +34 -0
  190. package/dist/widgets/left-panel/TaskBrowser.js +132 -0
  191. package/dist/widgets/log-modal/LogModal.js +211 -0
  192. package/dist/widgets/loop-form/CreateForm.js +82 -0
  193. package/dist/widgets/loop-form/TextField.js +9 -0
  194. package/dist/widgets/loop-form/WizardForm.js +199 -0
  195. package/dist/widgets/loop-form/useCreateSteps.js +135 -0
  196. package/dist/widgets/loop-form/useHandleComplete.js +94 -0
  197. package/dist/widgets/project-form/ProjectForm.js +83 -0
  198. package/dist/widgets/right-panel/Inspector.js +41 -0
  199. package/dist/widgets/right-panel/RightPanel.js +38 -0
  200. package/dist/widgets/right-panel/RunHistory.js +187 -0
  201. package/dist/widgets/task-form/TaskForm.js +272 -0
  202. package/package.json +119 -0
@@ -0,0 +1,72 @@
1
+ const OPENCODE_BINARIES = ["opencode"];
2
+ /**
3
+ * Detects `opencode run ...` invocations and enables OpenCode's
4
+ * native OpenTelemetry support, routing to loop-task's endpoint.
5
+ */
6
+ export class OpenCodeTelemetryIntegration {
7
+ constructor() {
8
+ this.id = "opencode";
9
+ }
10
+ matches(command, args) {
11
+ const basename = command.split("/").pop()?.replace(/\.exe$/, "").toLowerCase() ?? "";
12
+ return OPENCODE_BINARIES.includes(basename) && args[0] === "run";
13
+ }
14
+ prepare(invocation, _context) {
15
+ const env = { ...invocation.env };
16
+ // Enable OpenCode's experimental OpenTelemetry
17
+ env.OPENCODE_EXPERIMENTAL_OPEN_TELEMETRY = "true";
18
+ // Adapt the OTLP endpoint/proprotocol from the parent context
19
+ // OpenCode currently reads the standard OTEL_* environment variables
20
+ // which are already set by the adapter's prepareChildProcess
21
+ // We just need to make sure the integration is activated.
22
+ return { command: invocation.command, args: [...invocation.args], env };
23
+ }
24
+ parseUsage(result) {
25
+ if (result.exitCode !== 0 || !result.stdout)
26
+ return undefined;
27
+ try {
28
+ // OpenCode may output JSON with --format json
29
+ // Try to parse structured output for usage data
30
+ const lines = result.stdout.trim().split("\n");
31
+ const lastLine = lines[lines.length - 1];
32
+ if (!lastLine)
33
+ return undefined;
34
+ const parsed = JSON.parse(lastLine);
35
+ if (typeof parsed !== "object" || parsed === null)
36
+ return undefined;
37
+ const usage = {};
38
+ if (typeof parsed.inputTokens === "number")
39
+ usage.inputTokens = parsed.inputTokens;
40
+ if (typeof parsed.input_tokens === "number")
41
+ usage.inputTokens = parsed.input_tokens;
42
+ if (typeof parsed.outputTokens === "number")
43
+ usage.outputTokens = parsed.outputTokens;
44
+ if (typeof parsed.output_tokens === "number")
45
+ usage.outputTokens = parsed.output_tokens;
46
+ if (typeof parsed.cacheReadTokens === "number")
47
+ usage.cacheReadTokens = parsed.cacheReadTokens;
48
+ if (typeof parsed.cache_read_tokens === "number")
49
+ usage.cacheReadTokens = parsed.cache_read_tokens;
50
+ if (typeof parsed.cacheWriteTokens === "number")
51
+ usage.cacheWriteTokens = parsed.cacheWriteTokens;
52
+ if (typeof parsed.cache_write_tokens === "number")
53
+ usage.cacheWriteTokens = parsed.cache_write_tokens;
54
+ if (typeof parsed.cost === "number")
55
+ usage.costUsd = parsed.cost;
56
+ if (typeof parsed.cost_usd === "number")
57
+ usage.costUsd = parsed.cost_usd;
58
+ if (typeof parsed.model === "string")
59
+ usage.model = parsed.model;
60
+ if (typeof parsed.provider === "string")
61
+ usage.provider = parsed.provider;
62
+ if (typeof parsed.sessionId === "string")
63
+ usage.sessionId = parsed.sessionId;
64
+ if (Object.keys(usage).length === 0)
65
+ return undefined;
66
+ return usage;
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
72
+ }
@@ -0,0 +1,6 @@
1
+ export { TelemetryManager } from "./telemetry-manager.js";
2
+ export { NoopTelemetryAdapter } from "./noop-telemetry-adapter.js";
3
+ export { OpenTelemetryAdapter } from "./open-telemetry-adapter.js";
4
+ export * from "./telemetry-types.js";
5
+ export * from "./telemetry-redaction.js";
6
+ export * from "./agent-integrations/index.js";
@@ -0,0 +1,53 @@
1
+ class NoopSpan {
2
+ setAttribute() { }
3
+ setAttributes() { }
4
+ recordError() { }
5
+ ok() { }
6
+ end() { }
7
+ getTraceContext() {
8
+ return {};
9
+ }
10
+ }
11
+ /**
12
+ * No-op telemetry adapter. Used when telemetry is disabled or
13
+ * when no endpoint is configured. All operations are safe no-ops.
14
+ */
15
+ export class NoopTelemetryAdapter {
16
+ constructor(statusOverrides) {
17
+ this.status = {
18
+ enabled: false,
19
+ exporterConfigured: false,
20
+ endpoint: undefined,
21
+ protocol: "http/protobuf",
22
+ serviceName: "loop-task",
23
+ autoInstrumentAgents: false,
24
+ captureContent: false,
25
+ captureCommandOutput: false,
26
+ exporterState: "disabled",
27
+ ...statusOverrides,
28
+ };
29
+ }
30
+ startLoop() {
31
+ return new NoopSpan();
32
+ }
33
+ startTask() {
34
+ return new NoopSpan();
35
+ }
36
+ startCommand() {
37
+ return new NoopSpan();
38
+ }
39
+ recordRetry() { }
40
+ recordFailure() { }
41
+ recordAgentUsage() { }
42
+ prepareChildProcess(_invocation, _context, _integrationOverride) {
43
+ return { env: {} };
44
+ }
45
+ getStatus() {
46
+ return { ...this.status };
47
+ }
48
+ updateStatus(partial) {
49
+ this.status = { ...this.status, ...partial };
50
+ }
51
+ async flush() { }
52
+ async shutdown() { }
53
+ }
@@ -0,0 +1,464 @@
1
+ import { SPAN_NAMES, CORRELATION_KEYS, METRIC_NAMES } from "./telemetry-types.js";
2
+ import { detectAgentIntegration, getAgentIntegrations } from "./agent-integrations/index.js";
3
+ import { NodeSDK } from "@opentelemetry/sdk-node";
4
+ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
5
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
6
+ import { resourceFromAttributes } from "@opentelemetry/resources";
7
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
8
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
9
+ import { OTLPTraceExporter as OTLPTraceExporterGrpc } from "@opentelemetry/exporter-trace-otlp-grpc";
10
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
11
+ import { OTLPMetricExporter as OTLPMetricExporterGrpc } from "@opentelemetry/exporter-metrics-otlp-grpc";
12
+ import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
13
+ import { trace, context, propagation, metrics, SpanStatusCode, SpanKind } from "@opentelemetry/api";
14
+ import { daemonLog } from "../daemon-log.js";
15
+ class OtelSpan {
16
+ constructor(span, ctx, onEnd) {
17
+ this.span = span;
18
+ this.ctx = ctx;
19
+ this.ended = false;
20
+ this.onEnd = onEnd;
21
+ this.startTime = Date.now();
22
+ }
23
+ setAttribute(key, value) {
24
+ this.span.setAttribute(key, value);
25
+ }
26
+ setAttributes(attrs) {
27
+ for (const [k, v] of Object.entries(attrs)) {
28
+ this.span.setAttribute(k, v);
29
+ }
30
+ }
31
+ recordError(error) {
32
+ if (error instanceof Error) {
33
+ this.span.recordException(error);
34
+ }
35
+ else {
36
+ this.span.recordException(String(error));
37
+ }
38
+ this.span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) });
39
+ }
40
+ ok() {
41
+ this.span.setStatus({ code: SpanStatusCode.OK });
42
+ this.span.end();
43
+ this.emitMetric("ok");
44
+ }
45
+ end(status) {
46
+ if (status === "error") {
47
+ this.span.setStatus({ code: SpanStatusCode.ERROR });
48
+ }
49
+ else if (status === "cancelled") {
50
+ this.span.setStatus({ code: SpanStatusCode.ERROR, message: "cancelled" });
51
+ }
52
+ this.span.end();
53
+ this.emitMetric(status ?? "ok");
54
+ }
55
+ getTraceContext() {
56
+ const carrier = {};
57
+ propagation.inject(this.ctx, carrier, {
58
+ set: (c, k, v) => { c[k] = v; },
59
+ });
60
+ return {
61
+ traceParent: carrier.traceparent,
62
+ traceState: carrier.tracestate,
63
+ };
64
+ }
65
+ emitMetric(status) {
66
+ if (this.ended)
67
+ return;
68
+ this.ended = true;
69
+ if (this.onEnd) {
70
+ this.onEnd(Date.now() - this.startTime, status);
71
+ }
72
+ }
73
+ }
74
+ /**
75
+ * OpenTelemetry SDK-backed adapter. Manages the full SDK lifecycle
76
+ * including tracer, meter, and exporters.
77
+ */
78
+ export class OpenTelemetryAdapter {
79
+ constructor(settings) {
80
+ this.sdk = null;
81
+ this.tracerProvider = null;
82
+ this.settings = settings;
83
+ this.tracer = trace.getTracer("loop-task");
84
+ this.meter = metrics.getMeter("loop-task");
85
+ this.status = {
86
+ enabled: settings.telemetryEnabled,
87
+ exporterConfigured: !!settings.telemetryEndpoint,
88
+ endpoint: settings.telemetryEndpoint,
89
+ protocol: settings.telemetryProtocol,
90
+ serviceName: settings.telemetryServiceName,
91
+ autoInstrumentAgents: settings.telemetryAutoInstrumentAgents,
92
+ captureContent: settings.telemetryCaptureContent,
93
+ captureCommandOutput: settings.telemetryCaptureCommandOutput,
94
+ exporterState: this.resolveExporterState(settings),
95
+ };
96
+ // Initialize metric instruments (always created — no-op when no SDK)
97
+ this.runCounter = this.meter.createCounter(METRIC_NAMES.RUNS);
98
+ this.runDurationHistogram = this.meter.createHistogram(METRIC_NAMES.RUN_DURATION);
99
+ this.taskCounter = this.meter.createCounter(METRIC_NAMES.TASKS);
100
+ this.taskDurationHistogram = this.meter.createHistogram(METRIC_NAMES.TASK_DURATION);
101
+ this.taskRetriesCounter = this.meter.createCounter(METRIC_NAMES.TASK_RETRIES);
102
+ this.commandCounter = this.meter.createCounter(METRIC_NAMES.COMMANDS);
103
+ this.commandDurationHistogram = this.meter.createHistogram(METRIC_NAMES.COMMAND_DURATION);
104
+ this.agentExecCounter = this.meter.createCounter(METRIC_NAMES.AGENT_EXECUTIONS);
105
+ this.agentDurationHistogram = this.meter.createHistogram(METRIC_NAMES.AGENT_DURATION);
106
+ this.agentInputTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_INPUT_TOKENS);
107
+ this.agentOutputTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_OUTPUT_TOKENS);
108
+ this.agentCacheReadTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_CACHE_READ_TOKENS);
109
+ this.agentCacheWriteTokensCounter = this.meter.createCounter(METRIC_NAMES.AGENT_CACHE_WRITE_TOKENS);
110
+ this.agentCostCounter = this.meter.createCounter(METRIC_NAMES.AGENT_COST);
111
+ this.failureCounter = this.meter.createCounter(METRIC_NAMES.FAILURES);
112
+ if (settings.telemetryEnabled && settings.telemetryEndpoint) {
113
+ this.initializeSdk(settings);
114
+ }
115
+ }
116
+ resolveExporterState(settings) {
117
+ if (!settings.telemetryEnabled)
118
+ return "disabled";
119
+ if (!settings.telemetryEndpoint)
120
+ return "not-configured";
121
+ return "configured";
122
+ }
123
+ initializeSdk(settings) {
124
+ try {
125
+ const resource = resourceFromAttributes({
126
+ [ATTR_SERVICE_NAME]: settings.telemetryServiceName,
127
+ [ATTR_SERVICE_VERSION]: "2.2.6",
128
+ });
129
+ const isGrpc = settings.telemetryProtocol === "grpc";
130
+ const headers = this.resolveHeaders();
131
+ const endpoint = settings.telemetryEndpoint ?? "";
132
+ const tracesUrl = endpoint.endsWith("/v1/traces")
133
+ ? endpoint
134
+ : `${endpoint}/v1/traces`;
135
+ const metricsUrl = endpoint.endsWith("/v1/metrics")
136
+ ? endpoint
137
+ : `${endpoint}/v1/metrics`;
138
+ const traceExporter = isGrpc
139
+ ? new OTLPTraceExporterGrpc({ url: settings.telemetryEndpoint })
140
+ : new OTLPTraceExporter({ url: tracesUrl, headers });
141
+ const spanProcessor = new BatchSpanProcessor(traceExporter, {
142
+ maxQueueSize: 2048,
143
+ scheduledDelayMillis: 5000,
144
+ maxExportBatchSize: 512,
145
+ });
146
+ const tracerProvider = new NodeTracerProvider({
147
+ resource,
148
+ spanProcessors: [spanProcessor],
149
+ });
150
+ this.tracerProvider = tracerProvider;
151
+ tracerProvider.register();
152
+ const metricExporter = isGrpc
153
+ ? new OTLPMetricExporterGrpc({ url: settings.telemetryEndpoint })
154
+ : new OTLPMetricExporter({ url: metricsUrl, headers });
155
+ const metricReader = new PeriodicExportingMetricReader({
156
+ exporter: metricExporter,
157
+ exportIntervalMillis: 30_000,
158
+ });
159
+ this.sdk = new NodeSDK({
160
+ resource,
161
+ metricReader,
162
+ instrumentations: [],
163
+ });
164
+ this.sdk.start();
165
+ this.tracer = trace.getTracer(settings.telemetryServiceName);
166
+ this.status.exporterState = "configured";
167
+ daemonLog(`telemetry: SDK initialized, endpoint=${settings.telemetryEndpoint}`);
168
+ }
169
+ catch (err) {
170
+ daemonLog(`telemetry: SDK initialization failed: ${String(err)}`);
171
+ this.status.exporterState = "unavailable";
172
+ this.lastExportError = err instanceof Error ? err.message : String(err);
173
+ }
174
+ }
175
+ resolveHeaders() {
176
+ const headers = {};
177
+ const envHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
178
+ ?? process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS;
179
+ if (envHeaders) {
180
+ for (const pair of envHeaders.split(",")) {
181
+ const [k, v] = pair.split("=", 2);
182
+ if (k && v)
183
+ headers[k.trim()] = v.trim();
184
+ }
185
+ }
186
+ return headers;
187
+ }
188
+ resolveEndpoint() {
189
+ return this.settings.telemetryEndpoint
190
+ ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
191
+ }
192
+ resolveProtocol() {
193
+ const envProtocol = process.env.OTEL_EXPORTER_OTLP_PROTOCOL;
194
+ if (envProtocol === "grpc")
195
+ return "grpc";
196
+ if (envProtocol === "http/protobuf")
197
+ return "http/protobuf";
198
+ return this.settings.telemetryProtocol;
199
+ }
200
+ startLoop(input) {
201
+ const span = this.tracer.startSpan(SPAN_NAMES.LOOP_RUN, { kind: SpanKind.SERVER });
202
+ span.setAttributes({
203
+ [CORRELATION_KEYS.RUN_ID]: input.runId,
204
+ [CORRELATION_KEYS.LOOP_ID]: input.loopId,
205
+ [CORRELATION_KEYS.LOOP_NAME]: input.loopName,
206
+ ...(input.projectId ? { [CORRELATION_KEYS.PROJECT_ID]: input.projectId } : {}),
207
+ ...(input.projectName ? { [CORRELATION_KEYS.PROJECT_NAME]: input.projectName } : {}),
208
+ });
209
+ const ctx = trace.setSpan(context.active(), span);
210
+ this.runCounter.add(1, { "loop_task.loop.id": input.loopId, "loop_task.loop.name": input.loopName });
211
+ return new OtelSpan(span, ctx, (durationMs, status) => {
212
+ this.runDurationHistogram.record(durationMs, { "loop_task.loop.id": input.loopId, status });
213
+ if (status === "error")
214
+ this.failureCounter.add(1, { "loop_task.span": "loop" });
215
+ });
216
+ }
217
+ startTask(input, parent) {
218
+ const parentCtx = parent instanceof OtelSpan ? parent["ctx"] : context.active();
219
+ const span = this.tracer.startSpan(SPAN_NAMES.TASK_EXECUTE, { kind: SpanKind.CONSUMER }, parentCtx);
220
+ span.setAttributes({
221
+ [CORRELATION_KEYS.RUN_ID]: input.runId,
222
+ [CORRELATION_KEYS.LOOP_ID]: input.loopId,
223
+ [CORRELATION_KEYS.LOOP_NAME]: input.loopName,
224
+ [CORRELATION_KEYS.TASK_ID]: input.taskId,
225
+ [CORRELATION_KEYS.TASK_NAME]: input.taskName,
226
+ ...(input.projectId ? { [CORRELATION_KEYS.PROJECT_ID]: input.projectId } : {}),
227
+ ...(input.projectName ? { [CORRELATION_KEYS.PROJECT_NAME]: input.projectName } : {}),
228
+ });
229
+ const ctx = trace.setSpan(parentCtx, span);
230
+ this.taskCounter.add(1, { "loop_task.task.name": input.taskName });
231
+ return new OtelSpan(span, ctx, (durationMs, status) => {
232
+ this.taskDurationHistogram.record(durationMs, { "loop_task.task.name": input.taskName, status });
233
+ if (status === "error")
234
+ this.failureCounter.add(1, { "loop_task.span": "task" });
235
+ });
236
+ }
237
+ startCommand(input, parent) {
238
+ const parentCtx = parent instanceof OtelSpan ? parent["ctx"] : context.active();
239
+ const span = this.tracer.startSpan(SPAN_NAMES.COMMAND_EXECUTE, { kind: SpanKind.CLIENT }, parentCtx);
240
+ span.setAttributes({
241
+ "process.executable.name": input.command,
242
+ "loop_task.command.argument_count": input.argumentCount,
243
+ [CORRELATION_KEYS.RUN_ID]: input.runId,
244
+ [CORRELATION_KEYS.LOOP_ID]: input.loopId,
245
+ ...(input.taskId ? { [CORRELATION_KEYS.TASK_ID]: input.taskId } : {}),
246
+ ...(input.taskName ? { [CORRELATION_KEYS.TASK_NAME]: input.taskName } : {}),
247
+ ...(input.cwd ? { "loop_task.command.cwd": input.cwd } : {}),
248
+ ...(input.integrationId ? { [CORRELATION_KEYS.AGENT_INTEGRATION]: input.integrationId } : {}),
249
+ });
250
+ if (this.settings.telemetryCaptureContent && input.commandLine) {
251
+ span.setAttribute("loop_task.command.full", input.commandLine);
252
+ }
253
+ const ctx = trace.setSpan(parentCtx, span);
254
+ this.commandCounter.add(1, { "process.executable.name": input.command });
255
+ return new OtelSpan(span, ctx, (durationMs, status) => {
256
+ this.commandDurationHistogram.record(durationMs, { "process.executable.name": input.command, status });
257
+ if (status === "error")
258
+ this.failureCounter.add(1, { "loop_task.span": "command" });
259
+ });
260
+ }
261
+ recordRetry(input) {
262
+ const activeSpan = trace.getActiveSpan();
263
+ if (activeSpan) {
264
+ activeSpan.addEvent("retry", {
265
+ "loop_task.retry.attempt": input.attempt,
266
+ "loop_task.retry.max_attempts": input.maxAttempts,
267
+ [CORRELATION_KEYS.RUN_ID]: input.runId,
268
+ [CORRELATION_KEYS.LOOP_ID]: input.loopId,
269
+ ...(input.taskId ? { [CORRELATION_KEYS.TASK_ID]: input.taskId } : {}),
270
+ });
271
+ }
272
+ this.taskRetriesCounter.add(1, {
273
+ ...(input.taskId ? { "loop_task.task.id": input.taskId } : {}),
274
+ });
275
+ }
276
+ recordFailure(error, attributes) {
277
+ const activeSpan = trace.getActiveSpan();
278
+ if (activeSpan) {
279
+ if (error instanceof Error) {
280
+ activeSpan.recordException(error);
281
+ }
282
+ activeSpan.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : String(error) });
283
+ if (attributes) {
284
+ for (const [k, v] of Object.entries(attributes)) {
285
+ activeSpan.setAttribute(k, v);
286
+ }
287
+ }
288
+ }
289
+ }
290
+ recordAgentUsage(input, parent) {
291
+ const activeSpan = parent instanceof OtelSpan ? parent["span"] : trace.getActiveSpan();
292
+ if (!activeSpan)
293
+ return;
294
+ if (input.inputTokens !== undefined) {
295
+ activeSpan.setAttribute("gen_ai.usage.input_tokens", input.inputTokens);
296
+ this.agentInputTokensCounter.add(input.inputTokens, {
297
+ ...(input.integration ? { "loop_task.agent.integration": input.integration } : {}),
298
+ ...(input.model ? { "gen_ai.request.model": input.model } : {}),
299
+ });
300
+ }
301
+ if (input.outputTokens !== undefined) {
302
+ activeSpan.setAttribute("gen_ai.usage.output_tokens", input.outputTokens);
303
+ this.agentOutputTokensCounter.add(input.outputTokens, {
304
+ ...(input.integration ? { "loop_task.agent.integration": input.integration } : {}),
305
+ ...(input.model ? { "gen_ai.request.model": input.model } : {}),
306
+ });
307
+ }
308
+ if (input.cacheReadTokens !== undefined) {
309
+ activeSpan.setAttribute("loop_task.agent.cache_read_tokens", input.cacheReadTokens);
310
+ this.agentCacheReadTokensCounter.add(input.cacheReadTokens);
311
+ }
312
+ if (input.cacheWriteTokens !== undefined) {
313
+ activeSpan.setAttribute("loop_task.agent.cache_write_tokens", input.cacheWriteTokens);
314
+ this.agentCacheWriteTokensCounter.add(input.cacheWriteTokens);
315
+ }
316
+ if (input.costUsd !== undefined) {
317
+ activeSpan.setAttribute("loop_task.agent.cost_usd", input.costUsd);
318
+ this.agentCostCounter.add(input.costUsd);
319
+ }
320
+ if (input.provider) {
321
+ activeSpan.setAttribute("gen_ai.provider.name", input.provider);
322
+ }
323
+ if (input.model) {
324
+ activeSpan.setAttribute("gen_ai.request.model", input.model);
325
+ }
326
+ }
327
+ prepareChildProcess(invocation, childContext, integrationOverride) {
328
+ const env = {};
329
+ const endpoint = this.resolveEndpoint();
330
+ const protocol = this.resolveProtocol();
331
+ if (!this.settings.telemetryEnabled || !endpoint) {
332
+ return { env };
333
+ }
334
+ if (childContext.traceParent) {
335
+ env.TRACEPARENT = childContext.traceParent;
336
+ if (childContext.traceState) {
337
+ env.TRACESTATE = childContext.traceState;
338
+ }
339
+ }
340
+ env.OTEL_EXPORTER_OTLP_ENDPOINT = endpoint;
341
+ env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = endpoint;
342
+ env.OTEL_EXPORTER_OTLP_PROTOCOL = protocol;
343
+ env.OTEL_TRACES_EXPORTER = "otlp";
344
+ const authHeaders = process.env.OTEL_EXPORTER_OTLP_HEADERS
345
+ ?? process.env.OTEL_EXPORTER_OTLP_TRACES_HEADERS;
346
+ if (authHeaders) {
347
+ env.OTEL_EXPORTER_OTLP_HEADERS = authHeaders;
348
+ }
349
+ const correlationAttrs = {
350
+ [CORRELATION_KEYS.RUN_ID]: childContext.runId,
351
+ [CORRELATION_KEYS.LOOP_ID]: childContext.loopId,
352
+ };
353
+ if (childContext.taskId)
354
+ correlationAttrs[CORRELATION_KEYS.TASK_ID] = childContext.taskId;
355
+ if (childContext.projectId)
356
+ correlationAttrs[CORRELATION_KEYS.PROJECT_ID] = childContext.projectId;
357
+ const mergedResourceAttrs = this.mergeResourceAttributes(process.env.OTEL_RESOURCE_ATTRIBUTES, correlationAttrs);
358
+ env.OTEL_RESOURCE_ATTRIBUTES = mergedResourceAttrs;
359
+ // Apply agent-specific integrations when auto-instrumentation is enabled
360
+ let integrationId;
361
+ if (this.settings.telemetryAutoInstrumentAgents || integrationOverride) {
362
+ // Per-task override takes precedence
363
+ if (integrationOverride === "none") {
364
+ // Explicitly disable agent integration — no agent-specific env
365
+ integrationId = undefined;
366
+ }
367
+ else if (integrationOverride && integrationOverride !== "auto") {
368
+ // Force a specific integration: opencode, claude-code, or generic
369
+ if (integrationOverride === "generic") {
370
+ // Generic: no agent-specific activation, just the base OTLP env
371
+ integrationId = undefined;
372
+ }
373
+ else {
374
+ const allIntegrations = [
375
+ ...getAgentIntegrations(),
376
+ ];
377
+ const target = allIntegrations.find((i) => i.id === integrationOverride);
378
+ if (target) {
379
+ integrationId = target.id;
380
+ const prepared = target.prepare({ ...invocation, env: { ...invocation.env, ...env } }, childContext);
381
+ Object.assign(env, prepared.env);
382
+ }
383
+ }
384
+ }
385
+ else {
386
+ // Auto-detect
387
+ const integration = detectAgentIntegration(invocation.command, invocation.args);
388
+ if (integration) {
389
+ integrationId = integration.id;
390
+ const prepared = integration.prepare({ ...invocation, env: { ...invocation.env, ...env } }, childContext);
391
+ Object.assign(env, prepared.env);
392
+ }
393
+ }
394
+ }
395
+ return { env, integrationId };
396
+ }
397
+ mergeResourceAttributes(existing, additions = {}) {
398
+ const map = new Map();
399
+ if (existing) {
400
+ for (const pair of existing.split(",")) {
401
+ const [k, v] = pair.split("=", 2);
402
+ if (k && v)
403
+ map.set(k.trim(), v.trim());
404
+ }
405
+ }
406
+ for (const [k, v] of Object.entries(additions)) {
407
+ map.set(k, v);
408
+ }
409
+ return [...map.entries()].map(([k, v]) => `${k}=${v}`).join(",");
410
+ }
411
+ getStatus() {
412
+ return { ...this.status, lastSuccessfulExportAt: this.lastSuccessfulExport, lastExportError: this.lastExportError };
413
+ }
414
+ async flush() {
415
+ if (!this.sdk)
416
+ return;
417
+ try {
418
+ const flushes = [];
419
+ if (this.tracerProvider) {
420
+ flushes.push(this.tracerProvider.forceFlush());
421
+ }
422
+ const sdkInternals = this.sdk;
423
+ if (typeof sdkInternals._meterProvider?.forceFlush === "function") {
424
+ flushes.push(sdkInternals._meterProvider.forceFlush());
425
+ }
426
+ if (flushes.length === 0) {
427
+ throw new Error("OpenTelemetry SDK providers do not support forceFlush");
428
+ }
429
+ await Promise.all(flushes);
430
+ daemonLog("telemetry: SDK flushed");
431
+ this.lastSuccessfulExport = new Date().toISOString();
432
+ if (this.status.exporterState !== "healthy") {
433
+ this.status.exporterState = "healthy";
434
+ }
435
+ }
436
+ catch (err) {
437
+ daemonLog(`telemetry: flush failed: ${String(err)}`);
438
+ this.lastExportError = err instanceof Error ? err.message : String(err);
439
+ throw err;
440
+ }
441
+ }
442
+ async shutdown() {
443
+ if (this.tracerProvider) {
444
+ try {
445
+ await this.tracerProvider.forceFlush();
446
+ await this.tracerProvider.shutdown();
447
+ }
448
+ catch {
449
+ // best effort
450
+ }
451
+ this.tracerProvider = null;
452
+ }
453
+ if (!this.sdk)
454
+ return;
455
+ try {
456
+ await this.sdk.shutdown();
457
+ daemonLog("telemetry: SDK shut down");
458
+ }
459
+ catch (err) {
460
+ daemonLog(`telemetry: shutdown error: ${String(err)}`);
461
+ }
462
+ this.sdk = null;
463
+ }
464
+ }