@opengeni/db 0.27.11 → 0.28.9

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 (54) hide show
  1. package/dist/{chunk-JYQPJ6Y5.js → chunk-P6CUHXQZ.js} +2967 -2272
  2. package/dist/chunk-P6CUHXQZ.js.map +1 -0
  3. package/dist/{chunk-RPHPNVWW.js → chunk-VHBFHMPC.js} +15 -1
  4. package/dist/chunk-VHBFHMPC.js.map +1 -0
  5. package/dist/environment-crypto.d.ts +8 -4
  6. package/dist/index.d.ts +272 -26
  7. package/dist/index.js +7798 -5008
  8. package/dist/index.js.map +1 -1
  9. package/dist/lossless-columns.d.ts +58 -0
  10. package/dist/lossless-json.d.ts +39 -0
  11. package/dist/memory-domain.d.ts +3 -6
  12. package/dist/persistence-errors.d.ts +7 -14
  13. package/dist/provision-roles.js +1 -1
  14. package/dist/runtime-posture.d.ts +2 -2
  15. package/dist/schema.d.ts +1420 -271
  16. package/dist/schema.js +25 -1
  17. package/dist/session-realtime-terminal.d.ts +7 -0
  18. package/dist/transcription-recordings-schema.d.ts +1264 -0
  19. package/dist/transcription-recordings.d.ts +228 -0
  20. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  21. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  22. package/drizzle/0170_resumable_transcription_recordings.sql +440 -0
  23. package/drizzle/0175_resumable_transcription_provider_deadline.sql +24 -0
  24. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  25. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  26. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  27. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  28. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  29. package/drizzle/0181_connected_machine_removal.sql +52 -0
  30. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  31. package/package.json +4 -4
  32. package/src/database.ts +7 -1
  33. package/src/environment-crypto.ts +22 -9
  34. package/src/index.ts +4368 -1678
  35. package/src/lossless-columns.ts +35 -0
  36. package/src/lossless-json.ts +322 -0
  37. package/src/memory-domain.ts +5 -44
  38. package/src/persistence-errors.ts +29 -34
  39. package/src/runtime-posture.ts +14 -0
  40. package/src/schema.ts +264 -42
  41. package/src/session-control.ts +125 -76
  42. package/src/session-queue-commands.ts +325 -234
  43. package/src/session-realtime-context.ts +18 -5
  44. package/src/session-realtime-ledger.ts +29 -7
  45. package/src/session-realtime-mirror.ts +4 -2
  46. package/src/session-realtime-terminal.ts +89 -21
  47. package/src/session-realtime.ts +3 -2
  48. package/src/session-tool-call-settlement.ts +29 -15
  49. package/src/transcription-recordings-schema.ts +253 -0
  50. package/src/transcription-recordings.ts +1538 -0
  51. package/dist/chunk-JYQPJ6Y5.js.map +0 -1
  52. package/dist/chunk-RPHPNVWW.js.map +0 -1
  53. package/dist/event-payload-sanitizer.d.ts +0 -32
  54. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,35 @@
