@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.
- package/CHANGELOG.md +162 -0
- package/README.md +36 -0
- package/dist/cli/bin.js +26 -2
- package/dist/cli/bin.js.map +1 -1
- package/dist/cli/consent.d.ts +29 -1
- package/dist/cli/consent.d.ts.map +1 -1
- package/dist/cli/consent.js +33 -11
- package/dist/cli/consent.js.map +1 -1
- package/dist/core/adapters/adapter.d.ts +72 -0
- package/dist/core/adapters/adapter.d.ts.map +1 -0
- package/dist/core/adapters/adapter.js +18 -0
- package/dist/core/adapters/adapter.js.map +1 -0
- package/dist/core/adapters/claude-code-info.d.ts +11 -0
- package/dist/core/adapters/claude-code-info.d.ts.map +1 -0
- package/dist/core/adapters/claude-code-info.js +27 -0
- package/dist/core/adapters/claude-code-info.js.map +1 -0
- package/dist/core/adapters/claude-code.d.ts.map +1 -1
- package/dist/core/adapters/claude-code.js +9 -8
- package/dist/core/adapters/claude-code.js.map +1 -1
- package/dist/core/adapters/codex-info.d.ts +10 -0
- package/dist/core/adapters/codex-info.d.ts.map +1 -0
- package/dist/core/adapters/codex-info.js +32 -0
- package/dist/core/adapters/codex-info.js.map +1 -0
- package/dist/core/adapters/conductor.d.ts +8 -0
- package/dist/core/adapters/conductor.d.ts.map +1 -0
- package/dist/core/adapters/conductor.js +57 -0
- package/dist/core/adapters/conductor.js.map +1 -0
- package/dist/core/adapters/detect.d.ts +15 -0
- package/dist/core/adapters/detect.d.ts.map +1 -0
- package/dist/core/adapters/detect.js +62 -0
- package/dist/core/adapters/detect.js.map +1 -0
- package/dist/core/adapters/opencode.d.ts +8 -0
- package/dist/core/adapters/opencode.d.ts.map +1 -0
- package/dist/core/adapters/opencode.js +35 -0
- package/dist/core/adapters/opencode.js.map +1 -0
- package/dist/core/adapters/pi.d.ts +8 -0
- package/dist/core/adapters/pi.d.ts.map +1 -0
- package/dist/core/adapters/pi.js +24 -0
- package/dist/core/adapters/pi.js.map +1 -0
- package/dist/core/adapters/registry.d.ts +42 -0
- package/dist/core/adapters/registry.d.ts.map +1 -0
- package/dist/core/adapters/registry.js +67 -0
- package/dist/core/adapters/registry.js.map +1 -0
- package/dist/core/canvas-enrich.d.ts +9 -0
- package/dist/core/canvas-enrich.d.ts.map +1 -1
- package/dist/core/canvas-enrich.js +8 -0
- package/dist/core/canvas-enrich.js.map +1 -1
- package/dist/core/collector/analytics-emitter.d.ts +57 -0
- package/dist/core/collector/analytics-emitter.d.ts.map +1 -0
- package/dist/core/collector/analytics-emitter.js +131 -0
- package/dist/core/collector/analytics-emitter.js.map +1 -0
- package/dist/core/collector/identity-migration.d.ts +25 -0
- package/dist/core/collector/identity-migration.d.ts.map +1 -0
- package/dist/core/collector/identity-migration.js +68 -0
- package/dist/core/collector/identity-migration.js.map +1 -0
- package/dist/core/collector/store-retention.d.ts +51 -0
- package/dist/core/collector/store-retention.d.ts.map +1 -0
- package/dist/core/collector/store-retention.js +117 -0
- package/dist/core/collector/store-retention.js.map +1 -0
- package/dist/core/collector/store.d.ts +17 -1
- package/dist/core/collector/store.d.ts.map +1 -1
- package/dist/core/collector/store.js +24 -3
- package/dist/core/collector/store.js.map +1 -1
- package/dist/core/errors.d.ts +71 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +86 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/inject/claude-settings.d.ts +3 -1
- package/dist/core/inject/claude-settings.d.ts.map +1 -1
- package/dist/core/inject/claude-settings.js +3 -1
- package/dist/core/inject/claude-settings.js.map +1 -1
- package/dist/core/inject/skills-plugin.d.ts +44 -0
- package/dist/core/inject/skills-plugin.d.ts.map +1 -0
- package/dist/core/inject/skills-plugin.js +146 -0
- package/dist/core/inject/skills-plugin.js.map +1 -0
- package/dist/core/macro-runner.d.ts.map +1 -1
- package/dist/core/macro-runner.js +20 -1
- package/dist/core/macro-runner.js.map +1 -1
- package/dist/core/macros.d.ts.map +1 -1
- package/dist/core/macros.js +3 -0
- package/dist/core/macros.js.map +1 -1
- package/dist/core/session-manager.d.ts +37 -18
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +85 -34
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/task-manager.d.ts +25 -4
- package/dist/core/task-manager.d.ts.map +1 -1
- package/dist/core/task-manager.js +118 -6
- package/dist/core/task-manager.js.map +1 -1
- package/dist/core/workflow-registry.d.ts +7 -0
- package/dist/core/workflow-registry.d.ts.map +1 -1
- package/dist/core/workflow-registry.js +74 -50
- package/dist/core/workflow-registry.js.map +1 -1
- package/dist/core/workspace-watcher.d.ts +13 -1
- package/dist/core/workspace-watcher.d.ts.map +1 -1
- package/dist/core/workspace-watcher.js +78 -4
- package/dist/core/workspace-watcher.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/server/index.d.ts +16 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +0 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/ingest.d.ts +8 -0
- package/dist/server/ingest.d.ts.map +1 -1
- package/dist/server/ingest.js +4 -0
- package/dist/server/ingest.js.map +1 -1
- package/dist/server/macros.d.ts +5 -2
- package/dist/server/macros.d.ts.map +1 -1
- package/dist/server/macros.js +5 -2
- package/dist/server/macros.js.map +1 -1
- package/dist/server/rest.d.ts +26 -1
- package/dist/server/rest.d.ts.map +1 -1
- package/dist/server/rest.js +139 -6
- package/dist/server/rest.js.map +1 -1
- package/dist/server/skills.d.ts +45 -0
- package/dist/server/skills.d.ts.map +1 -0
- package/dist/server/skills.js +286 -0
- package/dist/server/skills.js.map +1 -0
- package/dist/shared/types.d.ts +75 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +12 -1
- package/dist/shared/types.js.map +1 -1
- package/dist/web/assets/index-BTm-QNRt.css +32 -0
- package/dist/web/assets/index-CvNiFngP.js +204 -0
- package/dist/web/index.html +2 -2
- package/package.json +19 -7
- package/dist/core/collector/batcher.d.ts +0 -57
- package/dist/core/collector/batcher.d.ts.map +0 -1
- package/dist/core/collector/batcher.js +0 -143
- package/dist/core/collector/batcher.js.map +0 -1
- package/dist/web/assets/index-B9R4TzH7.css +0 -32
- 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
|
|
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
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
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"}
|