@opengeni/db 0.28.1 → 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.
- package/dist/{chunk-HT5T62CC.js → chunk-P6CUHXQZ.js} +651 -206
- package/dist/chunk-P6CUHXQZ.js.map +1 -0
- package/dist/{chunk-DBS5HPEW.js → chunk-VHBFHMPC.js} +7 -1
- package/dist/chunk-VHBFHMPC.js.map +1 -0
- package/dist/environment-crypto.d.ts +8 -4
- package/dist/index.d.ts +265 -20
- package/dist/index.js +3966 -2185
- package/dist/index.js.map +1 -1
- package/dist/lossless-columns.d.ts +58 -0
- package/dist/lossless-json.d.ts +39 -0
- package/dist/memory-domain.d.ts +3 -6
- package/dist/persistence-errors.d.ts +7 -14
- package/dist/provision-roles.js +1 -1
- package/dist/runtime-posture.d.ts +2 -2
- package/dist/schema.d.ts +1419 -271
- package/dist/schema.js +9 -1
- package/dist/session-realtime-terminal.d.ts +7 -0
- package/dist/transcription-recordings-schema.d.ts +44 -6
- package/dist/transcription-recordings.d.ts +1 -1
- package/drizzle/0065_enrollment_credential_generation.sql +10 -0
- package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
- package/drizzle/0176_lossless_canonical_json.sql +975 -0
- package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
- package/drizzle/0178_permissioned_secret_reads.sql +14 -0
- package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
- package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
- package/drizzle/0181_connected_machine_removal.sql +52 -0
- package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
- package/package.json +4 -4
- package/src/database.ts +7 -1
- package/src/environment-crypto.ts +22 -9
- package/src/index.ts +4253 -1601
- package/src/lossless-columns.ts +35 -0
- package/src/lossless-json.ts +322 -0
- package/src/memory-domain.ts +5 -44
- package/src/persistence-errors.ts +29 -34
- package/src/runtime-posture.ts +6 -0
- package/src/schema.ts +260 -38
- package/src/session-control.ts +125 -76
- package/src/session-queue-commands.ts +325 -234
- package/src/session-realtime-context.ts +18 -5
- package/src/session-realtime-ledger.ts +29 -7
- package/src/session-realtime-mirror.ts +4 -2
- package/src/session-realtime-terminal.ts +89 -21
- package/src/session-realtime.ts +3 -2
- package/src/session-tool-call-settlement.ts +29 -15
- package/src/transcription-recordings-schema.ts +5 -2
- package/src/transcription-recordings.ts +70 -40
- package/dist/chunk-DBS5HPEW.js.map +0 -1
- package/dist/chunk-HT5T62CC.js.map +0 -1
- package/dist/event-payload-sanitizer.d.ts +0 -32
- 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
|
+
}
|
package/src/memory-domain.ts
CHANGED
|
@@ -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)
|
|
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
|
-
//
|
|
455
|
+
// Exact stored memory content
|
|
456
456
|
// ---------------------------------------------------------------------------
|
|
457
457
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
|
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
|
|
102
|
-
*
|
|
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
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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:
|
|
164
|
+
readonly cause: unknown;
|
|
179
165
|
|
|
180
|
-
constructor(
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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");
|
package/src/runtime-posture.ts
CHANGED
|
@@ -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",
|
|
@@ -136,6 +138,7 @@ export const FORCE_RLS_TABLES = [
|
|
|
136
138
|
"workspace_instruction_policy_snapshots",
|
|
137
139
|
"workspace_model_policies",
|
|
138
140
|
"workspace_packs",
|
|
141
|
+
"workspace_screenshot_quotas",
|
|
139
142
|
"workspace_session_activity_revisions",
|
|
140
143
|
"workspace_variable_set_variables",
|
|
141
144
|
"workspace_variable_sets",
|
|
@@ -204,10 +207,12 @@ export const RUNTIME_FULL_DML_TABLES = [
|
|
|
204
207
|
"knowledge_memories",
|
|
205
208
|
"machine_metrics_latest",
|
|
206
209
|
"machine_metrics_series",
|
|
210
|
+
"machine_removal_operations",
|
|
207
211
|
"managed_accounts",
|
|
208
212
|
"model_call_facts",
|
|
209
213
|
"new_session_drafts",
|
|
210
214
|
"pack_installations",
|
|
215
|
+
"retained_screenshot_artifacts",
|
|
211
216
|
"rig_changes",
|
|
212
217
|
"rig_versions",
|
|
213
218
|
"rigs",
|
|
@@ -265,6 +270,7 @@ export const RUNTIME_FULL_DML_TABLES = [
|
|
|
265
270
|
"workspace_memberships",
|
|
266
271
|
"workspace_model_policies",
|
|
267
272
|
"workspace_packs",
|
|
273
|
+
"workspace_screenshot_quotas",
|
|
268
274
|
"workspace_session_activity_revisions",
|
|
269
275
|
"workspace_variable_set_variables",
|
|
270
276
|
"workspace_variable_sets",
|