@sapiom/harness 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +36 -0
  3. package/dist/cli/bin.js +26 -2
  4. package/dist/cli/bin.js.map +1 -1
  5. package/dist/cli/consent.d.ts +29 -1
  6. package/dist/cli/consent.d.ts.map +1 -1
  7. package/dist/cli/consent.js +33 -11
  8. package/dist/cli/consent.js.map +1 -1
  9. package/dist/core/adapters/adapter.d.ts +72 -0
  10. package/dist/core/adapters/adapter.d.ts.map +1 -0
  11. package/dist/core/adapters/adapter.js +18 -0
  12. package/dist/core/adapters/adapter.js.map +1 -0
  13. package/dist/core/adapters/claude-code-info.d.ts +11 -0
  14. package/dist/core/adapters/claude-code-info.d.ts.map +1 -0
  15. package/dist/core/adapters/claude-code-info.js +27 -0
  16. package/dist/core/adapters/claude-code-info.js.map +1 -0
  17. package/dist/core/adapters/claude-code.d.ts.map +1 -1
  18. package/dist/core/adapters/claude-code.js +9 -8
  19. package/dist/core/adapters/claude-code.js.map +1 -1
  20. package/dist/core/adapters/codex-info.d.ts +10 -0
  21. package/dist/core/adapters/codex-info.d.ts.map +1 -0
  22. package/dist/core/adapters/codex-info.js +32 -0
  23. package/dist/core/adapters/codex-info.js.map +1 -0
  24. package/dist/core/adapters/conductor.d.ts +8 -0
  25. package/dist/core/adapters/conductor.d.ts.map +1 -0
  26. package/dist/core/adapters/conductor.js +57 -0
  27. package/dist/core/adapters/conductor.js.map +1 -0
  28. package/dist/core/adapters/detect.d.ts +15 -0
  29. package/dist/core/adapters/detect.d.ts.map +1 -0
  30. package/dist/core/adapters/detect.js +62 -0
  31. package/dist/core/adapters/detect.js.map +1 -0
  32. package/dist/core/adapters/opencode.d.ts +8 -0
  33. package/dist/core/adapters/opencode.d.ts.map +1 -0
  34. package/dist/core/adapters/opencode.js +35 -0
  35. package/dist/core/adapters/opencode.js.map +1 -0
  36. package/dist/core/adapters/pi.d.ts +8 -0
  37. package/dist/core/adapters/pi.d.ts.map +1 -0
  38. package/dist/core/adapters/pi.js +24 -0
  39. package/dist/core/adapters/pi.js.map +1 -0
  40. package/dist/core/adapters/registry.d.ts +42 -0
  41. package/dist/core/adapters/registry.d.ts.map +1 -0
  42. package/dist/core/adapters/registry.js +67 -0
  43. package/dist/core/adapters/registry.js.map +1 -0
  44. package/dist/core/canvas-enrich.d.ts +9 -0
  45. package/dist/core/canvas-enrich.d.ts.map +1 -1
  46. package/dist/core/canvas-enrich.js +8 -0
  47. package/dist/core/canvas-enrich.js.map +1 -1
  48. package/dist/core/collector/analytics-emitter.d.ts +57 -0
  49. package/dist/core/collector/analytics-emitter.d.ts.map +1 -0
  50. package/dist/core/collector/analytics-emitter.js +131 -0
  51. package/dist/core/collector/analytics-emitter.js.map +1 -0
  52. package/dist/core/collector/identity-migration.d.ts +25 -0
  53. package/dist/core/collector/identity-migration.d.ts.map +1 -0
  54. package/dist/core/collector/identity-migration.js +68 -0
  55. package/dist/core/collector/identity-migration.js.map +1 -0
  56. package/dist/core/collector/store-retention.d.ts +51 -0
  57. package/dist/core/collector/store-retention.d.ts.map +1 -0
  58. package/dist/core/collector/store-retention.js +117 -0
  59. package/dist/core/collector/store-retention.js.map +1 -0
  60. package/dist/core/collector/store.d.ts +17 -1
  61. package/dist/core/collector/store.d.ts.map +1 -1
  62. package/dist/core/collector/store.js +24 -3
  63. package/dist/core/collector/store.js.map +1 -1
  64. package/dist/core/errors.d.ts +71 -0
  65. package/dist/core/errors.d.ts.map +1 -0
  66. package/dist/core/errors.js +86 -0
  67. package/dist/core/errors.js.map +1 -0
  68. package/dist/core/inject/claude-settings.d.ts +3 -1
  69. package/dist/core/inject/claude-settings.d.ts.map +1 -1
  70. package/dist/core/inject/claude-settings.js +3 -1
  71. package/dist/core/inject/claude-settings.js.map +1 -1
  72. package/dist/core/inject/skills-plugin.d.ts +44 -0
  73. package/dist/core/inject/skills-plugin.d.ts.map +1 -0
  74. package/dist/core/inject/skills-plugin.js +146 -0
  75. package/dist/core/inject/skills-plugin.js.map +1 -0
  76. package/dist/core/macro-runner.d.ts.map +1 -1
  77. package/dist/core/macro-runner.js +20 -1
  78. package/dist/core/macro-runner.js.map +1 -1
  79. package/dist/core/macros.d.ts.map +1 -1
  80. package/dist/core/macros.js +3 -0
  81. package/dist/core/macros.js.map +1 -1
  82. package/dist/core/session-manager.d.ts +37 -18
  83. package/dist/core/session-manager.d.ts.map +1 -1
  84. package/dist/core/session-manager.js +85 -34
  85. package/dist/core/session-manager.js.map +1 -1
  86. package/dist/core/task-manager.d.ts +25 -4
  87. package/dist/core/task-manager.d.ts.map +1 -1
  88. package/dist/core/task-manager.js +118 -6
  89. package/dist/core/task-manager.js.map +1 -1
  90. package/dist/core/workflow-registry.d.ts +7 -0
  91. package/dist/core/workflow-registry.d.ts.map +1 -1
  92. package/dist/core/workflow-registry.js +74 -50
  93. package/dist/core/workflow-registry.js.map +1 -1
  94. package/dist/core/workspace-watcher.d.ts +13 -1
  95. package/dist/core/workspace-watcher.d.ts.map +1 -1
  96. package/dist/core/workspace-watcher.js +78 -4
  97. package/dist/core/workspace-watcher.js.map +1 -1
  98. package/dist/index.d.ts +3 -0
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +2 -0
  101. package/dist/index.js.map +1 -1
  102. package/dist/server/index.d.ts +16 -1
  103. package/dist/server/index.d.ts.map +1 -1
  104. package/dist/server/index.js +0 -0
  105. package/dist/server/index.js.map +1 -1
  106. package/dist/server/ingest.d.ts +8 -0
  107. package/dist/server/ingest.d.ts.map +1 -1
  108. package/dist/server/ingest.js +4 -0
  109. package/dist/server/ingest.js.map +1 -1
  110. package/dist/server/macros.d.ts +5 -2
  111. package/dist/server/macros.d.ts.map +1 -1
  112. package/dist/server/macros.js +5 -2
  113. package/dist/server/macros.js.map +1 -1
  114. package/dist/server/rest.d.ts +26 -1
  115. package/dist/server/rest.d.ts.map +1 -1
  116. package/dist/server/rest.js +139 -6
  117. package/dist/server/rest.js.map +1 -1
  118. package/dist/server/skills.d.ts +45 -0
  119. package/dist/server/skills.d.ts.map +1 -0
  120. package/dist/server/skills.js +286 -0
  121. package/dist/server/skills.js.map +1 -0
  122. package/dist/shared/types.d.ts +75 -2
  123. package/dist/shared/types.d.ts.map +1 -1
  124. package/dist/shared/types.js +12 -1
  125. package/dist/shared/types.js.map +1 -1
  126. package/dist/web/assets/index-BTm-QNRt.css +32 -0
  127. package/dist/web/assets/index-CvNiFngP.js +204 -0
  128. package/dist/web/index.html +2 -2
  129. package/package.json +19 -7
  130. package/dist/core/collector/batcher.d.ts +0 -57
  131. package/dist/core/collector/batcher.d.ts.map +0 -1
  132. package/dist/core/collector/batcher.js +0 -143
  133. package/dist/core/collector/batcher.js.map +0 -1
  134. package/dist/web/assets/index-B9R4TzH7.css +0 -32
  135. package/dist/web/assets/index-Dh97QAgq.js +0 -192
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Harness remote telemetry emitter — a thin adapter that bridges the
3
+ * harness-internal `AnalyticsEvent` shape into `@sapiom/analytics-core`'s
4
+ * `track()` API, forwarding events to the hosted collector at
5
+ * `POST /v1/analytics/collector`.
6
+ *
7
+ * Design contract:
8
+ * - ONE analytics-core instance per harness server process. Multiple
9
+ * concurrent harness sessions are multiplexed onto it via per-event
10
+ * `session_id` overrides — no timer/listener multiplication.
11
+ * - Consent is wired via the `disabled` flag on the analytics instance.
12
+ * When the settings toggle changes, the current instance is shut down and
13
+ * a new one is created with the updated flag.
14
+ * - The local ndjson store (store.ts) is written ALWAYS, independent of
15
+ * consent; this emitter gates only the remote collector path.
16
+ * - `SAPIOM_TELEMETRY_DISABLED=1` and `DO_NOT_TRACK=1` are honored by
17
+ * analytics-core's own consent resolution, at highest precedence.
18
+ * - Event shape follows the harness convergence additions in the collector
19
+ * contract: `data.seq`, `data.context`, `data.harness_session_id`,
20
+ * `data.agent_session_id`, `data.harness_kind`, `source: "harness"`.
21
+ */
22
+ import { createAnalytics, SAPIOM_COLLECTOR_ENDPOINT, } from "@sapiom/analytics-core";
23
+ /**
24
+ * Build the `data` payload that analytics-core will send for a harness
25
+ * analytics event. The harness contract adds:
26
+ * - `seq` — monotonic per session, producer-assigned
27
+ * - `context` — {app_version, os, arch, node} per event
28
+ * - `harness_session_id` — first-class analysis dimension
29
+ * - `agent_session_id` — first-class analysis dimension (null if unknown)
30
+ * - `harness_kind` — first-class analysis dimension
31
+ * Plus the event's own free-form `payload` is merged in.
32
+ */
33
+ function buildEventData(event, context) {
34
+ return {
35
+ ...event.payload,
36
+ seq: event.seq,
37
+ harness_session_id: event.harnessSessionId,
38
+ agent_session_id: event.agentSessionId ?? null,
39
+ harness_kind: event.harness,
40
+ ...(event.tenantId !== null ? { tenant_id: event.tenantId } : {}),
41
+ context: {
42
+ app_version: context.harnessVersion,
43
+ os: context.os,
44
+ arch: context.arch,
45
+ node: context.nodeVersion,
46
+ },
47
+ };
48
+ }
49
+ function buildConfig(options, telemetryOptIn, apiKey) {
50
+ return {
51
+ source: "harness",
52
+ sdkName: options.sdkName,
53
+ sdkVersion: options.sdkVersion,
54
+ // Point at the live collector; SAPIOM_ANALYTICS_ENDPOINT env override
55
+ // (used in tests) takes precedence inside analytics-core's resolveEndpoint.
56
+ endpoint: options.endpoint ?? SAPIOM_COLLECTOR_ENDPOINT,
57
+ apiKey: apiKey ?? undefined,
58
+ fetchImpl: options.fetchImpl,
59
+ debug: options.onDebug
60
+ ? (message, detail) => {
61
+ const msg = detail !== undefined ? `${message}: ${String(detail)}` : message;
62
+ options.onDebug(msg);
63
+ }
64
+ : undefined,
65
+ // Consent: the `disabled` flag gates the entire instance. When the
66
+ // harness settings toggle changes, setTelemetryOptIn() shuts down the
67
+ // current instance and creates a new one. Env flags (SAPIOM_TELEMETRY_DISABLED /
68
+ // DO_NOT_TRACK) are checked by analytics-core at highest precedence, so
69
+ // they always override the stored consent state.
70
+ disabled: !telemetryOptIn,
71
+ };
72
+ }
73
+ export function createHarnessEmitter(options) {
74
+ let telemetryOptIn = options.telemetryOptIn;
75
+ let apiKey = options.apiKey ?? null;
76
+ let analytics = createAnalytics(buildConfig(options, telemetryOptIn, apiKey));
77
+ function recreate(discardPrevQueue = false) {
78
+ // Install the new instance synchronously so enqueue() calls that arrive
79
+ // during the async shutdown drain land on the correct new instance.
80
+ const prev = analytics;
81
+ analytics = createAnalytics(buildConfig(options, telemetryOptIn, apiKey));
82
+ if (discardPrevQueue) {
83
+ // Privacy-sensitive direction (opt-OUT): the user revoked consent —
84
+ // discard buffered events before shutdown so they are not delivered.
85
+ // In-flight sends (already on the wire) still complete normally;
86
+ // only the in-memory buffer is dropped.
87
+ prev.discard?.();
88
+ }
89
+ // The void is intentional: callers don't await this path and
90
+ // analytics-core guarantees shutdown() never rejects.
91
+ void prev.shutdown();
92
+ }
93
+ return {
94
+ enqueue(event) {
95
+ analytics.track(event.type, buildEventData(event, options.context), {
96
+ // Per-event session override: the harness server hosts multiple
97
+ // concurrent sessions; each event carries its own session_id so
98
+ // the single analytics instance can multiplex them correctly.
99
+ session_id: event.harnessSessionId,
100
+ event_id: event.eventId,
101
+ });
102
+ },
103
+ setTelemetryOptIn(optIn) {
104
+ if (telemetryOptIn === optIn)
105
+ return;
106
+ const wasOptedIn = telemetryOptIn;
107
+ telemetryOptIn = optIn;
108
+ // Recreate the instance with the updated consent state. The new
109
+ // instance is installed synchronously so subsequent enqueue() calls
110
+ // see it immediately.
111
+ // Opting OUT: discard any buffered events — the user revoked consent,
112
+ // so delivering them would violate their intent. Opting IN (or rotating
113
+ // the apiKey): drain gracefully; those aren't privacy-sensitive.
114
+ recreate(wasOptedIn && !optIn);
115
+ },
116
+ setApiKey(key) {
117
+ if (apiKey === key)
118
+ return;
119
+ apiKey = key;
120
+ // Recreate with the new key. Drain gracefully (not a consent change).
121
+ recreate(false);
122
+ },
123
+ async flush() {
124
+ await analytics.flush();
125
+ },
126
+ async close() {
127
+ await analytics.shutdown();
128
+ },
129
+ };
130
+ }
131
+ //# sourceMappingURL=analytics-emitter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analytics-emitter.js","sourceRoot":"","sources":["../../../src/core/collector/analytics-emitter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * One-way identity migration: seed `~/.sapiom/analytics.json` from the
3
+ * harness's legacy `~/.sapiom/harness/machine-id` when analytics.json does
4
+ * not yet exist. Idempotent: a subsequent call after analytics.json exists is
5
+ * always a no-op.
6
+ *
7
+ * Purpose: existing installs of harness 0.1.x had a stable anonymous id in
8
+ * the harness-specific machine-id file. Seeding the canonical analytics.json
9
+ * from that value preserves the longitudinal join key so prior sessions stay
10
+ * attributable to the same install after the upgrade.
11
+ *
12
+ * Contract:
13
+ * - Reads machine-id only if analytics.json is absent (avoids file-system
14
+ * I/O on the hot path for already-migrated installs).
15
+ * - Never throws; degrades silently on any I/O failure (unwritable HOME, etc.)
16
+ * - Call once at harness server boot, before the analytics emitter is created
17
+ * (so the emitter's IdentityStore finds the seeded file on first track()).
18
+ */
19
+ /**
20
+ * @param legacyMachineIdPath Absolute path to `~/.sapiom/harness/machine-id`.
21
+ * @param analyticsJsonPath Absolute path to `~/.sapiom/analytics.json`.
22
+ * Defaults to the canonical location.
23
+ */
24
+ export declare function migrateHarnessIdentity(legacyMachineIdPath: string, analyticsJsonPath?: string): Promise<void>;
25
+ //# sourceMappingURL=identity-migration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-migration.d.ts","sourceRoot":"","sources":["../../../src/core/collector/identity-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAiBH;;;;GAIG;AACH,wBAAsB,sBAAsB,CAC1C,mBAAmB,EAAE,MAAM,EAC3B,iBAAiB,CAAC,EAAE,MAAM,GACzB,OAAO,CAAC,IAAI,CAAC,CAiCf"}
@@ -0,0 +1,68 @@
1
+ /**
2
+ * One-way identity migration: seed `~/.sapiom/analytics.json` from the
3
+ * harness's legacy `~/.sapiom/harness/machine-id` when analytics.json does
4
+ * not yet exist. Idempotent: a subsequent call after analytics.json exists is
5
+ * always a no-op.
6
+ *
7
+ * Purpose: existing installs of harness 0.1.x had a stable anonymous id in
8
+ * the harness-specific machine-id file. Seeding the canonical analytics.json
9
+ * from that value preserves the longitudinal join key so prior sessions stay
10
+ * attributable to the same install after the upgrade.
11
+ *
12
+ * Contract:
13
+ * - Reads machine-id only if analytics.json is absent (avoids file-system
14
+ * I/O on the hot path for already-migrated installs).
15
+ * - Never throws; degrades silently on any I/O failure (unwritable HOME, etc.)
16
+ * - Call once at harness server boot, before the analytics emitter is created
17
+ * (so the emitter's IdentityStore finds the seeded file on first track()).
18
+ */
19
+ import * as fs from "node:fs/promises";
20
+ import * as path from "node:path";
21
+ import * as os from "node:os";
22
+ import { seedAnalyticsIdentity } from "@sapiom/analytics-core";
23
+ const UUID_SHAPE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
24
+ function resolveHomeDir() {
25
+ const fromEnv = process.env.HOME || process.env.USERPROFILE;
26
+ if (typeof fromEnv === "string" && fromEnv.length > 0)
27
+ return fromEnv;
28
+ return os.homedir();
29
+ }
30
+ /**
31
+ * @param legacyMachineIdPath Absolute path to `~/.sapiom/harness/machine-id`.
32
+ * @param analyticsJsonPath Absolute path to `~/.sapiom/analytics.json`.
33
+ * Defaults to the canonical location.
34
+ */
35
+ export async function migrateHarnessIdentity(legacyMachineIdPath, analyticsJsonPath) {
36
+ try {
37
+ const targetPath = analyticsJsonPath ??
38
+ path.join(resolveHomeDir(), ".sapiom", "analytics.json");
39
+ // analytics.json already exists — nothing to migrate.
40
+ try {
41
+ await fs.access(targetPath);
42
+ return;
43
+ }
44
+ catch {
45
+ // File doesn't exist — proceed to migration attempt.
46
+ }
47
+ // Read the legacy machine-id.
48
+ let machineId;
49
+ try {
50
+ machineId = (await fs.readFile(legacyMachineIdPath, "utf8")).trim();
51
+ }
52
+ catch {
53
+ // Legacy file doesn't exist (fresh install) — let analytics-core
54
+ // generate a new id naturally on first track().
55
+ return;
56
+ }
57
+ // Validate shape: a non-UUID machine-id shouldn't be propagated.
58
+ if (!UUID_SHAPE.test(machineId))
59
+ return;
60
+ // Seed analytics.json. seedAnalyticsIdentity handles atomic write +
61
+ // 0600 permissions + degrade-on-error.
62
+ seedAnalyticsIdentity(machineId);
63
+ }
64
+ catch {
65
+ // Migration failures must never crash the server.
66
+ }
67
+ }
68
+ //# sourceMappingURL=identity-migration.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"identity-migration.js","sourceRoot":"","sources":["../../../src/core/collector/identity-migration.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9B,OAAO,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,UAAU,GACd,iEAAiE,CAAC;AAEpE,SAAS,cAAc;IACrB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC;IAC5D,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,OAAO,CAAC;IACtE,OAAO,EAAE,CAAC,OAAO,EAAE,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,mBAA2B,EAC3B,iBAA0B;IAE1B,IAAI,CAAC;QACH,MAAM,UAAU,GACd,iBAAiB;YACjB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,gBAAgB,CAAC,CAAC;QAE3D,sDAAsD;QACtD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC5B,OAAO;QACT,CAAC;QAAC,MAAM,CAAC;YACP,qDAAqD;QACvD,CAAC;QAED,8BAA8B;QAC9B,IAAI,SAAiB,CAAC;QACtB,IAAI,CAAC;YACH,SAAS,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QACtE,CAAC;QAAC,MAAM,CAAC;YACP,iEAAiE;YACjE,gDAAgD;YAChD,OAAO;QACT,CAAC;QAED,iEAAiE;QACjE,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC;YAAE,OAAO;QAExC,oEAAoE;QACpE,uCAAuC;QACvC,qBAAqB,CAAC,SAAS,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,kDAAkD;IACpD,CAAC;AACH,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Retention cap enforcement for the local analytics sink (events.ndjson).
3
+ *
4
+ * Policy (enforced on server boot and periodically afterwards):
5
+ * - Size cap: 50 MB — truncate to the newest events that fit.
6
+ * - Age cap: 30 days — drop events older than this by their `ts` field.
7
+ * The stricter of the two wins; oldest-first truncation always preserves
8
+ * the newest events.
9
+ *
10
+ * Atomicity: rewrites happen via a temp-file rename so a crash mid-write
11
+ * never leaves a corrupt or empty events file. The write path must be in
12
+ * the same directory (same filesystem partition) as the target so rename()
13
+ * is atomic.
14
+ *
15
+ * Concurrency: callers MUST run sweepNdjson() through EventStore.runExclusive()
16
+ * so the sweep's read→filter→rename window never races a concurrent append.
17
+ * The store serializes all appends through a promise queue; runExclusive()
18
+ * chains the sweep onto that same queue, guaranteeing mutual exclusion.
19
+ * See store.ts and server/index.ts for the wiring.
20
+ *
21
+ * Corruption tolerance: lines that fail JSON.parse are silently kept rather
22
+ * than aborting the sweep — a single bad line never blocks retention.
23
+ */
24
+ /** 50 MB default size cap. */
25
+ export declare const DEFAULT_MAX_SIZE_BYTES: number;
26
+ /** 30 days default age cap. */
27
+ export declare const DEFAULT_MAX_AGE_MS: number;
28
+ export interface SweepOptions {
29
+ maxSizeBytes?: number;
30
+ maxAgeMs?: number;
31
+ }
32
+ export interface SweepResult {
33
+ /** Number of lines in the file before the sweep. */
34
+ linesBefore: number;
35
+ /** Number of lines retained after the sweep. */
36
+ linesAfter: number;
37
+ /** True when the file was actually rewritten (something was trimmed). */
38
+ rewritten: boolean;
39
+ }
40
+ /**
41
+ * Enforces size and age caps on an ndjson file, retaining the newest events.
42
+ *
43
+ * Returns a SweepResult describing what happened. Never throws on ENOENT
44
+ * (the file doesn't exist yet — that's fine). Propagates other I/O errors
45
+ * to the caller.
46
+ *
47
+ * IMPORTANT: callers must run this through EventStore.runExclusive() to
48
+ * prevent races with concurrent appends (see module-level docstring).
49
+ */
50
+ export declare function sweepNdjson(filePath: string, options?: SweepOptions): Promise<SweepResult>;
51
+ //# sourceMappingURL=store-retention.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-retention.d.ts","sourceRoot":"","sources":["../../../src/core/collector/store-retention.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAKH,8BAA8B;AAC9B,eAAO,MAAM,sBAAsB,QAAmB,CAAC;AACvD,+BAA+B;AAC/B,eAAO,MAAM,kBAAkB,QAA2B,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,yEAAyE;IACzE,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,YAAiB,GACzB,OAAO,CAAC,WAAW,CAAC,CA+EtB"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Retention cap enforcement for the local analytics sink (events.ndjson).
3
+ *
4
+ * Policy (enforced on server boot and periodically afterwards):
5
+ * - Size cap: 50 MB — truncate to the newest events that fit.
6
+ * - Age cap: 30 days — drop events older than this by their `ts` field.
7
+ * The stricter of the two wins; oldest-first truncation always preserves
8
+ * the newest events.
9
+ *
10
+ * Atomicity: rewrites happen via a temp-file rename so a crash mid-write
11
+ * never leaves a corrupt or empty events file. The write path must be in
12
+ * the same directory (same filesystem partition) as the target so rename()
13
+ * is atomic.
14
+ *
15
+ * Concurrency: callers MUST run sweepNdjson() through EventStore.runExclusive()
16
+ * so the sweep's read→filter→rename window never races a concurrent append.
17
+ * The store serializes all appends through a promise queue; runExclusive()
18
+ * chains the sweep onto that same queue, guaranteeing mutual exclusion.
19
+ * See store.ts and server/index.ts for the wiring.
20
+ *
21
+ * Corruption tolerance: lines that fail JSON.parse are silently kept rather
22
+ * than aborting the sweep — a single bad line never blocks retention.
23
+ */
24
+ import * as fs from "node:fs/promises";
25
+ import * as path from "node:path";
26
+ /** 50 MB default size cap. */
27
+ export const DEFAULT_MAX_SIZE_BYTES = 50 * 1024 * 1024;
28
+ /** 30 days default age cap. */
29
+ export const DEFAULT_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
30
+ /**
31
+ * Enforces size and age caps on an ndjson file, retaining the newest events.
32
+ *
33
+ * Returns a SweepResult describing what happened. Never throws on ENOENT
34
+ * (the file doesn't exist yet — that's fine). Propagates other I/O errors
35
+ * to the caller.
36
+ *
37
+ * IMPORTANT: callers must run this through EventStore.runExclusive() to
38
+ * prevent races with concurrent appends (see module-level docstring).
39
+ */
40
+ export async function sweepNdjson(filePath, options = {}) {
41
+ const maxSizeBytes = options.maxSizeBytes ?? DEFAULT_MAX_SIZE_BYTES;
42
+ const maxAgeMs = options.maxAgeMs ?? DEFAULT_MAX_AGE_MS;
43
+ const cutoffMs = Date.now() - maxAgeMs;
44
+ let content;
45
+ try {
46
+ content = await fs.readFile(filePath, "utf8");
47
+ }
48
+ catch (err) {
49
+ if (err.code === "ENOENT") {
50
+ return { linesBefore: 0, linesAfter: 0, rewritten: false };
51
+ }
52
+ throw err;
53
+ }
54
+ const rawLines = content.split("\n");
55
+ // Last line is often empty (trailing newline) — exclude it from the count.
56
+ const lines = rawLines[rawLines.length - 1] === "" ? rawLines.slice(0, -1) : rawLines;
57
+ const linesBefore = lines.length;
58
+ // Filter by age first — drop lines whose `ts` field is older than the cap.
59
+ // Lines that fail JSON.parse are kept (corrupted lines fall through the age
60
+ // filter; the size cap below may still drop them if needed).
61
+ const ageFiltered = lines.filter((line) => {
62
+ try {
63
+ const parsed = JSON.parse(line);
64
+ if (typeof parsed.ts !== "string")
65
+ return true; // keep if no ts
66
+ const lineMs = Date.parse(parsed.ts);
67
+ return !Number.isNaN(lineMs) && lineMs >= cutoffMs;
68
+ }
69
+ catch {
70
+ return true; // keep on parse failure
71
+ }
72
+ });
73
+ // Size cap: O(n) cumulative-bytes pass — compute the byte length of each
74
+ // line (with its trailing newline), accumulate from the BACK (newest first),
75
+ // and find the earliest index that still fits in maxSizeBytes. Avoids the
76
+ // O(n²) cost of re-joining the entire array on each iteration.
77
+ let dropCount = 0;
78
+ if (ageFiltered.length > 0) {
79
+ // Total byte count of all age-filtered lines.
80
+ const lineLengths = ageFiltered.map((line) => Buffer.byteLength(line + "\n", "utf8"));
81
+ const total = lineLengths.reduce((sum, n) => sum + n, 0);
82
+ if (total > maxSizeBytes) {
83
+ // Walk from the front, accumulating bytes to drop until the remaining
84
+ // content (total − dropped) fits within the cap.
85
+ let dropped = 0;
86
+ for (let i = 0; i < ageFiltered.length; i++) {
87
+ if (total - dropped <= maxSizeBytes)
88
+ break;
89
+ dropped += lineLengths[i];
90
+ dropCount = i + 1;
91
+ }
92
+ }
93
+ }
94
+ const kept = dropCount > 0 ? ageFiltered.slice(dropCount) : ageFiltered;
95
+ const linesAfter = kept.length;
96
+ const nothingChanged = linesAfter === linesBefore;
97
+ if (nothingChanged) {
98
+ return { linesBefore, linesAfter, rewritten: false };
99
+ }
100
+ // Atomic rewrite via temp file + rename. Both must be on the same filesystem
101
+ // (same directory) for the rename to be atomic.
102
+ const dir = path.dirname(filePath);
103
+ await fs.mkdir(dir, { recursive: true });
104
+ const tmpPath = path.join(dir, `.events-retention-tmp-${process.pid}-${Date.now()}.ndjson`);
105
+ try {
106
+ const newContent = kept.length === 0 ? "" : kept.join("\n") + "\n";
107
+ await fs.writeFile(tmpPath, newContent, "utf8");
108
+ await fs.rename(tmpPath, filePath);
109
+ }
110
+ catch (err) {
111
+ // Best-effort cleanup of the temp file; the rename failure is what matters.
112
+ await fs.unlink(tmpPath).catch(() => undefined);
113
+ throw err;
114
+ }
115
+ return { linesBefore, linesAfter, rewritten: true };
116
+ }
117
+ //# sourceMappingURL=store-retention.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store-retention.js","sourceRoot":"","sources":["../../../src/core/collector/store-retention.ts"],"names":[],"mappings":"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"}
@@ -2,11 +2,27 @@
2
2
  * Append-only local sink for analytics events. Always written, regardless
3
3
  * of telemetry opt-in — this is the "demo inspects this" local debug file,
4
4
  * independent of whether anything gets batched to a remote collector.
5
+ *
6
+ * Concurrency: every append is serialized through a promise queue (the same
7
+ * pattern as workflow-registry.ts and session-manager.ts). `runExclusive(fn)`
8
+ * chains `fn` onto the same queue so retention sweeps (read→filter→rename)
9
+ * never overlap with an in-flight append and no appended line can be lost
10
+ * in a sweep's read window. Overhead is negligible — appends are low-frequency
11
+ * (one per hook event), and the queue never holds more than O(sessions) entries.
5
12
  */
6
13
  import { type AnalyticsEvent } from "../../shared/types.js";
7
14
  export interface EventStore {
8
- /** Append one event as a single ndjson line. Crash-safe (fs.appendFile). */
15
+ /** Append one event as a single ndjson line, serialized through the queue. */
9
16
  append(event: AnalyticsEvent): Promise<void>;
17
+ /**
18
+ * Run `fn` exclusively — after all pending appends complete and blocking
19
+ * any new appends until `fn` resolves. Use this to run a retention sweep
20
+ * without racing concurrent writes.
21
+ *
22
+ * A failed `fn` never poisons the queue (later appends proceed normally).
23
+ * The return value of `fn` is forwarded to the caller.
24
+ */
25
+ runExclusive<T>(fn: () => Promise<T>): Promise<T>;
10
26
  }
11
27
  /**
12
28
  * @param filePath Defaults to `HARNESS_PATHS.events`
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/collector/store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG3E,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9C;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,MAA6B,GAAG,UAAU,CAiBpF"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../src/core/collector/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAG3E,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C;;;;;;;OAOG;IACH,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CACnD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,GAAE,MAA6B,GAAG,UAAU,CAqCpF"}
@@ -2,6 +2,13 @@
2
2
  * Append-only local sink for analytics events. Always written, regardless
3
3
  * of telemetry opt-in — this is the "demo inspects this" local debug file,
4
4
  * independent of whether anything gets batched to a remote collector.
5
+ *
6
+ * Concurrency: every append is serialized through a promise queue (the same
7
+ * pattern as workflow-registry.ts and session-manager.ts). `runExclusive(fn)`
8
+ * chains `fn` onto the same queue so retention sweeps (read→filter→rename)
9
+ * never overlap with an in-flight append and no appended line can be lost
10
+ * in a sweep's read window. Overhead is negligible — appends are low-frequency
11
+ * (one per hook event), and the queue never holds more than O(sessions) entries.
5
12
  */
6
13
  import * as fs from "node:fs/promises";
7
14
  import * as path from "node:path";
@@ -20,10 +27,24 @@ export function createEventStore(filePath = HARNESS_PATHS.events) {
20
27
  }
21
28
  return dirReady;
22
29
  }
