@scopebond/hook 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +210 -0
  2. package/dist/budget-load.d.ts +68 -0
  3. package/dist/budget-load.d.ts.map +1 -0
  4. package/dist/budget-load.js +164 -0
  5. package/dist/budget-load.js.map +1 -0
  6. package/dist/capabilities.d.ts +103 -0
  7. package/dist/capabilities.d.ts.map +1 -0
  8. package/dist/capabilities.js +212 -0
  9. package/dist/capabilities.js.map +1 -0
  10. package/dist/classify.d.ts +17 -0
  11. package/dist/classify.d.ts.map +1 -0
  12. package/dist/classify.js +78 -0
  13. package/dist/classify.js.map +1 -0
  14. package/dist/cli.d.ts.map +1 -1
  15. package/dist/cli.js +546 -16
  16. package/dist/cli.js.map +1 -1
  17. package/dist/cloud.d.ts +8 -0
  18. package/dist/cloud.d.ts.map +1 -1
  19. package/dist/cloud.js.map +1 -1
  20. package/dist/dispatch-cli.d.ts +2 -0
  21. package/dist/dispatch-cli.d.ts.map +1 -0
  22. package/dist/dispatch-cli.js +128 -0
  23. package/dist/dispatch-cli.js.map +1 -0
  24. package/dist/explain.d.ts.map +1 -1
  25. package/dist/explain.js +2 -1
  26. package/dist/explain.js.map +1 -1
  27. package/dist/group.d.ts +12 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +55 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +25 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +17 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/install.d.ts +10 -0
  36. package/dist/install.d.ts.map +1 -1
  37. package/dist/install.js +56 -0
  38. package/dist/install.js.map +1 -1
  39. package/dist/map.d.ts +4 -0
  40. package/dist/map.d.ts.map +1 -1
  41. package/dist/map.js +29 -1
  42. package/dist/map.js.map +1 -1
  43. package/dist/obs-emitter.d.ts +132 -0
  44. package/dist/obs-emitter.d.ts.map +1 -0
  45. package/dist/obs-emitter.js +415 -0
  46. package/dist/obs-emitter.js.map +1 -0
  47. package/dist/obs-store.d.ts +160 -0
  48. package/dist/obs-store.d.ts.map +1 -0
  49. package/dist/obs-store.js +367 -0
  50. package/dist/obs-store.js.map +1 -0
  51. package/dist/obs-upload.d.ts +24 -0
  52. package/dist/obs-upload.d.ts.map +1 -0
  53. package/dist/obs-upload.js +231 -0
  54. package/dist/obs-upload.js.map +1 -0
  55. package/dist/observation.d.ts +232 -0
  56. package/dist/observation.d.ts.map +1 -0
  57. package/dist/observation.js +308 -0
  58. package/dist/observation.js.map +1 -0
  59. package/dist/paths.d.ts +15 -0
  60. package/dist/paths.d.ts.map +1 -0
  61. package/dist/paths.js +79 -0
  62. package/dist/paths.js.map +1 -0
  63. package/dist/policy-load.d.ts +60 -0
  64. package/dist/policy-load.d.ts.map +1 -0
  65. package/dist/policy-load.js +151 -0
  66. package/dist/policy-load.js.map +1 -0
  67. package/dist/proof.d.ts +30 -0
  68. package/dist/proof.d.ts.map +1 -0
  69. package/dist/proof.js +186 -0
  70. package/dist/proof.js.map +1 -0
  71. package/dist/rules.d.ts +20 -0
  72. package/dist/rules.d.ts.map +1 -1
  73. package/dist/rules.js +50 -0
  74. package/dist/rules.js.map +1 -1
  75. package/dist/runtime.d.ts +18 -1
  76. package/dist/runtime.d.ts.map +1 -1
  77. package/dist/runtime.js +74 -10
  78. package/dist/runtime.js.map +1 -1
  79. package/dist/scan.d.ts +7 -0
  80. package/dist/scan.d.ts.map +1 -0
  81. package/dist/scan.js +38 -0
  82. package/dist/scan.js.map +1 -0
  83. package/dist/shell.d.ts.map +1 -1
  84. package/dist/shell.js +12 -3
  85. package/dist/shell.js.map +1 -1
  86. package/dist/sql-classify.d.ts +11 -0
  87. package/dist/sql-classify.d.ts.map +1 -0
  88. package/dist/sql-classify.js +251 -0
  89. package/dist/sql-classify.js.map +1 -0
  90. package/dist/typed-infra.d.ts +73 -0
  91. package/dist/typed-infra.d.ts.map +1 -0
  92. package/dist/typed-infra.js +758 -0
  93. package/dist/typed-infra.js.map +1 -0
  94. package/dist/typed-ops.d.ts +140 -0
  95. package/dist/typed-ops.d.ts.map +1 -0
  96. package/dist/typed-ops.js +549 -0
  97. package/dist/typed-ops.js.map +1 -0
  98. package/dist/vectors.d.ts +40 -0
  99. package/dist/vectors.d.ts.map +1 -0
  100. package/dist/vectors.js +219 -0
  101. package/dist/vectors.js.map +1 -0
  102. package/package.json +4 -4