1
+ import { customType, integer } from "drizzle-orm/pg-core";
2
+ import { toPostgresLosslessJson, toPostgresLosslessText } from "./lossless-json";
3
+
4
+ /**
5
+ * Codec truth is explicit and out-of-band. This column intentionally has no
6
+ * database or application default: a caller may set version 1 only in the same
7
+ * statement that writes the corresponding value through the lossless codec.
8
+ */
9
+ export function losslessCodecVersion<TName extends string>(name: TName) {
10
+ return integer(name);
11
+ }
12
+
13
+ export const losslessJsonb = customType<{ data: unknown; driverData: string }>({
14
+ dataType() {
15
+ return "jsonb";
16
+ },
17
+ toDriver(value) {
18
+ return JSON.stringify(toPostgresLosslessJson(value));
19
+ },
20
+ fromDriver(value) {
21
+ return typeof value === "string" ? JSON.parse(value) : value;
22
+ },
23
+ });
24
+
25
+ export const losslessText = customType<{ data: string; driverData: string }>({
26
+ dataType() {
27
+ return "text";
28
+ },
29
+ toDriver(value) {
30
+ return toPostgresLosslessText(value);
31
+ },
32
+ fromDriver(value) {
33
+ return value;
34
+ },
35
+ });
@@ -0,0 +1,322 @@
1
+ const JSON_STRING_PREFIX = "opengeni_lossless_json_string_v2_81f06e15:";
2
+ const JSON_KEY_PREFIX = "opengeni_lossless_json_key_v2_7ca6071d:";
3
+ const TEXT_PREFIX = "opengeni_lossless_text_v2_c4100a62:";
4
+ const MAX_JSON_DEPTH = 512;
5
+
6
+ /**
7
+ * Explicit out-of-band truth for values written by the lossless PostgreSQL
8
+ * compatibility codec. Historical rows have NULL in their companion version
9
+ * column and must never be decoded based on content shape alone.
10
+ */
11
+ export const LOSSLESS_CONTENT_CODEC_VERSION = 1 as const;
12
+ export const LOSSLESS_CONTENT_WRITER_APPLICATION_NAME = "opengeni-lossless-v1";
13
+
14
+ export function withLosslessContentWriteVersion<
15
+ const ContentKey extends string,
16
+ const VersionKey extends string,
17
+ const Value extends object,
18
+ >(
19
+ value: Value & Record<ContentKey, unknown>,
20
+ contentKey: ContentKey,
21
+ versionKey: VersionKey,
22
+ ): Value & Record<VersionKey, typeof LOSSLESS_CONTENT_CODEC_VERSION>;
23
+ export function withLosslessContentWriteVersion<
24
+ const ContentKey extends string,
25
+ const VersionKey extends string,
26
+ const Value extends object,
27
+ >(
28
+ value: readonly (Value & Record<ContentKey, unknown>)[],
29
+ contentKey: ContentKey,
30
+ versionKey: VersionKey,
31
+ ): Array<Value & Record<VersionKey, typeof LOSSLESS_CONTENT_CODEC_VERSION>>;
32
+ export function withLosslessContentWriteVersion(
33
+ value: Record<string, unknown> | readonly Record<string, unknown>[],
34
+ contentKey: string,
35
+ versionKey: string,
36
+ ): Record<string, unknown> | Record<string, unknown>[] {
37
+ const stamp = (entry: Record<string, unknown>) => {
38
+ if (!Object.hasOwn(entry, contentKey)) {
39
+ throw new Error(`Lossless content write omitted ${contentKey}`);
40
+ }
41
+ return { ...entry, [versionKey]: LOSSLESS_CONTENT_CODEC_VERSION };
42
+ };
43
+ return Array.isArray(value) ? value.map(stamp) : stamp(value as Record<string, unknown>);
44
+ }
45
+
46
+ export const LEGACY_LOSSLESS_JSON_ENVELOPE_KEY =
47
+ "$opengeniCanonicalV8_6d9b6f48_2a3e_4d8a_9e33_7611d9d08985";
48
+ export const LEGACY_LOSSLESS_TEXT_PREFIX = "opengeni-canonical-text-v1:";
49
+ export const LOSSLESS_JSON_STRING_PREFIX = JSON_STRING_PREFIX;
50
+ export const LOSSLESS_TEXT_PREFIX = TEXT_PREFIX;
51
+
52
+ export class UnsupportedCanonicalValueError extends TypeError {
53
+ override readonly name = "UnsupportedCanonicalValueError";
54
+ }
55
+
56
+ type TransformResult = { value: unknown; changed: boolean };
57
+
58
+ /**
59
+ * Preserve JSON structure and SQL-queryable control keys. Only strings that
60
+ * PostgreSQL cannot represent (or that collide with this unshipped v2 tag) are
61
+ * encoded. Non-JSON graph values are rejected instead of silently rewritten.
62
+ */
63
+ export function toPostgresLosslessJson(value: unknown): unknown {
64
+ return encodeJsonValue(value, new Set<object>(), 0).value;
65
+ }
66
+
67
+ /**
68
+ * Restore an explicitly versioned JSON value after a PostgreSQL read. A NULL
69
+ * version denotes literal legacy/old-writer data, including strings that happen
70
+ * to be valid active-marker encodings.
71
+ */
72
+ export function fromPostgresLosslessJson<T>(value: T, codecVersion: number | null | undefined): T {
73
+ return codecVersion === LOSSLESS_CONTENT_CODEC_VERSION
74
+ ? (decodeJsonValue(value, 0).value as T)
75
+ : value;
76
+ }
77
+
78
+ /**
79
+ * Lossless text-column boundary for NUL, lone UTF-16, and v2-prefix text.
80
+ * Only the unrepresentable code unit is tagged, with SQL-visible spaces around
81
+ * the tag, so ordinary surrounding words retain their full-text-search shape.
82
+ */
83
+ export function toPostgresLosslessText(value: string): string {
84
+ if (isPostgresSafeString(value) && !value.includes(TEXT_PREFIX)) return value;
85
+ let stored = "";
86
+ for (let index = 0; index < value.length; index += 1) {
87
+ const code = value.charCodeAt(index);
88
+ if (value.startsWith(TEXT_PREFIX, index)) {
89
+ stored += encodeTextCodeUnit(code);
90
+ continue;
91
+ }
92
+ if (code === 0 || (code >= 0xdc00 && code <= 0xdfff)) {
93
+ stored += encodeTextCodeUnit(code);
94
+ continue;
95
+ }
96
+ if (code >= 0xd800 && code <= 0xdbff) {
97
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0;
98
+ if (next < 0xdc00 || next > 0xdfff) {
99
+ stored += encodeTextCodeUnit(code);
100
+ continue;
101
+ }
102
+ stored += value.slice(index, index + 2);
103
+ index += 1;
104
+ continue;
105
+ }
106
+ stored += value[index];
107
+ }
108
+ return stored;
109
+ }
110
+
111
+ /**
112
+ * Decode only an explicitly versioned value introduced by this migration. The
113
+ * nullable companion column, not the producer string, is the codec truth.
114
+ */
115
+ export function fromPostgresLosslessText(
116
+ value: string,
117
+ codecVersion: number | null | undefined,
118
+ ): string {
119
+ return codecVersion === LOSSLESS_CONTENT_CODEC_VERSION
120
+ ? value.replaceAll(
121
+ new RegExp(` ${TEXT_PREFIX}([0-9a-f]{4}); `, "g"),
122
+ (_match, encoded: string) => String.fromCharCode(Number.parseInt(encoded, 16)),
123
+ )
124
+ : value;
125
+ }
126
+
127
+ function encodeTextCodeUnit(code: number): string {
128
+ return ` ${TEXT_PREFIX}${code.toString(16).padStart(4, "0")}; `;
129
+ }
130
+
131
+ function encodeJsonValue(value: unknown, ancestors: Set<object>, depth: number): TransformResult {
132
+ if (depth > MAX_JSON_DEPTH) {
133
+ throw new UnsupportedCanonicalValueError(
134
+ `Canonical JSON exceeds the maximum supported depth of ${MAX_JSON_DEPTH}`,
135
+ );
136
+ }
137
+ if (value === null || typeof value === "boolean") return { value, changed: false };
138
+ if (typeof value === "string") {
139
+ if (isPostgresSafeString(value) && !value.startsWith(JSON_STRING_PREFIX)) {
140
+ return { value, changed: false };
141
+ }
142
+ return { value: `${JSON_STRING_PREFIX}${encodeUtf16(value)}`, changed: true };
143
+ }
144
+ if (typeof value === "number") {
145
+ if (!Number.isFinite(value) || Object.is(value, -0)) {
146
+ throw new UnsupportedCanonicalValueError(
147
+ "Canonical JSON requires finite non-negative-zero numbers",
148
+ );
149
+ }
150
+ return { value, changed: false };
151
+ }
152
+ if (
153
+ typeof value === "undefined" ||
154
+ typeof value === "bigint" ||
155
+ typeof value === "function" ||
156
+ typeof value === "symbol"
157
+ ) {
158
+ throw new UnsupportedCanonicalValueError(`Canonical JSON cannot contain ${typeof value}`);
159
+ }
160
+ if (!isPlainObject(value) && !Array.isArray(value)) {
161
+ throw new UnsupportedCanonicalValueError("Canonical JSON requires arrays and plain objects");
162
+ }
163
+ if (ancestors.has(value)) {
164
+ throw new UnsupportedCanonicalValueError("Canonical JSON cannot contain cyclic references");
165
+ }
166
+ if (Object.getOwnPropertySymbols(value).length > 0) {
167
+ throw new UnsupportedCanonicalValueError("Canonical JSON cannot contain symbol keys");
168
+ }
169
+ ancestors.add(value);
170
+ try {
171
+ if (Array.isArray(value)) {
172
+ const descriptors = Object.getOwnPropertyDescriptors(value);
173
+ const propertyNames = Object.getOwnPropertyNames(descriptors);
174
+ const indexNames = propertyNames.filter((name) => name !== "length");
175
+ if (
176
+ indexNames.length !== value.length ||
177
+ indexNames.some((name) => {
178
+ const index = Number(name);
179
+ return (
180
+ !Number.isInteger(index) || index < 0 || index >= value.length || String(index) !== name
181
+ );
182
+ })
183
+ ) {
184
+ throw new UnsupportedCanonicalValueError(
185
+ "Canonical JSON arrays cannot contain non-index properties or holes",
186
+ );
187
+ }
188
+ const output: unknown[] = [];
189
+ let changed = false;
190
+ for (let index = 0; index < value.length; index += 1) {
191
+ const descriptor = descriptors[String(index)];
192
+ if (!descriptor || !descriptor.enumerable || !("value" in descriptor)) {
193
+ throw new UnsupportedCanonicalValueError(
194
+ "Canonical JSON arrays cannot contain holes, accessors, or hidden elements",
195
+ );
196
+ }
197
+ const encoded = encodeJsonValue(descriptor.value, ancestors, depth + 1);
198
+ output.push(encoded.value);
199
+ changed ||= encoded.changed;
200
+ }
201
+ return changed ? { value: output, changed: true } : { value, changed: false };
202
+ }
203
+
204
+ const descriptors = Object.getOwnPropertyDescriptors(value);
205
+ const output: Record<string, unknown> = {};
206
+ let changed = false;
207
+ for (const [key, descriptor] of Object.entries(descriptors)) {
208
+ if (!descriptor.enumerable || !("value" in descriptor)) {
209
+ throw new UnsupportedCanonicalValueError(
210
+ "Canonical JSON objects cannot contain accessors or hidden properties",
211
+ );
212
+ }
213
+ const encodedKey =
214
+ isPostgresSafeString(key) && !key.startsWith(JSON_KEY_PREFIX)
215
+ ? key
216
+ : `${JSON_KEY_PREFIX}${encodeUtf16(key)}`;
217
+ if (Object.prototype.hasOwnProperty.call(output, encodedKey)) {
218
+ throw new UnsupportedCanonicalValueError("Canonical JSON key encoding collided");
219
+ }
220
+ const encodedValue = encodeJsonValue(descriptor.value, ancestors, depth + 1);
221
+ defineJsonDataProperty(output, encodedKey, encodedValue.value);
222
+ changed ||= encodedKey !== key || encodedValue.changed;
223
+ }
224
+ return changed ? { value: output, changed: true } : { value, changed: false };
225
+ } finally {
226
+ ancestors.delete(value);
227
+ }
228
+ }
229
+
230
+ function decodeJsonValue(value: unknown, depth: number): TransformResult {
231
+ if (depth > MAX_JSON_DEPTH) return { value, changed: false };
232
+ if (typeof value === "string") {
233
+ const decoded = decodeTaggedString(value, JSON_STRING_PREFIX);
234
+ return decoded === null ? { value, changed: false } : { value: decoded, changed: true };
235
+ }
236
+ if (!value || typeof value !== "object") return { value, changed: false };
237
+ if (Array.isArray(value)) {
238
+ const output: unknown[] = [];
239
+ let changed = false;
240
+ for (const entry of value) {
241
+ const decoded = decodeJsonValue(entry, depth + 1);
242
+ output.push(decoded.value);
243
+ changed ||= decoded.changed;
244
+ }
245
+ return changed ? { value: output, changed: true } : { value, changed: false };
246
+ }
247
+ if (!isPlainObject(value)) return { value, changed: false };
248
+
249
+ const output: Record<string, unknown> = {};
250
+ let changed = false;
251
+ for (const [key, entry] of Object.entries(value)) {
252
+ const decodedKey = decodeTaggedString(key, JSON_KEY_PREFIX) ?? key;
253
+ if (Object.prototype.hasOwnProperty.call(output, decodedKey)) {
254
+ return { value, changed: false };
255
+ }
256
+ const decodedValue = decodeJsonValue(entry, depth + 1);
257
+ defineJsonDataProperty(output, decodedKey, decodedValue.value);
258
+ changed ||= decodedKey !== key || decodedValue.changed;
259
+ }
260
+ return changed ? { value: output, changed: true } : { value, changed: false };
261
+ }
262
+
263
+ /**
264
+ * JSON permits own keys such as `__proto__`. Assignment into an ordinary `{}`
265
+ * would invoke the inherited legacy setter and silently replace the object's
266
+ * prototype instead of creating data. Define every transformed key explicitly
267
+ * so encode/decode preserve hostile-but-valid JSON keys and property order.
268
+ */
269
+ function defineJsonDataProperty(
270
+ target: Record<string, unknown>,
271
+ key: string,
272
+ value: unknown,
273
+ ): void {
274
+ Object.defineProperty(target, key, {
275
+ value,
276
+ enumerable: true,
277
+ configurable: true,
278
+ writable: true,
279
+ });
280
+ }
281
+
282
+ function encodeUtf16(value: string): string {
283
+ const bytes = Buffer.allocUnsafe(value.length * 2);
284
+ for (let index = 0; index < value.length; index += 1) {
285
+ bytes.writeUInt16LE(value.charCodeAt(index), index * 2);
286
+ }
287
+ return bytes.toString("base64");
288
+ }
289
+
290
+ function decodeTaggedString(value: string, prefix: string): string | null {
291
+ if (!value.startsWith(prefix)) return null;
292
+ const encoded = value.slice(prefix.length);
293
+ if (encoded.length === 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) return null;
294
+ const bytes = Buffer.from(encoded, "base64");
295
+ if (bytes.byteLength % 2 !== 0 || bytes.toString("base64") !== encoded) return null;
296
+ let decoded = "";
297
+ for (let offset = 0; offset < bytes.byteLength; offset += 2) {
298
+ decoded += String.fromCharCode(bytes.readUInt16LE(offset));
299
+ }
300
+ return decoded;
301
+ }
302
+
303
+ function isPostgresSafeString(value: string): boolean {
304
+ for (let index = 0; index < value.length; index += 1) {
305
+ const code = value.charCodeAt(index);
306
+ if (code === 0) return false;
307
+ if (code >= 0xd800 && code <= 0xdbff) {
308
+ const next = index + 1 < value.length ? value.charCodeAt(index + 1) : 0;
309
+ if (next < 0xdc00 || next > 0xdfff) return false;
310
+ index += 1;
311
+ continue;
312
+ }
313
+ if (code >= 0xdc00 && code <= 0xdfff) return false;
314
+ }
315
+ return true;
316
+ }
317
+
318
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
319
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
320
+ const prototype = Object.getPrototypeOf(value);
321
+ return prototype === Object.prototype || prototype === null;
322
+ }
@@ -431,7 +431,7 @@ export const MEMORY_SEARCH_TOOL_DESCRIPTION =
431
431
  "Search this workspace's shared long-lived memory (semantic + keyword). Use it before starting a new non-trivial task when the injected notes or current conversation do not already answer how the workspace does something. Results persist in conversation context: do not repeat the same search as routine setup on every continuation, resume, or interrupted turn. Returns scored records with ids.";
