@arcadeai/hypotenuse 0.13.1 → 0.14.1

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.1",
346
+ version: "0.14.1",
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: {} };
@@ -3164,6 +3598,13 @@ async function resolveProjectId(client, selector) {
3164
3598
  }
3165
3599
  }
3166
3600
  }
3601
+ async function onlyProject(client) {
3602
+ const { projects, pagination } = await client.request("GET", "/projects", { query: { page: 1, limit: 2 } });
3603
+ return {
3604
+ project: pagination.total === 1 ? projects[0] ?? null : null,
3605
+ total: pagination.total
3606
+ };
3607
+ }
3167
3608
 
3168
3609
  // src/api/url.ts
3169
3610
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
@@ -3318,10 +3759,14 @@ class CliContext {
3318
3759
  }
3319
3760
  async projectId() {
3320
3761
  const selector = this.options.project ?? this.env.HYPOTENUSE_PROJECT ?? (this.credentials.servers[this.server.origin]?.project != null ? String(this.credentials.servers[this.server.origin]?.project) : undefined);
3321
- if (!selector) {
3322
- throw new Error("No project selected. Pass --project <id|slug> or run `hypotenuse project use <id|slug>`.");
3762
+ if (selector)
3763
+ return resolveProjectId(this.signedInClient(), selector);
3764
+ const { project, total } = await onlyProject(this.signedInClient());
3765
+ if (project) {
3766
+ logger.info(`Using project ${project.slug}, the only one you are in.`);
3767
+ return project.id;
3323
3768
  }
3324
- return resolveProjectId(this.signedInClient(), selector);
3769
+ throw new Error(total === 0 ? "You are not in any project yet. Create one in the dashboard, or accept an invite." : "No project selected, and you are in several. Pass --project <id|slug> or run `hypotenuse project use <id|slug>`.");
3325
3770
  }
3326
3771
  }
3327
3772
  function contextFor(command) {
@@ -3602,6 +4047,389 @@ import { join as join10 } from "path";
3602
4047
  import { mkdirSync as mkdirSync6, readFileSync as readFileSync7 } from "fs";
3603
4048
  import { join as join8 } from "path";
3604
4049
 
4050
+ // src/telemetry/acp.ts
4051
+ import { isAbsolute as isAbsolute4, relative as relative3 } from "path";
4052
+ class AcpTelemetry {
4053
+ turn;
4054
+ cwd;
4055
+ tools = new Map;
4056
+ spawn = null;
4057
+ prompt = null;
4058
+ chunk = null;
4059
+ promptSpanId = newSpanId();
4060
+ constructor(turn, cwd) {
4061
+ this.turn = turn;
4062
+ this.cwd = cwd;
4063
+ }
4064
+ get traceparent() {
4065
+ return this.turn?.traceparentFor(this.promptSpanId) ?? null;
4066
+ }
4067
+ rel(path) {
4068
+ if (!isAbsolute4(path))
4069
+ return path;
4070
+ const r = relative3(this.cwd, path);
4071
+ return r.startsWith("..") ? path : r || ".";
4072
+ }
4073
+ begin(name, attrs = {}) {
4074
+ return this.turn?.span(name, attrs, this.prompt) ?? null;
4075
+ }
4076
+ async around(name, attrs, fn) {
4077
+ const span = this.begin(name, attrs);
4078
+ try {
4079
+ return await fn(span);
4080
+ } catch (error) {
4081
+ span?.fail(error instanceof Error ? error.message : String(error));
4082
+ throw error;
4083
+ } finally {
4084
+ span?.end();
4085
+ }
4086
+ }
4087
+ spawned(attrs, command) {
4088
+ this.spawn = this.begin("agent.spawn", attrs);
4089
+ this.spawn?.content("agent.command", command);
4090
+ }
4091
+ initialized(attrs) {
4092
+ this.spawn?.set(attrs);
4093
+ }
4094
+ exited(exitCode) {
4095
+ this.spawn?.end({ "process.exit.code": exitCode ?? null });
4096
+ }
4097
+ promptStarted(attrs, text) {
4098
+ if (!this.turn)
4099
+ return;
4100
+ this.prompt = this.turn.span("acp.prompt", { "gen_ai.operation.name": "invoke_agent", ...attrs }, null, "client", this.promptSpanId);
4101
+ this.prompt.content("prompt", text);
4102
+ this.turn.promptSpan = this.prompt;
4103
+ }
4104
+ promptEnded(attrs, error) {
4105
+ this.flushChunk();
4106
+ for (const [, span] of this.tools)
4107
+ span.end({ "hypotenuse.tool.finished": false });
4108
+ this.tools.clear();
4109
+ if (error)
4110
+ this.prompt?.fail(error);
4111
+ this.prompt?.end(attrs);
4112
+ if (this.turn)
4113
+ this.turn.promptSpan = null;
4114
+ const input = attrs["gen_ai.usage.input_tokens"];
4115
+ const output = attrs["gen_ai.usage.output_tokens"];
4116
+ if (typeof input === "number" || typeof output === "number")
4117
+ this.turn?.usage({
4118
+ inputTokens: typeof input === "number" ? input : undefined,
4119
+ outputTokens: typeof output === "number" ? output : undefined
4120
+ });
4121
+ }
4122
+ flushChunk() {
4123
+ const chunk = this.chunk;
4124
+ this.chunk = null;
4125
+ if (!chunk || !this.prompt || !this.turn)
4126
+ return;
4127
+ this.prompt.event(`agent.${chunk.kind}`, this.turn.contentAttrs("text", chunk.text));
4128
+ }
4129
+ update(update) {
4130
+ if (!this.turn || !this.prompt)
4131
+ return;
4132
+ const u = update;
4133
+ const kind = String(u.sessionUpdate);
4134
+ if (kind === "agent_thought_chunk" || kind === "agent_message_chunk") {
4135
+ const content = u.content;
4136
+ if (content?.type !== "text")
4137
+ return;
4138
+ const which = kind === "agent_thought_chunk" ? "thought" : "message";
4139
+ if (this.chunk && this.chunk.kind !== which)
4140
+ this.flushChunk();
4141
+ this.chunk ??= { kind: which, text: "" };
4142
+ this.chunk.text += content.text ?? "";
4143
+ return;
4144
+ }
4145
+ this.flushChunk();
4146
+ switch (kind) {
4147
+ case "tool_call": {
4148
+ const id = String(u.toolCallId);
4149
+ const span = this.turn.span("tool_call", {
4150
+ "gen_ai.operation.name": "execute_tool",
4151
+ "gen_ai.tool.call.id": id,
4152
+ "gen_ai.tool.type": typeof u.kind === "string" ? u.kind : null,
4153
+ "hypotenuse.tool.status": typeof u.status === "string" ? u.status : "pending"
4154
+ }, this.prompt);
4155
+ span.content("tool.title", typeof u.title === "string" ? u.title : null);
4156
+ this.toolDetail(span, u);
4157
+ this.tools.set(id, span);
4158
+ if (u.status === "completed" || u.status === "failed")
4159
+ this.endTool(id, u);
4160
+ return;
4161
+ }
4162
+ case "tool_call_update": {
4163
+ const id = String(u.toolCallId);
4164
+ const span = this.tools.get(id);
4165
+ if (!span)
4166
+ return;
4167
+ if (typeof u.kind === "string")
4168
+ span.set({ "gen_ai.tool.type": u.kind });
4169
+ if (typeof u.title === "string")
4170
+ span.content("tool.title", u.title);
4171
+ this.toolDetail(span, u);
4172
+ if (u.status === "completed" || u.status === "failed")
4173
+ this.endTool(id, u);
4174
+ return;
4175
+ }
4176
+ case "plan": {
4177
+ const entries = u.entries ?? [];
4178
+ this.prompt.event("plan", {
4179
+ "hypotenuse.plan.entries": entries.length,
4180
+ "hypotenuse.plan.completed": entries.filter((e) => e.status === "completed").length,
4181
+ ...this.turn.contentAttrs("plan", JSON.stringify(entries))
4182
+ });
4183
+ return;
4184
+ }
4185
+ case "usage_update": {
4186
+ const cost = u.cost;
4187
+ this.prompt.event("usage", {
4188
+ "hypotenuse.context.used": typeof u.used === "number" ? u.used : null,
4189
+ "hypotenuse.context.size": typeof u.size === "number" ? u.size : null,
4190
+ "hypotenuse.usage.cost": cost?.amount ?? null,
4191
+ "hypotenuse.usage.currency": cost?.currency ?? null
4192
+ });
4193
+ return;
4194
+ }
4195
+ case "current_mode_update":
4196
+ this.prompt.event("mode", {
4197
+ "hypotenuse.mode": typeof u.currentModeId === "string" ? u.currentModeId : null
4198
+ });
4199
+ return;
4200
+ default:
4201
+ this.prompt.event("session_update", { "hypotenuse.update.kind": kind });
4202
+ }
4203
+ }
4204
+ toolDetail(span, u) {
4205
+ const locations = Array.isArray(u.locations) ? u.locations : null;
4206
+ if (locations?.length)
4207
+ span.set({
4208
+ "hypotenuse.tool.locations": locations.map((l) => l.path ? this.rel(l.path) : "").filter(Boolean).join(",")
4209
+ });
4210
+ if (u.rawInput !== undefined)
4211
+ span.content("tool.input", JSON.stringify(u.rawInput));
4212
+ if (u.rawOutput !== undefined)
4213
+ span.content("tool.output", JSON.stringify(u.rawOutput));
4214
+ else if (Array.isArray(u.content) && u.content.length)
4215
+ span.content("tool.output", JSON.stringify(u.content));
4216
+ }
4217
+ endTool(id, u) {
4218
+ const span = this.tools.get(id);
4219
+ this.tools.delete(id);
4220
+ if (!span)
4221
+ return;
4222
+ if (u.status === "failed")
4223
+ span.fail("the tool call failed");
4224
+ span.end({ "hypotenuse.tool.status": String(u.status) });
4225
+ }
4226
+ permission(toolCall, policy, options) {
4227
+ const parent = this.tools.get(toolCall.toolCallId) ?? this.prompt;
4228
+ const span = this.turn?.span("permission.wait", {
4229
+ "gen_ai.tool.call.id": toolCall.toolCallId,
4230
+ "gen_ai.tool.type": toolCall.kind ?? null,
4231
+ "hypotenuse.permission.policy": policy,
4232
+ "hypotenuse.permission.options": options.map((o) => o.kind ?? o.optionId).join(",")
4233
+ }, parent) ?? null;
4234
+ span?.content("tool.title", toolCall.title ?? null);
4235
+ return (answer) => span?.end({
4236
+ "hypotenuse.permission.outcome": answer.outcome,
4237
+ "hypotenuse.permission.option": answer.optionId ?? null,
4238
+ "hypotenuse.permission.decided_by": answer.decidedBy
4239
+ });
4240
+ }
4241
+ fs(op, path) {
4242
+ const span = this.begin(`fs.${op}`, { "file.path": this.rel(path) });
4243
+ return (result) => {
4244
+ if (result.refused)
4245
+ span?.fail(result.refused);
4246
+ span?.end({
4247
+ "hypotenuse.fs.bytes": result.bytes ?? null,
4248
+ "hypotenuse.fs.refused": result.refused ?? null
4249
+ });
4250
+ };
4251
+ }
4252
+ frame(line, message) {
4253
+ this.turn?.frame(line, message, this.prompt);
4254
+ }
4255
+ stderr(line) {
4256
+ this.turn?.log({
4257
+ body: line,
4258
+ level: "content",
4259
+ eventName: "agent.stderr",
4260
+ severity: "warn",
4261
+ span: this.spawn
4262
+ });
4263
+ }
4264
+ }
4265
+
4266
+ // src/telemetry/native.ts
4267
+ var NATIVE_TELEMETRY_PRESETS = {
4268
+ claude: "native",
4269
+ codex: "native",
4270
+ gemini: "native",
4271
+ qwen: "native",
4272
+ goose: "standard-otel",
4273
+ opencode: "standard-otel"
4274
+ };
4275
+ function nativeTelemetrySupport(preset) {
4276
+ return preset && NATIVE_TELEMETRY_PRESETS[preset] || "unknown";
4277
+ }
4278
+ function dropInheritedOtel(env) {
4279
+ for (const key of Object.keys(env))
4280
+ if (key.startsWith("OTEL_"))
4281
+ delete env[key];
4282
+ }
4283
+ function nativeTelemetryLaunch(preset, target) {
4284
+ const full = target.level === "full";
4285
+ const env = {
4286
+ HYPOTENUSE_OTLP_ENDPOINT: target.endpoint,
4287
+ OTEL_EXPORTER_OTLP_ENDPOINT: target.endpoint,
4288
+ OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
4289
+ OTEL_TRACES_EXPORTER: "otlp",
4290
+ OTEL_METRICS_EXPORTER: "otlp",
4291
+ OTEL_LOGS_EXPORTER: "otlp",
4292
+ OTEL_METRIC_EXPORT_INTERVAL: "5000",
4293
+ OTEL_LOGS_EXPORT_INTERVAL: "2000",
4294
+ OTEL_BSP_SCHEDULE_DELAY: "1000"
4295
+ };
4296
+ const args = [];
4297
+ switch (preset) {
4298
+ case "claude":
4299
+ env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
4300
+ env.CLAUDE_CODE_ENHANCED_TELEMETRY_BETA = "1";
4301
+ if (full) {
4302
+ env.OTEL_LOG_USER_PROMPTS = "1";
4303
+ env.OTEL_LOG_TOOL_DETAILS = "1";
4304
+ }
4305
+ break;
4306
+ case "gemini":
4307
+ case "qwen": {
4308
+ const prefix = preset === "gemini" ? "GEMINI" : "QWEN";
4309
+ env[`${prefix}_TELEMETRY_ENABLED`] = "true";
4310
+ env[`${prefix}_TELEMETRY_TARGET`] = "local";
4311
+ env[`${prefix}_TELEMETRY_OTLP_ENDPOINT`] = target.endpoint;
4312
+ env[`${prefix}_TELEMETRY_OTLP_PROTOCOL`] = "http";
4313
+ env[`${prefix}_TELEMETRY_LOG_PROMPTS`] = full ? "true" : "false";
4314
+ break;
4315
+ }
4316
+ case "codex":
4317
+ args.push("-c", `otel.exporter={otlp-http={endpoint="${target.endpoint}",protocol="json"}}`, "-c", `otel.log_user_prompt=${full ? "true" : "false"}`);
4318
+ break;
4319
+ }
4320
+ return { env, args };
4321
+ }
4322
+
4323
+ // src/telemetry/receiver.ts
4324
+ var MAX_BODY_BYTES = 8 * 1024 * 1024;
4325
+ var DRAIN_MS = 1500;
4326
+
4327
+ class NativeReceiver {
4328
+ turn;
4329
+ nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(18))).toString("base64url");
4330
+ server = null;
4331
+ received = {
4332
+ traces: 0,
4333
+ metrics: 0,
4334
+ logs: 0
4335
+ };
4336
+ lastReceivedAt = 0;
4337
+ constructor(turn) {
4338
+ this.turn = turn;
4339
+ }
4340
+ start() {
4341
+ this.server = Bun.serve({
4342
+ hostname: "127.0.0.1",
4343
+ port: 0,
4344
+ maxRequestBodySize: MAX_BODY_BYTES,
4345
+ fetch: (req) => this.handle(req)
4346
+ });
4347
+ return this.endpoint;
4348
+ }
4349
+ get endpoint() {
4350
+ return `http://127.0.0.1:${this.server?.port ?? 0}/${this.nonce}`;
4351
+ }
4352
+ async handle(req) {
4353
+ const url = new URL(req.url);
4354
+ const prefix = `/${this.nonce}/`;
4355
+ const signal = signalOfPath(url.pathname);
4356
+ if (req.method !== "POST" || !url.pathname.startsWith(prefix) || !signal)
4357
+ return new Response(null, { status: 404 });
4358
+ const contentType = req.headers.get("content-type");
4359
+ try {
4360
+ let bytes = new Uint8Array(await req.arrayBuffer());
4361
+ if ((req.headers.get("content-encoding") ?? "").includes("gzip"))
4362
+ bytes = new Uint8Array(Bun.gunzipSync(bytes));
4363
+ if (bytes.byteLength > MAX_BODY_BYTES)
4364
+ throw new Error("too large");
4365
+ this.accept(signal, decodeOtlpBody(signal, bytes, contentType));
4366
+ } catch {
4367
+ return new Response(null, { status: 400 });
4368
+ }
4369
+ const json = (contentType ?? "").includes("json");
4370
+ return new Response(json ? "{}" : new Uint8Array(0), {
4371
+ status: 200,
4372
+ headers: {
4373
+ "Content-Type": json ? "application/json" : "application/x-protobuf"
4374
+ }
4375
+ });
4376
+ }
4377
+ accept(signal, request) {
4378
+ this.received[signal]++;
4379
+ this.lastReceivedAt = Date.now();
4380
+ const turn = this.turn;
4381
+ const level = {
4382
+ key: LEVEL_ATTRIBUTE,
4383
+ value: { stringValue: turn.level }
4384
+ };
4385
+ const tag = (attrs) => [
4386
+ ...(attrs ?? []).filter((a) => a.key !== LEVEL_ATTRIBUTE),
4387
+ level
4388
+ ];
4389
+ if (signal === "traces") {
4390
+ if (!turn.sampled)
4391
+ return;
4392
+ for (const rs of request.resourceSpans ?? [])
4393
+ for (const ss of rs.scopeSpans ?? []) {
4394
+ ss.spans = (ss.spans ?? []).map((s) => {
4395
+ const root = !s.parentSpanId || /^0+$/.test(s.parentSpanId);
4396
+ return {
4397
+ ...s,
4398
+ traceId: turn.traceId,
4399
+ ...root ? { parentSpanId: turn.turnSpan.spanId } : {},
4400
+ attributes: tag(s.attributes)
4401
+ };
4402
+ });
4403
+ }
4404
+ } else if (signal === "logs") {
4405
+ if (!turn.sampled)
4406
+ return;
4407
+ for (const rl of request.resourceLogs ?? [])
4408
+ for (const sl of rl.scopeLogs ?? [])
4409
+ sl.logRecords = (sl.logRecords ?? []).map((l) => ({
4410
+ ...l,
4411
+ traceId: turn.traceId,
4412
+ ...l.traceId && l.traceId !== turn.traceId ? { spanId: turn.turnSpan.spanId } : {},
4413
+ attributes: tag(l.attributes)
4414
+ }));
4415
+ }
4416
+ stampResource(signal, request, [
4417
+ ...turn.resourceAttributes.filter((a) => a.key !== "hypotenuse.source" && !a.key.startsWith("service.")),
4418
+ ...attributes({ "hypotenuse.source": "agent_native" })
4419
+ ]);
4420
+ turn.enqueue(signal, request);
4421
+ }
4422
+ async close(drainMs = DRAIN_MS) {
4423
+ if (!this.server)
4424
+ return;
4425
+ const quietFor = Date.now() - this.lastReceivedAt;
4426
+ if (quietFor < drainMs)
4427
+ await Bun.sleep(drainMs - quietFor);
4428
+ this.server.stop(true);
4429
+ this.server = null;
4430
+ }
4431
+ }
4432
+
3605
4433
  // src/worktree/co-author.ts