@@ -0,0 +1,160 @@
1
+ import { type ObservationDraft, type ObservationPayload, type SignedObservation, type EnvelopeContext, type ObservationSigner } from "./observation.js";
2
+ export declare const OBSERVATION_DB = "observations.db";
3
+ /** Bounds on the local queue. Past them new observations are counted as dropped (visible
4
+ * in `status`) rather than growing without limit; enforcement never depends on the queue. */
5
+ export declare const MAX_PENDING = 5000;
6
+ export declare const MAX_PENDING_BYTES: number;
7
+ export declare const MAX_TERMINAL = 1000;
8
+ /** How long a route found missing is left alone before one upload probes it again. */
9
+ /** How long a batch stays reserved for the process that took it. */
10
+ export declare const INFLIGHT_MS = 30000;
11
+ export declare const UNSUPPORTED_RECHECK_MS: number;
12
+ export type Capability = "active" | "unsupported" | "blocked";
13
+ export interface StoreState {
14
+ generation: number;
15
+ next_sequence: number;
16
+ capability: Capability;
17
+ reason: string | null;
18
+ capability_at: number | null;
19
+ backoff_until: number;
20
+ last_error: string | null;
21
+ last_upload_at: number | null;
22
+ dropped: number;
23
+ batch_limit: number;
24
+ }
25
+ export interface PendingRow {
26
+ sequence: number;
27
+ generation: number;
28
+ observation_id: string;
29
+ observation_hash: string;
30
+ kind: string;
31
+ wrapper: SignedObservation;
32
+ bytes: number;
33
+ enqueued_at: number;
34
+ attempts: number;
35
+ next_attempt_at: number;
36
+ }
37
+ export interface TerminalRow {
38
+ id: number;
39
+ generation: number;
40
+ sequence: number;
41
+ observation_id: string;
42
+ kind: string;
43
+ code: string;
44
+ at: number;
45
+ wrapper: SignedObservation | null;
46
+ }
47
+ export interface SessionRow {
48
+ session_id: string;
49
+ state: "active" | "stopped";
50
+ started_at: number;
51
+ last_activity_at: number;
52
+ last_heartbeat_at: number;
53
+ heartbeat_lease_until: number;
54
+ repository_id: string | null;
55
+ stopped_reason: string | null;
56
+ }
57
+ export interface CallRow {
58
+ call_key: string;
59
+ parent_action_id: string;
60
+ source_receipt_hash: string;
61
+ session_id: string | null;
62
+ operation: Record<string, unknown>;
63
+ at: number;
64
+ }
65
+ export type EnqueueResult = {
66
+ queued: true;
67
+ duplicate: boolean;
68
+ sequence: number;
69
+ observation_id: string;
70
+ } | {
71
+ queued: false;
72
+ reason: "unsupported" | "blocked" | "capacity" | "oversize" | "generation_mismatch";
73
+ detail?: string;
74
+ };
75
+ export declare class ObservationStore {
76
+ private readonly now;
77
+ private readonly db;
78
+ constructor(path: string, now?: () => number);
79
+ close(): void;
80
+ private tx;
81
+ state(): StoreState | null;
82
+ /**
83
+ * Bind the outbox to the installation generation the workspace enrolled. A higher
84
+ * generation than the stored one means the key or credential was rotated: sequence
85
+ * restarts at 1, and everything still pending under the old generation moves to the
86
+ * terminal queue as `stale_generation` (kept locally, never uploaded). A generation
87
+ * lower than the stored one is a stale connection file and reported, not adopted.
88
+ */
89
+ bindGeneration(generation: number): {
90
+ ok: true;
91
+ retired: number;
92
+ } | {
93
+ ok: false;
94
+ reason: string;
95
+ };
96
+ /**
97
+ * Allocate the next sequence, build and sign the payload, and store it, atomically.
98
+ * `observationId` makes the call idempotent: an id already stored (pending or already
99
+ * sent) returns without consuming a sequence.
100
+ */
101
+ enqueue(draft: ObservationDraft, context: EnvelopeContext, signer: ObservationSigner): EnqueueResult;
102
+ private rowToPending;
103
+ /** The next batch to send: current generation, in sequence order, due now, within the
104
+ * item and body limits. With `lease`, the rows are marked in flight for INFLIGHT_MS in the
105
+ * same transaction, so a second hook process uploading at the same moment skips them
106
+ * instead of sending the same items again. The sender releases or defers them afterwards
107
+ * (a crashed sender's lease simply lapses). */
108
+ nextBatch(limit?: number, lease?: boolean): PendingRow[];
109
+ /** Give leased rows back without counting an attempt (the send did not happen). */
110
+ release(observationIds: string[]): void;
111
+ pendingSummary(): {
112
+ count: number;
113
+ oldest_at: number | null;
114
+ };
115
+ /** Remove acknowledged items (accepted, duplicate, pending_link) — and only those. */
116
+ acknowledge(observationIds: string[]): void;
117
+ /** Move permanently refused items to the visible terminal queue. */
118
+ reject(items: Array<{
119
+ observation_id: string;
120
+ code: string;
121
+ }>): void;
122
+ /** Keep items pending but retry them later (server said deferred, or the send failed). */
123
+ defer(observationIds: string[], delayMs: number): void;
124
+ setBackoff(untilMs: number, error: string | null): void;
125
+ setBatchLimit(limit: number): void;
126
+ /** Mark the capability unsupported (route missing, batch version refused) or blocked
127
+ * (stale generation). New observations are then not queued; local enforcement is
128
+ * untouched. Pending items are kept. */
129
+ setCapability(capability: Capability, reason: string | null): void;
130
+ /** Move everything pending to the terminal queue with `code` (a stale generation makes
131
+ * the whole backlog unsendable). */
132
+ rejectAllPending(code: string): number;
133
+ terminal(limit?: number): TerminalRow[];
134
+ terminalCounts(): Record<string, number>;
135
+ private trimTerminal;
136
+ mark(name: string, value: number): void;
137
+ getMark(name: string): number | null;
138
+ session(sessionId: string): SessionRow | null;
139
+ /** Begin (or resume) a session. Returns true when the session was not active before,
140
+ * so the caller emits exactly one start. */
141
+ activateSession(sessionId: string, repositoryId: string | null): boolean;
142
+ touchSession(sessionId: string): void;
143
+ /** End a session. Returns true when it was active, so exactly one stop is emitted. */
144
+ deactivateSession(sessionId: string, reason: string): boolean;
145
+ activeSessions(): SessionRow[];
146
+ /** Claim the single heartbeat loop for a session, or refuse when another live loop
147
+ * already holds it. The lease is short and renewed each tick, so a crashed loop frees
148
+ * the claim within `leaseMs`. This is what keeps the number of helper processes at one
149
+ * per active session no matter how many hook calls run. */
150
+ claimHeartbeat(sessionId: string, leaseMs: number): boolean;
151
+ /** Renew the loop's own claim; false when the session ended or the claim was lost. */
152
+ renewHeartbeat(sessionId: string, leaseMs: number, sentHeartbeat: boolean): boolean;
153
+ releaseHeartbeat(sessionId: string): void;
154
+ recordCall(call: Omit<CallRow, "at">): void;
155
+ /** The calls recorded under a harness call id, consumed by the after-action hook. */
156
+ takeCalls(callPrefix: string): CallRow[];
157
+ }
158
+ /** Convenience for callers that only have a payload. */
159
+ export type { ObservationPayload };
160
+ //# sourceMappingURL=obs-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obs-store.d.ts","sourceRoot":"","sources":["../src/obs-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,367 @@
1
+ // The local observation outbox.
2
+ //
3
+ // One SQLite file (`observations.db`, beside the receipts) holds everything that must
4
+ // survive a process exit, because the hook is a short-lived process per tool call:
5
+ //
6
+ // state the installation generation, the next sequence to allocate, the upload
7
+ // capability (active / unsupported / blocked) and a backoff deadline
8
+ // pending signed observations not yet acknowledged, in sequence order
9
+ // terminal observations the server refused permanently (or a stale generation made
10
+ // unsendable). They are kept, visible in `status`, and never retried.
11
+ // sessions which harness sessions are active, and the heartbeat owner lease
12
+ // calls the operation of each dispatched tool call, so the after-action hook can
13
+ // echo the same binding
14
+ //
15
+ // Sequence allocation and the pending row are written in ONE `BEGIN IMMEDIATE`
16
+ // transaction, so a crash between the two cannot leave a gap or a reused number, and two
17
+ // hook processes cannot allocate the same one. An observation id is unique, so a resend
18
+ // of the same logical record returns the row that exists rather than allocating again.
19
+ import { createRequire } from "node:module";
20
+ import { mkdirSync } from "node:fs";
21
+ import { dirname } from "node:path";
22
+ import { MAX_BATCH_BODY_BYTES, MAX_BATCH_ITEMS, observationHash, buildPayload, signObservation } from "./observation.js";
23
+ export const OBSERVATION_DB = "observations.db";
24
+ /** Bounds on the local queue. Past them new observations are counted as dropped (visible
25
+ * in `status`) rather than growing without limit; enforcement never depends on the queue. */
26
+ export const MAX_PENDING = 5_000;
27
+ export const MAX_PENDING_BYTES = 32 * 1024 * 1024;
28
+ export const MAX_TERMINAL = 1_000;
29
+ /** How long a route found missing is left alone before one upload probes it again. */
30
+ /** How long a batch stays reserved for the process that took it. */
31
+ export const INFLIGHT_MS = 30_000;
32
+ export const UNSUPPORTED_RECHECK_MS = 24 * 60 * 60 * 1000;
33
+ function open(path) {
34
+ mkdirSync(dirname(path), { recursive: true });
35
+ const require = createRequire(import.meta.url);
36
+ const { DatabaseSync } = require("node:sqlite");
37
+ const db = new DatabaseSync(path);
38
+ db.exec("PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;");
39
+ return db;
40
+ }
41
+ export class ObservationStore {
42
+ now;
43
+ db;
44
+ constructor(path, now = Date.now) {
45
+ this.now = now;
46
+ this.db = open(path);
47
+ this.db.exec(`
48
+ CREATE TABLE IF NOT EXISTS state (
49
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
50
+ generation INTEGER NOT NULL, next_sequence INTEGER NOT NULL,
51
+ capability TEXT NOT NULL DEFAULT 'active', reason TEXT, capability_at INTEGER,
52
+ backoff_until INTEGER NOT NULL DEFAULT 0, last_error TEXT, last_upload_at INTEGER,
53
+ dropped INTEGER NOT NULL DEFAULT 0, batch_limit INTEGER NOT NULL DEFAULT ${MAX_BATCH_ITEMS}
54
+ );
55
+ CREATE TABLE IF NOT EXISTS pending (
56
+ generation INTEGER NOT NULL, sequence INTEGER NOT NULL,
57
+ observation_id TEXT NOT NULL UNIQUE, observation_hash TEXT NOT NULL, kind TEXT NOT NULL,
58
+ wrapper TEXT NOT NULL, bytes INTEGER NOT NULL, enqueued_at INTEGER NOT NULL,
59
+ attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at INTEGER NOT NULL DEFAULT 0,
60
+ PRIMARY KEY (generation, sequence)
61
+ );
62
+ CREATE TABLE IF NOT EXISTS terminal (
63
+ id INTEGER PRIMARY KEY AUTOINCREMENT, generation INTEGER NOT NULL, sequence INTEGER NOT NULL,
64
+ observation_id TEXT NOT NULL, kind TEXT NOT NULL, code TEXT NOT NULL, at INTEGER NOT NULL, wrapper TEXT
65
+ );
66
+ CREATE TABLE IF NOT EXISTS sessions (
67
+ session_id TEXT PRIMARY KEY, state TEXT NOT NULL, started_at INTEGER NOT NULL,
68
+ last_activity_at INTEGER NOT NULL, last_heartbeat_at INTEGER NOT NULL DEFAULT 0,
69
+ heartbeat_lease_until INTEGER NOT NULL DEFAULT 0, repository_id TEXT, stopped_reason TEXT
70
+ );
71
+ CREATE TABLE IF NOT EXISTS calls (
72
+ call_key TEXT PRIMARY KEY, parent_action_id TEXT NOT NULL, source_receipt_hash TEXT NOT NULL,
73
+ session_id TEXT, operation TEXT NOT NULL, at INTEGER NOT NULL
74
+ );
75
+ CREATE TABLE IF NOT EXISTS marks (name TEXT PRIMARY KEY, value INTEGER NOT NULL);
76
+ `);
77
+ }
78
+ close() { try {
79
+ this.db.close();
80
+ }
81
+ catch { /* already closed */ } }
82
+ tx(work) {
83
+ this.db.exec("BEGIN IMMEDIATE");
84
+ try {
85
+ const result = work();
86
+ this.db.exec("COMMIT");
87
+ return result;
88
+ }
89
+ catch (error) {
90
+ try {
91
+ this.db.exec("ROLLBACK");
92
+ }
93
+ catch { /* the original error matters */ }
94
+ throw error;
95
+ }
96
+ }
97
+ state() {
98
+ return this.db.prepare("SELECT generation, next_sequence, capability, reason, capability_at, backoff_until, last_error, last_upload_at, dropped, batch_limit FROM state WHERE singleton = 1").get() ?? null;
99
+ }
100
+ /**
101
+ * Bind the outbox to the installation generation the workspace enrolled. A higher
102
+ * generation than the stored one means the key or credential was rotated: sequence
103
+ * restarts at 1, and everything still pending under the old generation moves to the
104
+ * terminal queue as `stale_generation` (kept locally, never uploaded). A generation
105
+ * lower than the stored one is a stale connection file and reported, not adopted.
106
+ */
107
+ bindGeneration(generation) {
108
+ return this.tx(() => {
109
+ const state = this.state();
110
+ const at = this.now();
111
+ if (!state) {
112
+ this.db.prepare("INSERT INTO state (singleton, generation, next_sequence) VALUES (1, ?, 1)").run(generation);
113
+ return { ok: true, retired: 0 };
114
+ }
115
+ if (generation < state.generation)
116
+ return { ok: false, reason: `connection generation ${generation} is older than the local generation ${state.generation}` };
117
+ if (generation === state.generation)
118
+ return { ok: true, retired: 0 };
119
+ const stale = this.db.prepare("SELECT generation, sequence, observation_id, kind, wrapper FROM pending WHERE generation < ?").all(generation);
120
+ for (const row of stale) {
121
+ this.db.prepare("INSERT INTO terminal (generation, sequence, observation_id, kind, code, at, wrapper) VALUES (?, ?, ?, ?, 'stale_generation', ?, ?)").run(row.generation, row.sequence, row.observation_id, row.kind, at, row.wrapper);
122
+ }
123
+ this.db.prepare("DELETE FROM pending WHERE generation < ?").run(generation);
124
+ this.db.prepare("UPDATE state SET generation = ?, next_sequence = 1, capability = CASE WHEN capability = 'blocked' THEN 'active' ELSE capability END, reason = CASE WHEN capability = 'blocked' THEN NULL ELSE reason END, backoff_until = 0 WHERE singleton = 1").run(generation);
125
+ this.trimTerminal();
126
+ return { ok: true, retired: stale.length };
127
+ });
128
+ }
129
+ /**
130
+ * Allocate the next sequence, build and sign the payload, and store it, atomically.
131
+ * `observationId` makes the call idempotent: an id already stored (pending or already
132
+ * sent) returns without consuming a sequence.
133
+ */
134
+ enqueue(draft, context, signer) {
135
+ return this.tx(() => {
136
+ const state = this.state();
137
+ const at = this.now();
138
+ if (!state || state.generation !== context.generation)
139
+ return { queued: false, reason: "generation_mismatch" };
140
+ if (draft.observationId) {
141
+ const existing = this.db.prepare("SELECT sequence FROM pending WHERE observation_id = ?").get(draft.observationId);
142
+ if (existing)
143
+ return { queued: true, duplicate: true, sequence: existing.sequence, observation_id: draft.observationId };
144
+ }
145
+ if (state.capability === "blocked")
146
+ return { queued: false, reason: "blocked", detail: state.reason ?? undefined };
147
+ if (state.capability === "unsupported") {
148
+ if ((state.capability_at ?? 0) + UNSUPPORTED_RECHECK_MS > at)
149
+ return { queued: false, reason: "unsupported", detail: state.reason ?? undefined };
150
+ // One probe after the recheck interval: the server may have been upgraded.
151
+ this.db.prepare("UPDATE state SET capability = 'active', reason = NULL, capability_at = NULL, backoff_until = 0 WHERE singleton = 1").run();
152
+ }
153
+ const totals = this.db.prepare("SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM pending").get();
154
+ const sequence = state.next_sequence;
155
+ let wrapper;
156
+ try {
157
+ wrapper = signObservation(buildPayload(draft, context, sequence), signer);
158
+ }
159
+ catch (error) {
160
+ if (error instanceof RangeError)
161
+ return { queued: false, reason: "oversize", detail: error.message };
162
+ throw error;
163
+ }
164
+ const json = JSON.stringify(wrapper);
165
+ const bytes = Buffer.byteLength(json, "utf8");
166
+ if (totals.n >= MAX_PENDING || totals.b + bytes > MAX_PENDING_BYTES) {
167
+ this.db.prepare("UPDATE state SET dropped = dropped + 1 WHERE singleton = 1").run();
168
+ return { queued: false, reason: "capacity" };
169
+ }
170
+ this.db.prepare("INSERT INTO pending (generation, sequence, observation_id, observation_hash, kind, wrapper, bytes, enqueued_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
171
+ .run(context.generation, sequence, wrapper.payload.observation_id, observationHash(wrapper.payload), wrapper.payload.kind, json, bytes, at);
172
+ this.db.prepare("UPDATE state SET next_sequence = ? WHERE singleton = 1").run(sequence + 1);
173
+ return { queued: true, duplicate: false, sequence, observation_id: wrapper.payload.observation_id };
174
+ });
175
+ }
176
+ rowToPending(row) {
177
+ return { ...row, wrapper: JSON.parse(String(row.wrapper)) };
178
+ }
179
+ /** The next batch to send: current generation, in sequence order, due now, within the
180
+ * item and body limits. With `lease`, the rows are marked in flight for INFLIGHT_MS in the
181
+ * same transaction, so a second hook process uploading at the same moment skips them
182
+ * instead of sending the same items again. The sender releases or defers them afterwards
183
+ * (a crashed sender's lease simply lapses). */
184
+ nextBatch(limit, lease = false) {
185
+ return this.tx(() => {
186
+ const state = this.state();
187
+ if (!state || state.capability !== "active")
188
+ return [];
189
+ const at = this.now();
190
+ if (state.backoff_until > at)
191
+ return [];
192
+ const rows = this.db.prepare("SELECT * FROM pending WHERE generation = ? AND next_attempt_at <= ? ORDER BY sequence LIMIT ?")
193
+ .all(state.generation, at, Math.max(1, Math.min(limit ?? state.batch_limit, state.batch_limit, MAX_BATCH_ITEMS)));
194
+ const out = [];
195
+ let body = 64;
196
+ for (const row of rows) {
197
+ const bytes = Number(row.bytes) + 1;
198
+ if (out.length > 0 && body + bytes > MAX_BATCH_BODY_BYTES)
199
+ break;
200
+ body += bytes;
201
+ out.push(this.rowToPending(row));
202
+ }
203
+ if (lease)
204
+ for (const row of out)
205
+ this.db.prepare("UPDATE pending SET next_attempt_at = ? WHERE observation_id = ?").run(at + INFLIGHT_MS, row.observation_id);
206
+ return out;
207
+ });
208
+ }
209
+ /** Give leased rows back without counting an attempt (the send did not happen). */
210
+ release(observationIds) {
211
+ this.tx(() => { for (const id of observationIds)
212
+ this.db.prepare("UPDATE pending SET next_attempt_at = 0 WHERE observation_id = ?").run(id); });
213
+ }
214
+ pendingSummary() {
215
+ const row = this.db.prepare("SELECT COUNT(*) AS n, MIN(enqueued_at) AS o FROM pending").get();
216
+ return { count: row.n, oldest_at: row.o };
217
+ }
218
+ /** Remove acknowledged items (accepted, duplicate, pending_link) — and only those. */
219
+ acknowledge(observationIds) {
220
+ this.tx(() => { for (const id of observationIds)
221
+ this.db.prepare("DELETE FROM pending WHERE observation_id = ?").run(id); });
222
+ this.mark("last_ack", this.now());
223
+ this.db.prepare("UPDATE state SET last_upload_at = ?, last_error = NULL WHERE singleton = 1").run(this.now());
224
+ }
225
+ /** Move permanently refused items to the visible terminal queue. */
226
+ reject(items) {
227
+ this.tx(() => {
228
+ const at = this.now();
229
+ for (const item of items) {
230
+ const row = this.db.prepare("SELECT generation, sequence, kind, wrapper FROM pending WHERE observation_id = ?").get(item.observation_id);
231
+ if (!row)
232
+ continue;
233
+ this.db.prepare("INSERT INTO terminal (generation, sequence, observation_id, kind, code, at, wrapper) VALUES (?, ?, ?, ?, ?, ?, ?)").run(row.generation, row.sequence, item.observation_id, row.kind, item.code, at, row.wrapper);
234
+ this.db.prepare("DELETE FROM pending WHERE observation_id = ?").run(item.observation_id);
235
+ }
236
+ this.trimTerminal();
237
+ });
238
+ }
239
+ /** Keep items pending but retry them later (server said deferred, or the send failed). */
240
+ defer(observationIds, delayMs) {
241
+ const at = this.now() + Math.max(0, delayMs);
242
+ this.tx(() => { for (const id of observationIds)
243
+ this.db.prepare("UPDATE pending SET attempts = attempts + 1, next_attempt_at = ? WHERE observation_id = ?").run(at, id); });
244
+ }
245
+ setBackoff(untilMs, error) {
246
+ this.db.prepare("UPDATE state SET backoff_until = ?, last_error = ? WHERE singleton = 1").run(untilMs, error);
247
+ }
248
+ setBatchLimit(limit) {
249
+ this.db.prepare("UPDATE state SET batch_limit = ? WHERE singleton = 1").run(Math.max(1, Math.min(MAX_BATCH_ITEMS, Math.trunc(limit))));
250
+ }
251
+ /** Mark the capability unsupported (route missing, batch version refused) or blocked
252
+ * (stale generation). New observations are then not queued; local enforcement is
253
+ * untouched. Pending items are kept. */
254
+ setCapability(capability, reason) {
255
+ this.db.prepare("UPDATE state SET capability = ?, reason = ?, capability_at = ? WHERE singleton = 1").run(capability, reason, capability === "active" ? null : this.now());
256
+ }
257
+ /** Move everything pending to the terminal queue with `code` (a stale generation makes
258
+ * the whole backlog unsendable). */
259
+ rejectAllPending(code) {
260
+ const ids = this.db.prepare("SELECT observation_id FROM pending").all().map((r) => r.observation_id);
261
+ this.reject(ids.map((observation_id) => ({ observation_id, code })));
262
+ return ids.length;
263
+ }
264
+ terminal(limit = 50) {
265
+ return this.db.prepare("SELECT * FROM terminal ORDER BY id DESC LIMIT ?").all(limit)
266
+ .map((r) => ({ ...r, wrapper: r.wrapper ? JSON.parse(String(r.wrapper)) : null }));
267
+ }
268
+ terminalCounts() {
269
+ const out = {};
270
+ for (const r of this.db.prepare("SELECT code, COUNT(*) AS n FROM terminal GROUP BY code").all())
271
+ out[r.code] = r.n;
272
+ return out;
273
+ }
274
+ trimTerminal() {
275
+ this.db.prepare("DELETE FROM terminal WHERE id NOT IN (SELECT id FROM terminal ORDER BY id DESC LIMIT ?)").run(MAX_TERMINAL);
276
+ }
277
+ // ---- marks (small named integers, e.g. when queue telemetry last ran) --------------------
278
+ mark(name, value) {
279
+ this.db.prepare("INSERT INTO marks (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value").run(name, value);
280
+ }
281
+ getMark(name) {
282
+ const row = this.db.prepare("SELECT value FROM marks WHERE name = ?").get(name);
283
+ return row ? row.value : null;
284
+ }
285
+ // ---- sessions and the heartbeat lease --------------------------------------------------------
286
+ session(sessionId) {
287
+ return this.db.prepare("SELECT * FROM sessions WHERE session_id = ?").get(sessionId) ?? null;
288
+ }
289
+ /** Begin (or resume) a session. Returns true when the session was not active before,
290
+ * so the caller emits exactly one start. */
291
+ activateSession(sessionId, repositoryId) {
292
+ return this.tx(() => {
293
+ const at = this.now();
294
+ const row = this.session(sessionId);
295
+ if (row?.state === "active") {
296
+ this.db.prepare("UPDATE sessions SET last_activity_at = ? WHERE session_id = ?").run(at, sessionId);
297
+ return false;
298
+ }
299
+ this.db.prepare("INSERT INTO sessions (session_id, state, started_at, last_activity_at, repository_id) VALUES (?, 'active', ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET state = 'active', stopped_reason = NULL, started_at = excluded.started_at, last_activity_at = excluded.last_activity_at, heartbeat_lease_until = 0, repository_id = excluded.repository_id")
300
+ .run(sessionId, at, at, repositoryId);
301
+ return true;
302
+ });
303
+ }
304
+ touchSession(sessionId) {
305
+ this.db.prepare("UPDATE sessions SET last_activity_at = ? WHERE session_id = ? AND state = 'active'").run(this.now(), sessionId);
306
+ }
307
+ /** End a session. Returns true when it was active, so exactly one stop is emitted. */
308
+ deactivateSession(sessionId, reason) {
309
+ return this.tx(() => {
310
+ const row = this.session(sessionId);
311
+ if (!row || row.state !== "active")
312
+ return false;
313
+ this.db.prepare("UPDATE sessions SET state = 'stopped', stopped_reason = ?, heartbeat_lease_until = 0 WHERE session_id = ?").run(reason, sessionId);
314
+ return true;
315
+ });
316
+ }
317
+ activeSessions() {
318
+ return this.db.prepare("SELECT * FROM sessions WHERE state = 'active' ORDER BY started_at").all();
319
+ }
320
+ /** Claim the single heartbeat loop for a session, or refuse when another live loop
321
+ * already holds it. The lease is short and renewed each tick, so a crashed loop frees
322
+ * the claim within `leaseMs`. This is what keeps the number of helper processes at one
323
+ * per active session no matter how many hook calls run. */
324
+ claimHeartbeat(sessionId, leaseMs) {
325
+ return this.tx(() => {
326
+ const row = this.session(sessionId);
327
+ const at = this.now();
328
+ if (!row || row.state !== "active" || row.heartbeat_lease_until > at)
329
+ return false;
330
+ this.db.prepare("UPDATE sessions SET heartbeat_lease_until = ? WHERE session_id = ?").run(at + leaseMs, sessionId);
331
+ return true;
332
+ });
333
+ }
334
+ /** Renew the loop's own claim; false when the session ended or the claim was lost. */
335
+ renewHeartbeat(sessionId, leaseMs, sentHeartbeat) {
336
+ return this.tx(() => {
337
+ const row = this.session(sessionId);
338
+ if (!row || row.state !== "active")
339
+ return false;
340
+ const at = this.now();
341
+ this.db.prepare("UPDATE sessions SET heartbeat_lease_until = ?, last_heartbeat_at = CASE WHEN ? THEN ? ELSE last_heartbeat_at END WHERE session_id = ?").run(at + leaseMs, sentHeartbeat ? 1 : 0, at, sessionId);
342
+ return true;
343
+ });
344
+ }
345
+ releaseHeartbeat(sessionId) {
346
+ this.db.prepare("UPDATE sessions SET heartbeat_lease_until = 0 WHERE session_id = ?").run(sessionId);
347
+ }
348
+ // ---- dispatched-call records -----------------------------------------------------------------
349
+ recordCall(call) {
350
+ const at = this.now();
351
+ this.tx(() => {
352
+ this.db.prepare("INSERT OR REPLACE INTO calls (call_key, parent_action_id, source_receipt_hash, session_id, operation, at) VALUES (?, ?, ?, ?, ?, ?)")
353
+ .run(call.call_key, call.parent_action_id, call.source_receipt_hash, call.session_id, JSON.stringify(call.operation), at);
354
+ // Calls whose outcome never arrived (a denied action, a crashed tool) are dropped after a day.
355
+ this.db.prepare("DELETE FROM calls WHERE at < ?").run(at - 24 * 60 * 60 * 1000);
356
+ });
357
+ }
358
+ /** The calls recorded under a harness call id, consumed by the after-action hook. */
359
+ takeCalls(callPrefix) {
360
+ return this.tx(() => {
361
+ const rows = this.db.prepare("SELECT * FROM calls WHERE call_key LIKE ? ESCAPE '\\' ORDER BY call_key").all(`${callPrefix.replace(/[\\%_]/g, "\\$&")}%`);
362
+ this.db.prepare("DELETE FROM calls WHERE call_key LIKE ? ESCAPE '\\'").run(`${callPrefix.replace(/[\\%_]/g, "\\$&")}%`);
363
+ return rows.map((r) => ({ ...r, operation: JSON.parse(String(r.operation)) }));
364
+ });
365
+ }
366
+ }
367
+ //# sourceMappingURL=obs-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"obs-store.js","sourceRoot":"","sources":["../src/obs-store.ts"],"names":[],"mappings":"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","sourcesContent":["// The local observation outbox.\n//\n// One SQLite file (`observations.db`, beside the receipts) holds everything that must\n// survive a process exit, because the hook is a short-lived process per tool call:\n//\n// state the installation generation, the next sequence to allocate, the upload\n// capability (active / unsupported / blocked) and a backoff deadline\n// pending signed observations not yet acknowledged, in sequence order\n// terminal observations the server refused permanently (or a stale generation made\n// unsendable). They are kept, visible in `status`, and never retried.\n// sessions which harness sessions are active, and the heartbeat owner lease\n// calls the operation of each dispatched tool call, so the after-action hook can\n// echo the same binding\n//\n// Sequence allocation and the pending row are written in ONE `BEGIN IMMEDIATE`\n// transaction, so a crash between the two cannot leave a gap or a reused number, and two\n// hook processes cannot allocate the same one. An observation id is unique, so a resend\n// of the same logical record returns the row that exists rather than allocating again.\n\nimport { createRequire } from \"node:module\";\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { MAX_BATCH_BODY_BYTES, MAX_BATCH_ITEMS, observationHash, type ObservationDraft, type ObservationPayload, type SignedObservation, type EnvelopeContext, buildPayload, signObservation, type ObservationSigner } from \"./observation.js\";\n\ninterface SqliteStatement { run(...args: unknown[]): unknown; all(...args: unknown[]): unknown[]; get(...args: unknown[]): unknown }\ninterface SqliteDb { exec(sql: string): void; prepare(sql: string): SqliteStatement; close(): void }\n\nexport const OBSERVATION_DB = \"observations.db\";\n\n/** Bounds on the local queue. Past them new observations are counted as dropped (visible\n * in `status`) rather than growing without limit; enforcement never depends on the queue. */\nexport const MAX_PENDING = 5_000;\nexport const MAX_PENDING_BYTES = 32 * 1024 * 1024;\nexport const MAX_TERMINAL = 1_000;\n/** How long a route found missing is left alone before one upload probes it again. */\n/** How long a batch stays reserved for the process that took it. */\nexport const INFLIGHT_MS = 30_000;\nexport const UNSUPPORTED_RECHECK_MS = 24 * 60 * 60 * 1000;\n\nexport type Capability = \"active\" | \"unsupported\" | \"blocked\";\n\nexport interface StoreState {\n generation: number;\n next_sequence: number;\n capability: Capability;\n reason: string | null;\n capability_at: number | null;\n backoff_until: number;\n last_error: string | null;\n last_upload_at: number | null;\n dropped: number;\n batch_limit: number;\n}\n\nexport interface PendingRow {\n sequence: number;\n generation: number;\n observation_id: string;\n observation_hash: string;\n kind: string;\n wrapper: SignedObservation;\n bytes: number;\n enqueued_at: number;\n attempts: number;\n next_attempt_at: number;\n}\n\nexport interface TerminalRow {\n id: number;\n generation: number;\n sequence: number;\n observation_id: string;\n kind: string;\n code: string;\n at: number;\n wrapper: SignedObservation | null;\n}\n\nexport interface SessionRow {\n session_id: string;\n state: \"active\" | \"stopped\";\n started_at: number;\n last_activity_at: number;\n last_heartbeat_at: number;\n heartbeat_lease_until: number;\n repository_id: string | null;\n stopped_reason: string | null;\n}\n\nexport interface CallRow {\n call_key: string;\n parent_action_id: string;\n source_receipt_hash: string;\n session_id: string | null;\n operation: Record<string, unknown>;\n at: number;\n}\n\nexport type EnqueueResult =\n | { queued: true; duplicate: boolean; sequence: number; observation_id: string }\n | { queued: false; reason: \"unsupported\" | \"blocked\" | \"capacity\" | \"oversize\" | \"generation_mismatch\"; detail?: string };\n\nfunction open(path: string): SqliteDb {\n mkdirSync(dirname(path), { recursive: true });\n const require = createRequire(import.meta.url);\n const { DatabaseSync } = require(\"node:sqlite\") as { DatabaseSync: new (p: string) => SqliteDb };\n const db = new DatabaseSync(path);\n db.exec(\"PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA synchronous = FULL;\");\n return db;\n}\n\nexport class ObservationStore {\n private readonly db: SqliteDb;\n\n constructor(path: string, private readonly now: () => number = Date.now) {\n this.db = open(path);\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS state (\n singleton INTEGER PRIMARY KEY CHECK (singleton = 1),\n generation INTEGER NOT NULL, next_sequence INTEGER NOT NULL,\n capability TEXT NOT NULL DEFAULT 'active', reason TEXT, capability_at INTEGER,\n backoff_until INTEGER NOT NULL DEFAULT 0, last_error TEXT, last_upload_at INTEGER,\n dropped INTEGER NOT NULL DEFAULT 0, batch_limit INTEGER NOT NULL DEFAULT ${MAX_BATCH_ITEMS}\n );\n CREATE TABLE IF NOT EXISTS pending (\n generation INTEGER NOT NULL, sequence INTEGER NOT NULL,\n observation_id TEXT NOT NULL UNIQUE, observation_hash TEXT NOT NULL, kind TEXT NOT NULL,\n wrapper TEXT NOT NULL, bytes INTEGER NOT NULL, enqueued_at INTEGER NOT NULL,\n attempts INTEGER NOT NULL DEFAULT 0, next_attempt_at INTEGER NOT NULL DEFAULT 0,\n PRIMARY KEY (generation, sequence)\n );\n CREATE TABLE IF NOT EXISTS terminal (\n id INTEGER PRIMARY KEY AUTOINCREMENT, generation INTEGER NOT NULL, sequence INTEGER NOT NULL,\n observation_id TEXT NOT NULL, kind TEXT NOT NULL, code TEXT NOT NULL, at INTEGER NOT NULL, wrapper TEXT\n );\n CREATE TABLE IF NOT EXISTS sessions (\n session_id TEXT PRIMARY KEY, state TEXT NOT NULL, started_at INTEGER NOT NULL,\n last_activity_at INTEGER NOT NULL, last_heartbeat_at INTEGER NOT NULL DEFAULT 0,\n heartbeat_lease_until INTEGER NOT NULL DEFAULT 0, repository_id TEXT, stopped_reason TEXT\n );\n CREATE TABLE IF NOT EXISTS calls (\n call_key TEXT PRIMARY KEY, parent_action_id TEXT NOT NULL, source_receipt_hash TEXT NOT NULL,\n session_id TEXT, operation TEXT NOT NULL, at INTEGER NOT NULL\n );\n CREATE TABLE IF NOT EXISTS marks (name TEXT PRIMARY KEY, value INTEGER NOT NULL);\n `);\n }\n\n close(): void { try { this.db.close(); } catch { /* already closed */ } }\n\n private tx<T>(work: () => T): T {\n this.db.exec(\"BEGIN IMMEDIATE\");\n try { const result = work(); this.db.exec(\"COMMIT\"); return result; }\n catch (error) { try { this.db.exec(\"ROLLBACK\"); } catch { /* the original error matters */ } throw error; }\n }\n\n state(): StoreState | null {\n return (this.db.prepare(\"SELECT generation, next_sequence, capability, reason, capability_at, backoff_until, last_error, last_upload_at, dropped, batch_limit FROM state WHERE singleton = 1\").get() as StoreState | undefined) ?? null;\n }\n\n /**\n * Bind the outbox to the installation generation the workspace enrolled. A higher\n * generation than the stored one means the key or credential was rotated: sequence\n * restarts at 1, and everything still pending under the old generation moves to the\n * terminal queue as `stale_generation` (kept locally, never uploaded). A generation\n * lower than the stored one is a stale connection file and reported, not adopted.\n */\n bindGeneration(generation: number): { ok: true; retired: number } | { ok: false; reason: string } {\n return this.tx(() => {\n const state = this.state();\n const at = this.now();\n if (!state) {\n this.db.prepare(\"INSERT INTO state (singleton, generation, next_sequence) VALUES (1, ?, 1)\").run(generation);\n return { ok: true as const, retired: 0 };\n }\n if (generation < state.generation) return { ok: false as const, reason: `connection generation ${generation} is older than the local generation ${state.generation}` };\n if (generation === state.generation) return { ok: true as const, retired: 0 };\n const stale = this.db.prepare(\"SELECT generation, sequence, observation_id, kind, wrapper FROM pending WHERE generation < ?\").all(generation) as Array<{ generation: number; sequence: number; observation_id: string; kind: string; wrapper: string }>;\n for (const row of stale) {\n this.db.prepare(\"INSERT INTO terminal (generation, sequence, observation_id, kind, code, at, wrapper) VALUES (?, ?, ?, ?, 'stale_generation', ?, ?)\").run(row.generation, row.sequence, row.observation_id, row.kind, at, row.wrapper);\n }\n this.db.prepare(\"DELETE FROM pending WHERE generation < ?\").run(generation);\n this.db.prepare(\"UPDATE state SET generation = ?, next_sequence = 1, capability = CASE WHEN capability = 'blocked' THEN 'active' ELSE capability END, reason = CASE WHEN capability = 'blocked' THEN NULL ELSE reason END, backoff_until = 0 WHERE singleton = 1\").run(generation);\n this.trimTerminal();\n return { ok: true as const, retired: stale.length };\n });\n }\n\n /**\n * Allocate the next sequence, build and sign the payload, and store it, atomically.\n * `observationId` makes the call idempotent: an id already stored (pending or already\n * sent) returns without consuming a sequence.\n */\n enqueue(draft: ObservationDraft, context: EnvelopeContext, signer: ObservationSigner): EnqueueResult {\n return this.tx((): EnqueueResult => {\n const state = this.state();\n const at = this.now();\n if (!state || state.generation !== context.generation) return { queued: false, reason: \"generation_mismatch\" };\n if (draft.observationId) {\n const existing = this.db.prepare(\"SELECT sequence FROM pending WHERE observation_id = ?\").get(draft.observationId) as { sequence: number } | undefined;\n if (existing) return { queued: true, duplicate: true, sequence: existing.sequence, observation_id: draft.observationId };\n }\n if (state.capability === \"blocked\") return { queued: false, reason: \"blocked\", detail: state.reason ?? undefined };\n if (state.capability === \"unsupported\") {\n if ((state.capability_at ?? 0) + UNSUPPORTED_RECHECK_MS > at) return { queued: false, reason: \"unsupported\", detail: state.reason ?? undefined };\n // One probe after the recheck interval: the server may have been upgraded.\n this.db.prepare(\"UPDATE state SET capability = 'active', reason = NULL, capability_at = NULL, backoff_until = 0 WHERE singleton = 1\").run();\n }\n const totals = this.db.prepare(\"SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM pending\").get() as { n: number; b: number };\n const sequence = state.next_sequence;\n let wrapper: SignedObservation;\n try { wrapper = signObservation(buildPayload(draft, context, sequence), signer); }\n catch (error) {\n if (error instanceof RangeError) return { queued: false, reason: \"oversize\", detail: error.message };\n throw error;\n }\n const json = JSON.stringify(wrapper);\n const bytes = Buffer.byteLength(json, \"utf8\");\n if (totals.n >= MAX_PENDING || totals.b + bytes > MAX_PENDING_BYTES) {\n this.db.prepare(\"UPDATE state SET dropped = dropped + 1 WHERE singleton = 1\").run();\n return { queued: false, reason: \"capacity\" };\n }\n this.db.prepare(\"INSERT INTO pending (generation, sequence, observation_id, observation_hash, kind, wrapper, bytes, enqueued_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)\")\n .run(context.generation, sequence, wrapper.payload.observation_id, observationHash(wrapper.payload), wrapper.payload.kind, json, bytes, at);\n this.db.prepare(\"UPDATE state SET next_sequence = ? WHERE singleton = 1\").run(sequence + 1);\n return { queued: true, duplicate: false, sequence, observation_id: wrapper.payload.observation_id };\n });\n }\n\n private rowToPending(row: Record<string, unknown>): PendingRow {\n return { ...(row as unknown as PendingRow), wrapper: JSON.parse(String(row.wrapper)) as SignedObservation };\n }\n\n /** The next batch to send: current generation, in sequence order, due now, within the\n * item and body limits. With `lease`, the rows are marked in flight for INFLIGHT_MS in the\n * same transaction, so a second hook process uploading at the same moment skips them\n * instead of sending the same items again. The sender releases or defers them afterwards\n * (a crashed sender's lease simply lapses). */\n nextBatch(limit?: number, lease = false): PendingRow[] {\n return this.tx(() => {\n const state = this.state();\n if (!state || state.capability !== \"active\") return [];\n const at = this.now();\n if (state.backoff_until > at) return [];\n const rows = this.db.prepare(\"SELECT * FROM pending WHERE generation = ? AND next_attempt_at <= ? ORDER BY sequence LIMIT ?\")\n .all(state.generation, at, Math.max(1, Math.min(limit ?? state.batch_limit, state.batch_limit, MAX_BATCH_ITEMS))) as Array<Record<string, unknown>>;\n const out: PendingRow[] = [];\n let body = 64;\n for (const row of rows) {\n const bytes = Number(row.bytes) + 1;\n if (out.length > 0 && body + bytes > MAX_BATCH_BODY_BYTES) break;\n body += bytes;\n out.push(this.rowToPending(row));\n }\n if (lease) for (const row of out) this.db.prepare(\"UPDATE pending SET next_attempt_at = ? WHERE observation_id = ?\").run(at + INFLIGHT_MS, row.observation_id);\n return out;\n });\n }\n\n /** Give leased rows back without counting an attempt (the send did not happen). */\n release(observationIds: string[]): void {\n this.tx(() => { for (const id of observationIds) this.db.prepare(\"UPDATE pending SET next_attempt_at = 0 WHERE observation_id = ?\").run(id); });\n }\n\n pendingSummary(): { count: number; oldest_at: number | null } {\n const row = this.db.prepare(\"SELECT COUNT(*) AS n, MIN(enqueued_at) AS o FROM pending\").get() as { n: number; o: number | null };\n return { count: row.n, oldest_at: row.o };\n }\n\n /** Remove acknowledged items (accepted, duplicate, pending_link) — and only those. */\n acknowledge(observationIds: string[]): void {\n this.tx(() => { for (const id of observationIds) this.db.prepare(\"DELETE FROM pending WHERE observation_id = ?\").run(id); });\n this.mark(\"last_ack\", this.now());\n this.db.prepare(\"UPDATE state SET last_upload_at = ?, last_error = NULL WHERE singleton = 1\").run(this.now());\n }\n\n /** Move permanently refused items to the visible terminal queue. */\n reject(items: Array<{ observation_id: string; code: string }>): void {\n this.tx(() => {\n const at = this.now();\n for (const item of items) {\n const row = this.db.prepare(\"SELECT generation, sequence, kind, wrapper FROM pending WHERE observation_id = ?\").get(item.observation_id) as { generation: number; sequence: number; kind: string; wrapper: string } | undefined;\n if (!row) continue;\n this.db.prepare(\"INSERT INTO terminal (generation, sequence, observation_id, kind, code, at, wrapper) VALUES (?, ?, ?, ?, ?, ?, ?)\").run(row.generation, row.sequence, item.observation_id, row.kind, item.code, at, row.wrapper);\n this.db.prepare(\"DELETE FROM pending WHERE observation_id = ?\").run(item.observation_id);\n }\n this.trimTerminal();\n });\n }\n\n /** Keep items pending but retry them later (server said deferred, or the send failed). */\n defer(observationIds: string[], delayMs: number): void {\n const at = this.now() + Math.max(0, delayMs);\n this.tx(() => { for (const id of observationIds) this.db.prepare(\"UPDATE pending SET attempts = attempts + 1, next_attempt_at = ? WHERE observation_id = ?\").run(at, id); });\n }\n\n setBackoff(untilMs: number, error: string | null): void {\n this.db.prepare(\"UPDATE state SET backoff_until = ?, last_error = ? WHERE singleton = 1\").run(untilMs, error);\n }\n\n setBatchLimit(limit: number): void {\n this.db.prepare(\"UPDATE state SET batch_limit = ? WHERE singleton = 1\").run(Math.max(1, Math.min(MAX_BATCH_ITEMS, Math.trunc(limit))));\n }\n\n /** Mark the capability unsupported (route missing, batch version refused) or blocked\n * (stale generation). New observations are then not queued; local enforcement is\n * untouched. Pending items are kept. */\n setCapability(capability: Capability, reason: string | null): void {\n this.db.prepare(\"UPDATE state SET capability = ?, reason = ?, capability_at = ? WHERE singleton = 1\").run(capability, reason, capability === \"active\" ? null : this.now());\n }\n\n /** Move everything pending to the terminal queue with `code` (a stale generation makes\n * the whole backlog unsendable). */\n rejectAllPending(code: string): number {\n const ids = (this.db.prepare(\"SELECT observation_id FROM pending\").all() as Array<{ observation_id: string }>).map((r) => r.observation_id);\n this.reject(ids.map((observation_id) => ({ observation_id, code })));\n return ids.length;\n }\n\n terminal(limit = 50): TerminalRow[] {\n return (this.db.prepare(\"SELECT * FROM terminal ORDER BY id DESC LIMIT ?\").all(limit) as Array<Record<string, unknown>>)\n .map((r) => ({ ...(r as unknown as TerminalRow), wrapper: r.wrapper ? (JSON.parse(String(r.wrapper)) as SignedObservation) : null }));\n }\n\n terminalCounts(): Record<string, number> {\n const out: Record<string, number> = {};\n for (const r of this.db.prepare(\"SELECT code, COUNT(*) AS n FROM terminal GROUP BY code\").all() as Array<{ code: string; n: number }>) out[r.code] = r.n;\n return out;\n }\n\n private trimTerminal(): void {\n this.db.prepare(\"DELETE FROM terminal WHERE id NOT IN (SELECT id FROM terminal ORDER BY id DESC LIMIT ?)\").run(MAX_TERMINAL);\n }\n\n // ---- marks (small named integers, e.g. when queue telemetry last ran) --------------------\n\n mark(name: string, value: number): void {\n this.db.prepare(\"INSERT INTO marks (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value\").run(name, value);\n }\n getMark(name: string): number | null {\n const row = this.db.prepare(\"SELECT value FROM marks WHERE name = ?\").get(name) as { value: number } | undefined;\n return row ? row.value : null;\n }\n\n // ---- sessions and the heartbeat lease --------------------------------------------------------\n\n session(sessionId: string): SessionRow | null {\n return (this.db.prepare(\"SELECT * FROM sessions WHERE session_id = ?\").get(sessionId) as SessionRow | undefined) ?? null;\n }\n\n /** Begin (or resume) a session. Returns true when the session was not active before,\n * so the caller emits exactly one start. */\n activateSession(sessionId: string, repositoryId: string | null): boolean {\n return this.tx(() => {\n const at = this.now();\n const row = this.session(sessionId);\n if (row?.state === \"active\") {\n this.db.prepare(\"UPDATE sessions SET last_activity_at = ? WHERE session_id = ?\").run(at, sessionId);\n return false;\n }\n this.db.prepare(\"INSERT INTO sessions (session_id, state, started_at, last_activity_at, repository_id) VALUES (?, 'active', ?, ?, ?) ON CONFLICT(session_id) DO UPDATE SET state = 'active', stopped_reason = NULL, started_at = excluded.started_at, last_activity_at = excluded.last_activity_at, heartbeat_lease_until = 0, repository_id = excluded.repository_id\")\n .run(sessionId, at, at, repositoryId);\n return true;\n });\n }\n\n touchSession(sessionId: string): void {\n this.db.prepare(\"UPDATE sessions SET last_activity_at = ? WHERE session_id = ? AND state = 'active'\").run(this.now(), sessionId);\n }\n\n /** End a session. Returns true when it was active, so exactly one stop is emitted. */\n deactivateSession(sessionId: string, reason: string): boolean {\n return this.tx(() => {\n const row = this.session(sessionId);\n if (!row || row.state !== \"active\") return false;\n this.db.prepare(\"UPDATE sessions SET state = 'stopped', stopped_reason = ?, heartbeat_lease_until = 0 WHERE session_id = ?\").run(reason, sessionId);\n return true;\n });\n }\n\n activeSessions(): SessionRow[] {\n return this.db.prepare(\"SELECT * FROM sessions WHERE state = 'active' ORDER BY started_at\").all() as unknown as SessionRow[];\n }\n\n /** Claim the single heartbeat loop for a session, or refuse when another live loop\n * already holds it. The lease is short and renewed each tick, so a crashed loop frees\n * the claim within `leaseMs`. This is what keeps the number of helper processes at one\n * per active session no matter how many hook calls run. */\n claimHeartbeat(sessionId: string, leaseMs: number): boolean {\n return this.tx(() => {\n const row = this.session(sessionId);\n const at = this.now();\n if (!row || row.state !== \"active\" || row.heartbeat_lease_until > at) return false;\n this.db.prepare(\"UPDATE sessions SET heartbeat_lease_until = ? WHERE session_id = ?\").run(at + leaseMs, sessionId);\n return true;\n });\n }\n\n /** Renew the loop's own claim; false when the session ended or the claim was lost. */\n renewHeartbeat(sessionId: string, leaseMs: number, sentHeartbeat: boolean): boolean {\n return this.tx(() => {\n const row = this.session(sessionId);\n if (!row || row.state !== \"active\") return false;\n const at = this.now();\n this.db.prepare(\"UPDATE sessions SET heartbeat_lease_until = ?, last_heartbeat_at = CASE WHEN ? THEN ? ELSE last_heartbeat_at END WHERE session_id = ?\").run(at + leaseMs, sentHeartbeat ? 1 : 0, at, sessionId);\n return true;\n });\n }\n\n releaseHeartbeat(sessionId: string): void {\n this.db.prepare(\"UPDATE sessions SET heartbeat_lease_until = 0 WHERE session_id = ?\").run(sessionId);\n }\n\n // ---- dispatched-call records -----------------------------------------------------------------\n\n recordCall(call: Omit<CallRow, \"at\">): void {\n const at = this.now();\n this.tx(() => {\n this.db.prepare(\"INSERT OR REPLACE INTO calls (call_key, parent_action_id, source_receipt_hash, session_id, operation, at) VALUES (?, ?, ?, ?, ?, ?)\")\n .run(call.call_key, call.parent_action_id, call.source_receipt_hash, call.session_id, JSON.stringify(call.operation), at);\n // Calls whose outcome never arrived (a denied action, a crashed tool) are dropped after a day.\n this.db.prepare(\"DELETE FROM calls WHERE at < ?\").run(at - 24 * 60 * 60 * 1000);\n });\n }\n\n /** The calls recorded under a harness call id, consumed by the after-action hook. */\n takeCalls(callPrefix: string): CallRow[] {\n return this.tx(() => {\n const rows = this.db.prepare(\"SELECT * FROM calls WHERE call_key LIKE ? ESCAPE '\\\\' ORDER BY call_key\").all(`${callPrefix.replace(/[\\\\%_]/g, \"\\\\$&\")}%`) as Array<Record<string, unknown>>;\n this.db.prepare(\"DELETE FROM calls WHERE call_key LIKE ? ESCAPE '\\\\'\").run(`${callPrefix.replace(/[\\\\%_]/g, \"\\\\$&\")}%`);\n return rows.map((r) => ({ ...(r as unknown as CallRow), operation: JSON.parse(String(r.operation)) as Record<string, unknown> }));\n });\n }\n}\n\n/** Convenience for callers that only have a payload. */\nexport type { ObservationPayload };\n"]}
@@ -0,0 +1,24 @@
1
+ import type { ObservationStore } from "./obs-store.js";
2
+ export declare const OBSERVATIONS_PATH = "/v1/observations";
3
+ export declare const BATCH_VERSION = "1.0";
4
+ export interface UploadOutcome {
5
+ /** What happened, for `status` and tests. */
6
+ result: "empty" | "paused" | "sent" | "unsupported" | "blocked" | "backoff" | "error";
7
+ acknowledged: number;
8
+ deferred: number;
9
+ rejected: number;
10
+ detail?: string;
11
+ }
12
+ export interface UploadOptions {
13
+ url: string;
14
+ credential: string;
15
+ fetch?: typeof fetch;
16
+ now?: () => number;
17
+ timeoutMs?: number;
18
+ /** Send at most this many batches in one call. */
19
+ maxBatches?: number;
20
+ }
21
+ /** Parse Retry-After: delta-seconds or an HTTP date. Bounded; undefined when unusable. */
22
+ export declare function parseRetryAfter(header: string | null, now: number): number | undefined;
23
+ export declare function uploadPending(store: ObservationStore, options: UploadOptions): Promise<UploadOutcome>;
24
+ //# sourceMappingURL=obs-upload.d.ts.map