432
432
 
433
433
  export const MEMORY_SAVE_TOOL_DESCRIPTION =
434
- "Save one durable, future-useful fact to this workspace's shared memory: a stable preference, an environment fact, a procedure that worked, or a decision and its reason. Write it compactly (1–3 sentences), self-contained (no 'this session/above' references, absolute dates, name concrete things), so a future session can act on it alone. Do NOT save: session-specific state, speculation, anything derivable from the repo/docs, near-duplicates of existing memories (search first — to refine or replace an existing record pass replaces_id), or secrets/tokens/credentials. Most turns have nothing worth saving.";
434
+ "Save one durable, future-useful fact to this workspace's shared memory: a stable preference, an environment fact, a procedure that worked, or a decision and its reason. Write it compactly (1–3 sentences), self-contained (no 'this session/above' references, absolute dates, name concrete things), so a future session can act on it alone. Do NOT save: session-specific state, speculation, anything derivable from the repo/docs, or near-duplicates of existing memories (search first — to refine or replace an existing record pass replaces_id). Most turns have nothing worth saving.";
435
435
 
436
436
  export const MEMORY_CORRECT_TOOL_DESCRIPTION =
437
437
  "Flag a workspace memory as wrong or outdated the moment you discover it — this is the most valuable memory action, because a wrong memory misleads every future session. Pass the record's id (as shown in [brackets]); optionally give replacement_text with the corrected fact, otherwise the record is archived.";