30
+ // Promise queue — same pattern as workflow-registry.ts:106-135.
31
+ // Chains each operation so they execute strictly one-at-a-time.
32
+ // A failed run never poisons subsequent operations.
33
+ let queue = Promise.resolve();
34
+ function enqueue(run) {
35
+ const next = queue.catch(() => { }).then(run);
36
+ queue = next.then(() => { }, () => { });
37
+ return next;
38
+ }
23
39
  return {
24
- async append(event) {
25
- await ensureDir();
26
- await fs.appendFile(resolvedPath, `${JSON.stringify(event)}\n`, "utf8");
40
+ append(event) {
41
+ return enqueue(async () => {
42
+ await ensureDir();
43
+ await fs.appendFile(resolvedPath, `${JSON.stringify(event)}\n`, "utf8");
44
+ });
45
+ },
46
+ runExclusive(fn) {
47
+ return enqueue(fn);
27
48
  },
28
49
  };
29
50
  }
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/core/collector/store.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,aAAa,EAAuB,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAOzC;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,aAAa,CAAC,MAAM;IACtE,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,QAAQ,GAAyB,IAAI,CAAC;IAE1C,SAAS,SAAS;QAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,KAAqB;YAChC,MAAM,SAAS,EAAE,CAAC;YAClB,MAAM,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC1E,CAAC;KACF,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../../src/core/collector/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,aAAa,EAAuB,MAAM,uBAAuB,CAAC;AAC3E,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAgBzC;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,WAAmB,aAAa,CAAC,MAAM;IACtE,MAAM,YAAY,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,QAAQ,GAAyB,IAAI,CAAC;IAE1C,SAAS,SAAS;QAChB,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,QAAQ,GAAG,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC7F,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,gEAAgE;IAChE,gEAAgE;IAChE,oDAAoD;IACpD,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAE7C,SAAS,OAAO,CAAI,GAAqB;QACvC,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7C,KAAK,GAAG,IAAI,CAAC,IAAI,CACf,GAAG,EAAE,GAAE,CAAC,EACR,GAAG,EAAE,GAAE,CAAC,CACT,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO;QACL,MAAM,CAAC,KAAqB;YAC1B,OAAO,OAAO,CAAC,KAAK,IAAI,EAAE;gBACxB,MAAM,SAAS,EAAE,CAAC;gBAClB,MAAM,EAAE,CAAC,UAAU,CAAC,YAAY,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC1E,CAAC,CAAC,CAAC;QACL,CAAC;QAED,YAAY,CAAI,EAAoB;YAClC,OAAO,OAAO,CAAC,EAAE,CAAC,CAAC;QACrB,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Typed errors for session and spawn failures. Each carries a stable `code`
3
+ * so callers can react programmatically to specific failure modes instead of
4
+ * parsing error message strings.
5
+ *
6
+ * HTTP mappings (server/rest.ts, server/macros.ts):
7
+ * UnknownSessionError → 404
8
+ * SessionNotReadyError → 409
9
+ * SessionAlreadyLiveError → 409
10
+ * SessionNotResumeableError → 409
11
+ * AdapterNotFoundError → 400
12
+ */
13
+ /** Base class for all typed harness errors. */
14
+ export declare class HarnessError extends Error {
15
+ /** Stable machine-readable code callers can branch on without parsing messages. */
16
+ readonly code: string;
17
+ /** Underlying error, when this error wraps another. */
18
+ readonly cause?: unknown;
19
+ constructor(code: string, message: string, cause?: unknown);
20
+ }
21
+ /**
22
+ * Thrown when an operation references a session id that does not exist in the
23
+ * registry. Maps to HTTP 404.
24
+ */
25
+ export declare class UnknownSessionError extends HarnessError {
26
+ constructor(id: string);
27
+ }
28
+ /**
29
+ * Thrown by `submitInput()` when a session's pty is alive but never became
30
+ * interactive within the grace period — the trust-dialog race this readiness
31
+ * mechanism exists to catch. Maps to HTTP 409.
32
+ */
33
+ export declare class SessionNotReadyError extends HarnessError {
34
+ constructor(id: string);
35
+ }
36
+ /**
37
+ * Thrown by `resume()` when the session record has no `agentSessionId` to
38
+ * resume from (it was never fully started, or is history-only with no
39
+ * recorded session). Maps to HTTP 409.
40
+ */
41
+ export declare class SessionNotResumeableError extends HarnessError {
42
+ constructor(id: string);
43
+ }
44
+ /**
45
+ * Thrown by `resume()` when the session already has a live pty — double-resume
46
+ * is a no-op caller error. Maps to HTTP 409.
47
+ */
48
+ export declare class SessionAlreadyLiveError extends HarnessError {
49
+ constructor(id: string);
50
+ }
51
+ /**
52
+ * Thrown when an operation requires a harness adapter that has not been
53
+ * registered. Maps to HTTP 400.
54
+ */
55
+ export declare class AdapterNotFoundError extends HarnessError {
56
+ constructor(harness: string);
57
+ }
58
+ /**
59
+ * Thrown when a spawn or send operation is attempted on an external-mode
60
+ * harness adapter (e.g. Conductor) whose sessions are managed by its own
61
+ * companion app — the harness cannot spawn or inject into them. Maps to
62
+ * HTTP 409.
63
+ *
64
+ * The `harness` field names the adapter so the UI can show a targeted message
65
+ * (e.g. "Conductor sessions are managed by the Conductor app").
66
+ */
67
+ export declare class ExternalHarnessError extends HarnessError {
68
+ readonly harness: string;
69
+ constructor(harness: string, label: string);
70
+ }
71
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,+CAA+C;AAC/C,qBAAa,YAAa,SAAQ,KAAK;IACrC,mFAAmF;IACnF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uDAAuD;IACvD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;gBAEb,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAQ3D;AAED;;;GAGG;AACH,qBAAa,mBAAoB,SAAQ,YAAY;gBACvC,EAAE,EAAE,MAAM;CAGvB;AAED;;;;GAIG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,EAAE,EAAE,MAAM;CAMvB;AAED;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,YAAY;gBAC7C,EAAE,EAAE,MAAM;CAGvB;AAED;;;GAGG;AACH,qBAAa,uBAAwB,SAAQ,YAAY;gBAC3C,EAAE,EAAE,MAAM;CAGvB;AAED;;;GAGG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;gBACxC,OAAO,EAAE,MAAM;CAG5B;AAED;;;;;;;;GAQG;AACH,qBAAa,oBAAqB,SAAQ,YAAY;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;CAO3C"}