3606
4434
  import { chmodSync as chmodSync2, mkdirSync as mkdirSync3, renameSync as renameSync2, writeFileSync as writeFileSync2 } from "fs";
3607
4435
  import { join as join5 } from "path";
@@ -4620,7 +5448,7 @@ import {
4620
5448
  rmSync as rmSync3,
4621
5449
  writeFileSync as writeFileSync4
4622
5450
  } from "fs";
4623
- import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute4, join as join7, resolve as resolve2, sep } from "path";
5451
+ import { basename as basename2, dirname as dirname6, isAbsolute as isAbsolute5, join as join7, resolve as resolve2, sep } from "path";
4624
5452
 
4625
5453
  class OutsideWorktree extends Error {
4626
5454
  path;
@@ -4679,7 +5507,7 @@ class ConfinedFs {
4679
5507
  const root = this.roots[0];
4680
5508
  if (!root)
4681
5509
  throw new OutsideWorktree(path);
4682
- return isAbsolute4(path) ? resolve2(path) : resolve2(root, path);
5510
+ return isAbsolute5(path) ? resolve2(path) : resolve2(root, path);
4683
5511
  }
4684
5512
  read(path, line, limit) {
4685
5513
  const real = this.resolveExisting(path);
@@ -4973,7 +5801,7 @@ function callText(call) {
4973
5801
  }
4974
5802
 
4975
5803
  // src/acp/mcp-servers.ts
4976
- import { isAbsolute as isAbsolute5 } from "path";
5804
+ import { isAbsolute as isAbsolute6 } from "path";
4977
5805
  var HYPOTENUSE_SERVER_NAME = "hypotenuse";
4978
5806
  var VARIABLE = /\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g;
4979
5807
 
@@ -5023,7 +5851,7 @@ function turnMcpServers(role, caps, env, hypotenuse) {
5023
5851
  const [command, ...args] = s.command;
5024
5852
  servers.push({
5025
5853
  name: s.name,
5026
- command: isAbsolute5(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
5854
+ command: isAbsolute6(command) ? command : Bun.which(command, { PATH: env.PATH ?? "" }) ?? command,
5027
5855
  args,
5028
5856
  env: pairs(s.name, s.env, env)
5029
5857
  });
@@ -5473,6 +6301,8 @@ class Turn {
5473
6301
  where;
5474
6302
  readOnly;
5475
6303
  agentNote;
6304
+ otel;
6305
+ receiver = null;
5476
6306
  constructor(ctx, options) {
5477
6307
  this.ctx = ctx;
5478
6308
  this.options = options;
@@ -5498,6 +6328,7 @@ class Turn {
5498
6328
  }
5499
6329
  };
5500
6330
  this.mapper = new TurnMapper(this.mapperOptions);
6331
+ this.otel = new AcpTelemetry(ctx.telemetry ?? null, this.cwd);
5501
6332
  this.questions = new QuestionQueue({
5502
6333
  post: (event) => this.emit(event),
5503
6334
  waiting: (waiting) => {
@@ -5609,6 +6440,7 @@ class Turn {
5609
6440
  throw error;
5610
6441
  } finally {
5611
6442
  clearInterval(timer);
6443
+ this.otel.promptEnded({ "hypotenuse.end_reason": this.endReason }, this.endReason && this.endReason !== "stop" ? this.endReason : null);
5612
6444
  await this.shutdown();
5613
6445
  await this.workspace?.release().catch((error) => ctx.log.warn(`releasing the workspace failed: ${errorMessage(error)}`, {
5614
6446
  session: ctx.assignment.agentSessionId
@@ -5619,10 +6451,28 @@ class Turn {
5619
6451
  const { ctx } = this;
5620
6452
  const { assignment } = ctx;
5621
6453
  if (ctx.prepareWorkspace) {
6454
+ const prepare = ctx.prepareWorkspace;
5622
6455
  try {
5623
- this.workspace = await ctx.prepareWorkspace({
5624
- output: (line) => this.log.note(line),
5625
- env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
6456
+ this.workspace = await this.otel.around("worktree.prepare", {
6457
+ "hypotenuse.repository": assignment.repository?.remote ?? null,
6458
+ "hypotenuse.work_item.fresh": assignment.fresh
6459
+ }, async (span) => {
6460
+ const workspace = await prepare({
6461
+ output: (line) => {
6462
+ this.log.note(line);
6463
+ this.otel.turn?.log({
6464
+ body: line,
6465
+ eventName: "worktree.output",
6466
+ span
6467
+ });
6468
+ },
6469
+ env: (branch) => sessionEnvironment(ctx.env, sessionVariables(assignment, branch))
6470
+ });
6471
+ span?.set({
6472
+ "hypotenuse.worktree.mode": workspace.mode,
6473
+ "vcs.ref.head.name": workspace.branch
6474
+ });
6475
+ return workspace;
5626
6476
  });
5627
6477
  } catch (error) {
5628
6478
  const remote = ctx.assignment.repository?.remote ?? null;
@@ -5656,6 +6506,7 @@ class Turn {
5656
6506
  if (this.endReason)
5657
6507
  throw new TurnFailure("stopped");
5658
6508
  this.mapperOptions.cwd = this.cwd;
6509
+ this.otel.cwd = this.cwd;
5659
6510
  const remote = this.workspace?.repository?.remote ?? null;
5660
6511
  if (remote) {
5661
6512
  this.mapperOptions.pullRequests = new PrDetector(remote, this.workspace?.repositoryConfig?.pr_url_patterns ?? [], ctx.pullRequests ?? []);
@@ -5684,13 +6535,17 @@ class Turn {
5684
6535
  if (coAuthor && this.workspace?.repository) {
5685
6536
  Object.assign(launch.env, coAuthorEnvironment(launch.env, installCoAuthorHooks(ctx.stateDir), coAuthor));
5686
6537
  }
6538
+ this.withTelemetry(launch);
5687
6539
  this.log.note(`spawn ${launch.command.join(" ")} in ${this.cwd}`);
5688
6540
  try {
5689
6541
  this.conn = AcpConnection.spawn({
5690
6542
  command: launch.command,
5691
6543
  cwd: this.cwd,
5692
6544
  env: launch.env,
5693
- stderr: (line) => this.log.stderr(line),
6545
+ stderr: (line) => {
6546
+ this.log.stderr(line);
6547
+ this.otel.stderr(line);
6548
+ },
5694
6549
  noise: (line) => this.noise(line),
5695
6550
  trace: (line, message) => {
5696
6551
  if (line.startsWith("<-")) {
@@ -5698,37 +6553,45 @@ class Turn {
5698
6553
  this.noiseInARow = 0;
5699
6554
  }
5700
6555
  this.log.rpc(line, message);
6556
+ this.otel.frame(line, message);
5701
6557
  },
5702
6558
  handlers: {
5703
6559
  update: (n) => {
5704
6560
  if (this.acpSessionId && n.sessionId !== this.acpSessionId)
5705
6561
  return;
6562
+ this.otel.update(n.update);
5706
6563
  this.emitAll(this.mapper.update(n.update));
5707
6564
  },
5708
6565
  requestPermission: (params) => this.permission(params),
5709
6566
  readTextFile: async (params) => {
6567
+ const done = this.otel.fs("read", params.path);
5710
6568
  try {
5711
- return {
5712
- content: this.fs.read(params.path, params.line, params.limit)
5713
- };
6569
+ const content = this.fs.read(params.path, params.line, params.limit);
6570
+ done({ bytes: Buffer.byteLength(content) });
6571
+ return { content };
5714
6572
  } catch (error) {
6573
+ done({ refused: errorMessage(error) });
5715
6574
  throw this.fsError(error);
5716
6575
  }
5717
6576
  },
5718
6577
  writeTextFile: async (params) => {
6578
+ const done = this.otel.fs("write", params.path);
5719
6579
  if (this.readOnly) {
5720
6580
  this.log.note(`refused a write (read-only): ${params.path}`);
6581
+ done({ refused: "read-only" });
5721
6582
  throw new AcpError(RESOURCE_REFUSED, READ_ONLY_MESSAGE, {
5722
6583
  path: params.path
5723
6584
  });
5724
6585
  }
5725
6586
  try {
5726
6587
  this.fs.write(params.path, params.content);
6588
+ done({ bytes: Buffer.byteLength(params.content) });
5727
6589
  } catch (error) {
6590
+ done({ refused: errorMessage(error) });
5728
6591
  throw this.fsError(error);
5729
6592
  }
5730
6593
  },
5731
- createElicitation: (params) => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId))),
6594
+ createElicitation: (params) => this.otel.around("elicitation.wait", {}, () => this.questions.ask(elicitationQuestion(params, this.agentTitle, providerSlug(this.agentId)))),
5732
6595
  unknown: (method) => this.log.note(`unhandled ${method}`)
5733
6596
  }
5734
6597
  });
@@ -5739,7 +6602,20 @@ class Turn {
5739
6602
  const conn = this.conn;
5740
6603
  ctx.setPid(conn.pid);
5741
6604
  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);
6605
+ this.otel.spawned({
6606
+ "process.pid": conn.pid,
6607
+ "hypotenuse.agent.id": this.agentId,
6608
+ "hypotenuse.agent.preset": launch.preset ?? "custom",
6609
+ "hypotenuse.telemetry.native": this.receiver !== null
6610
+ }, launch.command.join(" "));
6611
+ 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));
6612
+ this.otel.initialized({
6613
+ "gen_ai.agent.name": init.agentInfo?.name ?? this.agentId,
6614
+ "hypotenuse.agent.version": init.agentInfo?.version ?? null,
6615
+ "hypotenuse.acp.protocol_version": init.protocolVersion,
6616
+ "hypotenuse.acp.load_session": Boolean(init.agentCapabilities?.loadSession),
6617
+ "hypotenuse.acp.auth_methods": (init.authMethods ?? []).length
6618
+ });
5743
6619
  this.agentTitle = init.agentInfo?.title ?? init.agentInfo?.name ?? this.agentId;
5744
6620
  this.agentVersion = init.agentInfo?.version ?? null;
5745
6621
  ctx.log.debug(`${this.agentId} initialized: ${init.agentInfo?.name ?? "?"} ${init.agentInfo?.version ?? ""}`, { session: assignment.agentSessionId });
@@ -5795,7 +6671,7 @@ class Turn {
5795
6671
  if (!method)
5796
6672
  throw new AuthRequired(this.loginNeed(), "not logged in");
5797
6673
  this.log.note(`authenticate ${method}`);
5798
- await this.guard(this.conn.authenticate(method), 0, "");
6674
+ await this.otel.around("acp.authenticate", { "hypotenuse.acp.auth_method": method }, () => this.guard(this.conn.authenticate(method), 0, ""));
5799
6675
  authenticated = true;
5800
6676
  }
5801
6677
  }
@@ -5823,12 +6699,12 @@ class Turn {
5823
6699
  if (previousAcpSessionId && !assignment.fresh && assignment.kind === "prompt") {
5824
6700
  try {
5825
6701
  if (caps.sessionCapabilities?.resume) {
5826
- const answer = await this.guard(conn.resumeSession({
6702
+ const answer = await this.otel.around("acp.session.resume", {}, () => this.guard(conn.resumeSession({
5827
6703
  sessionId: previousAcpSessionId,
5828
6704
  cwd: this.cwd,
5829
6705
  mcpServers,
5830
6706
  ...directories
5831
- }), 0, "");
6707
+ }), 0, ""));
5832
6708
  this.acpSessionId = previousAcpSessionId;
5833
6709
  this.resumed = true;
5834
6710
  await this.applyMode(answer);
@@ -5838,12 +6714,12 @@ class Turn {
5838
6714
  this.acpSessionId = previousAcpSessionId;
5839
6715
  this.mapper.replaying = true;
5840
6716
  try {
5841
- const answer = await this.guard(conn.loadSession({
6717
+ const answer = await this.otel.around("acp.session.load", {}, () => this.guard(conn.loadSession({
5842
6718
  sessionId: previousAcpSessionId,
5843
6719
  cwd: this.cwd,
5844
6720
  mcpServers,
5845
6721
  ...directories
5846
- }), 0, "");
6722
+ }), 0, ""));
5847
6723
  this.resumed = true;
5848
6724
  await this.applyMode(answer);
5849
6725
  return;
@@ -5859,7 +6735,7 @@ class Turn {
5859
6735
  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
6736
  }
5861
6737
  }
5862
- const created = await this.guard(conn.newSession({ cwd: this.cwd, mcpServers, ...directories }), 0, "");
6738
+ 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
6739
  this.acpSessionId = created.sessionId;
5864
6740
  await this.applyMode(created);
5865
6741
  }
@@ -5931,6 +6807,17 @@ class Turn {
5931
6807
  }, (name) => ctx.log.warn(`the prompt template uses an unknown variable {{${name}}}; it rendered empty`));
5932
6808
  this.promptStartedAt = this.now();
5933
6809
  this.lastTraffic = this.now();
6810
+ const promptText = blocks.map((b) => b.text ?? "").join(`
6811
+
6812
+ `);
6813
+ this.otel.promptStarted({
6814
+ "hypotenuse.prompt.blocks": blocks.length,
6815
+ "hypotenuse.prompt.bytes": Buffer.byteLength(promptText),
6816
+ "hypotenuse.prompt.history": ctx.assignment.history.length,
6817
+ "hypotenuse.prompt.guidance": ctx.assignment.guidance.length,
6818
+ "hypotenuse.prompt.context_bytes": Buffer.byteLength(ctx.assignment.promptContext ?? ""),
6819
+ "hypotenuse.acp.resumed": this.resumed
6820
+ }, promptText);
5934
6821
  const request = conn.prompt(sessionId, blocks);
5935
6822
  this.promptSettled = request.then(() => {}, () => {});
5936
6823
  const answer = await this.guard(request, 0, "").catch((error) => {
@@ -5938,6 +6825,12 @@ class Turn {
5938
6825
  throw error;
5939
6826
  throw new TurnFailure(`${this.agentId} failed the turn: ${errorMessage(error)}${quoteTail(this.log.tail())}`);
5940
6827
  });
6828
+ const usage = answer.usage;
6829
+ this.otel.promptEnded({
6830
+ "gen_ai.response.finish_reasons": answer.stopReason,
6831
+ "gen_ai.usage.input_tokens": usage?.inputTokens,
6832
+ "gen_ai.usage.output_tokens": usage?.outputTokens
6833
+ }, answer.stopReason === "end_turn" || answer.stopReason === "cancelled" ? null : `stopReason: ${answer.stopReason}`);
5941
6834
  if (this.endReason)
5942
6835
  return await this.stopped();
5943
6836
  this.emit({ type: "status", state: "finishing" });
@@ -6022,6 +6915,33 @@ class Turn {
6022
6915
  await within(conn.exited, 2000, null);
6023
6916
  }
6024
6917
  async shutdown() {
6918
+ await this.closeAgent();
6919
+ if (this.conn)
6920
+ this.otel.exited(await within(this.conn.exited, 500, null));
6921
+ await this.receiver?.close();
6922
+ this.receiver = null;
6923
+ }
6924
+ withTelemetry(launch) {
6925
+ const turn = this.ctx.telemetry;
6926
+ if (!turn)
6927
+ return;
6928
+ const traceparent = this.otel.traceparent;
6929
+ if (traceparent)
6930
+ launch.env.TRACEPARENT = traceparent;
6931
+ if (!turn.agentNative)
6932
+ return;
6933
+ this.receiver = new NativeReceiver(turn);
6934
+ const endpoint = this.receiver.start();
6935
+ turn.addSecret(this.receiver.nonce);
6936
+ dropInheritedOtel(launch.env);
6937
+ const native = nativeTelemetryLaunch(launch.preset, {
6938
+ endpoint,
6939
+ level: turn.level
6940
+ });
6941
+ Object.assign(launch.env, native.env);
6942
+ launch.command = [...launch.command, ...native.args];
6943
+ }
6944
+ async closeAgent() {
6025
6945
  const conn = this.conn;
6026
6946
  if (!conn)
6027
6947
  return;
@@ -6074,13 +6994,26 @@ class Turn {
6074
6994
  async permission(params) {
6075
6995
  const policy = this.role?.permission_policy ?? "allow";
6076
6996
  const decision = decide(policy, params.toolCall.kind);
6997
+ const waited = this.otel.permission(params.toolCall, typeof policy === "string" ? policy : JSON.stringify(policy), params.options);
6077
6998
  if (decision !== "ask") {
6078
6999
  const optionId = automaticOption(params.options, decision);
6079
7000
  this.log.note(`permission ${params.toolCall.toolCallId}: ${decision} \u2192 ${optionId ?? "cancelled"}`);
7001
+ waited({
7002
+ outcome: optionId ? "selected" : "cancelled",
7003
+ optionId: optionId ?? undefined,
7004
+ decidedBy: "policy"
7005
+ });
6080
7006
  return optionId ? { outcome: { outcome: "selected", optionId } } : { outcome: { outcome: "cancelled" } };
6081
7007
  }
6082
7008
  this.emitAll(this.mapper.flushThought(true));
6083
- return this.questions.ask(permissionQuestion(params, this.where, this.cwd));
7009
+ const answer = await this.questions.ask(permissionQuestion(params, this.where, this.cwd));
7010
+ const outcome = answer.outcome;
7011
+ waited({
7012
+ outcome: outcome?.outcome ?? "cancelled",
7013
+ optionId: outcome?.optionId,
7014
+ decidedBy: "person"
7015
+ });
7016
+ return answer;
6084
7017
  }
6085
7018
  fsError(error) {
6086
7019
  if (error instanceof OutsideWorktree) {
@@ -6282,6 +7215,9 @@ class DaemonApi {
6282
7215
  "User-Agent": `hypotenuse/${package_default.version} (${platform()})`
6283
7216
  };
6284
7217
  }
7218
+ traced(traceparent) {
7219
+ return traceparent ? { ...this.headers, [TRACEPARENT_HEADER]: traceparent } : this.headers;
7220
+ }
6285
7221
  async heartbeat(body) {
6286
7222
  const answer = await this.client.request("POST", "/daemon/heartbeat", {
6287
7223
  body,
@@ -6309,26 +7245,41 @@ class DaemonApi {
6309
7245
  });
6310
7246
  return ClaimResponse.parse(answer).assignment;
6311
7247
  }
6312
- async events(body) {
7248
+ async events(body, traceparent) {
6313
7249
  const answer = await this.client.request("POST", "/daemon/events", {
6314
7250
  body,
6315
- headers: this.headers,
7251
+ headers: this.traced(traceparent),
6316
7252
  timeoutMs: REQUEST_TIMEOUT_MS
6317
7253
  });
6318
7254
  return EventsResponse.parse(answer);
6319
7255
  }
6320
- async complete(body) {
7256
+ async complete(body, traceparent) {
6321
7257
  const answer = await this.client.request("POST", "/daemon/complete", {
6322
7258
  body,
6323
- headers: this.headers,
7259
+ headers: this.traced(traceparent),
6324
7260
  timeoutMs: REQUEST_TIMEOUT_MS
6325
7261
  });
6326
7262
  return CompleteResponse.parse(answer);
6327
7263
  }
6328
- async tool(route, body) {
7264
+ async telemetry(batch) {
7265
+ const answer = await this.client.request("POST", "/daemon/telemetry", {
7266
+ rawBody: batch.gzipped,
7267
+ headers: {
7268
+ ...this.headers,
7269
+ "Content-Type": "application/json",
7270
+ "Content-Encoding": "gzip",
7271
+ [TELEMETRY_SIGNAL_HEADER]: batch.signal,
7272
+ [TELEMETRY_WORK_ITEM_HEADER]: String(batch.workItemId),
7273
+ [TRACEPARENT_HEADER]: batch.traceparent
7274
+ },
7275
+ timeoutMs: REQUEST_TIMEOUT_MS
7276
+ });
7277
+ return TelemetryResponse.parse(answer);
7278
+ }
7279
+ async tool(route, body, traceparent) {
6329
7280
  const answer = await this.client.request("POST", route, {
6330
7281
  body,
6331
- headers: this.headers,
7282
+ headers: this.traced(traceparent),
6332
7283
  timeoutMs: TOOL_REQUEST_TIMEOUT_MS
6333
7284
  });
6334
7285
  return ToolResult.parse(answer).result;
@@ -6953,11 +7904,575 @@ class StateDb {
6953
7904
 
6954
7905
  // src/daemon/supervisor.ts
6955
7906
  import { hostname as hostname3 } from "os";
6956
- import { isAbsolute as isAbsolute6 } from "path";
7907
+ import { isAbsolute as isAbsolute7 } from "path";
7908
+
7909
+ // src/telemetry/collector.ts
7910
+ var SCOPE = { name: "hypotenuse.daemon", version: package_default.version };
7911
+ var TOKEN_BOUNDS = [
7912
+ 100,
7913
+ 1000,
7914
+ 1e4,
7915
+ 50000,
7916
+ 1e5,
7917
+ 250000,
7918
+ 500000,
7919
+ 1e6
7920
+ ];
7921
+
7922
+ class SpanHandle {
7923
+ turn;
7924
+ name;
7925
+ parentSpanId;
7926
+ kind;
7927
+ spanId;
7928
+ startMs;
7929
+ attrs = {};
7930
+ events = [];
7931
+ ended = false;
7932
+ error = null;
7933
+ constructor(turn, name, parentSpanId, kind = "internal", attrs = {}, spanId = newSpanId()) {
7934
+ this.turn = turn;
7935
+ this.name = name;
7936
+ this.parentSpanId = parentSpanId;
7937
+ this.kind = kind;
7938
+ this.spanId = spanId;
7939
+ this.startMs = turn.now();
7940
+ Object.assign(this.attrs, attrs);
7941
+ }
7942
+ set(attrs) {
7943
+ Object.assign(this.attrs, attrs);
7944
+ return this;
7945
+ }
7946
+ content(key, text) {
7947
+ Object.assign(this.attrs, this.turn.contentAttrs(key, text));
7948
+ return this;
7949
+ }
7950
+ event(name, attrs = {}) {
7951
+ if (!this.ended)
7952
+ this.events?.push({
7953
+ name,
7954
+ timeUnixNano: unixNano(this.turn.now()),
7955
+ attributes: attributes(attrs)
7956
+ });
7957
+ return this;
7958
+ }
7959
+ fail(message) {
7960
+ this.error = message.slice(0, 1024);
7961
+ return this;
7962
+ }
7963
+ get traceparent() {
7964
+ return this.turn.traceparentFor(this.spanId);
7965
+ }
7966
+ get isEnded() {
7967
+ return this.ended;
7968
+ }
7969
+ end(attrs = {}) {
7970
+ if (this.ended)
7971
+ return;
7972
+ this.ended = true;
7973
+ Object.assign(this.attrs, attrs);
7974
+ this.turn.record({
7975
+ traceId: this.turn.traceId,
7976
+ spanId: this.spanId,
7977
+ parentSpanId: this.parentSpanId,
7978
+ name: this.name,
7979
+ kind: SPAN_KIND[this.kind],
7980
+ startTimeUnixNano: unixNano(this.startMs),
7981
+ endTimeUnixNano: unixNano(this.turn.now()),
7982
+ attributes: attributes(this.attrs),
7983
+ events: this.events,
7984
+ status: this.error ? { code: STATUS_CODE.error, message: this.error } : { code: STATUS_CODE.unset }
7985
+ });
7986
+ }
7987
+ }
7988
+
7989
+ class TurnTelemetry {
7990
+ options;
7991
+ traceId;
7992
+ rootSpanId;
7993
+ level;
7994
+ sampled;
7995
+ agentNative;
7996
+ now;
7997
+ turnSpan;
7998
+ resource;
7999
+ finished = false;
8000
+ extraSecrets = [];
8001
+ promptSpan = null;
8002
+ constructor(options) {
8003
+ this.options = options;
8004
+ const ctx = parseTraceparent(options.claim.traceparent);
8005
+ this.traceId = ctx?.traceId ?? "0".repeat(32);
8006
+ this.rootSpanId = ctx?.spanId ?? "0".repeat(16);
8007
+ this.level = options.claim.content;
8008
+ this.sampled = options.claim.sampled && ctx !== null;
8009
+ this.agentNative = options.claim.agentNative;
8010
+ this.now = options.now ?? Date.now;
8011
+ const a = options.assignment;
8012
+ this.resource = attributes({
8013
+ "service.name": "hypotenuse-daemon",
8014
+ "service.namespace": "hypotenuse",
8015
+ "service.version": package_default.version,
8016
+ "hypotenuse.source": "daemon",
8017
+ ...this.turnAttributes(),
8018
+ "hypotenuse.repository": a.repository?.remote ?? null
8019
+ });
8020
+ this.turnSpan = new SpanHandle(this, "daemon.turn", this.rootSpanId, "internal", {
8021
+ ...this.turnAttributes(),
8022
+ "hypotenuse.work_item.kind": a.kind,
8023
+ "hypotenuse.work_item.fresh": a.fresh,
8024
+ "hypotenuse.telemetry.content": this.level,
8025
+ "hypotenuse.telemetry.agent_native": this.agentNative
8026
+ });
8027
+ }
8028
+ turnAttributes() {
8029
+ const a = this.options.assignment;
8030
+ return {
8031
+ "hypotenuse.session.id": a.agentSessionId,
8032
+ "hypotenuse.work_item.id": a.workItemId,
8033
+ "hypotenuse.role": a.role,
8034
+ "hypotenuse.runner.name": this.options.runner.name,
8035
+ "hypotenuse.agent.id": this.options.agentId,
8036
+ "hypotenuse.agent.preset": this.options.preset ?? "custom",
8037
+ "hypotenuse.repository": a.repository?.remote ?? null,
8038
+ "linear.issue.identifier": a.issue.identifier,
8039
+ "linear.agent_session.id": a.externalSessionId,
8040
+ "linear.team.key": a.issue.teamKey
8041
+ };
8042
+ }
8043
+ addSecret(value) {
8044
+ this.extraSecrets.push(value);
8045
+ }
8046
+ get resourceAttributes() {
8047
+ return this.resource;
8048
+ }
8049
+ collects(level) {
8050
+ return levelRank(this.level) >= levelRank(level);
8051
+ }
8052
+ traceparentFor(spanId = this.turnSpan.spanId) {
8053
+ return formatTraceparent({
8054
+ traceId: this.traceId,
8055
+ spanId,
8056
+ sampled: this.sampled
8057
+ });
8058
+ }
8059
+ span(name, attrs = {}, parent = null, kind = "internal", spanId) {
8060
+ return new SpanHandle(this, name, (parent ?? this.turnSpan).spanId, kind, attrs, spanId);
8061
+ }
8062
+ contentAttrs(key, text) {
8063
+ if (text === null || text === undefined)
8064
+ return {};
8065
+ const out = {
8066
+ [`hypotenuse.${key}.bytes`]: Buffer.byteLength(text)
8067
+ };
8068
+ if (!this.collects("content"))
8069
+ return out;
8070
+ const { text: kept, originalBytes } = capContent(text);
8071
+ out[`${CONTENT_ATTRIBUTE_PREFIX}${key}`] = kept;
8072
+ if (originalBytes !== null)
8073
+ out[`${CONTENT_ATTRIBUTE_PREFIX}${key}${TRUNCATED_SUFFIX}`] = originalBytes;
8074
+ return out;
8075
+ }
8076
+ record(span) {
8077
+ if (!this.sampled || this.finished)
8078
+ return;
8079
+ this.enqueue("traces", {
8080
+ resourceSpans: [
8081
+ {
8082
+ resource: { attributes: this.resource },
8083
+ scopeSpans: [{ scope: SCOPE, spans: [span] }]
8084
+ }
8085
+ ]
8086
+ });
8087
+ }
8088
+ log(record) {
8089
+ const level = record.level ?? "content";
8090
+ if (!this.sampled || this.finished || !this.collects(level))
8091
+ return;
8092
+ const { text, originalBytes } = capContent(record.body);
8093
+ this.enqueue("logs", {
8094
+ resourceLogs: [
8095
+ {
8096
+ resource: { attributes: this.resource },
8097
+ scopeLogs: [
8098
+ {
8099
+ scope: SCOPE,
8100
+ logRecords: [
8101
+ {
8102
+ timeUnixNano: unixNano(this.now()),
8103
+ observedTimeUnixNano: unixNano(this.now()),
8104
+ severityNumber: SEVERITY[record.severity ?? "info"],
8105
+ severityText: (record.severity ?? "info").toUpperCase(),
8106
+ body: { stringValue: text },
8107
+ ...record.eventName ? { eventName: record.eventName } : {},
8108
+ attributes: attributes({
8109
+ ...record.attrs ?? {},
8110
+ [LEVEL_ATTRIBUTE]: level,
8111
+ ...originalBytes !== null ? {
8112
+ [`hypotenuse.body${TRUNCATED_SUFFIX}`]: originalBytes
8113
+ } : {}
8114
+ }),
8115
+ traceId: this.traceId,
8116
+ spanId: (record.span ?? this.turnSpan).spanId
8117
+ }
8118
+ ]
8119
+ }
8120
+ ]
8121
+ }
8122
+ ]
8123
+ });
8124
+ }
8125
+ frame(direction, message, span) {
8126
+ if (!this.collects("full"))
8127
+ return;
8128
+ const m = message;
8129
+ this.log({
8130
+ body: typeof message === "string" ? message : JSON.stringify(message),
8131
+ level: "full",
8132
+ eventName: "acp.frame",
8133
+ attrs: {
8134
+ "hypotenuse.acp.direction": direction.startsWith("<-") ? "in" : "out",
8135
+ "rpc.method": typeof m?.method === "string" ? m.method : null,
8136
+ "rpc.jsonrpc.request_id": typeof m?.id === "string" || typeof m?.id === "number" ? String(m.id) : null
8137
+ },
8138
+ span
8139
+ });
8140
+ }
8141
+ usage(usage) {
8142
+ if (this.finished)
8143
+ return;
8144
+ const points = [
8145
+ ["input", usage.inputTokens],
8146
+ ["output", usage.outputTokens]
8147
+ ].flatMap(([type, n]) => typeof n === "number" ? [
8148
+ {
8149
+ attributes: attributes({
8150
+ "gen_ai.token.type": type,
8151
+ "gen_ai.agent.name": this.options.agentId
8152
+ }),
8153
+ timeUnixNano: unixNano(this.now()),
8154
+ count: "1",
8155
+ sum: n,
8156
+ bucketCounts: [
8157
+ ...TOKEN_BOUNDS.map((b, i) => n <= b && (i === 0 || n > TOKEN_BOUNDS[i - 1]) ? "1" : "0"),
8158
+ n > TOKEN_BOUNDS.at(-1) ? "1" : "0"
8159
+ ],
8160
+ explicitBounds: TOKEN_BOUNDS,
8161
+ min: n,
8162
+ max: n
8163
+ }
8164
+ ] : []);
8165
+ if (points.length === 0)
8166
+ return;
8167
+ this.metric({
8168
+ name: "gen_ai.client.token.usage",
8169
+ unit: "{token}",
8170
+ histogram: {
8171
+ aggregationTemporality: TEMPORALITY.delta,
8172
+ dataPoints: points
8173
+ }
8174
+ });
8175
+ }
8176
+ slots(slots) {
8177
+ const at = unixNano(this.now());
8178
+ this.metric({
8179
+ name: "hypotenuse.runner.slots",
8180
+ unit: "{slot}",
8181
+ gauge: {
8182
+ dataPoints: [
8183
+ {
8184
+ attributes: attributes({ "hypotenuse.slot.state": "capacity" }),
8185
+ timeUnixNano: at,
8186
+ asInt: String(slots.capacity)
8187
+ },
8188
+ {
8189
+ attributes: attributes({ "hypotenuse.slot.state": "in_use" }),
8190
+ timeUnixNano: at,
8191
+ asInt: String(slots.inUse)
8192
+ }
8193
+ ]
8194
+ }
8195
+ });
8196
+ }
8197
+ metric(metric) {
8198
+ this.enqueue("metrics", {
8199
+ resourceMetrics: [
8200
+ {
8201
+ resource: { attributes: this.resource },
8202
+ scopeMetrics: [{ scope: SCOPE, metrics: [metric] }]
8203
+ }
8204
+ ]
8205
+ });
8206
+ }
8207
+ enqueue(signal, body) {
8208
+ const refused = this.options.queue.isRefused(this.options.assignment.workItemId);
8209
+ this.options.queue.add({
8210
+ workItemId: this.options.assignment.workItemId,
8211
+ traceparent: this.traceparentFor(),
8212
+ signal,
8213
+ body: refused ? body : redactOtlp(body, {
8214
+ values: [...this.options.secrets(), ...this.extraSecrets]
8215
+ })
8216
+ }, this.resource);
8217
+ }
8218
+ finish(outcome, error) {
8219
+ if (this.finished)
8220
+ return;
8221
+ if (error)
8222
+ this.turnSpan.fail(error);
8223
+ this.turnSpan.end({ "hypotenuse.outcome": outcome });
8224
+ this.finished = true;
8225
+ }
8226
+ }
8227
+
8228
+ // src/telemetry/queue.ts
8229
+ var FLUSH_INTERVAL_MS = 2000;
8230
+ var FLUSH_BYTES = 1024 * 1024;
8231
+
8232
+ class TelemetryQueue {
8233
+ sender;
8234
+ capacityBytes;
8235
+ log;
8236
+ now;
8237
+ batches = [];
8238
+ bytes = 0;
8239
+ timer = null;
8240
+ flushing = null;
8241
+ backoffUntil = 0;
8242
+ failures = 0;
8243
+ refused = new Set;
8244
+ losses = new Map;
8245
+ droppedTotal = new Map;
8246
+ constructor(sender, capacityBytes, log = () => {}, now = Date.now) {
8247
+ this.sender = sender;
8248
+ this.capacityBytes = capacityBytes;
8249
+ this.log = log;
8250
+ this.now = now;
8251
+ }
8252
+ start() {
8253
+ if (this.timer)
8254
+ return;
8255
+ this.timer = setInterval(() => void this.flush(), FLUSH_INTERVAL_MS);
8256
+ this.timer.unref?.();
8257
+ }
8258
+ stop() {
8259
+ if (this.timer)
8260
+ clearInterval(this.timer);
8261
+ this.timer = null;
8262
+ }
8263
+ get pendingBytes() {
8264
+ return this.bytes;
8265
+ }
8266
+ isRefused(workItemId) {
8267
+ return this.refused.has(workItemId);
8268
+ }
8269
+ add(batch, resource = []) {
8270
+ if (this.refused.has(batch.workItemId)) {
8271
+ this.countLoss(batch, resource);
8272
+ return;
8273
+ }
8274
+ const bytes = JSON.stringify(batch.body).length;
8275
+ this.batches.push({ ...batch, bytes });
8276
+ this.bytes += bytes;
8277
+ if (!this.losses.has(batch.workItemId))
8278
+ this.losses.set(batch.workItemId, {
8279
+ spans: 0,
8280
+ logs: 0,
8281
+ resource,
8282
+ traceparent: batch.traceparent
8283
+ });
8284
+ this.evict();
8285
+ if (this.bytes >= FLUSH_BYTES)
8286
+ this.flush();
8287
+ }
8288
+ countLoss(batch, resource = []) {
8289
+ const loss = this.losses.get(batch.workItemId) ?? {
8290
+ spans: 0,
8291
+ logs: 0,
8292
+ resource,
8293
+ traceparent: batch.traceparent
8294
+ };
8295
+ const n = countRecords(batch.signal, batch.body);
8296
+ if (batch.signal === "traces")
8297
+ loss.spans += n;
8298
+ else if (batch.signal === "logs")
8299
+ loss.logs += n;
8300
+ this.losses.set(batch.workItemId, loss);
8301
+ this.droppedTotal.set(batch.workItemId, (this.droppedTotal.get(batch.workItemId) ?? 0) + n);
8302
+ }
8303
+ evict() {
8304
+ for (const signal of ["logs", "traces"]) {
8305
+ while (this.bytes > this.capacityBytes) {
8306
+ const index = this.batches.findIndex((b) => b.signal === signal);
8307
+ if (index === -1)
8308
+ break;
8309
+ const [gone] = this.batches.splice(index, 1);
8310
+ if (!gone)
8311
+ break;
8312
+ this.bytes -= gone.bytes;
8313
+ this.countLoss(gone);
8314
+ }
8315
+ }
8316
+ }
8317
+ reportLosses() {
8318
+ for (const [workItemId, loss] of this.losses) {
8319
+ if (this.refused.has(workItemId))
8320
+ continue;
8321
+ if (loss.spans === 0 && loss.logs === 0)
8322
+ continue;
8323
+ const at = unixNano(this.now());
8324
+ const point = (kind, n) => ({
8325
+ attributes: attributes({ "hypotenuse.telemetry.kind": kind }),
8326
+ timeUnixNano: at,
8327
+ asInt: String(n)
8328
+ });
8329
+ const metrics = {
8330
+ resourceMetrics: [
8331
+ {
8332
+ resource: { attributes: loss.resource },
8333
+ scopeMetrics: [
8334
+ {
8335
+ scope: { name: "hypotenuse.daemon" },
8336
+ metrics: [
8337
+ {
8338
+ name: "hypotenuse.telemetry.dropped",
8339
+ unit: "{record}",
8340
+ sum: {
8341
+ aggregationTemporality: TEMPORALITY.delta,
8342
+ isMonotonic: true,
8343
+ dataPoints: [
8344
+ ...loss.spans ? [point("span", loss.spans)] : [],
8345
+ ...loss.logs ? [point("log", loss.logs)] : []
8346
+ ]
8347
+ }
8348
+ }
8349
+ ]
8350
+ }
8351
+ ]
8352
+ }
8353
+ ]
8354
+ };
8355
+ loss.spans = 0;
8356
+ loss.logs = 0;
8357
+ const bytes = JSON.stringify(metrics).length;
8358
+ this.batches.push({
8359
+ workItemId,
8360
+ traceparent: loss.traceparent,
8361
+ signal: "metrics",
8362
+ body: metrics,
8363
+ bytes
8364
+ });
8365
+ this.bytes += bytes;
8366
+ }
8367
+ }
8368
+ flush() {
8369
+ this.flushing ??= this.flushOnce().finally(() => {
8370
+ this.flushing = null;
8371
+ });
8372
+ return this.flushing;
8373
+ }
8374
+ async flushOnce() {
8375
+ if (this.now() < this.backoffUntil)
8376
+ return;
8377
+ this.reportLosses();
8378
+ if (this.batches.length === 0)
8379
+ return;
8380
+ const taken = this.batches;
8381
+ this.batches = [];
8382
+ this.bytes = 0;
8383
+ const groups = new Map;
8384
+ for (const b of taken) {
8385
+ const key = `${b.workItemId}|${b.signal}`;
8386
+ groups.set(key, [...groups.get(key) ?? [], b]);
8387
+ }
8388
+ const retry = [];
8389
+ let failed = false;
8390
+ for (const group of groups.values()) {
8391
+ const first = group[0];
8392
+ if (this.refused.has(first.workItemId)) {
8393
+ for (const b of group)
8394
+ this.countLoss(b);
8395
+ continue;
8396
+ }
8397
+ if (failed) {
8398
+ retry.push(...group);
8399
+ continue;
8400
+ }
8401
+ const body = mergeRequests(first.signal, group.map((b) => b.body));
8402
+ try {
8403
+ await this.sender.telemetry({
8404
+ workItemId: first.workItemId,
8405
+ traceparent: first.traceparent,
8406
+ signal: first.signal,
8407
+ gzipped: Bun.gzipSync(new TextEncoder().encode(JSON.stringify(body)))
8408
+ });
8409
+ this.failures = 0;
8410
+ } catch (error) {
8411
+ if (error instanceof ApiError && (error.status === 409 || error.status === 410 || error.status === 400)) {
8412
+ this.refused.add(first.workItemId);
8413
+ for (const b of group)
8414
+ this.countLoss(b);
8415
+ this.log(`telemetry for work item ${first.workItemId} refused (${error.status}): ${error.message}`);
8416
+ continue;
8417
+ }
8418
+ failed = true;
8419
+ retry.push(...group);
8420
+ this.failures++;
8421
+ const wait = error instanceof ApiError && error.retryAfterMs ? error.retryAfterMs : Math.min(1000 * 2 ** (this.failures - 1), 60000);
8422
+ this.backoffUntil = this.now() + wait;
8423
+ }
8424
+ }
8425
+ this.batches = [...retry, ...this.batches];
8426
+ this.bytes = this.batches.reduce((n, b) => n + b.bytes, 0);
8427
+ this.evict();
8428
+ }
8429
+ async drain(timeoutMs) {
8430
+ const deadline = this.now() + timeoutMs;
8431
+ for (;; ) {
8432
+ this.backoffUntil = Math.min(this.backoffUntil, this.now());
8433
+ await this.flush();
8434
+ if (this.batches.length === 0)
8435
+ return true;
8436
+ if (this.now() >= deadline)
8437
+ return false;
8438
+ await Bun.sleep(Math.min(500, Math.max(0, deadline - this.now())));
8439
+ }
8440
+ }
8441
+ }
8442
+ function countRecords(signal, body) {
8443
+ if (signal === "traces")
8444
+ return (body.resourceSpans ?? []).reduce((n, rs) => n + (rs.scopeSpans ?? []).reduce((m, s) => m + (s.spans?.length ?? 0), 0), 0);
8445
+ if (signal === "logs")
8446
+ return (body.resourceLogs ?? []).reduce((n, rl) => n + (rl.scopeLogs ?? []).reduce((m, s) => m + (s.logRecords?.length ?? 0), 0), 0);
8447
+ return 0;
8448
+ }
8449
+
8450
+ // src/telemetry/redact.ts
8451
+ function resolvedConfigSecrets(config, env) {
8452
+ const out = [];
8453
+ const take = (table) => {
8454
+ for (const value of Object.values(table ?? {})) {
8455
+ if (!value.includes("$"))
8456
+ continue;
8457
+ const resolved = expandVariables(value, env);
8458
+ if (resolved)
8459
+ out.push(resolved);
8460
+ }
8461
+ };
8462
+ for (const agent of Object.values(config.agents))
8463
+ take(agent.env);
8464
+ for (const role of [config.roles.manager, config.roles.worker]) {
8465
+ for (const server of role?.extra_mcp_servers ?? []) {
8466
+ take(server.env);
8467
+ take(server.headers);
8468
+ }
8469
+ }
8470
+ return out;
8471
+ }
6957
8472
 
6958
8473
  // src/worktree/cleanup.ts
6959
8474
  import { existsSync as existsSync15, readdirSync, rmSync as rmSync7 } from "fs";
6960
- import { join as join9, relative as relative3, resolve as resolve3 } from "path";
8475
+ import { join as join9, relative as relative4, resolve as resolve3 } from "path";
6961
8476
 
6962
8477
  // src/worktree/merge-state.ts
6963
8478
  var MERGE_STATE_TTL_MS = 10 * 60000;
@@ -7136,7 +8651,7 @@ class WorktreeCleaner {
7136
8651
  const branch = /^branch refs\/heads\/(.+)$/m.exec(block)?.[1];
7137
8652
  if (!path || !branch || known.has(path))
7138
8653
  continue;
7139
- const rel = relative3(root, path);
8654
+ const rel = relative4(root, path);
7140
8655
  if (rel.startsWith("..") || rel === "")
7141
8656
  continue;
7142
8657
  const session = await this.git(["config", "--worktree", "--get", "hypotenuse.session"], { cwd: path });
@@ -7361,6 +8876,11 @@ function buildCapabilities(config, machine, repositories) {
7361
8876
  teams: config.subscriptions.teams,
7362
8877
  projects: config.subscriptions.projects,
7363
8878
  labels: config.subscriptions.labels
8879
+ },
8880
+ telemetry: {
8881
+ enabled: config.telemetry.enabled,
8882
+ maxContent: config.telemetry.max_content,
8883
+ agentNative: config.telemetry.agent_native
7364
8884
  }
7365
8885
  };
7366
8886
  }
@@ -7434,6 +8954,7 @@ class EventSink {
7434
8954
  backoff = new Backoff;
7435
8955
  batchLimit = new Map;
7436
8956
  closed = false;
8957
+ traceparents = new Map;
7437
8958
  constructor(state, api, log, hooks) {
7438
8959
  this.state = state;
7439
8960
  this.api = api;
@@ -7460,6 +8981,12 @@ class EventSink {
7460
8981
  this.kick();
7461
8982
  return { eventId, seq };
7462
8983
  }
8984
+ trace(workItemId, traceparent) {
8985
+ if (traceparent)
8986
+ this.traceparents.set(workItemId, traceparent);
8987
+ else
8988
+ this.traceparents.delete(workItemId);
8989
+ }
7463
8990
  complete(completion) {
7464
8991
  this.state.queueCompletion(completion.request.workItemId, completion);
7465
8992
  this.kick();
@@ -7574,7 +9101,7 @@ class EventSink {
7574
9101
  const answer = await this.api.events({
7575
9102
  workItemId,
7576
9103
  events: batch.map((row) => row.payload)
7577
- });
9104
+ }, this.traceparents.get(workItemId));
7578
9105
  this.state.deleteEvents(ids);
7579
9106
  this.log.debug("events sent", {
7580
9107
  workItem: workItemId,
@@ -7632,7 +9159,8 @@ class EventSink {
7632
9159
  return;
7633
9160
  const { request } = row.payload;
7634
9161
  try {
7635
- const answer = await this.api.complete(request);
9162
+ const answer = await this.api.complete(request, this.traceparents.get(workItemId));
9163
+ this.traceparents.delete(workItemId);
7636
9164
  this.log.info(`reported work item ${request.outcome}`, {
7637
9165
  workItem: workItemId,
7638
9166
  alreadyTerminal: answer.alreadyTerminal
@@ -7702,7 +9230,7 @@ class ReadCache {
7702
9230
  }
7703
9231
 
7704
9232
  // src/mcp/tools.ts
7705
- import { z as z10 } from "zod";
9233
+ import { z as z11 } from "zod";
7706
9234
  var CACHED_TOOLS = new Set([
7707
9235
  "linear_get_issue",
7708
9236
  "linear_list_comments",
@@ -7710,7 +9238,7 @@ var CACHED_TOOLS = new Set([
7710
9238
  "linear_list_labels"
7711
9239
  ]);
7712
9240
  function describeTool(tool) {
7713
- const { $schema: _schema, ...schema } = z10.toJSONSchema(tool.input);
9241
+ const { $schema: _schema, ...schema } = z11.toJSONSchema(tool.input);
7714
9242
  return {
7715
9243
  name: tool.name,
7716
9244
  description: tool.description,
@@ -7789,11 +9317,14 @@ class McpBroker {
7789
9317
  if (!checked.ok)
7790
9318
  return { text: checked.message, isError: true };
7791
9319
  const { tool, input } = checked;
9320
+ const span = turn.trace?.(tool.name, input) ?? null;
7792
9321
  const cached = CACHED_TOOLS.has(tool.name);
7793
9322
  if (cached) {
7794
9323
  const hit = this.cache.get(turn.workItemId, tool.name, input);
7795
- if (hit)
9324
+ if (hit) {
9325
+ span?.end({ isError: false, cached: true, text: hit.text });
7796
9326
  return hit;
9327
+ }
7797
9328
  }
7798
9329
  if (tool.kind === "write")
7799
9330
  this.cache.invalidate(turn.workItemId);
@@ -7808,15 +9339,17 @@ class McpBroker {
7808
9339
  const result = await this.transport.tool(tool.route, {
7809
9340
  ...input,
7810
9341
  agentSessionId: turn.agentSessionId
7811
- });
9342
+ }, span?.traceparent);
7812
9343
  answer = { text: result.text, data: result.data };
7813
9344
  } catch (error) {
7814
9345
  if (asking)
7815
9346
  turn.askingUser(false);
7816
9347
  answer = { text: toolErrorText(error), isError: true };
7817
9348
  this.log(`${tool.name} failed: ${answer.text}`);
9349
+ span?.end({ isError: true, cached: false, text: answer.text });
7818
9350
  return answer;
7819
9351
  }
9352
+ span?.end({ isError: false, cached: false, text: answer.text });
7820
9353
  if (cached)
7821
9354
  this.cache.set(turn.workItemId, tool.name, input, answer);
7822
9355
  if (tool.name === "linear_report_pr" && answer.data?.linked)
@@ -8133,6 +9666,8 @@ class Supervisor {
8133
9666
  sweepTimer = null;
8134
9667
  diskWasLow = false;
8135
9668
  broker;
9669
+ telemetryQueue;
9670
+ secrets = [];
8136
9671
  constructor(options) {
8137
9672
  this.options = options;
8138
9673
  this.config = options.config;
@@ -8154,6 +9689,8 @@ class Supervisor {
8154
9689
  }
8155
9690
  });
8156
9691
  this.broker = new McpBroker(options.api, (line) => options.log.info(line));
9692
+ this.telemetryQueue = new TelemetryQueue(options.api, this.config.telemetry.buffer_mb * 1024 * 1024, (line) => options.log.info(line));
9693
+ this.secrets = this.baseSecrets();
8157
9694
  this.sink = new EventSink(options.state, options.api, options.log, {
8158
9695
  onCancelRequested: (id) => this.stop(id, "stop"),
8159
9696
  onNotYours: (id) => this.silence(id),
@@ -8183,10 +9720,42 @@ class Supervisor {
8183
9720
  }
8184
9721
  this.link.start();
8185
9722
  this.sink.kick(true);
9723
+ this.telemetryQueue.start();
8186
9724
  this.sweepTimer = setInterval(() => void this.cleanup({}), this.options.sweepIntervalMs ?? SWEEP_INTERVAL_MS);
8187
9725
  this.sweepTimer.unref?.();
8188
9726
  return interrupted;
8189
9727
  }
9728
+ baseSecrets() {
9729
+ return [
9730
+ ...this.options.secrets ?? [],
9731
+ ...resolvedConfigSecrets(this.config, this.options.env)
9732
+ ];
9733
+ }
9734
+ turnTelemetry(assignment, agentId, slot) {
9735
+ const claim = assignment.telemetry;
9736
+ if (!claim || !this.config.telemetry.enabled)
9737
+ return null;
9738
+ const preset = this.config.agents[agentId]?.preset ?? (isPreset(agentId) ? agentId : null);
9739
+ const max = this.config.telemetry.max_content;
9740
+ const telemetry = new TurnTelemetry({
9741
+ claim: {
9742
+ ...claim,
9743
+ content: minLevel(claim.content, max),
9744
+ agentNative: claim.agentNative && this.config.telemetry.agent_native
9745
+ },
9746
+ assignment,
9747
+ runner: { name: this.config.runner.name },
9748
+ agentId,
9749
+ preset,
9750
+ queue: this.telemetryQueue,
9751
+ secrets: () => slot.nonce ? [...this.secrets, slot.nonce] : this.secrets
9752
+ });
9753
+ telemetry.slots({
9754
+ capacity: this.config.runner.max_parallel_jobs,
9755
+ inUse: this.slots.size
9756
+ });
9757
+ return telemetry;
9758
+ }
8190
9759
  worktreeInUse(wt) {
8191
9760
  if (wt.state === "active")
8192
9761
  return true;
@@ -8392,6 +9961,9 @@ class Supervisor {
8392
9961
  };
8393
9962
  this.slots.set(assignment.workItemId, slot);
8394
9963
  const mcpCommand = this.options.mcpCommand === undefined ? selfCommand() : this.options.mcpCommand;
9964
+ const telemetry = this.turnTelemetry(assignment, agentId, slot);
9965
+ if (telemetry)
9966
+ this.sink.trace(assignment.workItemId, telemetry.traceparentFor());
8395
9967
  if (mcpCommand) {
8396
9968
  slot.nonce = this.broker.register({
8397
9969
  agentSessionId: assignment.agentSessionId,
@@ -8405,7 +9977,25 @@ class Supervisor {
8405
9977
  reportedPullRequest: (url) => {
8406
9978
  this.options.state.addPullRequest(slot.agentSessionId, url);
8407
9979
  slot.onReportedPullRequest?.(url);
8408
- }
9980
+ },
9981
+ trace: telemetry ? (tool, input) => {
9982
+ const span = telemetry.span("mcp.call", {
9983
+ "gen_ai.operation.name": "execute_tool",
9984
+ "gen_ai.tool.name": tool,
9985
+ "hypotenuse.mcp.server": "hypotenuse"
9986
+ }, telemetry.promptSpan, "client").content("mcp.arguments", JSON.stringify(input));
9987
+ return {
9988
+ traceparent: span.traceparent,
9989
+ end: (result) => {
9990
+ if (result.isError)
9991
+ span.fail(result.text.slice(0, 500));
9992
+ span.content("mcp.result", result.text).end({
9993
+ "hypotenuse.mcp.cached": result.cached,
9994
+ "hypotenuse.mcp.result.bytes": Buffer.byteLength(result.text)
9995
+ });
9996
+ }
9997
+ };
9998
+ } : undefined
8409
9999
  });
8410
10000
  }
8411
10001
  log.info(`claimed ${assignment.kind} for ${assignment.issue.identifier ?? "an issue"}`, {
@@ -8473,7 +10063,8 @@ class Supervisor {
8473
10063
  },
8474
10064
  onReportedPullRequest: (handler) => {
8475
10065
  slot.onReportedPullRequest = handler;
8476
- }
10066
+ },
10067
+ telemetry
8477
10068
  };
8478
10069
  slot.done = this.runTurn(slot, ctx);
8479
10070
  }
@@ -8527,8 +10118,18 @@ class Supervisor {
8527
10118
  });
8528
10119
  this.link.beatNow();
8529
10120
  const worktreeId = this.options.state.session(slot.agentSessionId)?.worktreeId;
8530
- if (worktreeId)
8531
- await this.cleanup({ only: [worktreeId] });
10121
+ const telemetry = ctx.telemetry ?? null;
10122
+ if (worktreeId) {
10123
+ const span = telemetry?.span("worktree.cleanup", {
10124
+ "hypotenuse.worktree.id": worktreeId
10125
+ });
10126
+ const report = await this.cleanup({ only: [worktreeId] });
10127
+ span?.end({
10128
+ "hypotenuse.worktree.removed": (report?.removed.length ?? 0) > 0,
10129
+ "hypotenuse.worktree.kept": (report?.kept.length ?? 0) > 0
10130
+ });
10131
+ }
10132
+ telemetry?.finish(slot.silent ? "taken_away" : result.outcome, result.error);
8532
10133
  }
8533
10134
  stop(workItemId, reason) {
8534
10135
  const slot = this.slots.get(workItemId);
@@ -8587,6 +10188,7 @@ class Supervisor {
8587
10188
  await this.waitIdle(this.config.sessions.stop_grace + 5000);
8588
10189
  }
8589
10190
  const flushed = await this.sink.drain(5000);
10191
+ await this.telemetryQueue.drain(5000);
8590
10192
  this.link.beatNow();
8591
10193
  return { drained: this.slots.size === 0 && flushed, cancelled };
8592
10194
  }
@@ -8648,6 +10250,8 @@ class Supervisor {
8648
10250
  }
8649
10251
  this.config = merged;
8650
10252
  this.options.log.level = merged.log.level;
10253
+ this.telemetryQueue.capacityBytes = merged.telemetry.buffer_mb * 1024 * 1024;
10254
+ this.secrets = this.baseSecrets();
8651
10255
  this.repositories = daemonRepositories(merged, this.options.machine);
8652
10256
  this.capabilities = this.buildCapabilities();
8653
10257
  this.options.log.info("config reloaded", {
@@ -8742,6 +10346,7 @@ class Supervisor {
8742
10346
  this.sweepTimer = null;
8743
10347
  this.link.stop();
8744
10348
  this.sink.close();
10349
+ this.telemetryQueue.stop();
8745
10350
  }
8746
10351
  }
8747
10352
  function changedKeys(a, b, prefix = "") {
@@ -8756,7 +10361,7 @@ function changedKeys(a, b, prefix = "") {
8756
10361
  return JSON.stringify(a) === JSON.stringify(b) ? [] : [prefix];
8757
10362
  }
8758
10363
  function resolveCommand(command, env) {
8759
- if (isAbsolute6(command))
10364
+ if (isAbsolute7(command))
8760
10365
  return command;
8761
10366
  return Bun.which(command, { PATH: env.PATH ?? "" }) ?? command;
8762
10367
  }
@@ -8854,6 +10459,7 @@ async function runDaemon(options) {
8854
10459
  let state;
8855
10460
  let origin;
8856
10461
  let runner;
10462
+ let credentials;
8857
10463
  try {
8858
10464
  const read = readDaemonConfig(options, env);
8859
10465
  if (!read.config)
@@ -8862,7 +10468,6 @@ async function runDaemon(options) {
8862
10468
  paths = daemonPaths(read.path, options.stateDir, config.log.file, env);
8863
10469
  log = new DaemonLog(paths.log, config.log.level, !options.detachedChild);
8864
10470
  const credentialsFile = credentialsPath(env);
8865
- let credentials;
8866
10471
  try {
8867
10472
  credentials = loadCredentials(credentialsFile);
8868
10473
  } catch (error) {
@@ -8938,7 +10543,11 @@ async function runDaemon(options) {
8938
10543
  stateDir: paths.stateDir,
8939
10544
  env,
8940
10545
  readConfig: () => readDaemonConfig(options, env),
8941
- requestExit
10546
+ requestExit,
10547
+ secrets: [
10548
+ runner.token,
10549
+ ...Object.values(credentials.servers).flatMap((s) => s.login ? [s.login.token] : [])
10550
+ ]
8942
10551
  });
8943
10552
  control = new ControlServer(paths.socket, {
8944
10553
  status: () => supervisor.status({ pid: process.pid, configPath: paths.config }),
@@ -10337,6 +11946,20 @@ async function runDoctor(ctx, options) {
10337
11946
  if (options.agent && !resolveAgents(config).some((a) => a.id === options.agent)) {
10338
11947
  list.fail("agents", `no agent "${options.agent}" in the config`);
10339
11948
  }
11949
+ const t = config.telemetry;
11950
+ if (!t.enabled)
11951
+ list.ok("telemetry", "off: this runner sends no telemetry (`[telemetry] enabled = false`)");
11952
+ else {
11953
+ list.ok("telemetry", `sends up to \`${t.max_content}\` for the project's exporters; buffer ${t.buffer_mb} MB`);
11954
+ const agents = resolveAgents(config).filter((a) => a.roles.length > 0);
11955
+ if (!t.agent_native)
11956
+ list.ok("telemetry", "agents' own telemetry is not captured");
11957
+ else
11958
+ for (const agent of agents) {
11959
+ const support = nativeTelemetrySupport(agent.preset);
11960
+ 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`);
11961
+ }
11962
+ }
10340
11963
  for (const repo of daemonRepositories(config, machine)) {
10341
11964
  if (options.offline) {
10342
11965
  list.ok("repositories", `${repo.name}: ${repo.remote} (${repo.baseBranch})`);
@@ -12003,6 +13626,161 @@ function registerTagCommands(program) {
12003
13626
  }
12004
13627
  }
12005
13628
 
13629
+ // src/commands/telemetry.ts
13630
+ function collect2(value, previous = []) {
13631
+ return [...previous, value];
13632
+ }
13633
+ function parsePairs(pairs, what) {
13634
+ const out = {};
13635
+ for (const pair of pairs) {
13636
+ const at = pair.indexOf("=");
13637
+ if (at <= 0)
13638
+ throw new Error(`${what} must be KEY=VALUE, got "${pair}"`);
13639
+ out[pair.slice(0, at).trim()] = pair.slice(at + 1);
13640
+ }
13641
+ return out;
13642
+ }
13643
+ function exporterHealth(e) {
13644
+ if (!e.enabled)
13645
+ return "paused";
13646
+ if (e.lastErrorAt && (!e.lastExportAt || e.lastErrorAt > e.lastExportAt))
13647
+ return `error ${when(e.lastErrorAt)}: ${e.lastError ?? ""}`;
13648
+ return e.lastExportAt ? `ok ${when(e.lastExportAt)}` : "nothing yet";
13649
+ }
13650
+ function describeExporter(e) {
13651
+ const attrs = Object.entries(e.resourceAttributes).map(([k, v]) => `${k}=${v}`).join(", ");
13652
+ return [
13653
+ `${e.name} (id ${e.id})${e.enabled ? "" : " paused"}`,
13654
+ `endpoint ${e.endpoint}`,
13655
+ `protocol ${e.protocol}`,
13656
+ `content ${e.content}`,
13657
+ `signals ${e.signals.join(", ")}`,
13658
+ `sampled ${Math.round(e.traceSampleRatio * 100)}% of turns`,
13659
+ `agent's own ${e.agentNative ? "captured" : "not captured"}`,
13660
+ `headers ${e.headerNames.length ? e.headerNames.join(", ") : "none"} (values are never shown)`,
13661
+ `attributes ${attrs || "none"}`,
13662
+ `last export ${when(e.lastExportAt)}`,
13663
+ `last error ${e.lastError ? `${when(e.lastErrorAt)}: ${e.lastError}` : "-"}`
13664
+ ].join(`
13665
+ `);
13666
+ }
13667
+ function exporterBody(opts) {
13668
+ const body = {};
13669
+ if (opts.name !== undefined)
13670
+ body.name = opts.name;
13671
+ if (opts.endpoint !== undefined)
13672
+ body.endpoint = opts.endpoint;
13673
+ if (opts.protocol !== undefined)
13674
+ body.protocol = opts.protocol;
13675
+ if (opts.clearHeaders)
13676
+ body.headers = {};
13677
+ else if (opts.header?.length)
13678
+ body.headers = parsePairs(opts.header, "--header");
13679
+ if (opts.signals !== undefined)
13680
+ body.signals = opts.signals.split(",").map((s) => s.trim()).filter(Boolean);
13681
+ if (opts.content !== undefined)
13682
+ body.content = opts.content;
13683
+ if (opts.agentNative !== undefined)
13684
+ body.agentNative = opts.agentNative;
13685
+ if (opts.sample !== undefined)
13686
+ body.traceSampleRatio = Number(opts.sample);
13687
+ if (opts.attribute?.length)
13688
+ body.resourceAttributes = parsePairs(opts.attribute, "--attribute");
13689
+ if (opts.enabled !== undefined)
13690
+ body.enabled = opts.enabled;
13691
+ return body;
13692
+ }
13693
+ function exporterOptions(command, adding) {
13694
+ if (adding)
13695
+ 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)");
13696
+ else
13697
+ command.option("--name <name>", "a new name").option("--endpoint <url>", "a new endpoint");
13698
+ 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");
13699
+ }
13700
+ function registerTelemetryCommands(program) {
13701
+ const telemetry = program.command("telemetry").description("Where the selected project's OpenTelemetry goes");
13702
+ const exporter = telemetry.command("exporter").description("The project's OpenTelemetry collectors");
13703
+ withPaging(exporter.command("list").description("List exporters")).action(async function() {
13704
+ await runCommand(this, async (ctx) => {
13705
+ const projectId = await ctx.projectId();
13706
+ const result = await ctx.signedInClient().request("GET", "/telemetry-exporters", {
13707
+ query: { projectId, ...pageQuery(this) }
13708
+ });
13709
+ emit(ctx, result, () => table(result.telemetryExporters.map((e) => ({
13710
+ id: e.id,
13711
+ name: e.name,
13712
+ endpoint: e.endpoint,
13713
+ content: e.content,
13714
+ signals: e.signals.join(","),
13715
+ sampled: `${Math.round(e.traceSampleRatio * 100)}%`,
13716
+ headers: e.headerNames.join(",") || "-",
13717
+ health: exporterHealth(e)
13718
+ })), [
13719
+ { key: "id" },
13720
+ { key: "name", maxWidth: 30 },
13721
+ { key: "endpoint", maxWidth: 50 },
13722
+ { key: "content" },
13723
+ { key: "signals" },
13724
+ { key: "sampled" },
13725
+ { key: "headers", maxWidth: 30 },
13726
+ { key: "health", maxWidth: 60 }
13727
+ ]) + pageFooter(result.pagination));
13728
+ });
13729
+ });
13730
+ exporter.command("view <id>").description("Show one exporter (header names only)").action(async function(id) {
13731
+ await runCommand(this, async (ctx) => {
13732
+ const projectId = await ctx.projectId();
13733
+ const result = await ctx.signedInClient().request("GET", `/telemetry-exporter/${id}`, { query: { projectId } });
13734
+ emit(ctx, result, () => describeExporter(result.telemetryExporter));
13735
+ });
13736
+ });
13737
+ exporterOptions(exporter.command("add").description("Export the project's telemetry to a collector (admin)"), true).option("--disabled", "create it paused").action(async function() {
13738
+ await runCommand(this, async (ctx) => {
13739
+ const opts = this.opts();
13740
+ const projectId = await ctx.projectId();
13741
+ const result = await ctx.signedInClient().request("PUT", "/telemetry-exporter", {
13742
+ body: {
13743
+ projectId,
13744
+ ...exporterBody(opts),
13745
+ ...opts.disabled ? { enabled: false } : {}
13746
+ }
13747
+ });
13748
+ 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`);
13749
+ });
13750
+ });
13751
+ 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) {
13752
+ await runCommand(this, async (ctx) => {
13753
+ const opts = this.opts();
13754
+ const projectId = await ctx.projectId();
13755
+ const body = exporterBody({
13756
+ ...opts,
13757
+ enabled: opts.enable ? true : opts.disable ? false : undefined
13758
+ });
13759
+ const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}`, { body: { projectId, ...body } });
13760
+ emit(ctx, result, () => describeExporter(result.telemetryExporter));
13761
+ });
13762
+ });
13763
+ exporter.command("remove <id>").description("Stop exporting to a collector and forget its headers (admin)").action(async function(id) {
13764
+ await runCommand(this, async (ctx) => {
13765
+ const projectId = await ctx.projectId();
13766
+ const result = await ctx.signedInClient().request("DELETE", `/telemetry-exporter/${id}`, {
13767
+ body: { projectId }
13768
+ });
13769
+ emit(ctx, result, () => `Removed exporter ${id}`);
13770
+ });
13771
+ });
13772
+ exporter.command("test <id>").description("Send the collector a probe span, log, and metric, and show its answers (admin)").action(async function(id) {
13773
+ await runCommand(this, async (ctx) => {
13774
+ const projectId = await ctx.projectId();
13775
+ const result = await ctx.signedInClient().request("POST", `/telemetry-exporter/${id}/test`, { body: { projectId } });
13776
+ emit(ctx, result, () => result.results.map((r) => `${glyph(r.ok ? "complete" : "error")} ${r.signal.padEnd(8)} ${r.status || "-"}${r.error ? ` ${r.error}` : ""}`).join(`
13777
+ `));
13778
+ if (!result.ok)
13779
+ process.exitCode = 1;
13780
+ });
13781
+ });
13782
+ }
13783
+
12006
13784
  // src/commands/upgrade.ts
12007
13785
  var UPGRADE_DRAIN_MS = 10 * 60000;
12008
13786
  async function restartRunningDaemon(socketPath, call = callDaemon) {
@@ -12272,6 +14050,7 @@ function buildProgram() {
12272
14050
  registerEventCommands(program);
12273
14051
  registerRunnerCommands(program);
12274
14052
  registerRoutingCommands(program);
14053
+ registerTelemetryCommands(program);
12275
14054
  registerIdentityCommands(program);
12276
14055
  registerConfigCommands(program);
12277
14056
  registerDaemonCommands(program);