@@ -452,51 +452,12 @@ export function hashMemoryText(text: string): string {
452
452
  }
453
453
 
454
454
  // ---------------------------------------------------------------------------
455
- // Sanitization + secret redaction
455
+ // Exact stored memory content
456
456
  // ---------------------------------------------------------------------------
457
457
 
458
- // Conservative secret patterns. This is slop/leak defense, not a guarantee; the
459
- // end-state reflector adds real scanning. Each match is replaced with [REDACTED].
460
- const SECRET_PATTERNS: readonly RegExp[] = [
461
- /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, // PEM private keys
462
- /AKIA[0-9A-Z]{16}/g, // AWS access key id
463
- /\bASIA[0-9A-Z]{16}/g, // AWS temporary access key id
464
- /\bsk-[A-Za-z0-9_-]{20,}/g, // OpenAI-style secret keys
465
- /\bgh[pousr]_[A-Za-z0-9]{20,}/g, // GitHub tokens
466
- /\bxox[baprs]-[A-Za-z0-9-]{10,}/g, // Slack tokens
467
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/g, // JWT (three b64url segments)
468
- /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi, // bearer credentials
469
- /\b(?:password|passwd|secret|api[_-]?key|token)\s*[=:]\s*\S{6,}/gi, // key=value secrets
470
- ];
471
-
472
- // Strip C0/C1 control characters, collapse whitespace to single spaces, trim.
473
- function stripControlAndCollapse(raw: string): string {
474
- // eslint-disable-next-line no-control-regex
475
- const withoutControls = raw.replace(/[\u0000-\u001F\u007F-\u009F]/g, " ");
476
- return withoutControls.replace(/\s+/g, " ").trim();
477
- }
478
-
479
- export type MemorySanitizeResult = {
480
- text: string;
481
- redactionCount: number;
482
- };
483
-
484
- // Produce the stored form of a memory text: control-stripped, single-line,
485
- // secret-redacted. Does NOT enforce the length cap (callers check
486
- // tooLong via isMemoryTextTooLong on the returned text so they can surface an
487
- // actionable error rather than silently truncating).
488
- export function sanitizeMemoryText(raw: string): MemorySanitizeResult {
489
- let text = stripControlAndCollapse(raw);
490
- let redactionCount = 0;
491
- for (const pattern of SECRET_PATTERNS) {
492
- text = text.replace(pattern, () => {
493
- redactionCount += 1;
494
- return "[REDACTED]";
495
- });
496
- }
497
- // Redaction can leave doubled spaces; re-collapse.
498
- text = text.replace(/\s+/g, " ").trim();
499
- return { text, redactionCount };
458
+ /** Accepted memory text is canonical content; validation must never rewrite it. */
459
+ export function memoryTextForStorage(raw: string): string {
460
+ return raw;
500
461
  }
501
462
 
502
463
  export function isMemoryTextTooLong(text: string): boolean {
@@ -1,5 +1,3 @@
1
- import { sanitizeEventString } from "./event-payload-sanitizer";
2
-
3
1
  export type DatabaseFailureCode = "db_deadlock" | "db_serialization_failure" | "db_failure";
4
2
 
5
3
  export type PersistenceRetryOutcome = "not_retryable" | "exhausted";
@@ -58,7 +56,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
58
56
 
59
57
  function safeFact(value: unknown): string | undefined {
60
58
  if (typeof value !== "string" || value.length === 0) return undefined;
61
- return sanitizeEventString(value).slice(0, 256);
59
+ return value;
62
60
  }
63
61
 
64
62
  /** Find the driver SQLSTATE even when Drizzle wrapped it under nested causes. */
@@ -98,8 +96,8 @@ export function isRetryablePersistenceSqlState(sqlState: string | null): boolean
98
96
 
99
97
  /**
100
98
  * Distinguish database/ORM failures from expected domain exceptions when a
101
- * driver omitted SQLSTATE. This checks shape only and never retains query text,
102
- * bound parameters, or a raw driver cause.
99
+ * driver omitted SQLSTATE. This checks shape only; callers retain the original
100
+ * failure independently as canonical error evidence.
103
101
  */
104
102
  export function isDatabasePersistenceFailure(error: unknown): boolean {
105
103
  if (nestedPostgresSqlState(error) !== null) return true;
@@ -156,36 +154,30 @@ export function safeDatabaseErrorFacts(error: unknown): SafeDatabaseErrorFacts {
156
154
  return facts;
157
155
  }
158
156
 
159
- /** Public-safe cause that preserves database classification without driver data. */
160
- export class SanitizedDatabasePersistenceCause extends Error {
161
- readonly name = "SanitizedDatabasePersistenceCause";
162
-
163
- constructor(
164
- readonly sqlState: string | null,
165
- readonly database: SafeDatabaseErrorFacts,
166
- ) {
167
- super(sqlState === null ? "Database driver failure" : `PostgreSQL failure ${sqlState}`);
168
- }
169
- }
170
-
171
157
  /**
172
- * Public-safe replacement for a raw Drizzle/postgres-js failure. Its `cause`
173
- * is a newly constructed sanitized projection; the original driver cause can
174
- * contain full SQL and bound parameters and is never retained.
158
+ * Typed persistence classification that retains the exact original failure as
159
+ * `cause`. Classification metadata supplements the cause; it never replaces or
160
+ * rewrites source error content.
175
161
  */
176
162
  export class SessionEventPersistenceError extends Error {
177
163
  readonly name = "SessionEventPersistenceError";
178
- readonly cause: SanitizedDatabasePersistenceCause;
164
+ readonly cause: unknown;
179
165
 
180
- constructor(readonly details: PersistenceFailureDetails) {
166
+ constructor(
167
+ readonly details: PersistenceFailureDetails,
168
+ cause?: unknown,
169
+ ) {
181
170
  const label =
182
171
  details.code === "db_deadlock"
183
172
  ? "Database deadlock"
184
173
  : details.code === "db_serialization_failure"
185
174
  ? "Database serialization failure"
186
175
  : "Database failure";
187
- super(`${label} while persisting ${details.eventTypes.join(", ") || "session events"}`);
188
- this.cause = new SanitizedDatabasePersistenceCause(details.sqlState, details.database);
176
+ const operation = `${label} while persisting ${details.eventTypes.join(", ") || "session events"}`;
177
+ const sourceMessage =
178
+ cause === undefined ? null : cause instanceof Error ? cause.message : String(cause);
179
+ super(sourceMessage ? `${operation}: ${sourceMessage}` : operation);
180
+ this.cause = cause;
189
181
  }
190
182
 
191
183
  get code(): DatabaseFailureCode {
@@ -236,16 +228,19 @@ export async function runIdempotentPersistenceTransaction<T>(
236
228
  continue;
237
229
  }
238
230
  if (!isDatabasePersistenceFailure(error)) throw error;
239
- throw new SessionEventPersistenceError({
240
- code: databaseFailureCode(sqlState),
241
- sqlState,
242
- stage: options.stage,
243
- eventTypes,
244
- correlationId,
245
- attempts: attempt,
246
- retryOutcome: retryable ? "exhausted" : "not_retryable",
247
- database: safeDatabaseErrorFacts(error),
248
- });
231
+ throw new SessionEventPersistenceError(
232
+ {
233
+ code: databaseFailureCode(sqlState),
234
+ sqlState,
235
+ stage: options.stage,
236
+ eventTypes,
237
+ correlationId,
238
+ attempts: attempt,
239
+ retryOutcome: retryable ? "exhausted" : "not_retryable",
240
+ database: safeDatabaseErrorFacts(error),
241
+ },
242
+ error,
243
+ );
249
244
  }
250
245
  }
251
246
  throw new Error("Unreachable persistence retry state");
@@ -67,6 +67,7 @@ export const FORCE_RLS_TABLES = [
67
67
  "knowledge_sync_runs",
68
68
  "machine_metrics_latest",
69
69
  "machine_metrics_series",
70
+ "machine_removal_operations",
70
71
  "model_call_facts",
71
72
  "new_session_drafts",
72
73
  "pack_installations",
@@ -74,6 +75,7 @@ export const FORCE_RLS_TABLES = [
74
75
  "preference_registry_preferences",
75
76
  "preference_registry_revisions",
76
77
  "preference_registry_snapshots",
78
+ "retained_screenshot_artifacts",
77
79
  "rig_changes",
78
80
  "rig_versions",
79
81
  "rigs",
@@ -118,6 +120,10 @@ export const FORCE_RLS_TABLES = [
118
120
  "slack_interactions",
119
121
  "social_connections",
120
122
  "social_posts",
123
+ "transcription_recording_chunks",
124
+ "transcription_recording_objects",
125
+ "transcription_recording_segments",
126
+ "transcription_recordings",
121
127
  "usage_events",
122
128
  "workspace_artifact_events",
123
129
  "workspace_artifact_versions",
@@ -132,6 +138,7 @@ export const FORCE_RLS_TABLES = [
132
138
  "workspace_instruction_policy_snapshots",
133
139
  "workspace_model_policies",
134
140
  "workspace_packs",
141
+ "workspace_screenshot_quotas",
135
142
  "workspace_session_activity_revisions",
136
143
  "workspace_variable_set_variables",
137
144
  "workspace_variable_sets",
@@ -200,10 +207,12 @@ export const RUNTIME_FULL_DML_TABLES = [
200
207
  "knowledge_memories",
201
208
  "machine_metrics_latest",
202
209
  "machine_metrics_series",
210
+ "machine_removal_operations",
203
211
  "managed_accounts",
204
212
  "model_call_facts",
205
213
  "new_session_drafts",
206
214
  "pack_installations",
215
+ "retained_screenshot_artifacts",
207
216
  "rig_changes",
208
217
  "rig_versions",
209
218
  "rigs",
@@ -248,6 +257,10 @@ export const RUNTIME_FULL_DML_TABLES = [
248
257
  "social_connections",
249
258
  "social_posts",
250
259
  "stripe_webhook_events",
260
+ "transcription_recording_chunks",
261
+ "transcription_recording_objects",
262
+ "transcription_recording_segments",
263
+ "transcription_recordings",
251
264
  "usage_events",
252
265
  "workspace_artifacts",
253
266
  "workspace_captures",
@@ -257,6 +270,7 @@ export const RUNTIME_FULL_DML_TABLES = [
257
270
  "workspace_memberships",
258
271
  "workspace_model_policies",
259
272
  "workspace_packs",
273
+ "workspace_screenshot_quotas",
260
274
  "workspace_session_activity_revisions",
261
275
  "workspace_variable_set_variables",
262
276
  "workspace_variable_sets",