@opensip-cli/session-store 0.1.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.
@@ -0,0 +1,178 @@
1
+ /**
2
+ * session-payload-decode — the shared, generic decoder for a stored session's
3
+ * opaque `payload` blob (the inverse of each tool's `build*SessionPayload`).
4
+ *
5
+ * Session replay needs every tool (`fit`/`graph`/`sim`)
6
+ * to read a persisted session back into a {@link SignalEnvelope} projection. The
7
+ * persisted detail shares ONE structural shape — `{ summary, checks[] }`, each
8
+ * check a `{ checkSlug, passed, violationCount?, durationMs, findings[] }` — so
9
+ * the decode of THAT structure lives here once, parameterised by the few
10
+ * per-tool differences (error-label, whether `filePath`/`violationCount` are
11
+ * required, whether findings carry a metadata bag).
12
+ *
13
+ * This holds NO tool vocabulary: it does not know about fit checks, graph rules,
14
+ * severity→category mapping, or signal IDs. Those projections stay in each
15
+ * engine's `session-replay.ts` (`replaySignal`). What lives here is purely the
16
+ * structural shape session-store persists — a faithful counterpart to the
17
+ * package's opaque-payload charter: session-store owns persistence AND the
18
+ * structural decode for replay, while tool semantics remain in the engines.
19
+ *
20
+ * The decoder tolerates legacy payloads that lack a top-level `__version`
21
+ * (treated as v1 with best-effort projection). Tools should prefer their own
22
+ * `*ReplayFromSession` functions (which call this decoder) for full projection
23
+ * to their live result types; this module only gives the common structural
24
+ * skeleton + the detected `payloadVersion` when present.
25
+ */
26
+ import { extractPayloadVersion } from '@opensip-cli/core';
27
+ /**
28
+ * Decode a stored session payload into its structural {@link DecodedSessionPayload}.
29
+ *
30
+ * @param payload - the opaque `StoredSession.payload` blob.
31
+ * @param opts - per-tool decode options (label + required-field toggles).
32
+ * @returns the decoded `{ summary, checks[] }` structure, plus `payloadVersion`
33
+ * (the detected inner `__version` if present and valid; undefined for legacy
34
+ * payloads that pre-date the convention — callers treat missing as v1).
35
+ * @throws {TypeError} when `payload`/`checks`/`findings` are not the expected
36
+ * object/array shapes.
37
+ * @throws {Error} when a required scalar field is missing or mistyped, or a
38
+ * finding severity is not `error`/`warning`.
39
+ */
40
+ export function decodeSessionPayload(payload, opts) {
41
+ if (payload === null || typeof payload !== 'object') {
42
+ throw new Error(`${opts.tool} session has no replay payload`);
43
+ }
44
+ const candidate = payload;
45
+ const summary = decodeSummary(candidate.summary, `${opts.tool} session summary`);
46
+ if (!Array.isArray(candidate.checks)) {
47
+ throw new TypeError(`${opts.tool} session payload is missing checks[]`);
48
+ }
49
+ const payloadVersion = extractPayloadVersion(payload);
50
+ return {
51
+ summary,
52
+ checks: candidate.checks.map((check) => decodeCheck(check, opts)),
53
+ ...(payloadVersion === undefined ? {} : { payloadVersion }),
54
+ };
55
+ }
56
+ /**
57
+ * Decode the `summary` verdict-counts block.
58
+ *
59
+ * @throws {Error} when the value is missing or any count is not a number.
60
+ */
61
+ export function decodeSummary(value, label) {
62
+ if (value === null || typeof value !== 'object') {
63
+ throw new Error(`${label} is missing`);
64
+ }
65
+ const summary = value;
66
+ return {
67
+ total: numberField(summary, 'total', label),
68
+ passed: numberField(summary, 'passed', label),
69
+ failed: numberField(summary, 'failed', label),
70
+ errors: numberField(summary, 'errors', label),
71
+ warnings: numberField(summary, 'warnings', label),
72
+ };
73
+ }
74
+ /**
75
+ * Decode one check row.
76
+ *
77
+ * @throws {Error} when the row is not an object or a required field is mistyped.
78
+ * @throws {TypeError} when `findings` is not an array.
79
+ */
80
+ function decodeCheck(value, opts) {
81
+ if (value === null || typeof value !== 'object') {
82
+ throw new Error(`${opts.tool} session check row is invalid`);
83
+ }
84
+ const check = value;
85
+ const label = `${opts.tool} session check`;
86
+ const checkSlug = stringField(check, 'checkSlug', label);
87
+ let violationCount;
88
+ if (opts.requireViolationCount) {
89
+ violationCount = numberField(check, 'violationCount', label);
90
+ }
91
+ else if (typeof check.violationCount === 'number') {
92
+ violationCount = check.violationCount;
93
+ }
94
+ if (!Array.isArray(check.findings)) {
95
+ throw new TypeError(`${opts.tool} session check ${checkSlug} is missing findings[]`);
96
+ }
97
+ return {
98
+ checkSlug,
99
+ passed: booleanField(check, 'passed', label),
100
+ ...(violationCount === undefined ? {} : { violationCount }),
101
+ durationMs: numberField(check, 'durationMs', label),
102
+ findings: check.findings.map((finding) => decodeFinding(finding, opts)),
103
+ };
104
+ }
105
+ /**
106
+ * Decode one finding row.
107
+ *
108
+ * @throws {Error} when the row is not an object, a required string is mistyped,
109
+ * or the severity is not `error`/`warning`.
110
+ */
111
+ function decodeFinding(value, opts) {
112
+ if (value === null || typeof value !== 'object') {
113
+ throw new Error(`${opts.tool} session finding is invalid`);
114
+ }
115
+ const finding = value;
116
+ const label = `${opts.tool} session finding`;
117
+ const severity = finding.severity;
118
+ if (severity !== 'error' && severity !== 'warning') {
119
+ throw new Error(`${opts.tool} session finding has invalid severity`);
120
+ }
121
+ const filePath = opts.requireFilePath
122
+ ? stringField(finding, 'filePath', label)
123
+ : optionalString(finding.filePath);
124
+ const line = optionalNumber(finding.line);
125
+ const column = optionalNumber(finding.column);
126
+ const suggestion = optionalString(finding.suggestion);
127
+ const metadata = opts.allowMetadata ? decodeMetadata(finding.metadata) : undefined;
128
+ return {
129
+ ruleId: stringField(finding, 'ruleId', label),
130
+ message: stringField(finding, 'message', label),
131
+ severity,
132
+ ...(filePath === undefined ? {} : { filePath }),
133
+ ...(line === undefined ? {} : { line }),
134
+ ...(column === undefined ? {} : { column }),
135
+ ...(suggestion === undefined ? {} : { suggestion }),
136
+ ...(metadata === undefined ? {} : { metadata }),
137
+ };
138
+ }
139
+ /** Narrow an open metadata bag to its scalar subset; undefined when nothing survives. */
140
+ function decodeMetadata(value) {
141
+ if (value === null || typeof value !== 'object')
142
+ return undefined;
143
+ const out = {};
144
+ for (const [key, entry] of Object.entries(value)) {
145
+ if (typeof entry === 'string' || typeof entry === 'number' || typeof entry === 'boolean') {
146
+ out[key] = entry;
147
+ }
148
+ }
149
+ return Object.keys(out).length === 0 ? undefined : out;
150
+ }
151
+ /** @throws {Error} when the field is not a number. */
152
+ export function numberField(source, field, label) {
153
+ const value = source[field];
154
+ if (typeof value !== 'number')
155
+ throw new Error(`${label}.${field} must be a number`);
156
+ return value;
157
+ }
158
+ /** @throws {Error} when the field is not a string. */
159
+ export function stringField(source, field, label) {
160
+ const value = source[field];
161
+ if (typeof value !== 'string')
162
+ throw new Error(`${label}.${field} must be a string`);
163
+ return value;
164
+ }
165
+ /** @throws {Error} when the field is not a boolean. */
166
+ export function booleanField(source, field, label) {
167
+ const value = source[field];
168
+ if (typeof value !== 'boolean')
169
+ throw new Error(`${label}.${field} must be a boolean`);
170
+ return value;
171
+ }
172
+ function optionalString(value) {
173
+ return typeof value === 'string' ? value : undefined;
174
+ }
175
+ function optionalNumber(value) {
176
+ return typeof value === 'number' ? value : undefined;
177
+ }
178
+ //# sourceMappingURL=session-payload-decode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-payload-decode.js","sourceRoot":"","sources":["../src/session-payload-decode.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { type DataStore } from '@opensip-cli/datastore';
2
+ import type { StoredSession, StoredSessionHostMetrics } from '@opensip-cli/contracts';
3
+ import type { ToolShortId } from '@opensip-cli/core';
4
+ /** Filters for {@link SessionRepo.list}: tool short-id and/or max row count. */
5
+ export interface SessionListOptions {
6
+ readonly tool?: ToolShortId;
7
+ readonly limit?: number;
8
+ }
9
+ /**
10
+ * Persistence layer for tool-run sessions. Stores generic session columns plus
11
+ * one opaque per-tool `payload` blob; holds ZERO tool vocabulary — it never
12
+ * inspects/validates the payload shape (the producing tool owns that). (Audit
13
+ * 2026-05-29, session split.)
14
+ */
15
+ export declare class SessionRepo {
16
+ private readonly datastore;
17
+ constructor(datastore: DataStore);
18
+ /**
19
+ * Persist a completed session row and its tool payload. Writes the generic
20
+ * columns including host-owned `startedAt` / `completedAt` (mapped to the
21
+ * physical `timestamp*` / `completed_at*` columns); `session.hostMetrics` is
22
+ * IGNORED here — metrics arrive later and attach via {@link SessionRepo.upsertHostMetrics}.
23
+ *
24
+ * @throws {ValidationError} When `startedAt` / `completedAt` are not finite
25
+ * dates — guarded eagerly so a bad value never corrupts the durable log.
26
+ */
27
+ save(session: StoredSession): void;
28
+ list(opts?: SessionListOptions): readonly StoredSession[];
29
+ get(id: string): StoredSession | null;
30
+ latest(opts?: {
31
+ tool?: ToolShortId;
32
+ }): StoredSession | null;
33
+ count(): number;
34
+ /** Delete sessions older than the given Date. Returns affected rowcount. */
35
+ purge(before: Date): number;
36
+ /** Delete every session. Returns affected rowcount. */
37
+ clearAll(): number;
38
+ /**
39
+ * Delete ONE tool's sessions (`tools data purge`, ADR-0042). Associated
40
+ * `session_tool_payload` rows go via the schema's `onDelete: 'cascade'` FK
41
+ * (the sqlite backend runs `PRAGMA foreign_keys = ON`). Returns the deleted
42
+ * session count.
43
+ */
44
+ clearForTool(toolId: string): number;
45
+ private hydrateSession;
46
+ /**
47
+ * Read the sibling host-metrics record for a session, or `undefined` when no
48
+ * metrics row exists. Null columns are dropped so the projection only carries
49
+ * metrics that were actually captured.
50
+ */
51
+ private readHostMetrics;
52
+ /**
53
+ * Best-effort upsert of host-side overhead metrics for a session
54
+ * (host-owned-run-timing §5.3). The host run plane calls this as render /
55
+ * persist / egress metrics become known; only the provided fields are written,
56
+ * merging onto any existing row. Never throws — metrics are observability.
57
+ */
58
+ upsertHostMetrics(sessionId: string, metrics: StoredSessionHostMetrics): void;
59
+ }
60
+ //# sourceMappingURL=session-repo.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-repo.d.ts","sourceRoot":"","sources":["../src/session-repo.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,KAAK,SAAS,EAEf,MAAM,wBAAwB,CAAC;AAKhC,OAAO,KAAK,EAAE,aAAa,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AACtF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIrD,gFAAgF;AAChF,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;gBAEjC,SAAS,EAAE,SAAS;IAIhC;;;;;;;;OAQG;IACH,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAkFlC,IAAI,CAAC,IAAI,GAAE,kBAAuB,GAAG,SAAS,aAAa,EAAE;IA6B7D,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI;IAKrC,MAAM,CAAC,IAAI,GAAE;QAAE,IAAI,CAAC,EAAE,WAAW,CAAA;KAAO,GAAG,aAAa,GAAG,IAAI;IAK/D,KAAK,IAAI,MAAM;IAKf,4EAA4E;IAC5E,KAAK,CAAC,MAAM,EAAE,IAAI,GAAG,MAAM;IA0B3B,uDAAuD;IACvD,QAAQ,IAAI,MAAM;IAWlB;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM;IAYpC,OAAO,CAAC,cAAc;IA4EtB;;;;OAIG;IACH,OAAO,CAAC,eAAe;IAgBvB;;;;;OAKG;IACH,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,wBAAwB,GAAG,IAAI;CAiC9E"}
@@ -0,0 +1,328 @@
1
+ import { SystemError, ValidationError, currentScope, extractPayloadVersion, isToolShortId, logger, } from '@opensip-cli/core';
2
+ import { requireDrizzleDataStore, } from '@opensip-cli/datastore';
3
+ import { desc, eq, lt } from 'drizzle-orm';
4
+ import { sessionHostMetrics, sessions, sessionToolPayload } from './schema/sessions.js';
5
+ const MODULE_NAME = 'session-store:session-repo';
6
+ /**
7
+ * Persistence layer for tool-run sessions. Stores generic session columns plus
8
+ * one opaque per-tool `payload` blob; holds ZERO tool vocabulary — it never
9
+ * inspects/validates the payload shape (the producing tool owns that). (Audit
10
+ * 2026-05-29, session split.)
11
+ */
12
+ export class SessionRepo {
13
+ datastore;
14
+ constructor(datastore) {
15
+ this.datastore = requireDrizzleDataStore(datastore);
16
+ }
17
+ /**
18
+ * Persist a completed session row and its tool payload. Writes the generic
19
+ * columns including host-owned `startedAt` / `completedAt` (mapped to the
20
+ * physical `timestamp*` / `completed_at*` columns); `session.hostMetrics` is
21
+ * IGNORED here — metrics arrive later and attach via {@link SessionRepo.upsertHostMetrics}.
22
+ *
23
+ * @throws {ValidationError} When `startedAt` / `completedAt` are not finite
24
+ * dates — guarded eagerly so a bad value never corrupts the durable log.
25
+ */
26
+ save(session) {
27
+ try {
28
+ // Validate timing eagerly: a bad value becomes a clear ValidationError
29
+ // instead of silent NaN/"Invalid Date" corruption in the durable log.
30
+ const startedMs = new Date(session.startedAt).getTime();
31
+ const completedMs = new Date(session.completedAt).getTime();
32
+ if (!Number.isFinite(startedMs) || !Number.isFinite(completedMs)) {
33
+ // @fitness-ignore-next-line result-pattern-consistency -- persistence boundary (DEC-015): invalid run timing is a write-time data-integrity guard the caller cannot recover from, so throw (not Result) is correct
34
+ throw new ValidationError(`Invalid session timing for session ${session.id} (tool=${session.tool}): startedAt=${JSON.stringify(session.startedAt)} completedAt=${JSON.stringify(session.completedAt)}`, { code: 'VALIDATION.SESSION.INVALID_TIMESTAMP' });
35
+ }
36
+ this.datastore.transaction((tx) => {
37
+ tx.insert(sessions)
38
+ .values({
39
+ id: session.id,
40
+ tool: session.tool,
41
+ timestamp: startedMs,
42
+ timestamp_iso: session.startedAt, // preserve original for fidelity (avoids Date roundtrip loss of sub-ms/lexical form)
43
+ completed_at: completedMs,
44
+ completed_at_iso: session.completedAt,
45
+ cwd: session.cwd,
46
+ recipe: session.recipe ?? null,
47
+ score: session.score,
48
+ passed: session.passed,
49
+ durationMs: session.durationMs,
50
+ })
51
+ .run();
52
+ // Tool-owned opaque detail (contracts never inspects the shape).
53
+ if (session.payload !== undefined) {
54
+ const hasInnerVersion = extractPayloadVersion(session.payload) !== undefined;
55
+ if (!hasInnerVersion) {
56
+ // Encourage tools to adopt the __version convention on new writes.
57
+ logger.warn({
58
+ evt: 'session.payload.missing_version',
59
+ module: MODULE_NAME,
60
+ sessionId: session.id,
61
+ tool: session.tool,
62
+ msg: "Tool wrote a session payload without top-level __version (treated as legacy v1). Update the tool's build*SessionPayload to include __version: 1.",
63
+ });
64
+ const scope = currentScope();
65
+ scope?.diagnostics?.event('persist', 'warn', 'session payload written without __version (legacy v1 treatment)', { sessionId: session.id, tool: session.tool });
66
+ }
67
+ tx.insert(sessionToolPayload)
68
+ .values({
69
+ sessionId: session.id,
70
+ tool: session.tool,
71
+ payload: session.payload,
72
+ payload_version: 1, // outer storage contract version (inner __version lives inside the JSON blob)
73
+ })
74
+ .run();
75
+ }
76
+ });
77
+ logger.info({
78
+ evt: 'session.save.complete',
79
+ module: MODULE_NAME,
80
+ msg: 'Session saved',
81
+ sessionId: session.id,
82
+ tool: session.tool,
83
+ hasPayload: session.payload !== undefined,
84
+ });
85
+ }
86
+ catch (error) {
87
+ logger.error({
88
+ evt: 'session.save.error',
89
+ module: MODULE_NAME,
90
+ msg: 'Failed to save session',
91
+ sessionId: session.id,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ ...(error instanceof Error && error.stack ? { stack: error.stack } : {}),
94
+ });
95
+ throw error;
96
+ }
97
+ }
98
+ list(opts = {}) {
99
+ try {
100
+ const baseQuery = opts.tool
101
+ ? this.datastore.db.select().from(sessions).where(eq(sessions.tool, opts.tool))
102
+ : this.datastore.db.select().from(sessions);
103
+ const ordered = baseQuery.orderBy(desc(sessions.timestamp));
104
+ const sessionRows = opts.limit ? ordered.limit(opts.limit).all() : ordered.all();
105
+ const results = [];
106
+ for (const row of sessionRows) {
107
+ results.push(this.hydrateSession(row));
108
+ }
109
+ logger.info({
110
+ evt: 'session.list.complete',
111
+ module: MODULE_NAME,
112
+ msg: 'Listed sessions',
113
+ count: results.length,
114
+ });
115
+ return results;
116
+ }
117
+ catch (error) {
118
+ logger.error({
119
+ evt: 'session.list.error',
120
+ module: MODULE_NAME,
121
+ msg: 'Failed to list sessions',
122
+ error: error instanceof Error ? error.message : String(error),
123
+ });
124
+ throw error;
125
+ }
126
+ }
127
+ get(id) {
128
+ const row = this.datastore.db.select().from(sessions).where(eq(sessions.id, id)).get();
129
+ return row ? this.hydrateSession(row) : null;
130
+ }
131
+ latest(opts = {}) {
132
+ const rows = this.list({ ...opts, limit: 1 });
133
+ return rows[0] ?? null;
134
+ }
135
+ count() {
136
+ const rows = this.datastore.db.select({ id: sessions.id }).from(sessions).all();
137
+ return rows.length;
138
+ }
139
+ /** Delete sessions older than the given Date. Returns affected rowcount. */
140
+ purge(before) {
141
+ try {
142
+ const cutoff = before.getTime();
143
+ const removed = this.datastore.db
144
+ .delete(sessions)
145
+ .where(lt(sessions.timestamp, cutoff))
146
+ .run();
147
+ logger.info({
148
+ evt: 'session.purge.complete',
149
+ module: MODULE_NAME,
150
+ msg: 'Purged sessions older than cutoff',
151
+ cutoff: before.toISOString(),
152
+ deleted: removed.changes,
153
+ });
154
+ return removed.changes;
155
+ }
156
+ catch (error) {
157
+ logger.error({
158
+ evt: 'session.purge.error',
159
+ module: MODULE_NAME,
160
+ msg: 'Failed to purge sessions',
161
+ error: error instanceof Error ? error.message : String(error),
162
+ });
163
+ throw error;
164
+ }
165
+ }
166
+ /** Delete every session. Returns affected rowcount. */
167
+ clearAll() {
168
+ const removed = this.datastore.db.delete(sessions).run();
169
+ logger.info({
170
+ evt: 'session.clear.complete',
171
+ module: MODULE_NAME,
172
+ msg: 'Cleared all sessions',
173
+ deleted: removed.changes,
174
+ });
175
+ return removed.changes;
176
+ }
177
+ /**
178
+ * Delete ONE tool's sessions (`tools data purge`, ADR-0042). Associated
179
+ * `session_tool_payload` rows go via the schema's `onDelete: 'cascade'` FK
180
+ * (the sqlite backend runs `PRAGMA foreign_keys = ON`). Returns the deleted
181
+ * session count.
182
+ */
183
+ clearForTool(toolId) {
184
+ const removed = this.datastore.db.delete(sessions).where(eq(sessions.tool, toolId)).run();
185
+ logger.info({
186
+ evt: 'session.clear_for_tool.complete',
187
+ module: MODULE_NAME,
188
+ msg: 'Cleared sessions for tool',
189
+ tool: toolId,
190
+ deleted: removed.changes,
191
+ });
192
+ return removed.changes;
193
+ }
194
+ hydrateSession(row) {
195
+ // Validate row.tool against the documented union — the SQLite column
196
+ // is plain text with no CHECK constraint, so a legacy or hand-edited
197
+ // row could carry a value outside the type. Casting blindly would
198
+ // silently misroute downstream consumers that branch on `tool`.
199
+ if (!isToolShortId(row.tool)) {
200
+ throw new SystemError(`Session ${row.id} has unknown tool value: ${JSON.stringify(row.tool)}`, { code: 'SYSTEM.DATA.UNKNOWN_TOOL' });
201
+ }
202
+ // Tool-owned opaque detail. drizzle returns the JSON column already
203
+ // parsed; contracts does not inspect or validate the shape — that is
204
+ // the owning tool's responsibility.
205
+ const payloadRow = this.datastore.db
206
+ .select({
207
+ payload: sessionToolPayload.payload,
208
+ payload_version: sessionToolPayload.payload_version,
209
+ })
210
+ .from(sessionToolPayload)
211
+ .where(eq(sessionToolPayload.sessionId, row.id))
212
+ .get();
213
+ const outerVersion = payloadRow?.payload_version ?? 1;
214
+ const innerVersion = extractPayloadVersion(payloadRow?.payload);
215
+ if (outerVersion > 1 || (innerVersion !== undefined && innerVersion > 1)) {
216
+ logger.warn({
217
+ evt: 'session.payload.future_version',
218
+ module: MODULE_NAME,
219
+ sessionId: row.id,
220
+ outerVersion,
221
+ innerVersion: innerVersion ?? null,
222
+ msg: 'Payload schema version newer than this CLI knows; treating as opaque (may lose fields on display).',
223
+ });
224
+ // Emit on the per-run DiagnosticsBus for --json / CommandOutcome consumers (cross-cutting observability).
225
+ // Uses currentScope per RunScope rules; safe no-op when no scope (e.g. some tests).
226
+ const scope = currentScope();
227
+ scope?.diagnostics?.event('load', 'warn', `session payload future version (outer=${outerVersion}, inner=${innerVersion ?? 'legacy'})`, {
228
+ sessionId: row.id,
229
+ outerVersion,
230
+ innerVersion: innerVersion ?? undefined,
231
+ });
232
+ }
233
+ const startedAt = row.timestamp_iso ?? new Date(row.timestamp).toISOString(); // prefer original for fidelity
234
+ // completedAt: prefer the stored ISO, else the stored ms, else (legacy rows
235
+ // written before completedAt existed) synthesize startedAt + durationMs.
236
+ const completedAt = row.completed_at_iso ??
237
+ (row.completed_at == null
238
+ ? new Date(row.timestamp + row.durationMs).toISOString() // legacy synth
239
+ : new Date(row.completed_at).toISOString());
240
+ const hostMetrics = this.readHostMetrics(row.id);
241
+ return {
242
+ id: row.id,
243
+ tool: row.tool,
244
+ startedAt,
245
+ completedAt,
246
+ cwd: row.cwd,
247
+ recipe: row.recipe ?? undefined,
248
+ score: row.score,
249
+ passed: row.passed,
250
+ durationMs: row.durationMs,
251
+ ...(hostMetrics ? { hostMetrics } : {}),
252
+ payload: payloadRow?.payload,
253
+ };
254
+ }
255
+ /**
256
+ * Read the sibling host-metrics record for a session, or `undefined` when no
257
+ * metrics row exists. Null columns are dropped so the projection only carries
258
+ * metrics that were actually captured.
259
+ */
260
+ readHostMetrics(sessionId) {
261
+ const row = this.datastore.db
262
+ .select()
263
+ .from(sessionHostMetrics)
264
+ .where(eq(sessionHostMetrics.sessionId, sessionId))
265
+ .get();
266
+ if (!row)
267
+ return undefined;
268
+ const metrics = {};
269
+ if (row.ttyBusyMs != null)
270
+ metrics.ttyBusyMs = row.ttyBusyMs;
271
+ if (row.renderMs != null)
272
+ metrics.renderMs = row.renderMs;
273
+ if (row.persistMs != null)
274
+ metrics.persistMs = row.persistMs;
275
+ if (row.egressMs != null)
276
+ metrics.egressMs = row.egressMs;
277
+ if (row.totalCommandMs != null)
278
+ metrics.totalCommandMs = row.totalCommandMs;
279
+ return Object.keys(metrics).length > 0 ? metrics : undefined;
280
+ }
281
+ /**
282
+ * Best-effort upsert of host-side overhead metrics for a session
283
+ * (host-owned-run-timing §5.3). The host run plane calls this as render /
284
+ * persist / egress metrics become known; only the provided fields are written,
285
+ * merging onto any existing row. Never throws — metrics are observability.
286
+ */
287
+ upsertHostMetrics(sessionId, metrics) {
288
+ try {
289
+ // `set` keys are the Drizzle COLUMN PROPERTY names (camelCase), NOT the
290
+ // SQL column names — Drizzle silently ignores unknown keys, so snake_case
291
+ // here would no-op the ON CONFLICT update and the merge would be lost.
292
+ const patch = {};
293
+ if (metrics.ttyBusyMs !== undefined)
294
+ patch.ttyBusyMs = metrics.ttyBusyMs;
295
+ if (metrics.renderMs !== undefined)
296
+ patch.renderMs = metrics.renderMs;
297
+ if (metrics.persistMs !== undefined)
298
+ patch.persistMs = metrics.persistMs;
299
+ if (metrics.egressMs !== undefined)
300
+ patch.egressMs = metrics.egressMs;
301
+ if (metrics.totalCommandMs !== undefined)
302
+ patch.totalCommandMs = metrics.totalCommandMs;
303
+ if (Object.keys(patch).length === 0)
304
+ return;
305
+ this.datastore.db
306
+ .insert(sessionHostMetrics)
307
+ .values({
308
+ sessionId,
309
+ ttyBusyMs: metrics.ttyBusyMs ?? null,
310
+ renderMs: metrics.renderMs ?? null,
311
+ persistMs: metrics.persistMs ?? null,
312
+ egressMs: metrics.egressMs ?? null,
313
+ totalCommandMs: metrics.totalCommandMs ?? null,
314
+ })
315
+ .onConflictDoUpdate({ target: sessionHostMetrics.sessionId, set: patch })
316
+ .run();
317
+ }
318
+ catch (error) {
319
+ logger.warn({
320
+ evt: 'session.host_metrics.upsert_failed',
321
+ module: MODULE_NAME,
322
+ sessionId,
323
+ error: error instanceof Error ? error.message : String(error),
324
+ });
325
+ }
326
+ }
327
+ }
328
+ //# sourceMappingURL=session-repo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-repo.js","sourceRoot":"","sources":["../src/session-repo.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Session id / filename runtime helpers.
3
+ *
4
+ * Moved out of `@opensip-cli/contracts` (audit 2026-05-29, contracts
5
+ * split) so contracts carries no runtime. The `StoredSession` type stays
6
+ * in contracts as the cross-tool contract surface.
7
+ */
8
+ /** Generate a unique session ID */
9
+ export declare function generateSessionId(): string;
10
+ /** Sanitize a string for use in a filename — strip path separators and special chars */
11
+ export declare function sanitizeForFilename(s: string): string;
12
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,mCAAmC;AACnC,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wFAAwF;AACxF,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD"}
package/dist/store.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Session id / filename runtime helpers.
3
+ *
4
+ * Moved out of `@opensip-cli/contracts` (audit 2026-05-29, contracts
5
+ * split) so contracts carries no runtime. The `StoredSession` type stays
6
+ * in contracts as the cross-tool contract surface.
7
+ */
8
+ import { randomUUID } from 'node:crypto';
9
+ /** Generate a unique session ID */
10
+ export function generateSessionId() {
11
+ return randomUUID();
12
+ }
13
+ /** Sanitize a string for use in a filename — strip path separators and special chars */
14
+ export function sanitizeForFilename(s) {
15
+ return s.replaceAll('..', '-').replaceAll(/[/\\:*?"<>|.]/g, '-');
16
+ }
17
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,mCAAmC;AACnC,MAAM,UAAU,iBAAiB;IAC/B,OAAO,UAAU,EAAE,CAAC;AACtB,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,mBAAmB,CAAC,CAAS;IAC3C,OAAO,CAAC,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,UAAU,CAAC,gBAAgB,EAAE,GAAG,CAAC,CAAC;AACnE,CAAC"}