@arcadeai/hypotenuse 0.13.0 → 0.14.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.
@@ -343,7 +343,7 @@ import { Command } from "commander";
343
343
  // package.json
344
344
  var package_default = {
345
345
  name: "@arcadeai/hypotenuse",
346
- version: "0.13.0",
346
+ version: "0.14.0",
347
347
  description: "Hypotenuse \u2014 run coding agents on your machine for Linear agent sessions",
348
348
  type: "module",
349
349
  license: "Apache-2.0",
@@ -385,6 +385,7 @@ var package_default = {
385
385
  "@modelcontextprotocol/sdk": "1.32.1",
386
386
  commander: "^14.0.3",
387
387
  nanospinner: "^1.2.2",
388
+ protobufjs: "^8.8.1",
388
389
  "smol-toml": "^1.9.1",
389
390
  upgradr: "^0.4.1",
390
391
  zod: "^4.3.6"
@@ -753,86 +754,226 @@ function isPreset(name) {
753
754
  import { basename } from "path";
754
755
 
755
756
  // ../protocol/src/capabilities.ts
757
+ import { z as z2 } from "zod";
758
+
759
+ // ../protocol/src/telemetry.ts
756
760
  import { z } from "zod";
761
+ var CONTENT_LEVELS = ["metadata", "content", "full"];
762
+ var ContentLevel = z.enum(CONTENT_LEVELS);
763
+ function levelRank(level) {
764
+ return CONTENT_LEVELS.indexOf(level);
765
+ }
766
+ function minLevel(a, b) {
767
+ return levelRank(a) <= levelRank(b) ? a : b;
768
+ }
769
+ var LEVEL_ATTRIBUTE = "hypotenuse.telemetry.level";
770
+ var CONTENT_ATTRIBUTE_PREFIX = "hypotenuse.content.";
771
+ var CONTENT_CAP_BYTES = 64 * 1024;
772
+ var TRUNCATED_SUFFIX = ".original_bytes";
773
+ var TELEMETRY_WORK_ITEM_HEADER = "X-Hypotenuse-Work-Item";
774
+ var TELEMETRY_SIGNAL_HEADER = "X-Hypotenuse-Signal";
775
+ var TRACEPARENT_HEADER = "traceparent";
776
+ var MAX_TELEMETRY_BODY_BYTES = 1024 * 1024;
777
+ var MAX_TELEMETRY_INFLATED_BYTES = 8 * 1024 * 1024;
778
+ var ClaimTelemetry = z.looseObject({
779
+ traceparent: z.string().describe("The turn's root span; every daemon:* call for it carries it"),
780
+ sampled: z.boolean().describe("Whether any exporter keeps this turn's spans and logs"),
781
+ content: ContentLevel.describe("The level to collect at, already lowered to this runner's cap"),
782
+ agentNative: z.boolean().describe("Whether to capture the coding agent's own telemetry")
783
+ });
784
+ var TelemetryCapability = z.strictObject({
785
+ enabled: z.boolean(),
786
+ maxContent: ContentLevel,
787
+ agentNative: z.boolean()
788
+ });
789
+ var TelemetryResponse = z.looseObject({
790
+ accepted: z.boolean()
791
+ });
792
+ function randomHex(bytes) {
793
+ for (;; ) {
794
+ const buf = crypto.getRandomValues(new Uint8Array(bytes));
795
+ if (buf.some((b) => b !== 0))
796
+ return Buffer.from(buf).toString("hex");
797
+ }
798
+ }
799
+ function newSpanId() {
800
+ return randomHex(8);
801
+ }
802
+ function formatTraceparent(ctx) {
803
+ return `00-${ctx.traceId}-${ctx.spanId}-${ctx.sampled ? "01" : "00"}`;
804
+ }
805
+ function parseTraceparent(value) {
806
+ const match = /^00-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/.exec((value ?? "").trim().toLowerCase());
807
+ if (!match)
808
+ return null;
809
+ const [, traceId, spanId, flags] = match;
810
+ if (/^0+$/.test(traceId) || /^0+$/.test(spanId))
811
+ return null;
812
+ return { traceId, spanId, sampled: (Number.parseInt(flags, 16) & 1) === 1 };
813
+ }
814
+ function capContent(text) {
815
+ const bytes = Buffer.byteLength(text);
816
+ if (bytes <= CONTENT_CAP_BYTES)
817
+ return { text, originalBytes: null };
818
+ let cut = Buffer.from(text).subarray(0, CONTENT_CAP_BYTES).toString("utf8");
819
+ if (cut.endsWith("\uFFFD"))
820
+ cut = cut.slice(0, -1);
821
+ return { text: cut, originalBytes: bytes };
822
+ }
823
+ var SECRET_PATTERNS = [
824
+ /hyp_(?:rt|cli)_[A-Za-z0-9_-]{8,}/g,
825
+ /sk-(?:ant-|proj-)?[A-Za-z0-9_-]{16,}/g,
826
+ /lin_(?:api|oauth)_[A-Za-z0-9]{16,}/g,
827
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
828
+ /github_pat_[A-Za-z0-9_]{20,}/g,
829
+ /xox[abposr]-[A-Za-z0-9-]{10,}/g,
830
+ /AIza[0-9A-Za-z_-]{30,}/g,
831
+ /AKIA[0-9A-Z]{16}/g,
832
+ /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g,
833
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
834
+ /\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{8,}/g
835
+ ];
836
+ var REDACTED = "[REDACTED]";
837
+ var SECRET_KEY = /((^|[._-])(authorization|cookie|set-cookie|token|secret|password|passwd)$)|([a-z0-9][_-](key|token)$)/i;
838
+ var MIN_SECRET_LENGTH = 6;
839
+ function redactText(text, values) {
840
+ let out = text;
841
+ for (const v of values)
842
+ if (out.includes(v))
843
+ out = out.split(v).join(REDACTED);
844
+ for (const p of SECRET_PATTERNS)
845
+ out = out.replace(p, REDACTED);
846
+ return out;
847
+ }
848
+ function redactOtlp(request, options = {}) {
849
+ const values = [...new Set(options.values ?? [])].filter((v) => v.length >= MIN_SECRET_LENGTH).sort((a, b) => b.length - a.length);
850
+ const keys = new Set(options.keys ?? []);
851
+ const isSecretKey = (k) => keys.has(k) || SECRET_KEY.test(k);
852
+ const walk = (value, key) => {
853
+ if (typeof value === "string") {
854
+ if (key === "traceId" || key === "spanId" || key === "parentSpanId")
855
+ return value;
856
+ return redactText(value, values);
857
+ }
858
+ if (Array.isArray(value))
859
+ return value.map((v) => walk(v, key));
860
+ if (value && typeof value === "object") {
861
+ const obj = value;
862
+ if (typeof obj.key === "string" && "value" in obj && isSecretKey(obj.key))
863
+ return { key: obj.key, value: { stringValue: REDACTED } };
864
+ const out = {};
865
+ for (const [k, v] of Object.entries(obj))
866
+ out[k] = walk(v, k);
867
+ return out;
868
+ }
869
+ return value;
870
+ };
871
+ return walk(request, "");
872
+ }
873
+ function stampResource(signal, request, attrs) {
874
+ const groups = signal === "traces" ? request.resourceSpans : signal === "logs" ? request.resourceLogs : request.resourceMetrics;
875
+ const keys = new Set(attrs.map((a) => a.key));
876
+ for (const group of groups ?? []) {
877
+ const existing = (group.resource?.attributes ?? []).filter((a) => !keys.has(a.key));
878
+ group.resource = { ...group.resource, attributes: [...existing, ...attrs] };
879
+ }
880
+ return request;
881
+ }
882
+ function mergeRequests(signal, requests) {
883
+ if (signal === "traces")
884
+ return {
885
+ resourceSpans: requests.flatMap((r) => r.resourceSpans ?? [])
886
+ };
887
+ if (signal === "logs")
888
+ return {
889
+ resourceLogs: requests.flatMap((r) => r.resourceLogs ?? [])
890
+ };
891
+ return {
892
+ resourceMetrics: requests.flatMap((r) => r.resourceMetrics ?? [])
893
+ };
894
+ }
895
+
896
+ // ../protocol/src/capabilities.ts
757
897
  var RUNNER_ROLES = ["manager", "worker"];
758
- var RunnerRole = z.enum(RUNNER_ROLES);
759
- var identifier = z.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`");
760
- var AgentCapability = z.strictObject({
898
+ var RunnerRole = z2.enum(RUNNER_ROLES);
899
+ var identifier = z2.string().trim().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`");
900
+ var AgentCapability = z2.strictObject({
761
901
  id: identifier.describe("The agent's id in the config file"),
762
902
  preset: identifier.describe("The preset it is built on, e.g. claude"),
763
- available: z.boolean().describe("Whether the daemon's probe succeeded"),
764
- version: z.string().max(128).optional().describe("The agent's version"),
765
- roles: z.array(RunnerRole).max(RUNNER_ROLES.length).optional().describe("The roles this agent plays; absent means every role"),
766
- error: z.string().max(1024).optional().describe("Why the probe failed, when it did")
903
+ available: z2.boolean().describe("Whether the daemon's probe succeeded"),
904
+ version: z2.string().max(128).optional().describe("The agent's version"),
905
+ roles: z2.array(RunnerRole).max(RUNNER_ROLES.length).optional().describe("The roles this agent plays; absent means every role"),
906
+ error: z2.string().max(1024).optional().describe("Why the probe failed, when it did")
767
907
  });
768
- var RepositoryCapability = z.strictObject({
908
+ var RepositoryCapability = z2.strictObject({
769
909
  name: identifier.describe("The repository's name in the config file"),
770
- remote: z.string().trim().min(1).max(512).describe("Its git remote; the server normalizes it to host/owner/name"),
771
- baseBranch: z.string().trim().min(1).max(255).describe("The base branch")
910
+ remote: z2.string().trim().min(1).max(512).describe("Its git remote; the server normalizes it to host/owner/name"),
911
+ baseBranch: z2.string().trim().min(1).max(255).describe("The base branch")
772
912
  });
773
- var subscriptionValues = z.array(z.string().trim().min(1).max(255)).max(200).default([]);
774
- var Subscriptions = z.strictObject({
913
+ var subscriptionValues = z2.array(z2.string().trim().min(1).max(255)).max(200).default([]);
914
+ var Subscriptions = z2.strictObject({
775
915
  teams: subscriptionValues.describe("Team keys, e.g. ENG"),
776
916
  projects: subscriptionValues.describe("Project ids"),
777
917
  labels: subscriptionValues.describe("Label names (matched lowercased)")
778
918
  });
779
- var RunnerCapabilities = z.strictObject({
780
- roles: z.array(RunnerRole).max(RUNNER_ROLES.length),
781
- agents: z.array(AgentCapability).max(64),
782
- repositories: z.array(RepositoryCapability).max(256),
783
- subscriptions: Subscriptions
919
+ var RunnerCapabilities = z2.strictObject({
920
+ roles: z2.array(RunnerRole).max(RUNNER_ROLES.length),
921
+ agents: z2.array(AgentCapability).max(64),
922
+ repositories: z2.array(RepositoryCapability).max(256),
923
+ subscriptions: Subscriptions,
924
+ telemetry: TelemetryCapability.optional().describe("The runner owner's cap on what telemetry leaves the machine; absent from older daemons, which send none")
784
925
  });
785
- var cap = z.number().int().min(0).max(1000);
786
- var RunnerLimits = z.strictObject({
787
- maxParallelJobs: z.number().int().min(1).max(1000),
788
- roles: z.partialRecord(RunnerRole, cap).optional(),
789
- agents: z.record(identifier, cap).optional()
926
+ var cap = z2.number().int().min(0).max(1000);
927
+ var RunnerLimits = z2.strictObject({
928
+ maxParallelJobs: z2.number().int().min(1).max(1000),
929
+ roles: z2.partialRecord(RunnerRole, cap).optional(),
930
+ agents: z2.record(identifier, cap).optional()
790
931
  });
791
932
  // ../protocol/src/complete.ts
792
- import { z as z2 } from "zod";
933
+ import { z as z3 } from "zod";
793
934
  var COMPLETE_OUTCOMES = ["completed", "failed", "cancelled"];
794
- var CompleteRequest = z2.strictObject({
795
- workItemId: z2.number().int().positive(),
796
- outcome: z2.enum(COMPLETE_OUTCOMES),
797
- error: z2.string().max(4096).nullable().optional(),
798
- acpSessionId: z2.string().max(255).nullable().optional(),
799
- branch: z2.string().max(255).nullable().optional(),
800
- answersConsumed: z2.array(z2.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt")
935
+ var CompleteRequest = z3.strictObject({
936
+ workItemId: z3.number().int().positive(),
937
+ outcome: z3.enum(COMPLETE_OUTCOMES),
938
+ error: z3.string().max(4096).nullable().optional(),
939
+ acpSessionId: z3.string().max(255).nullable().optional(),
940
+ branch: z3.string().max(255).nullable().optional(),
941
+ answersConsumed: z3.array(z3.number().int().positive()).max(100).optional().describe("The activity ids of delivered answers the turn took as answers; any other answer becomes the session's next prompt")
801
942
  });
802
- var CompleteResponse = z2.looseObject({
803
- alreadyTerminal: z2.boolean()
943
+ var CompleteResponse = z3.looseObject({
944
+ alreadyTerminal: z3.boolean()
804
945
  });
805
946
  // ../protocol/src/connect.ts
806
- import { z as z4 } from "zod";
947
+ import { z as z5 } from "zod";
807
948
 
808
949
  // ../protocol/src/heartbeat.ts
809
- import { z as z3 } from "zod";
950
+ import { z as z4 } from "zod";
810
951
  var ACTIVE_STATES = ["starting", "running", "cancelling"];
811
- var ActiveItem = z3.strictObject({
812
- workItemId: z3.number().int().positive(),
813
- agentSessionId: z3.number().int().positive(),
952
+ var ActiveItem = z4.strictObject({
953
+ workItemId: z4.number().int().positive(),
954
+ agentSessionId: z4.number().int().positive(),
814
955
  role: RunnerRole,
815
- agentId: z3.string().min(1).max(64),
816
- state: z3.enum(ACTIVE_STATES)
956
+ agentId: z4.string().min(1).max(64),
957
+ state: z4.enum(ACTIVE_STATES)
817
958
  });
818
- var MachineId = z3.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
819
- var HeartbeatRequest = z3.strictObject({
820
- protocolVersion: z3.number().int().positive(),
821
- daemonVersion: z3.string().trim().min(1).max(64),
822
- platform: z3.string().trim().min(1).max(64),
823
- hostname: z3.string().trim().min(1).max(256),
824
- draining: z3.boolean(),
959
+ var MachineId = z4.string().regex(/^sha256:[0-9a-f]{1,64}$/, "sha256:<hex>").describe("A stable hash of the OS machine id, `sha256:<hex>`");
960
+ var HeartbeatRequest = z4.strictObject({
961
+ protocolVersion: z4.number().int().positive(),
962
+ daemonVersion: z4.string().trim().min(1).max(64),
963
+ platform: z4.string().trim().min(1).max(64),
964
+ hostname: z4.string().trim().min(1).max(256),
965
+ draining: z4.boolean(),
825
966
  machineId: MachineId,
826
967
  limits: RunnerLimits,
827
- active: z3.array(ActiveItem).max(1000),
968
+ active: z4.array(ActiveItem).max(1000),
828
969
  capabilities: RunnerCapabilities,
829
- dryRun: z3.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
970
+ dryRun: z4.boolean().optional().describe("Validate and answer without recording anything: no presence, no capabilities, no leases, no offers")
830
971
  });
831
- var DryRunRunner = z3.looseObject({
832
- id: z3.number().int().positive(),
833
- name: z3.string(),
834
- projectId: z3.number().int().positive(),
835
- projectSlug: z3.string()
972
+ var DryRunRunner = z4.looseObject({
973
+ id: z4.number().int().positive(),
974
+ name: z4.string(),
975
+ projectId: z4.number().int().positive(),
976
+ projectSlug: z4.string()
836
977
  });
837
978
  var OFFER_KINDS = [
838
979
  "session_created",
@@ -840,12 +981,12 @@ var OFFER_KINDS = [
840
981
  "dispatch",
841
982
  "review"
842
983
  ];
843
- var Offer = z3.looseObject({
844
- workItemId: z3.number().int().positive(),
845
- agentSessionId: z3.number().int().positive(),
846
- kind: z3.enum(OFFER_KINDS),
984
+ var Offer = z4.looseObject({
985
+ workItemId: z4.number().int().positive(),
986
+ agentSessionId: z4.number().int().positive(),
987
+ kind: z4.enum(OFFER_KINDS),
847
988
  role: RunnerRole,
848
- offerExpiresAt: z3.number().int()
989
+ offerExpiresAt: z4.number().int()
849
990
  });
850
991
  var CANCEL_REASONS = [
851
992
  "stop",
@@ -856,11 +997,11 @@ var CANCEL_REASONS = [
856
997
  "integration_revoked",
857
998
  "superseded"
858
999
  ];
859
- var Cancel = z3.looseObject({
860
- workItemId: z3.number().int().positive(),
861
- reason: z3.union([z3.enum(CANCEL_REASONS), z3.string().min(1).max(64)]),
862
- by: z3.string().min(1).max(200).optional(),
863
- said: z3.string().min(1).max(2000).optional()
1000
+ var Cancel = z4.looseObject({
1001
+ workItemId: z4.number().int().positive(),
1002
+ reason: z4.union([z4.enum(CANCEL_REASONS), z4.string().min(1).max(64)]),
1003
+ by: z4.string().min(1).max(200).optional(),
1004
+ said: z4.string().min(1).max(2000).optional()
864
1005
  });
865
1006
  var STOP_QUOTE_MAX = 500;
866
1007
  function stopQuote(said) {
@@ -874,87 +1015,87 @@ function stopQuote(said) {
874
1015
  `).join(`
875
1016
  > `)}`;
876
1017
  }
877
- var Answer = z3.looseObject({
878
- workItemId: z3.number().int().positive(),
879
- activityId: z3.number().int().positive(),
880
- body: z3.string()
1018
+ var Answer = z4.looseObject({
1019
+ workItemId: z4.number().int().positive(),
1020
+ activityId: z4.number().int().positive(),
1021
+ body: z4.string()
881
1022
  });
882
- var HeartbeatResponse = z3.looseObject({
883
- serverTime: z3.number().int(),
884
- heartbeatIntervalMs: z3.number().int().positive(),
885
- leaseMs: z3.number().int().positive(),
886
- offers: z3.array(Offer),
887
- cancels: z3.array(Cancel),
888
- unknownActive: z3.array(z3.number().int().positive()),
889
- answers: z3.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
1023
+ var HeartbeatResponse = z4.looseObject({
1024
+ serverTime: z4.number().int(),
1025
+ heartbeatIntervalMs: z4.number().int().positive(),
1026
+ leaseMs: z4.number().int().positive(),
1027
+ offers: z4.array(Offer),
1028
+ cancels: z4.array(Cancel),
1029
+ unknownActive: z4.array(z4.number().int().positive()),
1030
+ answers: z4.array(Answer).default([]).describe("Replies to questions held turns are waiting on, oldest first"),
890
1031
  runner: DryRunRunner.optional().describe("A dry run only: the runner the token is"),
891
- unknownSubscriptions: z3.looseObject({ teams: z3.array(z3.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
1032
+ unknownSubscriptions: z4.looseObject({ teams: z4.array(z4.string()) }).nullable().optional().describe("A dry run only: subscribed team keys the project's Linear connection cannot see; null when it could not be asked")
892
1033
  });
893
1034
 
894
1035
  // ../protocol/src/connect.ts
895
- var ConnectRequest = z4.strictObject({
1036
+ var ConnectRequest = z5.strictObject({
896
1037
  machineId: MachineId
897
1038
  });
898
- var ConnectResponse = z4.looseObject({
899
- runnerId: z4.number().int().positive(),
900
- channel: z4.string().regex(/^runner:\d+$/),
901
- heartbeatIntervalMs: z4.number().int().positive(),
902
- leaseMs: z4.number().int().positive(),
903
- serverTime: z4.number().int()
1039
+ var ConnectResponse = z5.looseObject({
1040
+ runnerId: z5.number().int().positive(),
1041
+ channel: z5.string().regex(/^runner:\d+$/),
1042
+ heartbeatIntervalMs: z5.number().int().positive(),
1043
+ leaseMs: z5.number().int().positive(),
1044
+ serverTime: z5.number().int()
904
1045
  });
905
1046
  var MACHINE_IN_USE = "MACHINE_IN_USE";
906
1047
  var RUNNER_FRAME_EVENTS = ["work", "cancel", "revoked"];
907
- var RunnerFrame = z4.object({ event: z4.enum(RUNNER_FRAME_EVENTS) });
1048
+ var RunnerFrame = z5.object({ event: z5.enum(RUNNER_FRAME_EVENTS) });
908
1049
  function runnerChannel(runnerId) {
909
1050
  return `runner:${runnerId}`;
910
1051
  }
911
1052
  // ../protocol/src/events.ts
912
- import { z as z5 } from "zod";
1053
+ import { z as z6 } from "zod";
913
1054
  var MAX_EVENTS_PER_BATCH = 100;
914
1055
  var MAX_EVENTS_BATCH_BYTES = 256 * 1024;
915
1056
  var NOT_YOUR_WORK = "NOT_YOUR_WORK";
916
1057
  var SEQ_REGRESSION = "SEQ_REGRESSION";
917
- var body = z5.string().trim().min(1).max(64 * 1024);
1058
+ var body = z6.string().trim().min(1).max(64 * 1024);
918
1059
  var envelope = {
919
- eventId: z5.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
920
- seq: z5.number().int().positive().describe("Per work item, from 1, never going backwards"),
921
- at: z5.number().int().describe("When the daemon saw it, epoch ms")
1060
+ eventId: z6.uuid({ version: "v4" }).describe("Generated by the daemon; the idempotency key end to end (the Linear activity's client id)"),
1061
+ seq: z6.number().int().positive().describe("Per work item, from 1, never going backwards"),
1062
+ at: z6.number().int().describe("When the daemon saw it, epoch ms")
922
1063
  };
923
- var SelectMetadata = z5.strictObject({
924
- options: z5.array(z5.strictObject({
925
- label: z5.string().trim().min(1).max(255).optional(),
926
- value: z5.string().trim().min(1).max(1024)
1064
+ var SelectMetadata = z6.strictObject({
1065
+ options: z6.array(z6.strictObject({
1066
+ label: z6.string().trim().min(1).max(255).optional(),
1067
+ value: z6.string().trim().min(1).max(1024)
927
1068
  })).min(1).max(25)
928
1069
  });
929
- var AuthProvider = z5.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
930
- var AuthMetadata = z5.strictObject({
931
- url: z5.url().optional(),
1070
+ var AuthProvider = z6.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/).describe("An agent id, `github`, or a git host");
1071
+ var AuthMetadata = z6.strictObject({
1072
+ url: z6.url().optional(),
932
1073
  provider: AuthProvider.optional(),
933
- userId: z5.string().max(255).optional(),
934
- providerName: z5.string().max(255).optional()
1074
+ userId: z6.string().max(255).optional(),
1075
+ providerName: z6.string().max(255).optional()
935
1076
  }).refine((m) => m.url !== undefined || m.provider !== undefined, {
936
1077
  message: "an auth elicitation needs a url or a provider"
937
1078
  });
938
- var ThoughtEvent = z5.strictObject({
1079
+ var ThoughtEvent = z6.strictObject({
939
1080
  ...envelope,
940
- type: z5.literal("thought"),
1081
+ type: z6.literal("thought"),
941
1082
  body,
942
- ephemeral: z5.boolean().default(false)
1083
+ ephemeral: z6.boolean().default(false)
943
1084
  });
944
- var ActionEvent = z5.strictObject({
1085
+ var ActionEvent = z6.strictObject({
945
1086
  ...envelope,
946
- type: z5.literal("action"),
947
- action: z5.string().trim().min(1).max(255),
948
- parameter: z5.string().trim().min(1).max(4096),
949
- result: z5.string().max(64 * 1024).optional(),
950
- ephemeral: z5.boolean().default(false)
1087
+ type: z6.literal("action"),
1088
+ action: z6.string().trim().min(1).max(255),
1089
+ parameter: z6.string().trim().min(1).max(4096),
1090
+ result: z6.string().max(64 * 1024).optional(),
1091
+ ephemeral: z6.boolean().default(false)
951
1092
  });
952
- var ElicitationEvent = z5.strictObject({
1093
+ var ElicitationEvent = z6.strictObject({
953
1094
  ...envelope,
954
- type: z5.literal("elicitation"),
1095
+ type: z6.literal("elicitation"),
955
1096
  body,
956
- signal: z5.enum(["select", "auth"]).optional(),
957
- signalMetadata: z5.record(z5.string(), z5.unknown()).optional()
1097
+ signal: z6.enum(["select", "auth"]).optional(),
1098
+ signalMetadata: z6.record(z6.string(), z6.unknown()).optional()
958
1099
  }).superRefine((event, ctx) => {
959
1100
  if (!event.signal) {
960
1101
  if (event.signalMetadata) {
@@ -976,14 +1117,14 @@ var ElicitationEvent = z5.strictObject({
976
1117
  });
977
1118
  }
978
1119
  });
979
- var ResponseEvent = z5.strictObject({
1120
+ var ResponseEvent = z6.strictObject({
980
1121
  ...envelope,
981
- type: z5.literal("response"),
1122
+ type: z6.literal("response"),
982
1123
  body
983
1124
  });
984
- var ErrorEvent = z5.strictObject({
1125
+ var ErrorEvent = z6.strictObject({
985
1126
  ...envelope,
986
- type: z5.literal("error"),
1127
+ type: z6.literal("error"),
987
1128
  body
988
1129
  });
989
1130
  var PLAN_ENTRY_STATUSES = [
@@ -992,33 +1133,33 @@ var PLAN_ENTRY_STATUSES = [
992
1133
  "completed",
993
1134
  "canceled"
994
1135
  ];
995
- var PlanEvent = z5.strictObject({
1136
+ var PlanEvent = z6.strictObject({
996
1137
  ...envelope,
997
- type: z5.literal("plan"),
998
- entries: z5.array(z5.strictObject({
999
- content: z5.string().trim().min(1).max(1024),
1000
- status: z5.enum(PLAN_ENTRY_STATUSES)
1138
+ type: z6.literal("plan"),
1139
+ entries: z6.array(z6.strictObject({
1140
+ content: z6.string().trim().min(1).max(1024),
1141
+ status: z6.enum(PLAN_ENTRY_STATUSES)
1001
1142
  })).max(100)
1002
1143
  });
1003
- var SummaryEvent = z5.strictObject({
1144
+ var SummaryEvent = z6.strictObject({
1004
1145
  ...envelope,
1005
- type: z5.literal("summary"),
1006
- title: z5.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
1146
+ type: z6.literal("summary"),
1147
+ title: z6.string().min(1).max(255).regex(/^[^\r\n\0]*$/, "one line").refine((t) => t.trim().length > 0, "not blank")
1007
1148
  });
1008
- var ExternalUrlEvent = z5.strictObject({
1149
+ var ExternalUrlEvent = z6.strictObject({
1009
1150
  ...envelope,
1010
- type: z5.literal("external_url"),
1011
- label: z5.string().trim().min(1).max(255),
1012
- url: z5.url()
1151
+ type: z6.literal("external_url"),
1152
+ label: z6.string().trim().min(1).max(255),
1153
+ url: z6.url()
1013
1154
  });
1014
- var UsageEvent = z5.strictObject({
1155
+ var UsageEvent = z6.strictObject({
1015
1156
  ...envelope,
1016
- type: z5.literal("usage"),
1017
- inputTokens: z5.number().int().min(0).optional(),
1018
- outputTokens: z5.number().int().min(0).optional(),
1019
- costUsd: z5.number().min(0).optional(),
1020
- contextUsed: z5.number().int().min(0).optional(),
1021
- contextSize: z5.number().int().min(0).optional()
1157
+ type: z6.literal("usage"),
1158
+ inputTokens: z6.number().int().min(0).optional(),
1159
+ outputTokens: z6.number().int().min(0).optional(),
1160
+ costUsd: z6.number().min(0).optional(),
1161
+ contextUsed: z6.number().int().min(0).optional(),
1162
+ contextSize: z6.number().int().min(0).optional()
1022
1163
  });
1023
1164
  var STATUS_EVENT_STATES = [
1024
1165
  "starting",
@@ -1026,12 +1167,12 @@ var STATUS_EVENT_STATES = [
1026
1167
  "waiting_permission",
1027
1168
  "finishing"
1028
1169
  ];
1029
- var StatusEvent = z5.strictObject({
1170
+ var StatusEvent = z6.strictObject({
1030
1171
  ...envelope,
1031
- type: z5.literal("status"),
1032
- state: z5.enum(STATUS_EVENT_STATES)
1172
+ type: z6.literal("status"),
1173
+ state: z6.enum(STATUS_EVENT_STATES)
1033
1174
  });
1034
- var SessionEvent = z5.discriminatedUnion("type", [
1175
+ var SessionEvent = z6.discriminatedUnion("type", [
1035
1176
  ThoughtEvent,
1036
1177
  ActionEvent,
1037
1178
  ElicitationEvent,
@@ -1043,16 +1184,302 @@ var SessionEvent = z5.discriminatedUnion("type", [
1043
1184
  UsageEvent,
1044
1185
  StatusEvent
1045
1186
  ]);
1046
- var EventsRequest = z5.strictObject({
1047
- workItemId: z5.number().int().positive(),
1048
- events: z5.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
1187
+ var EventsRequest = z6.strictObject({
1188
+ workItemId: z6.number().int().positive(),
1189
+ events: z6.array(SessionEvent).min(1).max(MAX_EVENTS_PER_BATCH)
1049
1190
  });
1050
- var EventsResponse = z5.looseObject({
1051
- accepted: z5.number().int().min(0),
1052
- duplicates: z5.number().int().min(0),
1053
- dropped: z5.number().int().min(0),
1054
- cancelRequested: z5.boolean()
1191
+ var EventsResponse = z6.looseObject({
1192
+ accepted: z6.number().int().min(0),
1193
+ duplicates: z6.number().int().min(0),
1194
+ dropped: z6.number().int().min(0),
1195
+ cancelRequested: z6.boolean()
1055
1196
  });
1197
+ // ../protocol/src/otlp.ts
1198
+ import protobuf from "protobufjs/light.js";
1199
+ var SPAN_KIND = {
1200
+ internal: 1,
1201
+ server: 2,
1202
+ client: 3,
1203
+ producer: 4,
1204
+ consumer: 5
1205
+ };
1206
+ var STATUS_CODE = { unset: 0, ok: 1, error: 2 };
1207
+ var TEMPORALITY = { delta: 1, cumulative: 2 };
1208
+ var SEVERITY = { debug: 5, info: 9, warn: 13, error: 17 };
1209
+ var rep = (type, id) => ({ rule: "repeated", type, id });
1210
+ var DESCRIPTOR = {
1211
+ nested: {
1212
+ AnyValue: {
1213
+ oneofs: {
1214
+ value: {
1215
+ oneof: [
1216
+ "stringValue",
1217
+ "boolValue",
1218
+ "intValue",
1219
+ "doubleValue",
1220
+ "arrayValue",
1221
+ "kvlistValue",
1222
+ "bytesValue"
1223
+ ]
1224
+ }
1225
+ },
1226
+ fields: {
1227
+ stringValue: { type: "string", id: 1 },
1228
+ boolValue: { type: "bool", id: 2 },
1229
+ intValue: { type: "int64", id: 3 },
1230
+ doubleValue: { type: "double", id: 4 },
1231
+ arrayValue: { type: "ArrayValue", id: 5 },
1232
+ kvlistValue: { type: "KeyValueList", id: 6 },
1233
+ bytesValue: { type: "bytes", id: 7 }
1234
+ }
1235
+ },
1236
+ ArrayValue: { fields: { values: rep("AnyValue", 1) } },
1237
+ KeyValueList: { fields: { values: rep("KeyValue", 1) } },
1238
+ KeyValue: {
1239
+ fields: {
1240
+ key: { type: "string", id: 1 },
1241
+ value: { type: "AnyValue", id: 2 }
1242
+ }
1243
+ },
1244
+ InstrumentationScope: {
1245
+ fields: {
1246
+ name: { type: "string", id: 1 },
1247
+ version: { type: "string", id: 2 },
1248
+ attributes: rep("KeyValue", 3),
1249
+ droppedAttributesCount: { type: "uint32", id: 4 }
1250
+ }
1251
+ },
1252
+ Resource: {
1253
+ fields: {
1254
+ attributes: rep("KeyValue", 1),
1255
+ droppedAttributesCount: { type: "uint32", id: 2 }
1256
+ }
1257
+ },
1258
+ ExportTraceServiceRequest: {
1259
+ fields: { resourceSpans: rep("ResourceSpans", 1) }
1260
+ },
1261
+ ResourceSpans: {
1262
+ fields: {
1263
+ resource: { type: "Resource", id: 1 },
1264
+ scopeSpans: rep("ScopeSpans", 2),
1265
+ schemaUrl: { type: "string", id: 3 }
1266
+ }
1267
+ },
1268
+ ScopeSpans: {
1269
+ fields: {
1270
+ scope: { type: "InstrumentationScope", id: 1 },
1271
+ spans: rep("Span", 2),
1272
+ schemaUrl: { type: "string", id: 3 }
1273
+ }
1274
+ },
1275
+ Span: {
1276
+ fields: {
1277
+ traceId: { type: "bytes", id: 1 },
1278
+ spanId: { type: "bytes", id: 2 },
1279
+ traceState: { type: "string", id: 3 },
1280
+ parentSpanId: { type: "bytes", id: 4 },
1281
+ flags: { type: "fixed32", id: 16 },
1282
+ name: { type: "string", id: 5 },
1283
+ kind: { type: "int32", id: 6 },
1284
+ startTimeUnixNano: { type: "fixed64", id: 7 },
1285
+ endTimeUnixNano: { type: "fixed64", id: 8 },
1286
+ attributes: rep("KeyValue", 9),
1287
+ droppedAttributesCount: { type: "uint32", id: 10 },
1288
+ events: rep("SpanEvent", 11),
1289
+ droppedEventsCount: { type: "uint32", id: 12 },
1290
+ links: rep("SpanLink", 13),
1291
+ droppedLinksCount: { type: "uint32", id: 14 },
1292
+ status: { type: "SpanStatus", id: 15 }
1293
+ }
1294
+ },
1295
+ SpanEvent: {
1296
+ fields: {
1297
+ timeUnixNano: { type: "fixed64", id: 1 },
1298
+ name: { type: "string", id: 2 },
1299
+ attributes: rep("KeyValue", 3),
1300
+ droppedAttributesCount: { type: "uint32", id: 4 }
1301
+ }
1302
+ },
1303
+ SpanLink: {
1304
+ fields: {
1305
+ traceId: { type: "bytes", id: 1 },
1306
+ spanId: { type: "bytes", id: 2 },
1307
+ traceState: { type: "string", id: 3 },
1308
+ attributes: rep("KeyValue", 4),
1309
+ droppedAttributesCount: { type: "uint32", id: 5 },
1310
+ flags: { type: "fixed32", id: 6 }
1311
+ }
1312
+ },
1313
+ SpanStatus: {
1314
+ fields: {
1315
+ message: { type: "string", id: 2 },
1316
+ code: { type: "int32", id: 3 }
1317
+ }
1318
+ },
1319
+ ExportLogsServiceRequest: {
1320
+ fields: { resourceLogs: rep("ResourceLogs", 1) }
1321
+ },
1322
+ ResourceLogs: {
1323
+ fields: {
1324
+ resource: { type: "Resource", id: 1 },
1325
+ scopeLogs: rep("ScopeLogs", 2),
1326
+ schemaUrl: { type: "string", id: 3 }
1327
+ }
1328
+ },
1329
+ ScopeLogs: {
1330
+ fields: {
1331
+ scope: { type: "InstrumentationScope", id: 1 },
1332
+ logRecords: rep("LogRecord", 2),
1333
+ schemaUrl: { type: "string", id: 3 }
1334
+ }
1335
+ },
1336
+ LogRecord: {
1337
+ fields: {
1338
+ timeUnixNano: { type: "fixed64", id: 1 },
1339
+ observedTimeUnixNano: { type: "fixed64", id: 11 },
1340
+ severityNumber: { type: "int32", id: 2 },
1341
+ severityText: { type: "string", id: 3 },
1342
+ body: { type: "AnyValue", id: 5 },
1343
+ attributes: rep("KeyValue", 6),
1344
+ droppedAttributesCount: { type: "uint32", id: 7 },
1345
+ flags: { type: "fixed32", id: 8 },
1346
+ traceId: { type: "bytes", id: 9 },
1347
+ spanId: { type: "bytes", id: 10 },
1348
+ eventName: { type: "string", id: 12 }
1349
+ }
1350
+ },
1351
+ ExportMetricsServiceRequest: {
1352
+ fields: { resourceMetrics: rep("ResourceMetrics", 1) }
1353
+ },
1354
+ ResourceMetrics: {
1355
+ fields: {
1356
+ resource: { type: "Resource", id: 1 },
1357
+ scopeMetrics: rep("ScopeMetrics", 2),
1358
+ schemaUrl: { type: "string", id: 3 }
1359
+ }
1360
+ },
1361
+ ScopeMetrics: {
1362
+ fields: {
1363
+ scope: { type: "InstrumentationScope", id: 1 },
1364
+ metrics: rep("Metric", 2),
1365
+ schemaUrl: { type: "string", id: 3 }
1366
+ }
1367
+ },
1368
+ Metric: {
1369
+ oneofs: { data: { oneof: ["gauge", "sum", "histogram"] } },
1370
+ fields: {
1371
+ name: { type: "string", id: 1 },
1372
+ description: { type: "string", id: 2 },
1373
+ unit: { type: "string", id: 3 },
1374
+ gauge: { type: "Gauge", id: 5 },
1375
+ sum: { type: "Sum", id: 7 },
1376
+ histogram: { type: "Histogram", id: 9 },
1377
+ metadata: rep("KeyValue", 12)
1378
+ }
1379
+ },
1380
+ Gauge: { fields: { dataPoints: rep("NumberDataPoint", 1) } },
1381
+ Sum: {
1382
+ fields: {
1383
+ dataPoints: rep("NumberDataPoint", 1),
1384
+ aggregationTemporality: { type: "int32", id: 2 },
1385
+ isMonotonic: { type: "bool", id: 3 }
1386
+ }
1387
+ },
1388
+ Histogram: {
1389
+ fields: {
1390
+ dataPoints: rep("HistogramDataPoint", 1),
1391
+ aggregationTemporality: { type: "int32", id: 2 }
1392
+ }
1393
+ },
1394
+ NumberDataPoint: {
1395
+ oneofs: { value: { oneof: ["asDouble", "asInt"] } },
1396
+ fields: {
1397
+ attributes: rep("KeyValue", 7),
1398
+ startTimeUnixNano: { type: "fixed64", id: 2 },
1399
+ timeUnixNano: { type: "fixed64", id: 3 },
1400
+ asDouble: { type: "double", id: 4 },
1401
+ asInt: { type: "sfixed64", id: 6 },
1402
+ flags: { type: "uint32", id: 8 }
1403
+ }
1404
+ },
1405
+ HistogramDataPoint: {
1406
+ fields: {
1407
+ attributes: rep("KeyValue", 9),
1408
+ startTimeUnixNano: { type: "fixed64", id: 2 },
1409
+ timeUnixNano: { type: "fixed64", id: 3 },
1410
+ count: { type: "fixed64", id: 4 },
1411
+ sum: { type: "double", id: 5, options: { proto3_optional: true } },
1412
+ bucketCounts: { ...rep("fixed64", 6), options: { packed: true } },
1413
+ explicitBounds: { ...rep("double", 7), options: { packed: true } },
1414
+ flags: { type: "uint32", id: 10 },
1415
+ min: { type: "double", id: 11, options: { proto3_optional: true } },
1416
+ max: { type: "double", id: 12, options: { proto3_optional: true } }
1417
+ }
1418
+ }
1419
+ }
1420
+ };
1421
+ var root = protobuf.Root.fromJSON(DESCRIPTOR);
1422
+ var REQUEST_TYPES = {
1423
+ traces: root.lookupType("ExportTraceServiceRequest"),
1424
+ metrics: root.lookupType("ExportMetricsServiceRequest"),
1425
+ logs: root.lookupType("ExportLogsServiceRequest")
1426
+ };
1427
+ var HEX_ID_FIELDS = new Set(["traceId", "spanId", "parentSpanId"]);
1428
+ function toJson(value, key = "") {
1429
+ if (value instanceof Uint8Array) {
1430
+ return HEX_ID_FIELDS.has(key) ? Buffer.from(value).toString("hex") : Buffer.from(value).toString("base64");
1431
+ }
1432
+ if (Array.isArray(value))
1433
+ return value.map((v) => toJson(v, key));
1434
+ if (value && typeof value === "object") {
1435
+ const out = {};
1436
+ for (const [k, v] of Object.entries(value))
1437
+ out[k] = toJson(v, k);
1438
+ return out;
1439
+ }
1440
+ return value;
1441
+ }
1442
+ function decodeOtlpProtobuf(signal, bytes) {
1443
+ const type = REQUEST_TYPES[signal];
1444
+ const object = type.toObject(type.decode(bytes), {
1445
+ longs: String,
1446
+ defaults: false,
1447
+ arrays: false
1448
+ });
1449
+ return toJson(object);
1450
+ }
1451
+ function decodeOtlpBody(signal, bytes, contentType) {
1452
+ if ((contentType ?? "").toLowerCase().includes("json")) {
1453
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
1454
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
1455
+ throw new Error("not an OTLP request");
1456
+ return parsed;
1457
+ }
1458
+ return decodeOtlpProtobuf(signal, bytes);
1459
+ }
1460
+ function signalOfPath(path) {
1461
+ const match = /\/v1\/(traces|metrics|logs)\/?$/.exec(path);
1462
+ return match ? match[1] : null;
1463
+ }
1464
+ function attribute(key, value) {
1465
+ if (value === null || value === undefined)
1466
+ return null;
1467
+ if (typeof value === "boolean")
1468
+ return { key, value: { boolValue: value } };
1469
+ if (typeof value === "number") {
1470
+ return Number.isInteger(value) ? { key, value: { intValue: String(value) } } : { key, value: { doubleValue: value } };
1471
+ }
1472
+ return { key, value: { stringValue: value } };
1473
+ }
1474
+ function attributes(values) {
1475
+ return Object.entries(values).flatMap(([k, v]) => {
1476
+ const a = attribute(k, v);
1477
+ return a ? [a] : [];
1478
+ });
1479
+ }
1480
+ function unixNano(ms) {
1481
+ return (BigInt(Math.round(ms * 1000)) * 1000n).toString();
1482
+ }
1056
1483
  // ../protocol/src/remote.ts
1057
1484
  function normalizeRemote(remote) {
1058
1485
  let rest = remote.trim();
@@ -1076,14 +1503,14 @@ function normalizeRemote(remote) {
1076
1503
  return path ? `${host.toLowerCase()}/${path}` : host.toLowerCase();
1077
1504
  }
1078
1505
  // ../protocol/src/tools.ts
1079
- import { z as z6 } from "zod";
1080
- var issueRef = z6.string().trim().min(1).max(100).describe("An issue identifier like ENG-123, or the issue's id");
1081
- var teamRef = z6.string().trim().min(1).max(100).describe("A team key like ENG, or the team's id");
1082
- var projectRef = z6.string().trim().min(1).max(255).describe("A project's id or its exact name");
1083
- var markdown = (what) => z6.string().min(1).describe(`${what}, in Markdown. Mention people and issues by their Linear URLs, not @names; put long logs or diffs in a collapsible section (+++ Title \u2026 +++)`);
1084
- var labels = z6.array(z6.string().trim().min(1).max(255)).max(50).describe("Label names or ids (see linear_list_labels); replaces the issue's labels");
1085
- var priority = z6.number().int().min(0).max(4).describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
1086
- var first = z6.number().int().min(1).max(25).optional().describe("How many to return, at most 25 (default 25)");
1506
+ import { z as z7 } from "zod";
1507
+ var issueRef = z7.string().trim().min(1).max(100).describe("An issue identifier like ENG-123, or the issue's id");
1508
+ var teamRef = z7.string().trim().min(1).max(100).describe("A team key like ENG, or the team's id");
1509
+ var projectRef = z7.string().trim().min(1).max(255).describe("A project's id or its exact name");
1510
+ var markdown = (what) => z7.string().min(1).describe(`${what}, in Markdown. Mention people and issues by their Linear URLs, not @names; put long logs or diffs in a collapsible section (+++ Title \u2026 +++)`);
1511
+ var labels = z7.array(z7.string().trim().min(1).max(255)).max(50).describe("Label names or ids (see linear_list_labels); replaces the issue's labels");
1512
+ var priority = z7.number().int().min(0).max(4).describe("0 none, 1 urgent, 2 high, 3 medium, 4 low");
1513
+ var first = z7.number().int().min(1).max(25).optional().describe("How many to return, at most 25 (default 25)");
1087
1514
  var ISSUE_STATE_TYPES = [
1088
1515
  "triage",
1089
1516
  "backlog",
@@ -1111,7 +1538,7 @@ var HYPOTENUSE_TOOLS = [
1111
1538
  roles: BOTH,
1112
1539
  kind: "read",
1113
1540
  description: "Read a Linear issue: title, description, state, priority, labels, assignee, delegate, parent, sub-issues, relations, project, team, and URL. Defaults to the issue you are working on. People are listed with their profile URLs; use those URLs to mention someone.",
1114
- input: z6.strictObject({ issue: issueRef.optional() })
1541
+ input: z7.strictObject({ issue: issueRef.optional() })
1115
1542
  }),
1116
1543
  tool({
1117
1544
  name: "linear_list_comments",
@@ -1120,10 +1547,10 @@ var HYPOTENUSE_TOOLS = [
1120
1547
  roles: BOTH,
1121
1548
  kind: "read",
1122
1549
  description: "Read the comments on a Linear issue, oldest first, replies under the comment they answer. Defaults to the issue you are working on. Pass `after` (from the last page) to read further.",
1123
- input: z6.strictObject({
1550
+ input: z7.strictObject({
1124
1551
  issue: issueRef.optional(),
1125
1552
  first,
1126
- after: z6.string().trim().min(1).max(512).optional().describe("The cursor the previous page returned")
1553
+ after: z7.string().trim().min(1).max(512).optional().describe("The cursor the previous page returned")
1127
1554
  })
1128
1555
  }),
1129
1556
  tool({
@@ -1133,10 +1560,10 @@ var HYPOTENUSE_TOOLS = [
1133
1560
  roles: BOTH,
1134
1561
  kind: "write",
1135
1562
  description: "Post a comment on a Linear issue, as Hypotenuse. Your final answer already appears in Linear as a comment in this session's thread, so never use this to repeat it: use it to answer a question someone left elsewhere on the issue, or to note a blocker for someone who is not following this session. Pass `parentId` to reply in an existing thread.",
1136
- input: z6.strictObject({
1563
+ input: z7.strictObject({
1137
1564
  body: markdown("The comment"),
1138
1565
  issue: issueRef.optional().describe("The issue to comment on; defaults to the issue you are working on"),
1139
- parentId: z6.string().trim().min(1).max(100).optional().describe("The id of the comment to reply to")
1566
+ parentId: z7.string().trim().min(1).max(100).optional().describe("The id of the comment to reply to")
1140
1567
  })
1141
1568
  }),
1142
1569
  tool({
@@ -1146,8 +1573,8 @@ var HYPOTENUSE_TOOLS = [
1146
1573
  roles: BOTH,
1147
1574
  kind: "write",
1148
1575
  description: `Move a Linear issue to a workflow state, by the state's name ("In Review") or its type (triage, backlog, unstarted, started, completed, canceled). Defaults to the issue you are working on. A worker cannot complete or cancel an issue, and a manager can only where the project allows it: a person decides when work is done.`,
1149
- input: z6.strictObject({
1150
- state: z6.string().trim().min(1).max(255).describe("A state name, or a state type"),
1576
+ input: z7.strictObject({
1577
+ state: z7.string().trim().min(1).max(255).describe("A state name, or a state type"),
1151
1578
  issue: issueRef.optional()
1152
1579
  })
1153
1580
  }),
@@ -1158,10 +1585,10 @@ var HYPOTENUSE_TOOLS = [
1158
1585
  roles: BOTH,
1159
1586
  kind: "write",
1160
1587
  description: "Replace this session's plan in Linear with the whole list of steps. Only needed if your own plan updates do not already appear there.",
1161
- input: z6.strictObject({
1162
- steps: z6.array(z6.strictObject({
1163
- content: z6.string().trim().min(1).max(1024).describe("The step"),
1164
- status: z6.enum(PLAN_STEP_STATUSES).describe("pending, inProgress, completed, or canceled")
1588
+ input: z7.strictObject({
1589
+ steps: z7.array(z7.strictObject({
1590
+ content: z7.string().trim().min(1).max(1024).describe("The step"),
1591
+ status: z7.enum(PLAN_STEP_STATUSES).describe("pending, inProgress, completed, or canceled")
1165
1592
  })).min(1).max(100).describe("Every step, in order")
1166
1593
  })
1167
1594
  }),
@@ -1172,11 +1599,11 @@ var HYPOTENUSE_TOOLS = [
1172
1599
  roles: BOTH,
1173
1600
  kind: "write",
1174
1601
  description: "Ask the person a question in Linear when you need their decision to go on. With `options`, Linear shows them as choices (a GitHub repository URL as a value renders as that repository). Their answer arrives as your next prompt: after calling this, end your turn at once without a further message.",
1175
- input: z6.strictObject({
1176
- question: z6.string().trim().min(1).max(1e4).describe("The question"),
1177
- options: z6.array(z6.strictObject({
1178
- label: z6.string().trim().min(1).max(255).describe("What the person sees"),
1179
- value: z6.string().trim().min(1).max(2048).optional().describe("What you get back; defaults to the label")
1602
+ input: z7.strictObject({
1603
+ question: z7.string().trim().min(1).max(1e4).describe("The question"),
1604
+ options: z7.array(z7.strictObject({
1605
+ label: z7.string().trim().min(1).max(255).describe("What the person sees"),
1606
+ value: z7.string().trim().min(1).max(2048).optional().describe("What you get back; defaults to the label")
1180
1607
  })).min(2).max(10).optional().describe("2 to 10 choices; omit for a free-text answer")
1181
1608
  })
1182
1609
  }),
@@ -1187,8 +1614,8 @@ var HYPOTENUSE_TOOLS = [
1187
1614
  roles: BOTH,
1188
1615
  kind: "write",
1189
1616
  description: "Link the pull request you opened to this session in Linear. Call it as soon as the pull request exists. It must be a pull request on this session's repository.",
1190
- input: z6.strictObject({
1191
- url: z6.url().max(2048).describe("The pull request's URL")
1617
+ input: z7.strictObject({
1618
+ url: z7.url().max(2048).describe("The pull request's URL")
1192
1619
  })
1193
1620
  }),
1194
1621
  tool({
@@ -1198,11 +1625,11 @@ var HYPOTENUSE_TOOLS = [
1198
1625
  roles: BOTH,
1199
1626
  kind: "read",
1200
1627
  description: "Search Linear issues by text in their title or description, narrowed by team, state type, label, or project. At most 25 results.",
1201
- input: z6.strictObject({
1202
- query: z6.string().trim().min(1).max(255).optional().describe("Text to find in titles and descriptions"),
1628
+ input: z7.strictObject({
1629
+ query: z7.string().trim().min(1).max(255).optional().describe("Text to find in titles and descriptions"),
1203
1630
  team: teamRef.optional(),
1204
- stateType: z6.enum(ISSUE_STATE_TYPES).optional().describe("Only issues in states of this type"),
1205
- label: z6.string().trim().min(1).max(255).optional().describe("Only issues with this label name"),
1631
+ stateType: z7.enum(ISSUE_STATE_TYPES).optional().describe("Only issues in states of this type"),
1632
+ label: z7.string().trim().min(1).max(255).optional().describe("Only issues with this label name"),
1206
1633
  project: projectRef.optional(),
1207
1634
  first
1208
1635
  })
@@ -1214,8 +1641,8 @@ var HYPOTENUSE_TOOLS = [
1214
1641
  roles: MANAGER,
1215
1642
  kind: "write",
1216
1643
  description: "Create a Linear issue. Defaults to the team of the issue you are working on. Never assigns anyone: hand work to a worker with hypotenuse_dispatch.",
1217
- input: z6.strictObject({
1218
- title: z6.string().trim().min(1).max(255).describe("The title"),
1644
+ input: z7.strictObject({
1645
+ title: z7.string().trim().min(1).max(255).describe("The title"),
1219
1646
  description: markdown("The description").optional(),
1220
1647
  team: teamRef.optional(),
1221
1648
  labels: labels.optional(),
@@ -1231,14 +1658,14 @@ var HYPOTENUSE_TOOLS = [
1231
1658
  roles: MANAGER,
1232
1659
  kind: "write",
1233
1660
  description: "Edit a Linear issue you manage: its title, description, labels, priority, project, or estimate. It cannot change the assignee.",
1234
- input: z6.strictObject({
1661
+ input: z7.strictObject({
1235
1662
  issue: issueRef,
1236
- title: z6.string().trim().min(1).max(255).optional().describe("The title"),
1663
+ title: z7.string().trim().min(1).max(255).optional().describe("The title"),
1237
1664
  description: markdown("The description").optional(),
1238
1665
  labels: labels.optional(),
1239
1666
  priority: priority.optional(),
1240
1667
  project: projectRef.optional(),
1241
- estimate: z6.number().int().min(0).max(100).optional().describe("The estimate, in the team's points")
1668
+ estimate: z7.number().int().min(0).max(100).optional().describe("The estimate, in the team's points")
1242
1669
  })
1243
1670
  }),
1244
1671
  tool({
@@ -1248,9 +1675,9 @@ var HYPOTENUSE_TOOLS = [
1248
1675
  roles: MANAGER,
1249
1676
  kind: "write",
1250
1677
  description: "Create a sub-issue under an issue you manage \u2014 how a plan is broken into tickets. It goes in the parent's team unless you name another.",
1251
- input: z6.strictObject({
1678
+ input: z7.strictObject({
1252
1679
  parent: issueRef.describe("The parent issue"),
1253
- title: z6.string().trim().min(1).max(255).describe("The title"),
1680
+ title: z7.string().trim().min(1).max(255).describe("The title"),
1254
1681
  description: markdown("The description").optional(),
1255
1682
  team: teamRef.optional(),
1256
1683
  labels: labels.optional(),
@@ -1265,10 +1692,10 @@ var HYPOTENUSE_TOOLS = [
1265
1692
  roles: MANAGER,
1266
1693
  kind: "write",
1267
1694
  description: "Relate two Linear issues: `blocks` (issue blocks relatedIssue), `related`, or `duplicate` (issue duplicates relatedIssue). At least one of them must be an issue you manage.",
1268
- input: z6.strictObject({
1695
+ input: z7.strictObject({
1269
1696
  issue: issueRef,
1270
1697
  relatedIssue: issueRef.describe("The other issue"),
1271
- type: z6.enum(["blocks", "related", "duplicate"]).describe("blocks, related, or duplicate")
1698
+ type: z7.enum(["blocks", "related", "duplicate"]).describe("blocks, related, or duplicate")
1272
1699
  })
1273
1700
  }),
1274
1701
  tool({
@@ -1278,7 +1705,7 @@ var HYPOTENUSE_TOOLS = [
1278
1705
  roles: MANAGER,
1279
1706
  kind: "read",
1280
1707
  description: "Read a Linear project: its name, summary, content (the PRD), status, lead, how many issues are in each state, and its documents. Defaults to the project of the issue you are working on.",
1281
- input: z6.strictObject({ project: projectRef.optional() })
1708
+ input: z7.strictObject({ project: projectRef.optional() })
1282
1709
  }),
1283
1710
  tool({
1284
1711
  name: "linear_update_project",
@@ -1287,11 +1714,11 @@ var HYPOTENUSE_TOOLS = [
1287
1714
  roles: MANAGER,
1288
1715
  kind: "write",
1289
1716
  description: "Write a Linear project's content (the PRD, in Markdown), its one-line summary, or its status. Only the project of the issue you are working on. It never changes the lead or the members.",
1290
- input: z6.strictObject({
1717
+ input: z7.strictObject({
1291
1718
  project: projectRef.optional(),
1292
1719
  content: markdown("The project's content (the PRD)").optional(),
1293
- summary: z6.string().trim().min(1).max(255).optional().describe("The one-line summary shown under the project's name"),
1294
- status: z6.string().trim().min(1).max(255).optional().describe("A project status name, or its type (backlog, planned, started, paused, completed, canceled)")
1720
+ summary: z7.string().trim().min(1).max(255).optional().describe("The one-line summary shown under the project's name"),
1721
+ status: z7.string().trim().min(1).max(255).optional().describe("A project status name, or its type (backlog, planned, started, paused, completed, canceled)")
1295
1722
  })
1296
1723
  }),
1297
1724
  tool({
@@ -1301,8 +1728,8 @@ var HYPOTENUSE_TOOLS = [
1301
1728
  roles: MANAGER,
1302
1729
  kind: "write",
1303
1730
  description: "Create a Linear document attached to a project or an issue you manage. Defaults to the project of the issue you are working on.",
1304
- input: z6.strictObject({
1305
- title: z6.string().trim().min(1).max(255).describe("The title"),
1731
+ input: z7.strictObject({
1732
+ title: z7.string().trim().min(1).max(255).describe("The title"),
1306
1733
  content: markdown("The document"),
1307
1734
  project: projectRef.optional(),
1308
1735
  issue: issueRef.optional().describe("Attach it to this issue instead of a project")
@@ -1315,10 +1742,10 @@ var HYPOTENUSE_TOOLS = [
1315
1742
  roles: MANAGER,
1316
1743
  kind: "write",
1317
1744
  description: "Replace a Linear document's content (and optionally its title): one this session created, or one attached to the project of the issue you are working on.",
1318
- input: z6.strictObject({
1319
- document: z6.string().trim().min(1).max(100).describe("The document's id"),
1745
+ input: z7.strictObject({
1746
+ document: z7.string().trim().min(1).max(100).describe("The document's id"),
1320
1747
  content: markdown("The document's new content"),
1321
- title: z6.string().trim().min(1).max(255).optional().describe("A new title")
1748
+ title: z7.string().trim().min(1).max(255).optional().describe("A new title")
1322
1749
  })
1323
1750
  }),
1324
1751
  tool({
@@ -1328,7 +1755,7 @@ var HYPOTENUSE_TOOLS = [
1328
1755
  roles: MANAGER,
1329
1756
  kind: "read",
1330
1757
  description: "List the labels you can put on a team's issues (the team's own and the workspace's). Defaults to the team of the issue you are working on.",
1331
- input: z6.strictObject({ team: teamRef.optional() })
1758
+ input: z7.strictObject({ team: teamRef.optional() })
1332
1759
  }),
1333
1760
  tool({
1334
1761
  name: "hypotenuse_dispatch",
@@ -1337,11 +1764,11 @@ var HYPOTENUSE_TOOLS = [
1337
1764
  roles: MANAGER,
1338
1765
  kind: "write",
1339
1766
  description: 'Hand an issue you manage to a Hypotenuse worker: it is delegated to Hypotenuse and a worker session starts on it on whichever runner can take it. Only dispatch an issue whose acceptance criteria are written, one call per issue. When the worker finishes, you get a review turn with its result: accept it, request changes with mode "changes" and specific instructions, or file follow-up issues. Workers cannot dispatch, you cannot dispatch the issue you are working on, and you may have only a few dispatched issues in progress at once.',
1340
- input: z6.strictObject({
1767
+ input: z7.strictObject({
1341
1768
  issue: issueRef,
1342
- instructions: z6.string().trim().min(1).max(8192).optional().describe('Context for the worker that does not belong in the issue; for mode "changes", exactly what to change'),
1343
- agentPreference: z6.string().trim().min(1).max(64).optional().describe("An agent id the worker's runner should prefer (a preference, not a requirement)"),
1344
- mode: z6.enum(["new", "changes"]).optional().describe("new (default): start a worker on the issue. changes: send a finished worker back to the same session and branch with your instructions")
1769
+ instructions: z7.string().trim().min(1).max(8192).optional().describe('Context for the worker that does not belong in the issue; for mode "changes", exactly what to change'),
1770
+ agentPreference: z7.string().trim().min(1).max(64).optional().describe("An agent id the worker's runner should prefer (a preference, not a requirement)"),
1771
+ mode: z7.enum(["new", "changes"]).optional().describe("new (default): start a worker on the issue. changes: send a finished worker back to the same session and branch with your instructions")
1345
1772
  })
1346
1773
  })
1347
1774
  ];
@@ -1351,10 +1778,10 @@ function toolsForRole(role) {
1351
1778
  function toolByName(name) {
1352
1779
  return HYPOTENUSE_TOOLS.find((t) => t.name === name);
1353
1780
  }
1354
- var ToolResult = z6.looseObject({
1355
- result: z6.looseObject({
1356
- text: z6.string(),
1357
- data: z6.record(z6.string(), z6.unknown())
1781
+ var ToolResult = z7.looseObject({
1782
+ result: z7.looseObject({
1783
+ text: z7.string(),
1784
+ data: z7.record(z7.string(), z7.unknown())
1358
1785
  })
1359
1786
  });
1360
1787
  var TOOL_RESULT_MAX_BYTES = 16 * 1024;
@@ -1364,98 +1791,99 @@ var PROTOCOL_VERSION = 1;
1364
1791
  var PROTOCOL_HEADER = "X-Hypotenuse-Protocol";
1365
1792
  var PROTOCOL_UNSUPPORTED = "PROTOCOL_UNSUPPORTED";
1366
1793
  // ../protocol/src/work.ts
1367
- import { z as z7 } from "zod";
1368
- var ClaimRequest = z7.strictObject({
1369
- workItemIds: z7.array(z7.number().int().positive()).max(100).optional().describe("Claim only these work items; omitted means any offer")
1794
+ import { z as z8 } from "zod";
1795
+ var ClaimRequest = z8.strictObject({
1796
+ workItemIds: z8.array(z8.number().int().positive()).max(100).optional().describe("Claim only these work items; omitted means any offer")
1370
1797
  });
1371
- var AssignmentRepository = z7.looseObject({
1372
- remote: z7.string(),
1373
- baseBranch: z7.string().nullable()
1798
+ var AssignmentRepository = z8.looseObject({
1799
+ remote: z8.string(),
1800
+ baseBranch: z8.string().nullable()
1374
1801
  });
1375
- var AssignmentIssue = z7.looseObject({
1376
- id: z7.string(),
1377
- identifier: z7.string().nullable(),
1378
- title: z7.string().nullable(),
1379
- description: z7.string().nullable(),
1380
- url: z7.string().nullable(),
1381
- teamKey: z7.string().nullable(),
1382
- labels: z7.array(z7.string()),
1383
- state: z7.looseObject({ name: z7.string(), type: z7.string() }).nullable(),
1384
- parent: z7.looseObject({ id: z7.string() }).nullable(),
1385
- project: z7.looseObject({ id: z7.string(), name: z7.string() }).nullable()
1802
+ var AssignmentIssue = z8.looseObject({
1803
+ id: z8.string(),
1804
+ identifier: z8.string().nullable(),
1805
+ title: z8.string().nullable(),
1806
+ description: z8.string().nullable(),
1807
+ url: z8.string().nullable(),
1808
+ teamKey: z8.string().nullable(),
1809
+ labels: z8.array(z8.string()),
1810
+ state: z8.looseObject({ name: z8.string(), type: z8.string() }).nullable(),
1811
+ parent: z8.looseObject({ id: z8.string() }).nullable(),
1812
+ project: z8.looseObject({ id: z8.string(), name: z8.string() }).nullable()
1386
1813
  });
1387
- var AssignmentGuidance = z7.looseObject({
1388
- origin: z7.enum(["team", "workspace"]),
1389
- teamKey: z7.string().nullable().optional(),
1390
- body: z7.string()
1814
+ var AssignmentGuidance = z8.looseObject({
1815
+ origin: z8.enum(["team", "workspace"]),
1816
+ teamKey: z8.string().nullable().optional(),
1817
+ body: z8.string()
1391
1818
  });
1392
- var HistoryEntry = z7.looseObject({
1393
- seq: z7.number().int(),
1394
- activityId: z7.number().int(),
1395
- direction: z7.enum(["inbound", "outbound"]),
1396
- type: z7.string(),
1397
- body: z7.string().nullable(),
1398
- signal: z7.string().nullable()
1819
+ var HistoryEntry = z8.looseObject({
1820
+ seq: z8.number().int(),
1821
+ activityId: z8.number().int(),
1822
+ direction: z8.enum(["inbound", "outbound"]),
1823
+ type: z8.string(),
1824
+ body: z8.string().nullable(),
1825
+ signal: z8.string().nullable()
1399
1826
  });
1400
- var PromptEntry = z7.looseObject({
1401
- activityId: z7.number().int(),
1402
- body: z7.string()
1827
+ var PromptEntry = z8.looseObject({
1828
+ activityId: z8.number().int(),
1829
+ body: z8.string()
1403
1830
  });
1404
- var AssignmentLink = z7.looseObject({
1405
- label: z7.string(),
1406
- url: z7.string()
1831
+ var AssignmentLink = z8.looseObject({
1832
+ label: z8.string(),
1833
+ url: z8.string()
1407
1834
  });
1408
1835
  var DISPATCH_MODES = ["new", "changes"];
1409
- var AssignmentDispatch = z7.looseObject({
1410
- mode: z7.enum(DISPATCH_MODES),
1411
- reviewRound: z7.number().int().min(0),
1412
- instructions: z7.string().nullable().describe("Extra context from the planning agent; for `changes`, what to change"),
1413
- agentPreference: z7.string().nullable().describe("An [agents] id the planning agent prefers; a preference only"),
1414
- parent: z7.looseObject({
1415
- agentSessionId: z7.number().int().positive(),
1416
- issueIdentifier: z7.string().nullable(),
1417
- issueUrl: z7.string().nullable()
1836
+ var AssignmentDispatch = z8.looseObject({
1837
+ mode: z8.enum(DISPATCH_MODES),
1838
+ reviewRound: z8.number().int().min(0),
1839
+ instructions: z8.string().nullable().describe("Extra context from the planning agent; for `changes`, what to change"),
1840
+ agentPreference: z8.string().nullable().describe("An [agents] id the planning agent prefers; a preference only"),
1841
+ parent: z8.looseObject({
1842
+ agentSessionId: z8.number().int().positive(),
1843
+ issueIdentifier: z8.string().nullable(),
1844
+ issueUrl: z8.string().nullable()
1418
1845
  })
1419
1846
  });
1420
1847
  var REVIEW_OUTCOMES = ["completed", "failed", "cancelled"];
1421
- var AssignmentReview = z7.looseObject({
1422
- childAgentSessionId: z7.number().int().positive(),
1423
- childIssueIdentifier: z7.string().nullable(),
1424
- childIssueUrl: z7.string().nullable(),
1425
- reviewRound: z7.number().int().min(0),
1426
- outcome: z7.enum(REVIEW_OUTCOMES),
1427
- finalResponse: z7.string().nullable().describe("The body of the worker's last response or error"),
1428
- plan: z7.array(z7.looseObject({ content: z7.string(), status: z7.string() })).nullable(),
1429
- pullRequests: z7.array(AssignmentLink)
1848
+ var AssignmentReview = z8.looseObject({
1849
+ childAgentSessionId: z8.number().int().positive(),
1850
+ childIssueIdentifier: z8.string().nullable(),
1851
+ childIssueUrl: z8.string().nullable(),
1852
+ reviewRound: z8.number().int().min(0),
1853
+ outcome: z8.enum(REVIEW_OUTCOMES),
1854
+ finalResponse: z8.string().nullable().describe("The body of the worker's last response or error"),
1855
+ plan: z8.array(z8.looseObject({ content: z8.string(), status: z8.string() })).nullable(),
1856
+ pullRequests: z8.array(AssignmentLink)
1430
1857
  });
1431
- var WorkAssignment = z7.looseObject({
1432
- workItemId: z7.number().int().positive(),
1433
- agentSessionId: z7.number().int().positive(),
1434
- type: z7.string(),
1435
- externalSessionId: z7.string(),
1436
- externalSessionUrl: z7.string().nullable(),
1437
- dashboardUrl: z7.string(),
1438
- kind: z7.enum(OFFER_KINDS),
1858
+ var WorkAssignment = z8.looseObject({
1859
+ workItemId: z8.number().int().positive(),
1860
+ agentSessionId: z8.number().int().positive(),
1861
+ type: z8.string(),
1862
+ externalSessionId: z8.string(),
1863
+ externalSessionUrl: z8.string().nullable(),
1864
+ dashboardUrl: z8.string(),
1865
+ kind: z8.enum(OFFER_KINDS),
1439
1866
  role: RunnerRole,
1440
- agentId: z7.string().nullable(),
1441
- fresh: z7.boolean(),
1867
+ agentId: z8.string().nullable(),
1868
+ fresh: z8.boolean(),
1442
1869
  repository: AssignmentRepository.nullable(),
1443
- branch: z7.string().nullable(),
1870
+ branch: z8.string().nullable(),
1444
1871
  issue: AssignmentIssue,
1445
- instruction: z7.string().nullable().describe("What the mention said, a matched role prefix stripped; null for a delegation"),
1446
- promptContext: z7.string().nullable(),
1447
- guidance: z7.array(AssignmentGuidance),
1448
- previousComments: z7.array(z7.record(z7.string(), z7.unknown())).nullable(),
1449
- history: z7.array(HistoryEntry),
1450
- prompt: z7.array(PromptEntry).nullable(),
1451
- origin: z7.string(),
1452
- creator: z7.looseObject({ name: z7.string().nullable(), email: z7.string().nullable() }).nullable().optional().describe("Who started the session; null when an automation did"),
1453
- humanDelegated: z7.boolean(),
1454
- leaseExpiresAt: z7.number().int(),
1872
+ instruction: z8.string().nullable().describe("What the mention said, a matched role prefix stripped; null for a delegation"),
1873
+ promptContext: z8.string().nullable(),
1874
+ guidance: z8.array(AssignmentGuidance),
1875
+ previousComments: z8.array(z8.record(z8.string(), z8.unknown())).nullable(),
1876
+ history: z8.array(HistoryEntry),
1877
+ prompt: z8.array(PromptEntry).nullable(),
1878
+ origin: z8.string(),
1879
+ creator: z8.looseObject({ name: z8.string().nullable(), email: z8.string().nullable() }).nullable().optional().describe("Who started the session; null when an automation did"),
1880
+ humanDelegated: z8.boolean(),
1881
+ leaseExpiresAt: z8.number().int(),
1455
1882
  dispatch: AssignmentDispatch.nullable().optional().describe("Set when a manager session dispatched this worker turn"),
1456
- review: AssignmentReview.nullable().optional().describe("Set on a manager's review turn: the worker result to review")
1883
+ review: AssignmentReview.nullable().optional().describe("Set on a manager's review turn: the worker result to review"),
1884
+ telemetry: ClaimTelemetry.nullable().optional().describe("The turn's trace and what to collect for the project's exporters; null when nothing is exported")
1457
1885
  });
1458
- var ClaimResponse = z7.looseObject({
1886
+ var ClaimResponse = z8.looseObject({
1459
1887
  assignment: WorkAssignment.nullable()
1460
1888
  });
1461
1889
  // src/config/resolve.ts
@@ -1625,7 +2053,7 @@ function objectKeys(schema) {
1625
2053
 
1626
2054
  // src/config/schema.ts
1627
2055
  import { cpus, hostname } from "os";
1628
- import { z as z8 } from "zod";
2056
+ import { z as z9 } from "zod";
1629
2057
 
1630
2058
  // src/config/durations.ts
1631
2059
  var DURATION_UNITS = {
@@ -1772,83 +2200,83 @@ function machineContext(baseDir, env = process.env) {
1772
2200
  return { baseDir, env, cpuCount: cpus().length || 1, hostname: hostname() };
1773
2201
  }
1774
2202
  function buildConfigSchema(ctx) {
1775
- const path = () => z8.string().trim().min(1).transform((value) => expandPath(value, ctx.baseDir, ctx.env));
1776
- const duration = () => z8.string().regex(DURATION_PATTERN, "expected a duration such as 45s, 10m, 2h, or 7d").transform(parseDuration);
1777
- const size = () => z8.string().regex(SIZE_PATTERN, "expected a size such as 500MB or 2GB").transform(parseSize);
1778
- const parallel = () => z8.number().int().min(1).max(32);
1779
- const strings = () => z8.array(z8.string().trim().min(1));
1780
- const stringTable = () => z8.record(z8.string(), z8.string());
1781
- const permissionPolicy = (fallback) => z8.union([
1782
- z8.enum(POLICIES),
1783
- z8.strictObject({
1784
- default: z8.enum(POLICIES).optional(),
1785
- ...Object.fromEntries(TOOL_KINDS.map((kind) => [kind, z8.enum(POLICIES).optional()]))
2203
+ const path = () => z9.string().trim().min(1).transform((value) => expandPath(value, ctx.baseDir, ctx.env));
2204
+ const duration = () => z9.string().regex(DURATION_PATTERN, "expected a duration such as 45s, 10m, 2h, or 7d").transform(parseDuration);
2205
+ const size = () => z9.string().regex(SIZE_PATTERN, "expected a size such as 500MB or 2GB").transform(parseSize);
2206
+ const parallel = () => z9.number().int().min(1).max(32);
2207
+ const strings = () => z9.array(z9.string().trim().min(1));
2208
+ const stringTable = () => z9.record(z9.string(), z9.string());
2209
+ const permissionPolicy = (fallback) => z9.union([
2210
+ z9.enum(POLICIES),
2211
+ z9.strictObject({
2212
+ default: z9.enum(POLICIES).optional(),
2213
+ ...Object.fromEntries(TOOL_KINDS.map((kind) => [kind, z9.enum(POLICIES).optional()]))
1786
2214
  })
1787
2215
  ]).default(typeof fallback === "string" ? fallback : { ...fallback }).describe('allow, ask, or deny every permission request \u2014 or a table by ACP tool kind, e.g. { default = "allow", execute = "ask" }. Defaults: worker "allow"; manager read-only, { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }');
1788
- const mcpServer = z8.strictObject({
1789
- name: z8.string().trim().min(1).describe("Unique within the role; `hypotenuse` is reserved"),
2216
+ const mcpServer = z9.strictObject({
2217
+ name: z9.string().trim().min(1).describe("Unique within the role; `hypotenuse` is reserved"),
1790
2218
  command: strings().min(1).optional().describe("A stdio MCP server's command"),
1791
2219
  env: stringTable().optional().describe("Added to the server's environment; $VAR allowed"),
1792
- url: z8.url().optional().describe("An HTTP MCP server's URL"),
2220
+ url: z9.url().optional().describe("An HTTP MCP server's URL"),
1793
2221
  headers: stringTable().optional().describe("HTTP headers; $VAR allowed")
1794
2222
  });
1795
- const role = (fallbackPolicy) => z8.strictObject({
1796
- agent: z8.string().trim().min(1).describe("An [agents.<id>] id or a preset name"),
1797
- prompt: z8.string().min(1).optional().describe("The role's entry prompt template, inline"),
2223
+ const role = (fallbackPolicy) => z9.strictObject({
2224
+ agent: z9.string().trim().min(1).describe("An [agents.<id>] id or a preset name"),
2225
+ prompt: z9.string().min(1).optional().describe("The role's entry prompt template, inline"),
1798
2226
  prompt_file: path().optional().describe("A Markdown prompt template file"),
1799
2227
  permission_policy: permissionPolicy(fallbackPolicy),
1800
2228
  max_parallel: parallel().optional().describe("Jobs of this role at once"),
1801
- mode: z8.string().trim().min(1).optional().describe("The ACP session mode to select"),
2229
+ mode: z9.string().trim().min(1).optional().describe("The ACP session mode to select"),
1802
2230
  turn_timeout: duration().optional().describe("Overrides [sessions] turn_timeout"),
1803
- additional_directories: z8.array(path()).default([]).describe("Extra directories the agent may read and write"),
1804
- extra_mcp_servers: z8.array(mcpServer).default([]).describe("More MCP servers for the agent")
2231
+ additional_directories: z9.array(path()).default([]).describe("Extra directories the agent may read and write"),
2232
+ extra_mcp_servers: z9.array(mcpServer).default([]).describe("More MCP servers for the agent")
1805
2233
  });
1806
- const agent = z8.strictObject({
1807
- preset: z8.enum(PRESET_NAMES).optional().describe("A built-in agent launch command"),
2234
+ const agent = z9.strictObject({
2235
+ preset: z9.enum(PRESET_NAMES).optional().describe("A built-in agent launch command"),
1808
2236
  command: strings().min(1).optional().describe("The full launch command of an ACP agent on stdio"),
1809
2237
  args: strings().default([]).describe("Appended to the preset's or command's arguments"),
1810
2238
  env: stringTable().default({}).describe("Added to the agent's environment; $VAR allowed"),
1811
- auth_method: z8.string().trim().min(1).optional().describe("Which advertised ACP authMethod to use"),
2239
+ auth_method: z9.string().trim().min(1).optional().describe("Which advertised ACP authMethod to use"),
1812
2240
  startup_timeout: duration().prefault("60s").describe("Time allowed to answer initialize"),
1813
2241
  max_parallel: parallel().optional().describe("Jobs using this agent at once")
1814
2242
  });
1815
- const cleanup = z8.enum(["on-complete", "on-merge", "ttl", "never"]);
1816
- const worktrees = z8.enum(["always", "never"]);
1817
- const repository = z8.strictObject({
1818
- name: z8.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`").max(64).optional().describe("Unique; used in worktree paths"),
2243
+ const cleanup = z9.enum(["on-complete", "on-merge", "ttl", "never"]);
2244
+ const worktrees = z9.enum(["always", "never"]);
2245
+ const repository = z9.strictObject({
2246
+ name: z9.string().trim().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, `.`, `_`, `-`").max(64).optional().describe("Unique; used in worktree paths"),
1819
2247
  path: path().describe("An existing git clone"),
1820
- remote: z8.string().trim().min(1).optional().describe("The repository's remote; must match origin"),
1821
- base_branch: z8.string().trim().min(1).optional().describe("The branch worktrees start from"),
2248
+ remote: z9.string().trim().min(1).optional().describe("The repository's remote; must match origin"),
2249
+ base_branch: z9.string().trim().min(1).optional().describe("The branch worktrees start from"),
1822
2250
  setup: strings().default([]).describe("Shell commands run in each new worktree"),
1823
2251
  setup_timeout: duration().prefault("10m").describe("Per setup command"),
1824
- branch_prefix: z8.string().trim().min(1).default("hypotenuse").describe("Branch name prefix"),
2252
+ branch_prefix: z9.string().trim().min(1).default("hypotenuse").describe("Branch name prefix"),
1825
2253
  worktrees: worktrees.optional().describe("Overrides [sessions] worktrees"),
1826
2254
  cleanup: cleanup.optional().describe("Overrides [sessions] cleanup"),
1827
2255
  pr_url_patterns: strings().default([]).describe("Extra PR URL regexes for self-hosted forges")
1828
2256
  });
1829
- return z8.strictObject({
1830
- config_version: z8.number().int().describe(`The config format; must be ${CONFIG_VERSION}`),
1831
- server: z8.strictObject({
1832
- url: z8.url().optional().describe("The Hypotenuse server"),
1833
- project: z8.string().trim().min(1).optional().describe("The runner's project, by slug or id")
2257
+ return z9.strictObject({
2258
+ config_version: z9.number().int().describe(`The config format; must be ${CONFIG_VERSION}`),
2259
+ server: z9.strictObject({
2260
+ url: z9.url().optional().describe("The Hypotenuse server"),
2261
+ project: z9.string().trim().min(1).optional().describe("The runner's project, by slug or id")
1834
2262
  }).prefault({}),
1835
- runner: z8.strictObject({
1836
- name: z8.string().trim().min(1).max(64).default(ctx.hostname.replace(/\.local$/i, "").slice(0, 64) || "runner").describe("Display name"),
1837
- max_parallel_jobs: z8.number().int().min(1).max(32).default(defaultMaxParallelJobs(ctx.cpuCount)).describe("Jobs running at once on this machine"),
1838
- roles: z8.array(z8.enum(RUNNER_ROLES)).min(1).default(["worker"]).describe("The roles this runner plays")
2263
+ runner: z9.strictObject({
2264
+ name: z9.string().trim().min(1).max(64).default(ctx.hostname.replace(/\.local$/i, "").slice(0, 64) || "runner").describe("Display name"),
2265
+ max_parallel_jobs: z9.number().int().min(1).max(32).default(defaultMaxParallelJobs(ctx.cpuCount)).describe("Jobs running at once on this machine"),
2266
+ roles: z9.array(z9.enum(RUNNER_ROLES)).min(1).default(["worker"]).describe("The roles this runner plays")
1839
2267
  }).prefault({}),
1840
- subscriptions: z8.strictObject({
2268
+ subscriptions: z9.strictObject({
1841
2269
  teams: strings().default([]).describe("Linear team keys"),
1842
2270
  projects: strings().default([]).describe("Linear project ids or names"),
1843
2271
  labels: strings().default([]).describe("Label names; an issue needs one")
1844
2272
  }).prefault({}),
1845
- agents: z8.record(z8.string().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and `-`"), agent).default({}),
1846
- roles: z8.strictObject({
2273
+ agents: z9.record(z9.string().regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters, digits, and `-`"), agent).default({}),
2274
+ roles: z9.strictObject({
1847
2275
  manager: role(MANAGER_DEFAULT_POLICY).optional(),
1848
2276
  worker: role("allow").optional()
1849
2277
  }).prefault({}),
1850
- repositories: z8.array(repository).default([]),
1851
- sessions: z8.strictObject({
2278
+ repositories: z9.array(repository).default([]),
2279
+ sessions: z9.strictObject({
1852
2280
  worktrees: worktrees.default("always").describe("Whether sessions get a worktree"),
1853
2281
  worktree_root: path().prefault("~/.hypotenuse/worktrees").describe("Where worktrees live"),
1854
2282
  cleanup: cleanup.default("on-complete").describe("When a worktree may be removed"),
@@ -1859,10 +2287,16 @@ function buildConfigSchema(ctx) {
1859
2287
  keepalive_interval: duration().prefault("10m").describe("Silence before a keepalive thought"),
1860
2288
  stop_grace: duration().prefault("10s").describe("Wind-down time after a stop"),
1861
2289
  min_free_disk: size().prefault("2GB").describe("Free space needed for a new worktree"),
1862
- co_author: z8.string().default("Hypotenuse <noreply@hypotenuse.cloud>").describe("Co-Authored-By trailer on the agent's commits; empty for none")
2290
+ co_author: z9.string().default("Hypotenuse <noreply@hypotenuse.cloud>").describe("Co-Authored-By trailer on the agent's commits; empty for none")
1863
2291
  }).prefault({}),
1864
- log: z8.strictObject({
1865
- level: z8.enum(["error", "warn", "info", "debug", "trace"]).default("info").describe("Log level"),
2292
+ telemetry: z9.strictObject({
2293
+ enabled: z9.boolean().default(true).describe("Send this machine's telemetry for the project's exporters; false sends none"),
2294
+ max_content: z9.enum(CONTENT_LEVELS).default("full").describe("The most this runner sends: metadata, content, or full \u2014 lower than an exporter's level wins"),
2295
+ agent_native: z9.boolean().default(true).describe("Capture the coding agent's own OpenTelemetry; false never starts the receiver or sets OTEL_* variables"),
2296
+ buffer_mb: z9.number().int().min(1).max(1024).default(32).describe("The in-memory telemetry queue, in MB; when full the oldest logs, then spans, are dropped and counted")
2297
+ }).prefault({}),
2298
+ log: z9.strictObject({
2299
+ level: z9.enum(["error", "warn", "info", "debug", "trace"]).default("info").describe("Log level"),
1866
2300
  file: path().optional().describe("The daemon log file")
1867
2301
  }).prefault({})
1868
2302
  });
@@ -1874,7 +2308,7 @@ var ConfigSchema = buildConfigSchema({
1874
2308
  hostname: "runner"
1875
2309
  });
1876
2310
  function configJsonSchema() {
1877
- const schema = z8.toJSONSchema(ConfigSchema, {
2311
+ const schema = z9.toJSONSchema(ConfigSchema, {
1878
2312
  io: "input",
1879
2313
  unrepresentable: "any"
1880
2314
  });
@@ -2347,7 +2781,7 @@ function failedResult(call) {
2347
2781
  `).find((l) => l.trim()) ?? "";
2348
2782
  return first ? `Failed: ${truncate(first.trim(), 500)}` : "Failed";
2349
2783
  }
2350
- var SECRET_PATTERNS = [
2784
+ var SECRET_PATTERNS2 = [
2351
2785
  /\bsk-[A-Za-z0-9_-]{16,}/g,
2352
2786
  /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/g,
2353
2787
  /\bgh[pousr]_[A-Za-z0-9]{20,}/g,
@@ -2361,7 +2795,7 @@ var SECRET_PATTERNS = [
2361
2795
  ];
2362
2796
  function redact(text) {
2363
2797
  let out = text;
2364
- for (const pattern of SECRET_PATTERNS) {
2798
+ for (const pattern of SECRET_PATTERNS2) {
2365
2799
  out = out.replace(pattern, (match, prefix) => typeof prefix === "string" && /^Bearer/i.test(match) ? `${prefix}[redacted]` : "[redacted]");
2366
2800
  }
2367
2801
  return out;
@@ -2556,7 +2990,7 @@ class ApiClient {
2556
2990
  response = await fetch(url, {
2557
2991
  method,
2558
2992
  headers,
2559
- body: options.body === undefined ? undefined : JSON.stringify(options.body),
2993
+ body: options.body !== undefined ? JSON.stringify(options.body) : options.rawBody ? new Uint8Array(options.rawBody) : undefined,
2560
2994
  signal: options.timeoutMs ? AbortSignal.timeout(options.timeoutMs) : undefined
2561
2995
  });
2562
2996
  } catch (error) {
@@ -3061,32 +3495,32 @@ import {
3061
3495
  writeFileSync
3062
3496
  } from "fs";
3063
3497
  import { dirname as dirname4 } from "path";
3064
- import { z as z9 } from "zod";
3498
+ import { z as z10 } from "zod";
3065
3499
  function credentialsPathFor(env) {
3066
3500
  return credentialsPath(env);
3067
3501
  }
3068
- var LoginSchema = z9.object({
3069
- token: z9.string(),
3070
- cliTokenId: z9.number(),
3071
- device: z9.string(),
3072
- email: z9.string().optional(),
3073
- expiresAt: z9.number().optional()
3502
+ var LoginSchema = z10.object({
3503
+ token: z10.string(),
3504
+ cliTokenId: z10.number(),
3505
+ device: z10.string(),
3506
+ email: z10.string().optional(),
3507
+ expiresAt: z10.number().optional()
3074
3508
  });
3075
- var RunnerCredentialSchema = z9.object({
3076
- runnerId: z9.number(),
3077
- projectId: z9.number(),
3078
- name: z9.string(),
3079
- token: z9.string()
3509
+ var RunnerCredentialSchema = z10.object({
3510
+ runnerId: z10.number(),
3511
+ projectId: z10.number(),
3512
+ name: z10.string(),
3513
+ token: z10.string()
3080
3514
  });
3081
- var ServerEntrySchema = z9.object({
3515
+ var ServerEntrySchema = z10.object({
3082
3516
  login: LoginSchema.nullable().default(null),
3083
- project: z9.number().nullable().default(null),
3517
+ project: z10.number().nullable().default(null),
3084
3518
  runner: RunnerCredentialSchema.nullable().default(null)
3085
3519
  });
3086
- var CredentialsSchema = z9.object({
3087
- version: z9.literal(1),
3088
- current: z9.string().nullable().default(null),
3089
- servers: z9.record(z9.string(), ServerEntrySchema).default({})
3520
+ var CredentialsSchema = z10.object({
3521
+ version: z10.literal(1),
3522
+ current: z10.string().nullable().default(null),
3523
+ servers: z10.record(z10.string(), ServerEntrySchema).default({})
3090
3524
  });
3091
3525
  function emptyCredentials() {
3092
3526
  return { version: 1, current: null, servers: {} };
@@ -3602,6 +4036,389 @@ import { join as join10 } from "path";
3602
4036
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
3603
4037
  import { join as join8 } from "path";
3604
4038
 
4039
+ // src/telemetry/acp.ts
4040
+ import { isAbsolute as isAbsolute4, relative as relative3 } from "path";
4041
+ class AcpTelemetry {
4042
+ turn;
4043
+ cwd;
4044
+ tools = new Map;
4045
+ spawn = null;
4046
+ prompt = null;
4047
+ chunk = null;
4048
+ promptSpanId = newSpanId();
4049
+ constructor(turn, cwd) {
4050
+ this.turn = turn;
4051
+ this.cwd = cwd;
4052
+ }
4053
+ get traceparent() {
4054
+ return this.turn?.traceparentFor(this.promptSpanId) ?? null;
4055
+ }
4056
+ rel(path) {
4057
+ if (!isAbsolute4(path))
4058
+ return path;
4059
+ const r = relative3(this.cwd, path);
4060
+ return r.startsWith("..") ? path : r || ".";
4061
+ }
4062
+ begin(name, attrs = {}) {
4063
+ return this.turn?.span(name, attrs, this.prompt) ?? null;
4064
+ }
4065
+ async around(name, attrs, fn) {
4066
+ const span = this.begin(name, attrs);
4067
+ try {
4068
+ return await fn(span);
4069
+ } catch (error) {
4070
+ span?.fail(error instanceof Error ? error.message : String(error));
4071
+ throw error;
4072
+ } finally {
4073
+ span?.end();
4074
+ }
4075
+ }
4076
+ spawned(attrs, command) {
4077
+ this.spawn = this.begin("agent.spawn", attrs);
4078
+ this.spawn?.content("agent.command", command);
4079
+ }
4080
+ initialized(attrs) {
4081
+ this.spawn?.set(attrs);
4082
+ }
4083
+ exited(exitCode) {
4084
+ this.spawn?.end({ "process.exit.code": exitCode ?? null });
4085
+ }
4086
+ promptStarted(attrs, text) {
4087
+ if (!this.turn)
4088
+ return;
4089
+ this.prompt = this.turn.span("acp.prompt", { "gen_ai.operation.name": "invoke_agent", ...attrs }, null, "client", this.promptSpanId);
4090
+ this.prompt.content("prompt", text);
4091
+ this.turn.promptSpan = this.prompt;
4092
+ }
4093
+ promptEnded(attrs, error) {
4094
+ this.flushChunk();
4095
+ for (const [, span] of this.tools)
4096
+ span.end({ "hypotenuse.tool.finished": false });
4097
+ this.tools.clear();
4098
+ if (error)
4099
+ this.prompt?.fail(error);
4100
+ this.prompt?.end(attrs);
4101
+ if (this.turn)
4102
+ this.turn.promptSpan = null;
4103
+ const input = attrs["gen_ai.usage.input_tokens"];
4104
+ const output = attrs["gen_ai.usage.output_tokens"];
4105
+ if (typeof input === "number" || typeof output === "number")
4106
+ this.turn?.usage({
4107
+ inputTokens: typeof input === "number" ? input : undefined,
4108
+ outputTokens: typeof output === "number" ? output : undefined
4109
+ });
4110
+ }
4111
+ flushChunk() {
4112
+ const chunk = this.chunk;
4113
+ this.chunk = null;
4114
+ if (!chunk || !this.prompt || !this.turn)
4115
+ return;
4116
+ this.prompt.event(`agent.${chunk.kind}`, this.turn.contentAttrs("text", chunk.text));
4117
+ }
4118
+ update(update) {
4119
+ if (!this.turn || !this.prompt)
4120
+ return;
4121
+ const u = update;
4122
+ const kind = String(u.sessionUpdate);
4123
+ if (kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
4124
+ const content = u.content;
4125
+ if (content?.type !== "text")
4126
+ return;
4127
+ const which = kind === "agent_thought_chunk" ? "thought" : "message";
4128
+ if (this.chunk && this.chunk.kind !== which)
4129
+ this.flushChunk();
4130
+ this.chunk ??= { kind: which, text: "" };
4131
+ this.chunk.text += content.text ?? "";
4132
+ return;
4133
+ }
4134
+ this.flushChunk();
4135
+ switch (kind) {
4136
+ case "tool_call": {
4137
+ const id = String(u.toolCallId);
4138
+ const span = this.turn.span("tool_call", {
4139
+ "gen_ai.operation.name": "execute_tool",
4140
+ "gen_ai.tool.call.id": id,
4141
+ "gen_ai.tool.type": typeof u.kind === "string" ? u.kind : null,
4142
+ "hypotenuse.tool.status": typeof u.status === "string" ? u.status : "pending"
4143
+ }, this.prompt);
4144
+ span.content("tool.title", typeof u.title === "string" ? u.title : null);
4145
+ this.toolDetail(span, u);
4146
+ this.tools.set(id, span);
4147
+ if (u.status === "completed" || u.status === "failed")
4148
+ this.endTool(id, u);
4149
+ return;
4150
+ }
4151
+ case "tool_call_update": {
4152
+ const id = String(u.toolCallId);
4153
+ const span = this.tools.get(id);
4154
+ if (!span)
4155
+ return;
4156
+ if (typeof u.kind === "string")
4157
+ span.set({ "gen_ai.tool.type": u.kind });
4158
+ if (typeof u.title === "string")
4159
+ span.content("tool.title", u.title);
4160
+ this.toolDetail(span, u);
4161
+ if (u.status === "completed" || u.status === "failed")
4162
+ this.endTool(id, u);
4163
+ return;
4164
+ }
4165
+ case "plan": {
4166
+ const entries = u.entries ?? [];
4167
+ this.prompt.event("plan", {
4168
+ "hypotenuse.plan.entries": entries.length,
4169
+ "hypotenuse.plan.completed": entries.filter((e) => e.status === "completed").length,
4170
+ ...this.turn.contentAttrs("plan", JSON.stringify(entries))
4171
+ });
4172
+ return;
4173
+ }
4174
+ case "usage_update": {
4175
+ const cost = u.cost;
4176
+ this.prompt.event("usage", {
4177
+ "hypotenuse.context.used": typeof u.used === "number" ? u.used : null,
4178
+ "hypotenuse.context.size": typeof u.size === "number" ? u.size : null,
4179
+ "hypotenuse.usage.cost": cost?.amount ?? null,
4180
+ "hypotenuse.usage.currency": cost?.currency ?? null
4181
+ });
4182
+ return;
4183
+ }
4184
+ case "current_mode_update":
4185
+ this.prompt.event("mode", {
4186
+ "hypotenuse.mode": typeof u.currentModeId === "string" ? u.currentModeId : null
4187
+ });
4188
+ return;
4189
+ default:
4190
+ this.prompt.event("session_update", { "hypotenuse.update.kind": kind });
4191
+ }
4192
+ }
4193
+ toolDetail(span, u) {
4194
+ const locations = Array.isArray(u.locations) ? u.locations : null;
4195
+ if (locations?.length)
4196
+ span.set({
4197
+ "hypotenuse.tool.locations": locations.map((l) => l.path ? this.rel(l.path) : "").filter(Boolean).join(",")
4198
+ });
4199
+ if (u.rawInput !== undefined)
4200
+ span.content("tool.input", JSON.stringify(u.rawInput));
4201
+ if (u.rawOutput !== undefined)
4202
+ span.content("tool.output", JSON.stringify(u.rawOutput));
4203
+ else if (Array.isArray(u.content) && u.content.length)
4204
+ span.content("tool.output", JSON.stringify(u.content));
4205
+ }
4206
+ endTool(id, u) {
4207
+ const span = this.tools.get(id);
4208
+ this.tools.delete(id);
4209
+ if (!span)
4210
+ return;
4211
+ if (u.status === "failed")
4212
+ span.fail("the tool call failed");
4213
+ span.end({ "hypotenuse.tool.status": String(u.status) });
4214
+ }
4215
+ permission(toolCall, policy, options) {
4216
+ const parent = this.tools.get(toolCall.toolCallId) ?? this.prompt;
4217
+ const span = this.turn?.span("permission.wait", {
4218
+ "gen_ai.tool.call.id": toolCall.toolCallId,
4219
+ "gen_ai.tool.type": toolCall.kind ?? null,
4220
+ "hypotenuse.permission.policy": policy,
4221
+ "hypotenuse.permission.options": options.map((o) => o.kind ?? o.optionId).join(",")
4222
+ }, parent) ?? null;
4223
+ span?.content("tool.title", toolCall.title ?? null);
4224
+ return (answer) => span?.end({
4225
+ "hypotenuse.permission.outcome": answer.outcome,
4226
+ "hypotenuse.permission.option": answer.optionId ?? null,
4227
+ "hypotenuse.permission.decided_by": answer.decidedBy
4228
+ });
4229
+ }
4230
+ fs(op, path) {
4231
+ const span = this.begin(`fs.${op}`, { "file.path": this.rel(path) });
4232
+ return (result) => {
4233
+ if (result.refused)
4234
+ span?.fail(result.refused);
4235
+ span?.end({
4236
+ "hypotenuse.fs.bytes": result.bytes ?? null,
4237
+ "hypotenuse.fs.refused": result.refused ?? null
4238
+ });
4239
+ };
4240
+ }
4241
+ frame(line, message) {
4242
+ this.turn?.frame(line, message, this.prompt);
4243
+ }
4244
+ stderr(line) {
4245
+ this.turn?.log({
4246
+ body: line,
4247
+ level: "content",
4248
+ eventName: "agent.stderr",
4249
+ severity: "warn",
4250
+ span: this.spawn
4251
+ });
4252
+ }
4253
+ }
4254
+
4255
+ // src/telemetry/native.ts
4256
+ var NATIVE_TELEMETRY_PRESETS = {
4257
+ claude: "native",
4258
+ codex: "native",
4259
+ gemini: "native",
4260
+ qwen: "native",
4261
+ goose: "standard-otel",
4262
+ opencode: "standard-otel"
4263
+ };
4264
+ function nativeTelemetrySupport(preset) {
4265
+ return preset && NATIVE_TELEMETRY_PRESETS[preset] || "unknown";
4266
+ }
4267
+ function dropInheritedOtel(env) {
4268
+ for (const key of Object.keys(env))
4269
+ if (key.startsWith("OTEL_"))
4270
+ delete env[key];
4271
+ }
4272
+ function nativeTelemetryLaunch(preset, target) {
4273
+ const full = target.level === "full";
4274
+ const env = {
4275
+ HYPOTENUSE_OTLP_ENDPOINT: target.endpoint,
4276
+ OTEL_EXPORTER_OTLP_ENDPOINT: target.endpoint,
4277
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
4278
+ OTEL_TRACES_EXPORTER: "otlp",
4279
+ OTEL_METRICS_EXPORTER: "otlp",
4280
+ OTEL_LOGS_EXPORTER: "otlp",
4281
+ OTEL_METRIC_EXPORT_INTERVAL: "5000",
4282
+ OTEL_LOGS_EXPORT_INTERVAL: "2000",
4283
+ OTEL_BSP_SCHEDULE_DELAY: "1000"
4284
+ };
4285
+ const args = [];
4286
+ switch (preset) {
4287
+ case "claude":
4288
+ env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
4289
+ env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA = "1";
4290
+ if (full) {
4291
+ env.OTEL_LOG_USER_PROMPTS = "1";
4292
+ env.OTEL_LOG_TOOL_DETAILS = "1";
4293
+ }
4294
+ break;
4295
+ case "gemini":
4296
+ case "qwen": {
4297
+ const prefix = preset === "gemini" ? "GEMINI" : "QWEN";
4298
+ env[`${prefix}_TELEMETRY_ENABLED`] = "true";
4299
+ env[`${prefix}_TELEMETRY_TARGET`] = "local";
4300
+ env[`${prefix}_TELEMETRY_OTLP_ENDPOINT`] = target.endpoint;
4301
+ env[`${prefix}_TELEMETRY_OTLP_PROTOCOL`] = "http";
4302
+ env[`${prefix}_TELEMETRY_LOG_PROMPTS`] = full ? "true" : "false";
4303
+ break;
4304
+ }
4305
+ case "codex":
4306
+ args.push("-c", `otel.exporter={otlp-http={endpoint="${target.endpoint}",protocol="json"}}`, "-c", `otel.log_user_prompt=${full ? "true" : "false"}`);
4307
+ break;
4308
+ }
4309
+ return { env, args };
4310
+ }
4311
+
4312
+ // src/telemetry/receiver.ts
4313
+ var MAX_BODY_BYTES = 8 * 1024 * 1024;
4314
+ var DRAIN_MS = 1500;
4315
+
4316
+ class NativeReceiver {
4317
+ turn;
4318
+ nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(18))).toString("base64url");
4319
+ server = null;
4320
+ received = {
4321
+ traces: 0,
4322
+ metrics: 0,
4323
+ logs: 0
4324
+ };
4325
+ lastReceivedAt = 0;
4326
+ constructor(turn) {
4327
+ this.turn = turn;
4328
+ }
4329
+ start() {
4330
+ this.server = Bun.serve({
4331
+ hostname: "127.0.0.1",
4332
+ port: 0,
4333
+ maxRequestBodySize: MAX_BODY_BYTES,
4334
+ fetch: (req) => this.handle(req)
4335
+ });
4336
+ return this.endpoint;
4337
+ }
4338
+ get endpoint() {
4339
+ return `http://127.0.0.1:${this.server?.port ?? 0}/${this.nonce}`;
4340
+ }
4341
+ async handle(req) {
4342
+ const url = new URL(req.url);
4343
+ const prefix = `/${this.nonce}/`;
4344
+ const signal = signalOfPath(url.pathname);
4345
+ if (req.method !== "POST" || !url.pathname.startsWith(prefix) || !signal)
4346
+ return new Response(null, { status: 404 });
4347
+ const contentType = req.headers.get("content-type");
4348
+ try {
4349
+ let bytes = new Uint8Array(await req.arrayBuffer());
4350
+ if ((req.headers.get("content-encoding") ?? "").includes("gzip"))
4351
+ bytes = new Uint8Array(Bun.gunzipSync(bytes));
4352
+ if (bytes.byteLength > MAX_BODY_BYTES)
4353
+ throw new Error("too large");
4354
+ this.accept(signal, decodeOtlpBody(signal, bytes, contentType));
4355
+ } catch {
4356
+ return new Response(null, { status: 400 });
4357
+ }
4358
+ const json = (contentType ?? "").includes("json");
4359
+ return new Response(json ? "{}" : new Uint8Array(0), {
4360
+ status: 200,
4361
+ headers: {
4362
+ "Content-Type": json ? "application/json" : "application/x-protobuf"
4363
+ }
4364
+ });
4365
+ }
4366
+ accept(signal, request) {
4367
+ this.received[signal]++;
4368
+ this.lastReceivedAt = Date.now();
4369
+ const turn = this.turn;
4370
+ const level = {
4371
+ key: LEVEL_ATTRIBUTE,
4372
+ value: { stringValue: turn.level }
4373
+ };
4374
+ const tag = (attrs) => [
4375
+ ...(attrs ?? []).filter((a) => a.key !== LEVEL_ATTRIBUTE),
4376
+ level
4377
+ ];
4378
+ if (signal === "traces") {
4379
+ if (!turn.sampled)
4380
+ return;
4381
+ for (const rs of request.resourceSpans ?? [])
4382
+ for (const ss of rs.scopeSpans ?? []) {
4383
+ ss.spans = (ss.spans ?? []).map((s) => {
4384
+ const root = !s.parentSpanId || /^0+$/.test(s.parentSpanId);
4385
+ return {
4386
+ ...s,
4387
+ traceId: turn.traceId,
4388
+ ...root ? { parentSpanId: turn.turnSpan.spanId } : {},
4389
+ attributes: tag(s.attributes)
4390
+ };
4391
+ });
4392
+ }
4393
+ } else if (signal === "logs") {
4394
+ if (!turn.sampled)
4395
+ return;
4396
+ for (const rl of request.resourceLogs ?? [])
4397
+ for (const sl of rl.scopeLogs ?? [])
4398
+ sl.logRecords = (sl.logRecords ?? []).map((l) => ({
4399
+ ...l,
4400
+ traceId: turn.traceId,
4401
+ ...l.traceId && l.traceId !== turn.traceId ? { spanId: turn.turnSpan.spanId } : {},
4402
+ attributes: tag(l.attributes)
4403
+ }));
4404
+ }
4405
+ stampResource(signal, request, [
4406
+ ...turn.resourceAttributes.filter((a) => a.key !== "hypotenuse.source" && !a.key.startsWith("service.")),
4407
+ ...attributes({ "hypotenuse.source": "agent_native" })
4408
+ ]);
4409
+ turn.enqueue(signal, request);
4410
+ }
4411
+ async close(drainMs = DRAIN_MS) {
4412
+ if (!this.server)
4413
+ return;
4414
+ const quietFor = Date.now() - this.lastReceivedAt;
4415
+ if (quietFor < drainMs)
4416
+ await Bun.sleep(drainMs - quietFor);
4417
+ this.server.stop(true);
4418
+ this.server = null;
4419
+ }
4420
+ }
4421
+
3605
4422
  // src/worktree/co-author.ts
3606
4423
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
3607
4424
  import { join as join5 } from "path";
@@ -4620,7 +5437,7 @@ import {
4620
5437
  rmSync as rmSync3,
4621
5438
  writeFileSync as writeFileSync4
4622
5439
  } from "fs";
4623
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join7, resolve as resolve2, sep } from "path";
5440
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
4624
5441
 
4625
5442
  class OutsideWorktree extends Error {
4626
5443
  path;
@@ -4679,7 +5496,7 @@ class ConfinedFs {
4679
5496
  const root = this.roots[0];
4680
5497
  if (!root)
4681
5498
  throw new OutsideWorktree(path);
4682
- return isAbsolute4(path) ? resolve2(path) : resolve2(root, path);
5499
+ return isAbsolute5(path) ? resolve2(path) : resolve2(root, path);
4683
5500
  }
4684
5501
  read(path, line, limit) {
4685
5502
  const real = this.resolveExisting(path);
@@ -4973,7 +5790,7 @@ function callText(call) {
4973
5790
  }
4974
5791
 
4975
5792
  // src/acp/mcp-servers.ts
4976
- import { isAbsolute as isAbsolute5 } from "path";
5793
+ import { isAbsolute as isAbsolute6 } from "path";
4977
5794
  var HYPOTENUSE_SERVER_NAME = "hypotenuse";
4978
5795
  var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
4979
5796
 
@@ -5023,7 +5840,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
5023
5840
  const [command, ...args] = s.command;
5024
5841
  servers.push({
5025
5842
  name: s.name,
5026
- command: isAbsolute5(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
5843
+ command: isAbsolute6(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
5027
5844
  args,
5028
5845
  env: pairs(s.name, s.env, env)
5029
5846
  });
@@ -5473,6 +6290,8 @@ class Turn {
5473
6290
  where;
5474
6291
  readOnly;
5475
6292
  agentNote;
6293
+ otel;
6294
+ receiver = null;
5476
6295
  constructor(ctx, options) {
5477
6296
  this.ctx = ctx;
5478
6297
  this.options = options;
@@ -5498,6 +6317,7 @@ class Turn {
5498
6317
  }
5499
6318
  };
5500
6319
  this.mapper = new TurnMapper(this.mapperOptions);
6320
+ this.otel = new AcpTelemetry(ctx.telemetry ?? null, this.cwd);
5501
6321
  this.questions = new QuestionQueue({
5502
6322
  post: (event) => this.emit(event),
5503
6323
  waiting: (waiting) => {
@@ -5609,6 +6429,7 @@ class Turn {
5609
6429
  throw error;
5610
6430
  } finally {
5611
6431
  clearInterval(timer);
6432
+ this.otel.promptEnded({ "hypotenuse.end_reason": this.endReason }, this.endReason && this.endReason !== "stop" ? this.endReason : null);
5612
6433
  await this.shutdown();
5613
6434
  await this.workspace?.release().catch((error) => ctx.log.warn(`releasing the workspace failed: ${errorMessage(error)}`, {
5614
6435
  session: ctx.assignment.agentSessionId
@@ -5619,10 +6440,28 @@ class Turn {
5619
6440
  const { ctx } = this;
5620
6441
  const { assignment } = ctx;
5621
6442
  if (ctx.prepareWorkspace) {
6443
+ const prepare = ctx.prepareWorkspace;
5622
6444
  try {
5623
- this.workspace = await ctx.prepareWorkspace({
5624
- output: (line) => this.log.note(line),
5625
- env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
6445
+ this.workspace = await this.otel.around("worktree.prepare", {
6446
+ "hypotenuse.repository": assignment.repository?.remote ?? null,
6447
+ "hypotenuse.work_item.fresh": assignment.fresh
6448
+ }, async (span) => {
6449
+ const workspace = await prepare({
6450
+ output: (line) => {
6451
+ this.log.note(line);
6452
+ this.otel.turn?.log({
6453
+ body: line,
6454
+ eventName: "worktree.output",
6455
+ span
6456
+ });
6457
+ },
6458
+ env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
6459
+ });
6460
+ span?.set({
6461
+ "hypotenuse.worktree.mode": workspace.mode,
6462
+ "vcs.ref.head.name": workspace.branch
6463
+ });
6464
+ return workspace;
5626
6465
  });
5627
6466
  } catch (error) {
5628
6467
  const remote = ctx.assignment.repository?.remote ?? null;
@@ -5656,6 +6495,7 @@ class Turn {
5656
6495
  if (this.endReason)
5657
6496
  throw new TurnFailure("stopped");
5658
6497
  this.mapperOptions.cwd = this.cwd;
6498
+ this.otel.cwd = this.cwd;
5659
6499
  const remote = this.workspace?.repository?.remote ?? null;
5660
6500
  if (remote) {
5661
6501
  this.mapperOptions.pullRequests = new PrDetector(remote, this.workspace?.repositoryConfig?.pr_url_patterns ?? [], ctx.pullRequests ?? []);
@@ -5684,13 +6524,17 @@ class Turn {
5684
6524
  if (coAuthor && this.workspace?.repository) {
5685
6525
  Object.assign(launch.env, coAuthorEnvironment(launch.env, installCoAuthorHooks(ctx.stateDir), coAuthor));
5686
6526
  }
6527
+ this.withTelemetry(launch);
5687
6528
  this.log.note(`spawn ${launch.command.join(" ")} in ${this.cwd}`);
5688
6529
  try {
5689
6530
  this.conn = AcpConnection.spawn({
5690
6531
  command: launch.command,
5691
6532
  cwd: this.cwd,
5692
6533
  env: launch.env,
5693
- stderr: (line) => this.log.stderr(line),
6534
+ stderr: (line) => {
6535
+ this.log.stderr(line);
6536
+ this.otel.stderr(line);
6537
+ },
5694
6538
  noise: (line) => this.noise(line),
5695
6539
  trace: (line, message) => {
5696
6540
  if (line.startsWith("<-")) {
@@ -5698,37 +6542,45 @@ class Turn {
5698
6542
  this.noiseInARow = 0;
5699
6543
  }
5700
6544
  this.log.rpc(line, message);
6545
+ this.otel.frame(line, message);
5701
6546
  },
5702
6547
  handlers: {
5703
6548
  update: (n) => {
5704
6549
  if (this.acpSessionId && n.sessionId !== this.acpSessionId)
5705
6550
  return;
6551
+ this.otel.update(n.update);
5706
6552
  this.emitAll(this.mapper.update(n.update));
5707
6553
  },
5708
6554
  requestPermission: (params) => this.permission(params),
5709
6555
  readTextFile: async (params) => {
6556
+ const done = this.otel.fs("read", params.path);
5710
6557
  try {
5711
- return {
5712
- content: this.fs.read(params.path, params.line, params.limit)
5713
- };
6558
+ const content = this.fs.read(params.path, params.line, params.limit);
6559
+ done({ bytes: Buffer.byteLength(content) });
6560
+ return { content };
5714
6561
  } catch (error) {
6562
+ done({ refused: errorMessage(error) });
5715
6563
  throw this.fsError(error);
5716
6564
  }
5717
6565
  },
5718
6566
  writeTextFile: async (params) => {
6567
+ const done = this.otel.fs("write", params.path);
5719
6568
  if (this.readOnly) {
5720
6569
  this.log.note(`refused a write (read-only): ${params.path}`);
6570
+ done({ refused: "read-only" });
5721
6571
  throw new AcpError(RESOURCE_REFUSED, READ_ONLY_MESSAGE, {
5722
6572
  path: params.path
5723
6573
  });
5724
6574
  }
5725
6575
  try {
5726
6576
  this.fs.write(params.path, params.content);
6577
+ done({ bytes: Buffer.byteLength(params.content) });
5727
6578
  } catch (error) {
6579
+ done({ refused: errorMessage(error) });
5728
6580
  throw this.fsError(error);
5729
6581
  }
5730
6582
  },
5731
- createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId))),
6583
+ createElicitation: (params) => this.otel.around("elicitation.wait", {}, () => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId)))),
5732
6584
  unknown: (method) => this.log.note(`unhandled ${method}`)
5733
6585
  }
5734
6586
  });
@@ -5739,7 +6591,20 @@ class Turn {
5739
6591
  const conn = this.conn;
5740
6592
  ctx.setPid(conn.pid);
5741
6593
  ctx.setState("starting");
5742
- const init = await this.guard(conn.initialize(package_default.version), launch.startupTimeoutMs, `${this.agentId} did not answer initialize within ${launch.startupTimeoutMs / 1000}s (\`${launch.command.join(" ")}\`).`, true);
6594
+ this.otel.spawned({
6595
+ "process.pid": conn.pid,
6596
+ "hypotenuse.agent.id": this.agentId,
6597
+ "hypotenuse.agent.preset": launch.preset ?? "custom",
6598
+ "hypotenuse.telemetry.native": this.receiver !== null
6599
+ }, launch.command.join(" "));
6600
+ const init = await this.otel.around("acp.initialize", { "rpc.method": "initialize" }, () => this.guard(conn.initialize(package_default.version), launch.startupTimeoutMs, `${this.agentId} did not answer initialize within ${launch.startupTimeoutMs / 1000}s (\`${launch.command.join(" ")}\`).`, true));
6601
+ this.otel.initialized({
6602
+ "gen_ai.agent.name": init.agentInfo?.name ?? this.agentId,
6603
+ "hypotenuse.agent.version": init.agentInfo?.version ?? null,
6604
+ "hypotenuse.acp.protocol_version": init.protocolVersion,
6605
+ "hypotenuse.acp.load_session": Boolean(init.agentCapabilities?.loadSession),
6606
+ "hypotenuse.acp.auth_methods": (init.authMethods ?? []).length
6607
+ });
5743
6608
  this.agentTitle = init.agentInfo?.title ?? init.agentInfo?.name ?? this.agentId;
5744
6609
  this.agentVersion = init.agentInfo?.version ?? null;
5745
6610
  ctx.log.debug(`${this.agentId} initialized: ${init.agentInfo?.name ?? "?"} ${init.agentInfo?.version ?? ""}`, { session: assignment.agentSessionId });
@@ -5795,7 +6660,7 @@ class Turn {
5795
6660
  if (!method)
5796
6661
  throw new AuthRequired(this.loginNeed(), "not logged in");
5797
6662
  this.log.note(`authenticate ${method}`);
5798
- await this.guard(this.conn.authenticate(method), 0, "");
6663
+ await this.otel.around("acp.authenticate", { "hypotenuse.acp.auth_method": method }, () => this.guard(this.conn.authenticate(method), 0, ""));
5799
6664
  authenticated = true;
5800
6665
  }
5801
6666
  }
@@ -5823,12 +6688,12 @@ class Turn {
5823
6688
  if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
5824
6689
  try {
5825
6690
  if (caps.sessionCapabilities?.resume) {
5826
- const answer = await this.guard(conn.resumeSession({
6691
+ const answer = await this.otel.around("acp.session.resume", {}, () => this.guard(conn.resumeSession({
5827
6692
  sessionId: previousAcpSessionId,
5828
6693
  cwd: this.cwd,
5829
6694
  mcpServers,
5830
6695
  ...directories
5831
- }), 0, "");
6696
+ }), 0, ""));
5832
6697
  this.acpSessionId = previousAcpSessionId;
5833
6698
  this.resumed = true;
5834
6699
  await this.applyMode(answer);
@@ -5838,12 +6703,12 @@ class Turn {
5838
6703
  this.acpSessionId = previousAcpSessionId;
5839
6704
  this.mapper.replaying = true;
5840
6705
  try {
5841
- const answer = await this.guard(conn.loadSession({
6706
+ const answer = await this.otel.around("acp.session.load", {}, () => this.guard(conn.loadSession({
5842
6707
  sessionId: previousAcpSessionId,
5843
6708
  cwd: this.cwd,
5844
6709
  mcpServers,
5845
6710
  ...directories
5846
- }), 0, "");
6711
+ }), 0, ""));
5847
6712
  this.resumed = true;
5848
6713
  await this.applyMode(answer);
5849
6714
  return;
@@ -5859,7 +6724,7 @@ class Turn {
5859
6724
  this.ctx.log.info(`could not resume the agent's session; starting a new one with the history in the prompt: ${errorMessage(error)}`, { session: assignment.agentSessionId });
5860
6725
  }
5861
6726
  }
5862
- const created = await this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, "");
6727
+ const created = await this.otel.around("acp.session.new", { "hypotenuse.mcp.servers": mcpServers.length }, () => this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, ""));
5863
6728
  this.acpSessionId = created.sessionId;
5864
6729
  await this.applyMode(created);
5865
6730
  }
@@ -5931,6 +6796,17 @@ class Turn {
5931
6796
  }, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
5932
6797
  this.promptStartedAt = this.now();
5933
6798
  this.lastTraffic = this.now();
6799
+ const promptText = blocks.map((b) => b.text ?? "").join(`
6800
+
6801
+ `);
6802
+ this.otel.promptStarted({
6803
+ "hypotenuse.prompt.blocks": blocks.length,
6804
+ "hypotenuse.prompt.bytes": Buffer.byteLength(promptText),
6805
+ "hypotenuse.prompt.history": ctx.assignment.history.length,
6806
+ "hypotenuse.prompt.guidance": ctx.assignment.guidance.length,
6807
+ "hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
6808
+ "hypotenuse.acp.resumed": this.resumed
6809
+ }, promptText);
5934
6810
  const request = conn.prompt(sessionId, blocks);
5935
6811
  this.promptSettled = request.then(() => {}, () => {});
5936
6812
  const answer = await this.guard(request, 0, "").catch((error) => {
@@ -5938,6 +6814,12 @@ class Turn {
5938
6814
  throw error;
5939
6815
  throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
5940
6816
  });
6817
+ const usage = answer.usage;
6818
+ this.otel.promptEnded({
6819
+ "gen_ai.response.finish_reasons": answer.stopReason,
6820
+ "gen_ai.usage.input_tokens": usage?.inputTokens,
6821
+ "gen_ai.usage.output_tokens": usage?.outputTokens
6822
+ }, answer.stopReason === "end_turn" || answer.stopReason === "cancelled" ? null : `stopReason: ${answer.stopReason}`);
5941
6823
  if (this.endReason)
5942
6824
  return await this.stopped();
5943
6825
  this.emit({ type: "status", state: "finishing" });
@@ -6022,6 +6904,33 @@ class Turn {
6022
6904
  await within(conn.exited, 2000, null);
6023
6905
  }
6024
6906
  async shutdown() {
6907
+ await this.closeAgent();
6908
+ if (this.conn)
6909
+ this.otel.exited(await within(this.conn.exited, 500, null));
6910
+ await this.receiver?.close();
6911
+ this.receiver = null;
6912
+ }
6913
+ withTelemetry(launch) {
6914
+ const turn = this.ctx.telemetry;
6915
+ if (!turn)
6916
+ return;
6917
+ const traceparent = this.otel.traceparent;
6918
+ if (traceparent)
6919
+ launch.env.TRACEPARENT = traceparent;
6920
+ if (!turn.agentNative)
6921
+ return;
6922
+ this.receiver = new NativeReceiver(turn);
6923
+ const endpoint = this.receiver.start();
6924
+ turn.addSecret(this.receiver.nonce);
6925
+ dropInheritedOtel(launch.env);
6926
+ const native = nativeTelemetryLaunch(launch.preset, {
6927
+ endpoint,
6928
+ level: turn.level
6929
+ });
6930
+ Object.assign(launch.env, native.env);
6931
+ launch.command = [...launch.command, ...native.args];
6932
+ }
6933
+ async closeAgent() {
6025
6934
  const conn = this.conn;
6026
6935
  if (!conn)
6027
6936
  return;
@@ -6074,13 +6983,26 @@ class Turn {
6074
6983
  async permission(params) {
6075
6984
  const policy = this.role?.permission_policy ?? "allow";
6076
6985
  const decision = decide(policy, params.toolCall.kind);
6986
+ const waited = this.otel.permission(params.toolCall, typeof policy === "string" ? policy : JSON.stringify(policy), params.options);
6077
6987
  if (decision !== "ask") {
6078
6988
  const optionId = automaticOption(params.options, decision);
6079
6989
  this.log.note(`permission ${params.toolCall.toolCallId}: ${decision} \u2192 ${optionId ?? "cancelled"}`);
6990
+ waited({
6991
+ outcome: optionId ? "selected" : "cancelled",
6992
+ optionId: optionId ?? undefined,
6993
+ decidedBy: "policy"
6994
+ });
6080
6995
  return optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } };
6081
6996
  }
6082
6997
  this.emitAll(this.mapper.flushThought(true));
6083
- return this.questions.ask(permissionQuestion(params, this.where, this.cwd));
6998
+ const answer = await this.questions.ask(permissionQuestion(params, this.where, this.cwd));
6999
+ const outcome = answer.outcome;
7000
+ waited({
7001
+ outcome: outcome?.outcome ?? "cancelled",
7002
+ optionId: outcome?.optionId,
7003
+ decidedBy: "person"
7004
+ });
7005
+ return answer;
6084
7006
  }
6085
7007
  fsError(error) {
6086
7008
  if (error instanceof OutsideWorktree) {
@@ -6282,6 +7204,9 @@ class DaemonApi {
6282
7204
  "User-Agent": `hypotenuse/${package_default.version} (${platform()})`
6283
7205
  };
6284
7206
  }
7207
+ traced(traceparent) {
7208
+ return traceparent ? { ...this.headers, [TRACEPARENT_HEADER]: traceparent } : this.headers;
7209
+ }
6285
7210
  async heartbeat(body) {
6286
7211
  const answer = await this.client.request("POST", "/daemon/heartbeat", {
6287
7212
  body,
@@ -6309,26 +7234,41 @@ class DaemonApi {
6309
7234
  });
6310
7235
  return ClaimResponse.parse(answer).assignment;
6311
7236
  }
6312
- async events(body) {
7237
+ async events(body, traceparent) {
6313
7238
  const answer = await this.client.request("POST", "/daemon/events", {
6314
7239
  body,
6315
- headers: this.headers,
7240
+ headers: this.traced(traceparent),
6316
7241
  timeoutMs: REQUEST_TIMEOUT_MS
6317
7242
  });
6318
7243
  return EventsResponse.parse(answer);
6319
7244
  }
6320
- async complete(body) {
7245
+ async complete(body, traceparent) {
6321
7246
  const answer = await this.client.request("POST", "/daemon/complete", {
6322
7247
  body,
6323
- headers: this.headers,
7248
+ headers: this.traced(traceparent),
6324
7249
  timeoutMs: REQUEST_TIMEOUT_MS
6325
7250
  });
6326
7251
  return CompleteResponse.parse(answer);
6327
7252
  }
6328
- async tool(route, body) {
7253
+ async telemetry(batch) {
7254
+ const answer = await this.client.request("POST", "/daemon/telemetry", {
7255
+ rawBody: batch.gzipped,
7256
+ headers: {
7257
+ ...this.headers,
7258
+ "Content-Type": "application/json",
7259
+ "Content-Encoding": "gzip",
7260
+ [TELEMETRY_SIGNAL_HEADER]: batch.signal,
7261
+ [TELEMETRY_WORK_ITEM_HEADER]: String(batch.workItemId),
7262
+ [TRACEPARENT_HEADER]: batch.traceparent
7263
+ },
7264
+ timeoutMs: REQUEST_TIMEOUT_MS
7265
+ });
7266
+ return TelemetryResponse.parse(answer);
7267
+ }
7268
+ async tool(route, body, traceparent) {
6329
7269
  const answer = await this.client.request("POST", route, {
6330
7270
  body,
6331
- headers: this.headers,
7271
+ headers: this.traced(traceparent),
6332
7272
  timeoutMs: TOOL_REQUEST_TIMEOUT_MS
6333
7273
  });
6334
7274
  return ToolResult.parse(answer).result;
@@ -6953,11 +7893,575 @@ class StateDb {
6953
7893
 
6954
7894
  // src/daemon/supervisor.ts
6955
7895
  import { hostname as hostname3 } from "os";
6956
- import { isAbsolute as isAbsolute6 } from "path";
7896
+ import { isAbsolute as isAbsolute7 } from "path";
7897
+
7898
+ // src/telemetry/collector.ts
7899
+ var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
7900
+ var TOKEN_BOUNDS = [
7901
+ 100,
7902
+ 1000,
7903
+ 1e4,
7904
+ 50000,
7905
+ 1e5,
7906
+ 250000,
7907
+ 500000,
7908
+ 1e6
7909
+ ];
7910
+
7911
+ class SpanHandle {
7912
+ turn;
7913
+ name;
7914
+ parentSpanId;
7915
+ kind;
7916
+ spanId;
7917
+ startMs;
7918
+ attrs = {};
7919
+ events = [];
7920
+ ended = false;
7921
+ error = null;
7922
+ constructor(turn, name, parentSpanId, kind = "internal", attrs = {}, spanId = newSpanId()) {
7923
+ this.turn = turn;
7924
+ this.name = name;
7925
+ this.parentSpanId = parentSpanId;
7926
+ this.kind = kind;
7927
+ this.spanId = spanId;
7928
+ this.startMs = turn.now();
7929
+ Object.assign(this.attrs, attrs);
7930
+ }
7931
+ set(attrs) {
7932
+ Object.assign(this.attrs, attrs);
7933
+ return this;
7934
+ }
7935
+ content(key, text) {
7936
+ Object.assign(this.attrs, this.turn.contentAttrs(key, text));
7937
+ return this;
7938
+ }
7939
+ event(name, attrs = {}) {
7940
+ if (!this.ended)
7941
+ this.events?.push({
7942
+ name,
7943
+ timeUnixNano: unixNano(this.turn.now()),
7944
+ attributes: attributes(attrs)
7945
+ });
7946
+ return this;
7947
+ }
7948
+ fail(message) {
7949
+ this.error = message.slice(0, 1024);
7950
+ return this;
7951
+ }
7952
+ get traceparent() {
7953
+ return this.turn.traceparentFor(this.spanId);
7954
+ }
7955
+ get isEnded() {
7956
+ return this.ended;
7957
+ }
7958
+ end(attrs = {}) {
7959
+ if (this.ended)
7960
+ return;
7961
+ this.ended = true;
7962
+ Object.assign(this.attrs, attrs);
7963
+ this.turn.record({
7964
+ traceId: this.turn.traceId,
7965
+ spanId: this.spanId,
7966
+ parentSpanId: this.parentSpanId,
7967
+ name: this.name,
7968
+ kind: SPAN_KIND[this.kind],
7969
+ startTimeUnixNano: unixNano(this.startMs),
7970
+ endTimeUnixNano: unixNano(this.turn.now()),
7971
+ attributes: attributes(this.attrs),
7972
+ events: this.events,
7973
+ status: this.error ? { code: STATUS_CODE.error, message: this.error } : { code: STATUS_CODE.unset }
7974
+ });
7975
+ }
7976
+ }
7977
+
7978
+ class TurnTelemetry {
7979
+ options;
7980
+ traceId;
7981
+ rootSpanId;
7982
+ level;
7983
+ sampled;
7984
+ agentNative;
7985
+ now;
7986
+ turnSpan;
7987
+ resource;
7988
+ finished = false;
7989
+ extraSecrets = [];
7990
+ promptSpan = null;
7991
+ constructor(options) {
7992
+ this.options = options;
7993
+ const ctx = parseTraceparent(options.claim.traceparent);
7994
+ this.traceId = ctx?.traceId ?? "0".repeat(32);
7995
+ this.rootSpanId = ctx?.spanId ?? "0".repeat(16);
7996
+ this.level = options.claim.content;
7997
+ this.sampled = options.claim.sampled && ctx !== null;
7998
+ this.agentNative = options.claim.agentNative;
7999
+ this.now = options.now ?? Date.now;
8000
+ const a = options.assignment;
8001
+ this.resource = attributes({
8002
+ "service.name": "hypotenuse-daemon",
8003
+ "service.namespace": "hypotenuse",
8004
+ "service.version": package_default.version,
8005
+ "hypotenuse.source": "daemon",
8006
+ ...this.turnAttributes(),
8007
+ "hypotenuse.repository": a.repository?.remote ?? null
8008
+ });
8009
+ this.turnSpan = new SpanHandle(this, "daemon.turn", this.rootSpanId, "internal", {
8010
+ ...this.turnAttributes(),
8011
+ "hypotenuse.work_item.kind": a.kind,
8012
+ "hypotenuse.work_item.fresh": a.fresh,
8013
+ "hypotenuse.telemetry.content": this.level,
8014
+ "hypotenuse.telemetry.agent_native": this.agentNative
8015
+ });
8016
+ }
8017
+ turnAttributes() {
8018
+ const a = this.options.assignment;
8019
+ return {
8020
+ "hypotenuse.session.id": a.agentSessionId,
8021
+ "hypotenuse.work_item.id": a.workItemId,
8022
+ "hypotenuse.role": a.role,
8023
+ "hypotenuse.runner.name": this.options.runner.name,
8024
+ "hypotenuse.agent.id": this.options.agentId,
8025
+ "hypotenuse.agent.preset": this.options.preset ?? "custom",
8026
+ "hypotenuse.repository": a.repository?.remote ?? null,
8027
+ "linear.issue.identifier": a.issue.identifier,
8028
+ "linear.agent_session.id": a.externalSessionId,
8029
+ "linear.team.key": a.issue.teamKey
8030
+ };
8031
+ }
8032
+ addSecret(value) {
8033
+ this.extraSecrets.push(value);
8034
+ }
8035
+ get resourceAttributes() {
8036
+ return this.resource;
8037
+ }
8038
+ collects(level) {
8039
+ return levelRank(this.level) >= levelRank(level);
8040
+ }
8041
+ traceparentFor(spanId = this.turnSpan.spanId) {
8042
+ return formatTraceparent({
8043
+ traceId: this.traceId,
8044
+ spanId,
8045
+ sampled: this.sampled
8046
+ });
8047
+ }
8048
+ span(name, attrs = {}, parent = null, kind = "internal", spanId) {
8049
+ return new SpanHandle(this, name, (parent ?? this.turnSpan).spanId, kind, attrs, spanId);
8050
+ }
8051
+ contentAttrs(key, text) {
8052
+ if (text === null || text === undefined)
8053
+ return {};
8054
+ const out = {
8055
+ [`hypotenuse.${key}.bytes`]: Buffer.byteLength(text)
8056
+ };
8057
+ if (!this.collects("content"))
8058
+ return out;
8059
+ const { text: kept, originalBytes } = capContent(text);
8060
+ out[`${CONTENT_ATTRIBUTE_PREFIX}${key}`] = kept;
8061
+ if (originalBytes !== null)
8062
+ out[`${CONTENT_ATTRIBUTE_PREFIX}${key}${TRUNCATED_SUFFIX}`] = originalBytes;
8063
+ return out;
8064
+ }
8065
+ record(span) {
8066
+ if (!this.sampled || this.finished)
8067
+ return;
8068
+ this.enqueue("traces", {
8069
+ resourceSpans: [
8070
+ {
8071
+ resource: { attributes: this.resource },
8072
+ scopeSpans: [{ scope: SCOPE, spans: [span] }]
8073
+ }
8074
+ ]
8075
+ });
8076
+ }
8077
+ log(record) {
8078
+ const level = record.level ?? "content";
8079
+ if (!this.sampled || this.finished || !this.collects(level))
8080
+ return;
8081
+ const { text, originalBytes } = capContent(record.body);
8082
+ this.enqueue("logs", {
8083
+ resourceLogs: [
8084
+ {
8085
+ resource: { attributes: this.resource },
8086
+ scopeLogs: [
8087
+ {
8088
+ scope: SCOPE,
8089
+ logRecords: [
8090
+ {
8091
+ timeUnixNano: unixNano(this.now()),
8092
+ observedTimeUnixNano: unixNano(this.now()),
8093
+ severityNumber: SEVERITY[record.severity ?? "info"],
8094
+ severityText: (record.severity ?? "info").toUpperCase(),
8095
+ body: { stringValue: text },
8096
+ ...record.eventName ? { eventName: record.eventName } : {},
8097
+ attributes: attributes({
8098
+ ...record.attrs ?? {},
8099
+ [LEVEL_ATTRIBUTE]: level,
8100
+ ...originalBytes !== null ? {
8101
+ [`hypotenuse.body${TRUNCATED_SUFFIX}`]: originalBytes
8102
+ } : {}
8103
+ }),
8104
+ traceId: this.traceId,
8105
+ spanId: (record.span ?? this.turnSpan).spanId
8106
+ }
8107
+ ]
8108
+ }
8109
+ ]
8110
+ }
8111
+ ]
8112
+ });
8113
+ }
8114
+ frame(direction, message, span) {
8115
+ if (!this.collects("full"))
8116
+ return;
8117
+ const m = message;
8118
+ this.log({
8119
+ body: typeof message === "string" ? message : JSON.stringify(message),
8120
+ level: "full",
8121
+ eventName: "acp.frame",
8122
+ attrs: {
8123
+ "hypotenuse.acp.direction": direction.startsWith("<-") ? "in" : "out",
8124
+ "rpc.method": typeof m?.method === "string" ? m.method : null,
8125
+ "rpc.jsonrpc.request_id": typeof m?.id === "string" || typeof m?.id === "number" ? String(m.id) : null
8126
+ },
8127
+ span
8128
+ });
8129
+ }
8130
+ usage(usage) {
8131
+ if (this.finished)
8132
+ return;
8133
+ const points = [
8134
+ ["input", usage.inputTokens],
8135
+ ["output", usage.outputTokens]
8136
+ ].flatMap(([type, n]) => typeof n === "number" ? [
8137
+ {
8138
+ attributes: attributes({
8139
+ "gen_ai.token.type": type,
8140
+ "gen_ai.agent.name": this.options.agentId
8141
+ }),
8142
+ timeUnixNano: unixNano(this.now()),
8143
+ count: "1",
8144
+ sum: n,
8145
+ bucketCounts: [
8146
+ ...TOKEN_BOUNDS.map((b, i) => n <= b && (i === 0 || n > TOKEN_BOUNDS[i - 1]) ? "1" : "0"),
8147
+ n > TOKEN_BOUNDS.at(-1) ? "1" : "0"
8148
+ ],
8149
+ explicitBounds: TOKEN_BOUNDS,
8150
+ min: n,
8151
+ max: n
8152
+ }
8153
+ ] : []);
8154
+ if (points.length === 0)
8155
+ return;
8156
+ this.metric({
8157
+ name: "gen_ai.client.token.usage",
8158
+ unit: "{token}",
8159
+ histogram: {
8160
+ aggregationTemporality: TEMPORALITY.delta,
8161
+ dataPoints: points
8162
+ }
8163
+ });
8164
+ }
8165
+ slots(slots) {
8166
+ const at = unixNano(this.now());
8167
+ this.metric({
8168
+ name: "hypotenuse.runner.slots",
8169
+ unit: "{slot}",
8170
+ gauge: {
8171
+ dataPoints: [
8172
+ {
8173
+ attributes: attributes({ "hypotenuse.slot.state": "capacity" }),
8174
+ timeUnixNano: at,
8175
+ asInt: String(slots.capacity)
8176
+ },
8177
+ {
8178
+ attributes: attributes({ "hypotenuse.slot.state": "in_use" }),
8179
+ timeUnixNano: at,
8180
+ asInt: String(slots.inUse)
8181
+ }
8182
+ ]
8183
+ }
8184
+ });
8185
+ }
8186
+ metric(metric) {
8187
+ this.enqueue("metrics", {
8188
+ resourceMetrics: [
8189
+ {
8190
+ resource: { attributes: this.resource },
8191
+ scopeMetrics: [{ scope: SCOPE, metrics: [metric] }]
8192
+ }
8193
+ ]
8194
+ });
8195
+ }
8196
+ enqueue(signal, body) {
8197
+ const refused = this.options.queue.isRefused(this.options.assignment.workItemId);
8198
+ this.options.queue.add({
8199
+ workItemId: this.options.assignment.workItemId,
8200
+ traceparent: this.traceparentFor(),
8201
+ signal,
8202
+ body: refused ? body : redactOtlp(body, {
8203
+ values: [...this.options.secrets(), ...this.extraSecrets]
8204
+ })
8205
+ }, this.resource);
8206
+ }
8207
+ finish(outcome, error) {
8208
+ if (this.finished)
8209
+ return;
8210
+ if (error)
8211
+ this.turnSpan.fail(error);
8212
+ this.turnSpan.end({ "hypotenuse.outcome": outcome });
8213
+ this.finished = true;
8214
+ }
8215
+ }
8216
+
8217
+ // src/telemetry/queue.ts
8218
+ var FLUSH_INTERVAL_MS = 2000;
8219
+ var FLUSH_BYTES = 1024 * 1024;
8220
+
8221
+ class TelemetryQueue {
8222
+ sender;
8223
+ capacityBytes;
8224
+ log;
8225
+ now;
8226
+ batches = [];
8227
+ bytes = 0;
8228
+ timer = null;
8229
+ flushing = null;
8230
+ backoffUntil = 0;
8231
+ failures = 0;
8232
+ refused = new Set;
8233
+ losses = new Map;
8234
+ droppedTotal = new Map;
8235
+ constructor(sender, capacityBytes, log = () => {}, now = Date.now) {
8236
+ this.sender = sender;
8237
+ this.capacityBytes = capacityBytes;
8238
+ this.log = log;
8239
+ this.now = now;
8240
+ }
8241
+ start() {
8242
+ if (this.timer)
8243
+ return;
8244
+ this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
8245
+ this.timer.unref?.();
8246
+ }
8247
+ stop() {
8248
+ if (this.timer)
8249
+ clearInterval(this.timer);
8250
+ this.timer = null;
8251
+ }
8252
+ get pendingBytes() {
8253
+ return this.bytes;
8254
+ }
8255
+ isRefused(workItemId) {
8256
+ return this.refused.has(workItemId);
8257
+ }
8258
+ add(batch, resource = []) {
8259
+ if (this.refused.has(batch.workItemId)) {
8260
+ this.countLoss(batch, resource);
8261
+ return;
8262
+ }
8263
+ const bytes = JSON.stringify(batch.body).length;
8264
+ this.batches.push({ ...batch, bytes });
8265
+ this.bytes += bytes;
8266
+ if (!this.losses.has(batch.workItemId))
8267
+ this.losses.set(batch.workItemId, {
8268
+ spans: 0,
8269
+ logs: 0,
8270
+ resource,
8271
+ traceparent: batch.traceparent
8272
+ });
8273
+ this.evict();
8274
+ if (this.bytes >= FLUSH_BYTES)
8275
+ this.flush();
8276
+ }
8277
+ countLoss(batch, resource = []) {
8278
+ const loss = this.losses.get(batch.workItemId) ?? {
8279
+ spans: 0,
8280
+ logs: 0,
8281
+ resource,
8282
+ traceparent: batch.traceparent
8283
+ };
8284
+ const n = countRecords(batch.signal, batch.body);
8285
+ if (batch.signal === "traces")
8286
+ loss.spans += n;
8287
+ else if (batch.signal === "logs")
8288
+ loss.logs += n;
8289
+ this.losses.set(batch.workItemId, loss);
8290
+ this.droppedTotal.set(batch.workItemId, (this.droppedTotal.get(batch.workItemId) ?? 0) + n);
8291
+ }
8292
+ evict() {
8293
+ for (const signal of ["logs", "traces"]) {
8294
+ while (this.bytes > this.capacityBytes) {
8295
+ const index = this.batches.findIndex((b) => b.signal === signal);
8296
+ if (index === -1)
8297
+ break;
8298
+ const [gone] = this.batches.splice(index, 1);
8299
+ if (!gone)
8300
+ break;
8301
+ this.bytes -= gone.bytes;
8302
+ this.countLoss(gone);
8303
+ }
8304
+ }
8305
+ }
8306
+ reportLosses() {
8307
+ for (const [workItemId, loss] of this.losses) {
8308
+ if (this.refused.has(workItemId))
8309
+ continue;
8310
+ if (loss.spans === 0 && loss.logs === 0)
8311
+ continue;
8312
+ const at = unixNano(this.now());
8313
+ const point = (kind, n) => ({
8314
+ attributes: attributes({ "hypotenuse.telemetry.kind": kind }),
8315
+ timeUnixNano: at,
8316
+ asInt: String(n)
8317
+ });
8318
+ const metrics = {
8319
+ resourceMetrics: [
8320
+ {
8321
+ resource: { attributes: loss.resource },
8322
+ scopeMetrics: [
8323
+ {
8324
+ scope: { name: "hypotenuse.daemon" },
8325
+ metrics: [
8326
+ {
8327
+ name: "hypotenuse.telemetry.dropped",
8328
+ unit: "{record}",
8329
+ sum: {
8330
+ aggregationTemporality: TEMPORALITY.delta,
8331
+ isMonotonic: true,
8332
+ dataPoints: [
8333
+ ...loss.spans ? [point("span", loss.spans)] : [],
8334
+ ...loss.logs ? [point("log", loss.logs)] : []
8335
+ ]
8336
+ }
8337
+ }
8338
+ ]
8339
+ }
8340
+ ]
8341
+ }
8342
+ ]
8343
+ };
8344
+ loss.spans = 0;
8345
+ loss.logs = 0;
8346
+ const bytes = JSON.stringify(metrics).length;
8347
+ this.batches.push({
8348
+ workItemId,
8349
+ traceparent: loss.traceparent,
8350
+ signal: "metrics",
8351
+ body: metrics,
8352
+ bytes
8353
+ });
8354
+ this.bytes += bytes;
8355
+ }
8356
+ }
8357
+ flush() {
8358
+ this.flushing ??= this.flushOnce().finally(() => {
8359
+ this.flushing = null;
8360
+ });
8361
+ return this.flushing;
8362
+ }
8363
+ async flushOnce() {
8364
+ if (this.now() < this.backoffUntil)
8365
+ return;
8366
+ this.reportLosses();
8367
+ if (this.batches.length === 0)
8368
+ return;
8369
+ const taken = this.batches;
8370
+ this.batches = [];
8371
+ this.bytes = 0;
8372
+ const groups = new Map;
8373
+ for (const b of taken) {
8374
+ const key = `${b.workItemId}|${b.signal}`;
8375
+ groups.set(key, [...groups.get(key) ?? [], b]);
8376
+ }
8377
+ const retry = [];
8378
+ let failed = false;
8379
+ for (const group of groups.values()) {
8380
+ const first = group[0];
8381
+ if (this.refused.has(first.workItemId)) {
8382
+ for (const b of group)
8383
+ this.countLoss(b);
8384
+ continue;
8385
+ }
8386
+ if (failed) {
8387
+ retry.push(...group);
8388
+ continue;
8389
+ }
8390
+ const body = mergeRequests(first.signal, group.map((b) => b.body));
8391
+ try {
8392
+ await this.sender.telemetry({
8393
+ workItemId: first.workItemId,
8394
+ traceparent: first.traceparent,
8395
+ signal: first.signal,
8396
+ gzipped: Bun.gzipSync(new TextEncoder().encode(JSON.stringify(body)))
8397
+ });
8398
+ this.failures = 0;
8399
+ } catch (error) {
8400
+ if (error instanceof ApiError && (error.status === 409 || error.status === 410 || error.status === 400)) {
8401
+ this.refused.add(first.workItemId);
8402
+ for (const b of group)
8403
+ this.countLoss(b);
8404
+ this.log(`telemetry for work item ${first.workItemId} refused (${error.status}): ${error.message}`);
8405
+ continue;
8406
+ }
8407
+ failed = true;
8408
+ retry.push(...group);
8409
+ this.failures++;
8410
+ const wait = error instanceof ApiError && error.retryAfterMs ? error.retryAfterMs : Math.min(1000 * 2 ** (this.failures - 1), 60000);
8411
+ this.backoffUntil = this.now() + wait;
8412
+ }
8413
+ }
8414
+ this.batches = [...retry, ...this.batches];
8415
+ this.bytes = this.batches.reduce((n, b) => n + b.bytes, 0);
8416
+ this.evict();
8417
+ }
8418
+ async drain(timeoutMs) {
8419
+ const deadline = this.now() + timeoutMs;
8420
+ for (;; ) {
8421
+ this.backoffUntil = Math.min(this.backoffUntil, this.now());
8422
+ await this.flush();
8423
+ if (this.batches.length === 0)
8424
+ return true;
8425
+ if (this.now() >= deadline)
8426
+ return false;
8427
+ await Bun.sleep(Math.min(500, Math.max(0, deadline - this.now())));
8428
+ }
8429
+ }
8430
+ }
8431
+ function countRecords(signal, body) {
8432
+ if (signal === "traces")
8433
+ return (body.resourceSpans ?? []).reduce((n, rs) => n + (rs.scopeSpans ?? []).reduce((m, s) => m + (s.spans?.length ?? 0), 0), 0);
8434
+ if (signal === "logs")
8435
+ return (body.resourceLogs ?? []).reduce((n, rl) => n + (rl.scopeLogs ?? []).reduce((m, s) => m + (s.logRecords?.length ?? 0), 0), 0);
8436
+ return 0;
8437
+ }
8438
+
8439
+ // src/telemetry/redact.ts
8440
+ function resolvedConfigSecrets(config, env) {
8441
+ const out = [];
8442
+ const take = (table) => {
8443
+ for (const value of Object.values(table ?? {})) {
8444
+ if (!value.includes("$"))
8445
+ continue;
8446
+ const resolved = expandVariables(value, env);
8447
+ if (resolved)
8448
+ out.push(resolved);
8449
+ }
8450
+ };
8451
+ for (const agent of Object.values(config.agents))
8452
+ take(agent.env);
8453
+ for (const role of [config.roles.manager, config.roles.worker]) {
8454
+ for (const server of role?.extra_mcp_servers ?? []) {
8455
+ take(server.env);
8456
+ take(server.headers);
8457
+ }
8458
+ }
8459
+ return out;
8460
+ }
6957
8461
 
6958
8462
  // src/worktree/cleanup.ts
6959
8463
  import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
6960
- import { join as join9, relative as relative3, resolve as resolve3 } from "path";
8464
+ import { join as join9, relative as relative4, resolve as resolve3 } from "path";
6961
8465
 
6962
8466
  // src/worktree/merge-state.ts
6963
8467
  var MERGE_STATE_TTL_MS = 10 * 60000;
@@ -7136,7 +8640,7 @@ class WorktreeCleaner {
7136
8640
  const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
7137
8641
  if (!path || !branch || known.has(path))
7138
8642
  continue;
7139
- const rel = relative3(root, path);
8643
+ const rel = relative4(root, path);
7140
8644
  if (rel.startsWith("..") || rel === "")
7141
8645
  continue;
7142
8646
  const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
@@ -7361,6 +8865,11 @@ function buildCapabilities(config, machine, repositories) {
7361
8865
  teams: config.subscriptions.teams,
7362
8866
  projects: config.subscriptions.projects,
7363
8867
  labels: config.subscriptions.labels
8868
+ },
8869
+ telemetry: {
8870
+ enabled: config.telemetry.enabled,
8871
+ maxContent: config.telemetry.max_content,
8872
+ agentNative: config.telemetry.agent_native
7364
8873
  }
7365
8874
  };
7366
8875
  }
@@ -7434,6 +8943,7 @@ class EventSink {
7434
8943
  backoff = new Backoff;
7435
8944
  batchLimit = new Map;
7436
8945
  closed = false;
8946
+ traceparents = new Map;
7437
8947
  constructor(state, api, log, hooks) {
7438
8948
  this.state = state;
7439
8949
  this.api = api;
@@ -7460,6 +8970,12 @@ class EventSink {
7460
8970
  this.kick();
7461
8971
  return { eventId, seq };
7462
8972
  }
8973
+ trace(workItemId, traceparent) {
8974
+ if (traceparent)
8975
+ this.traceparents.set(workItemId, traceparent);
8976
+ else
8977
+ this.traceparents.delete(workItemId);
8978
+ }
7463
8979
  complete(completion) {
7464
8980
  this.state.queueCompletion(completion.request.workItemId, completion);
7465
8981
  this.kick();
@@ -7574,7 +9090,7 @@ class EventSink {
7574
9090
  const answer = await this.api.events({
7575
9091
  workItemId,
7576
9092
  events: batch.map((row) => row.payload)
7577
- });
9093
+ }, this.traceparents.get(workItemId));
7578
9094
  this.state.deleteEvents(ids);
7579
9095
  this.log.debug("events sent", {
7580
9096
  workItem: workItemId,
@@ -7632,7 +9148,8 @@ class EventSink {
7632
9148
  return;
7633
9149
  const { request } = row.payload;
7634
9150
  try {
7635
- const answer = await this.api.complete(request);
9151
+ const answer = await this.api.complete(request, this.traceparents.get(workItemId));
9152
+ this.traceparents.delete(workItemId);
7636
9153
  this.log.info(`reported work item ${request.outcome}`, {
7637
9154
  workItem: workItemId,
7638
9155
  alreadyTerminal: answer.alreadyTerminal
@@ -7702,7 +9219,7 @@ class ReadCache {
7702
9219
  }
7703
9220
 
7704
9221
  // src/mcp/tools.ts
7705
- import { z as z10 } from "zod";
9222
+ import { z as z11 } from "zod";
7706
9223
  var CACHED_TOOLS = new Set([
7707
9224
  "linear_get_issue",
7708
9225
  "linear_list_comments",
@@ -7710,7 +9227,7 @@ var CACHED_TOOLS = new Set([
7710
9227
  "linear_list_labels"
7711
9228
  ]);
7712
9229
  function describeTool(tool) {
7713
- const { $schema: _schema, ...schema } = z10.toJSONSchema(tool.input);
9230
+ const { $schema: _schema, ...schema } = z11.toJSONSchema(tool.input);
7714
9231
  return {
7715
9232
  name: tool.name,
7716
9233
  description: tool.description,
@@ -7789,11 +9306,14 @@ class McpBroker {
7789
9306
  if (!checked.ok)
7790
9307
  return { text: checked.message, isError: true };
7791
9308
  const { tool, input } = checked;
9309
+ const span = turn.trace?.(tool.name, input) ?? null;
7792
9310
  const cached = CACHED_TOOLS.has(tool.name);
7793
9311
  if (cached) {
7794
9312
  const hit = this.cache.get(turn.workItemId, tool.name, input);
7795
- if (hit)
9313
+ if (hit) {
9314
+ span?.end({ isError: false, cached: true, text: hit.text });
7796
9315
  return hit;
9316
+ }
7797
9317
  }
7798
9318
  if (tool.kind === "write")
7799
9319
  this.cache.invalidate(turn.workItemId);
@@ -7808,15 +9328,17 @@ class McpBroker {
7808
9328
  const result = await this.transport.tool(tool.route, {
7809
9329
  ...input,
7810
9330
  agentSessionId: turn.agentSessionId
7811
- });
9331
+ }, span?.traceparent);
7812
9332
  answer = { text: result.text, data: result.data };
7813
9333
  } catch (error) {
7814
9334
  if (asking)
7815
9335
  turn.askingUser(false);
7816
9336
  answer = { text: toolErrorText(error), isError: true };
7817
9337
  this.log(`${tool.name} failed: ${answer.text}`);
9338
+ span?.end({ isError: true, cached: false, text: answer.text });
7818
9339
  return answer;
7819
9340
  }
9341
+ span?.end({ isError: false, cached: false, text: answer.text });
7820
9342
  if (cached)
7821
9343
  this.cache.set(turn.workItemId, tool.name, input, answer);
7822
9344
  if (tool.name === "linear_report_pr" && answer.data?.linked)
@@ -8133,6 +9655,8 @@ class Supervisor {
8133
9655
  sweepTimer = null;
8134
9656
  diskWasLow = false;
8135
9657
  broker;
9658
+ telemetryQueue;
9659
+ secrets = [];
8136
9660
  constructor(options) {
8137
9661
  this.options = options;
8138
9662
  this.config = options.config;
@@ -8154,6 +9678,8 @@ class Supervisor {
8154
9678
  }
8155
9679
  });
8156
9680
  this.broker = new McpBroker(options.api, (line) => options.log.info(line));
9681
+ this.telemetryQueue = new TelemetryQueue(options.api, this.config.telemetry.buffer_mb * 1024 * 1024, (line) => options.log.info(line));
9682
+ this.secrets = this.baseSecrets();
8157
9683
  this.sink = new EventSink(options.state, options.api, options.log, {
8158
9684
  onCancelRequested: (id) => this.stop(id, "stop"),
8159
9685
  onNotYours: (id) => this.silence(id),
@@ -8183,10 +9709,42 @@ class Supervisor {
8183
9709
  }
8184
9710
  this.link.start();
8185
9711
  this.sink.kick(true);
9712
+ this.telemetryQueue.start();
8186
9713
  this.sweepTimer = setInterval(() => void this.cleanup({}), this.options.sweepIntervalMs ?? SWEEP_INTERVAL_MS);
8187
9714
  this.sweepTimer.unref?.();
8188
9715
  return interrupted;
8189
9716
  }
9717
+ baseSecrets() {
9718
+ return [
9719
+ ...this.options.secrets ?? [],
9720
+ ...resolvedConfigSecrets(this.config, this.options.env)
9721
+ ];
9722
+ }
9723
+ turnTelemetry(assignment, agentId, slot) {
9724
+ const claim = assignment.telemetry;
9725
+ if (!claim || !this.config.telemetry.enabled)
9726
+ return null;
9727
+ const preset = this.config.agents[agentId]?.preset ?? (isPreset(agentId) ? agentId : null);
9728
+ const max = this.config.telemetry.max_content;
9729
+ const telemetry = new TurnTelemetry({
9730
+ claim: {
9731
+ ...claim,
9732
+ content: minLevel(claim.content, max),
9733
+ agentNative: claim.agentNative && this.config.telemetry.agent_native
9734
+ },
9735
+ assignment,
9736
+ runner: { name: this.config.runner.name },
9737
+ agentId,
9738
+ preset,
9739
+ queue: this.telemetryQueue,
9740
+ secrets: () => slot.nonce ? [...this.secrets, slot.nonce] : this.secrets
9741
+ });
9742
+ telemetry.slots({
9743
+ capacity: this.config.runner.max_parallel_jobs,
9744
+ inUse: this.slots.size
9745
+ });
9746
+ return telemetry;
9747
+ }
8190
9748
  worktreeInUse(wt) {
8191
9749
  if (wt.state === "active")
8192
9750
  return true;
@@ -8392,6 +9950,9 @@ class Supervisor {
8392
9950
  };
8393
9951
  this.slots.set(assignment.workItemId, slot);
8394
9952
  const mcpCommand = this.options.mcpCommand === undefined ? selfCommand() : this.options.mcpCommand;
9953
+ const telemetry = this.turnTelemetry(assignment, agentId, slot);
9954
+ if (telemetry)
9955
+ this.sink.trace(assignment.workItemId, telemetry.traceparentFor());
8395
9956
  if (mcpCommand) {
8396
9957
  slot.nonce = this.broker.register({
8397
9958
  agentSessionId: assignment.agentSessionId,
@@ -8405,7 +9966,25 @@ class Supervisor {
8405
9966
  reportedPullRequest: (url) => {
8406
9967
  this.options.state.addPullRequest(slot.agentSessionId, url);
8407
9968
  slot.onReportedPullRequest?.(url);
8408
- }
9969
+ },
9970
+ trace: telemetry ? (tool, input) => {
9971
+ const span = telemetry.span("mcp.call", {
9972
+ "gen_ai.operation.name": "execute_tool",
9973
+ "gen_ai.tool.name": tool,
9974
+ "hypotenuse.mcp.server": "hypotenuse"
9975
+ }, telemetry.promptSpan, "client").content("mcp.arguments", JSON.stringify(input));
9976
+ return {
9977
+ traceparent: span.traceparent,
9978
+ end: (result) => {
9979
+ if (result.isError)
9980
+ span.fail(result.text.slice(0, 500));
9981
+ span.content("mcp.result", result.text).end({
9982
+ "hypotenuse.mcp.cached": result.cached,
9983
+ "hypotenuse.mcp.result.bytes": Buffer.byteLength(result.text)
9984
+ });
9985
+ }
9986
+ };
9987
+ } : undefined
8409
9988
  });
8410
9989
  }
8411
9990
  log.info(`claimed ${assignment.kind} for ${assignment.issue.identifier ?? "an issue"}`, {
@@ -8473,7 +10052,8 @@ class Supervisor {
8473
10052
  },
8474
10053
  onReportedPullRequest: (handler) => {
8475
10054
  slot.onReportedPullRequest = handler;
8476
- }
10055
+ },
10056
+ telemetry
8477
10057
  };
8478
10058
  slot.done = this.runTurn(slot, ctx);
8479
10059
  }
@@ -8527,8 +10107,18 @@ class Supervisor {
8527
10107
  });
8528
10108
  this.link.beatNow();
8529
10109
  const worktreeId = this.options.state.session(slot.agentSessionId)?.worktreeId;
8530
- if (worktreeId)
8531
- await this.cleanup({ only: [worktreeId] });
10110
+ const telemetry = ctx.telemetry ?? null;
10111
+ if (worktreeId) {
10112
+ const span = telemetry?.span("worktree.cleanup", {
10113
+ "hypotenuse.worktree.id": worktreeId
10114
+ });
10115
+ const report = await this.cleanup({ only: [worktreeId] });
10116
+ span?.end({
10117
+ "hypotenuse.worktree.removed": (report?.removed.length ?? 0) > 0,
10118
+ "hypotenuse.worktree.kept": (report?.kept.length ?? 0) > 0
10119
+ });
10120
+ }
10121
+ telemetry?.finish(slot.silent ? "taken_away" : result.outcome, result.error);
8532
10122
  }
8533
10123
  stop(workItemId, reason) {
8534
10124
  const slot = this.slots.get(workItemId);
@@ -8587,6 +10177,7 @@ class Supervisor {
8587
10177
  await this.waitIdle(this.config.sessions.stop_grace + 5000);
8588
10178
  }
8589
10179
  const flushed = await this.sink.drain(5000);
10180
+ await this.telemetryQueue.drain(5000);
8590
10181
  this.link.beatNow();
8591
10182
  return { drained: this.slots.size === 0 && flushed, cancelled };
8592
10183
  }
@@ -8648,6 +10239,8 @@ class Supervisor {
8648
10239
  }
8649
10240
  this.config = merged;
8650
10241
  this.options.log.level = merged.log.level;
10242
+ this.telemetryQueue.capacityBytes = merged.telemetry.buffer_mb * 1024 * 1024;
10243
+ this.secrets = this.baseSecrets();
8651
10244
  this.repositories = daemonRepositories(merged, this.options.machine);
8652
10245
  this.capabilities = this.buildCapabilities();
8653
10246
  this.options.log.info("config reloaded", {
@@ -8742,6 +10335,7 @@ class Supervisor {
8742
10335
  this.sweepTimer = null;
8743
10336
  this.link.stop();
8744
10337
  this.sink.close();
10338
+ this.telemetryQueue.stop();
8745
10339
  }
8746
10340
  }
8747
10341
  function changedKeys(a, b, prefix = "") {
@@ -8756,7 +10350,7 @@ function changedKeys(a, b, prefix = "") {
8756
10350
  return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
8757
10351
  }
8758
10352
  function resolveCommand(command, env) {
8759
- if (isAbsolute6(command))
10353
+ if (isAbsolute7(command))
8760
10354
  return command;
8761
10355
  return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
8762
10356
  }
@@ -8854,6 +10448,7 @@ async function runDaemon(options) {
8854
10448
  let state;
8855
10449
  let origin;
8856
10450
  let runner;
10451
+ let credentials;
8857
10452
  try {
8858
10453
  const read = readDaemonConfig(options, env);
8859
10454
  if (!read.config)
@@ -8862,7 +10457,6 @@ async function runDaemon(options) {
8862
10457
  paths = daemonPaths(read.path, options.stateDir, config.log.file, env);
8863
10458
  log = new DaemonLog(paths.log, config.log.level, !options.detachedChild);
8864
10459
  const credentialsFile = credentialsPath(env);
8865
- let credentials;
8866
10460
  try {
8867
10461
  credentials = loadCredentials(credentialsFile);
8868
10462
  } catch (error) {
@@ -8938,7 +10532,11 @@ async function runDaemon(options) {
8938
10532
  stateDir: paths.stateDir,
8939
10533
  env,
8940
10534
  readConfig: () => readDaemonConfig(options, env),
8941
- requestExit
10535
+ requestExit,
10536
+ secrets: [
10537
+ runner.token,
10538
+ ...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
10539
+ ]
8942
10540
  });
8943
10541
  control = new ControlServer(paths.socket, {
8944
10542
  status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
@@ -9765,7 +11363,7 @@ config_version = 1
9765
11363
  [runner]
9766
11364
  # name = "{{hostname}}" # display only
9767
11365
  # max_parallel_jobs = {{jobs}} # jobs (turns) at once on this machine
9768
- roles = ["worker"] # add "manager" to plan and dispatch work too
11366
+ roles = ["manager", "worker"] # drop "manager" to only implement issues
9769
11367
 
9770
11368
  [subscriptions]
9771
11369
  # teams = ["ENG"] # only these Linear teams (empty: every team)
@@ -9780,11 +11378,11 @@ agent = "{{agent}}"
9780
11378
  # prompt_file = "prompts/worker.md" # relative to this file; the built-in template otherwise
9781
11379
  # permission_policy = "allow" # or "ask" to approve tool calls from Linear
9782
11380
 
9783
- # [roles.manager]
9784
- # agent = "{{agent}}"
9785
- # # read-only by default: edits, deletes, moves, and commands are refused
9786
- # # permission_policy = { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }
9787
- # max_parallel = 1
11381
+ [roles.manager]
11382
+ agent = "{{agent}}"
11383
+ # read-only by default: edits, deletes, moves, and commands are refused
11384
+ # permission_policy = { default = "allow", edit = "deny", delete = "deny", move = "deny", execute = "deny" }
11385
+ # max_parallel = 1 # manager turns at once
9788
11386
 
9789
11387
  {{repositories}}
9790
11388
 
@@ -10337,6 +11935,20 @@ async function runDoctor(ctx, options) {
10337
11935
  if (options.agent && !resolveAgents(config).some((a) => a.id === options.agent)) {
10338
11936
  list.fail("agents", `no agent "${options.agent}" in the config`);
10339
11937
  }
11938
+ const t = config.telemetry;
11939
+ if (!t.enabled)
11940
+ list.ok("telemetry", "off: this runner sends no telemetry (`[telemetry] enabled = false`)");
11941
+ else {
11942
+ list.ok("telemetry", `sends up to \`${t.max_content}\` for the project's exporters; buffer ${t.buffer_mb} MB`);
11943
+ const agents = resolveAgents(config).filter((a) => a.roles.length > 0);
11944
+ if (!t.agent_native)
11945
+ list.ok("telemetry", "agents' own telemetry is not captured");
11946
+ else
11947
+ for (const agent of agents) {
11948
+ const support = nativeTelemetrySupport(agent.preset);
11949
+ list.ok("telemetry", support === "native" ? `${agent.id}: contributes its own telemetry (model requests, tokens, cost)` : support === "standard-otel" ? `${agent.id}: contributes its own telemetry if it reads the standard OTEL_* variables` : `${agent.id}: a custom command \u2014 it gets OTEL_EXPORTER_OTLP_ENDPOINT and HYPOTENUSE_OTLP_ENDPOINT`);
11950
+ }
11951
+ }
10340
11952
  for (const repo of daemonRepositories(config, machine)) {
10341
11953
  if (options.offline) {
10342
11954
  list.ok("repositories", `${repo.name}: ${repo.remote} (${repo.baseBranch})`);
@@ -12003,6 +13615,161 @@ function registerTagCommands(program) {
12003
13615
  }
12004
13616
  }
12005
13617
 
13618
+ // src/commands/telemetry.ts
13619
+ function collect2(value, previous = []) {
13620
+ return [...previous, value];
13621
+ }
13622
+ function parsePairs(pairs, what) {
13623
+ const out = {};
13624
+ for (const pair of pairs) {
13625
+ const at = pair.indexOf("=");
13626
+ if (at <= 0)
13627
+ throw new Error(`${what} must be KEY=VALUE, got "${pair}"`);
13628
+ out[pair.slice(0, at).trim()] = pair.slice(at + 1);
13629
+ }
13630
+ return out;
13631
+ }
13632
+ function exporterHealth(e) {
13633
+ if (!e.enabled)
13634
+ return "paused";
13635
+ if (e.lastErrorAt && (!e.lastExportAt || e.lastErrorAt > e.lastExportAt))
13636
+ return `error ${when(e.lastErrorAt)}: ${e.lastError ?? ""}`;
13637
+ return e.lastExportAt ? `ok ${when(e.lastExportAt)}` : "nothing yet";
13638
+ }
13639
+ function describeExporter(e) {
13640
+ const attrs = Object.entries(e.resourceAttributes).map(([k, v]) => `${k}=${v}`).join(", ");
13641
+ return [
13642
+ `${e.name} (id ${e.id})${e.enabled ? "" : " paused"}`,
13643
+ `endpoint ${e.endpoint}`,
13644
+ `protocol ${e.protocol}`,
13645
+ `content ${e.content}`,
13646
+ `signals ${e.signals.join(", ")}`,
13647
+ `sampled ${Math.round(e.traceSampleRatio * 100)}% of turns`,
13648
+ `agent's own ${e.agentNative ? "captured" : "not captured"}`,
13649
+ `headers ${e.headerNames.length ? e.headerNames.join(", ") : "none"} (values are never shown)`,
13650
+ `attributes ${attrs || "none"}`,
13651
+ `last export ${when(e.lastExportAt)}`,
13652
+ `last error ${e.lastError ? `${when(e.lastErrorAt)}: ${e.lastError}` : "-"}`
13653
+ ].join(`
13654
+ `);
13655
+ }
13656
+ function exporterBody(opts) {
13657
+ const body = {};
13658
+ if (opts.name !== undefined)
13659
+ body.name = opts.name;
13660
+ if (opts.endpoint !== undefined)
13661
+ body.endpoint = opts.endpoint;
13662
+ if (opts.protocol !== undefined)
13663
+ body.protocol = opts.protocol;
13664
+ if (opts.clearHeaders)
13665
+ body.headers = {};
13666
+ else if (opts.header?.length)
13667
+ body.headers = parsePairs(opts.header, "--header");
13668
+ if (opts.signals !== undefined)
13669
+ body.signals = opts.signals.split(",").map((s) => s.trim()).filter(Boolean);
13670
+ if (opts.content !== undefined)
13671
+ body.content = opts.content;
13672
+ if (opts.agentNative !== undefined)
13673
+ body.agentNative = opts.agentNative;
13674
+ if (opts.sample !== undefined)
13675
+ body.traceSampleRatio = Number(opts.sample);
13676
+ if (opts.attribute?.length)
13677
+ body.resourceAttributes = parsePairs(opts.attribute, "--attribute");
13678
+ if (opts.enabled !== undefined)
13679
+ body.enabled = opts.enabled;
13680
+ return body;
13681
+ }
13682
+ function exporterOptions(command, adding) {
13683
+ if (adding)
13684
+ command.requiredOption("--name <name>", "what it is called, unique in the project").requiredOption("--endpoint <url>", "the collector's OTLP/HTTP base URL (https; /v1/<signal> is appended)");
13685
+ else
13686
+ command.option("--name <name>", "a new name").option("--endpoint <url>", "a new endpoint");
13687
+ return command.option("--protocol <protocol>", "http/protobuf (default) or http/json").option("--header <KEY=VALUE>", "a header sent with every export, e.g. an API key; repeat for more (replaces all; never printed)", collect2).option("--content <level>", "metadata (default), content, or full \u2014 full sends prompts and source code").option("--signals <list>", "comma-separated: traces,metrics,logs").option("--sample <ratio>", "0\u20131: the share of turns whose traces are kept").option("--attribute <KEY=VALUE>", "an extra resource attribute; repeat for more (replaces all)", collect2).option("--agent-native", "capture the coding agent's own telemetry").option("--no-agent-native", "do not capture the agent's own telemetry");
13688
+ }
13689
+ function registerTelemetryCommands(program) {
13690
+ const telemetry = program.command("telemetry").description("Where the selected project's OpenTelemetry goes");
13691
+ const exporter = telemetry.command("exporter").description("The project's OpenTelemetry collectors");
13692
+ withPaging(exporter.command("list").description("List exporters")).action(async function() {
13693
+ await runCommand(this, async (ctx) => {
13694
+ const projectId = await ctx.projectId();
13695
+ const result = await ctx.signedInClient().request("GET", "/telemetry-exporters", {
13696
+ query: { projectId, ...pageQuery(this) }
13697
+ });
13698
+ emit(ctx, result, () => table(result.telemetryExporters.map((e) => ({
13699
+ id: e.id,
13700
+ name: e.name,
13701
+ endpoint: e.endpoint,
13702
+ content: e.content,
13703
+ signals: e.signals.join(","),
13704
+ sampled: `${Math.round(e.traceSampleRatio * 100)}%`,
13705
+ headers: e.headerNames.join(",") || "-",
13706
+ health: exporterHealth(e)
13707
+ })), [
13708
+ { key: "id" },
13709
+ { key: "name", maxWidth: 30 },
13710
+ { key: "endpoint", maxWidth: 50 },
13711
+ { key: "content" },
13712
+ { key: "signals" },
13713
+ { key: "sampled" },
13714
+ { key: "headers", maxWidth: 30 },
13715
+ { key: "health", maxWidth: 60 }
13716
+ ]) + pageFooter(result.pagination));
13717
+ });
13718
+ });
13719
+ exporter.command("view <id>").description("Show one exporter (header names only)").action(async function(id) {
13720
+ await runCommand(this, async (ctx) => {
13721
+ const projectId = await ctx.projectId();
13722
+ const result = await ctx.signedInClient().request("GET", `/telemetry-exporter/${id}`, { query: { projectId } });
13723
+ emit(ctx, result, () => describeExporter(result.telemetryExporter));
13724
+ });
13725
+ });
13726
+ exporterOptions(exporter.command("add").description("Export the project's telemetry to a collector (admin)"), true).option("--disabled", "create it paused").action(async function() {
13727
+ await runCommand(this, async (ctx) => {
13728
+ const opts = this.opts();
13729
+ const projectId = await ctx.projectId();
13730
+ const result = await ctx.signedInClient().request("PUT", "/telemetry-exporter", {
13731
+ body: {
13732
+ projectId,
13733
+ ...exporterBody(opts),
13734
+ ...opts.disabled ? { enabled: false } : {}
13735
+ }
13736
+ });
13737
+ emit(ctx, result, () => `Added exporter ${result.telemetryExporter.name} (id ${result.telemetryExporter.id}) at ${result.telemetryExporter.content}; \`hypotenuse telemetry exporter test ${result.telemetryExporter.id}\` sends a probe`);
13738
+ });
13739
+ });
13740
+ exporterOptions(exporter.command("edit <id>").description("Change an exporter; headers are kept unless given (admin)"), false).option("--clear-headers", "send no headers from now on").option("--enable", "resume exporting").option("--disable", "pause exporting").action(async function(id) {
13741
+ await runCommand(this, async (ctx) => {
13742
+ const opts = this.opts();
13743
+ const projectId = await ctx.projectId();
13744
+ const body = exporterBody({
13745
+ ...opts,
13746
+ enabled: opts.enable ? true : opts.disable ? false : undefined
13747
+ });
13748
+ const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}`, { body: { projectId, ...body } });
13749
+ emit(ctx, result, () => describeExporter(result.telemetryExporter));
13750
+ });
13751
+ });
13752
+ exporter.command("remove <id>").description("Stop exporting to a collector and forget its headers (admin)").action(async function(id) {
13753
+ await runCommand(this, async (ctx) => {
13754
+ const projectId = await ctx.projectId();
13755
+ const result = await ctx.signedInClient().request("DELETE", `/telemetry-exporter/${id}`, {
13756
+ body: { projectId }
13757
+ });
13758
+ emit(ctx, result, () => `Removed exporter ${id}`);
13759
+ });
13760
+ });
13761
+ exporter.command("test <id>").description("Send the collector a probe span, log, and metric, and show its answers (admin)").action(async function(id) {
13762
+ await runCommand(this, async (ctx) => {
13763
+ const projectId = await ctx.projectId();
13764
+ const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}/test`, { body: { projectId } });
13765
+ emit(ctx, result, () => result.results.map((r) => `${glyph(r.ok ? "complete" : "error")} ${r.signal.padEnd(8)} ${r.status || "-"}${r.error ? ` ${r.error}` : ""}`).join(`
13766
+ `));
13767
+ if (!result.ok)
13768
+ process.exitCode = 1;
13769
+ });
13770
+ });
13771
+ }
13772
+
12006
13773
  // src/commands/upgrade.ts
12007
13774
  var UPGRADE_DRAIN_MS = 10 * 60000;
12008
13775
  async function restartRunningDaemon(socketPath, call = callDaemon) {
@@ -12272,6 +14039,7 @@ function buildProgram() {
12272
14039
  registerEventCommands(program);
12273
14040
  registerRunnerCommands(program);
12274
14041
  registerRoutingCommands(program);
14042
+ registerTelemetryCommands(program);
12275
14043
  registerIdentityCommands(program);
12276
14044
  registerConfigCommands(program);
12277
14045
  registerDaemonCommands(program);