|
|
@@ -103,17 +103,16 @@ declare module 'cordis' {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-/** Detach, validate, and freeze the creation metadata published by a session. */
|
|
|
-function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHeader {
|
|
|
- const input: unknown = source === undefined
|
|
|
- ? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() }
|
|
|
- : source
|
|
|
- const snapshot = snapshotJsonValue(input)
|
|
|
- if (snapshot === undefined) throw new Error('session header is not losslessly JSON-serializable')
|
|
|
- if (snapshot === null || typeof snapshot !== 'object' || Array.isArray(snapshot)) {
|
|
|
+/** Validate and freeze one detached creation header in place. */
|
|
|
+function validateSessionHeader(id: SessionId, input: unknown): SessionHeader {
|
|
|
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
|
throw new Error('session header is not a plain JSON record')
|
|
|
}
|
|
|
- const record = snapshot as Record<string, unknown>
|
|
|
+ const prototype = Reflect.getPrototypeOf(input)
|
|
|
+ if (prototype !== Object.prototype && prototype !== null) {
|
|
|
+ throw new Error('session header is not a plain JSON record')
|
|
|
+ }
|
|
|
+ const record = input as Record<string, unknown>
|
|
|
if (record.version !== SESSION_FORMAT_VERSION) {
|
|
|
throw new Error(`session header version must be ${SESSION_FORMAT_VERSION}, got ${String(record.version)}`)
|
|
|
}
|
|
|
@@ -148,31 +147,52 @@ function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHe
|
|
|
return deepFreeze(record as unknown as SessionHeader)
|
|
|
}
|
|
|
|
|
|
+/** Detach, validate, and freeze the creation metadata published by a session. */
|
|
|
+function snapshotSessionHeader(id: SessionId, source?: SessionHeader): SessionHeader {
|
|
|
+ const input: unknown = source === undefined
|
|
|
+ ? { version: SESSION_FORMAT_VERSION, id, createdAt: Date.now() }
|
|
|
+ : source
|
|
|
+ const snapshot = snapshotJsonValue(input)
|
|
|
+ if (snapshot === undefined) throw new Error('session header is not losslessly JSON-serializable')
|
|
|
+ return validateSessionHeader(id, snapshot)
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
- * Detach one event while preserving deep immutability for its identified message.
|
|
|
- * @param event - event imported across a query or persistence boundary.
|
|
|
- * @returns a detached event snapshot with a validated, deeply frozen message.
|
|
|
+ * Validate an exclusively owned event and deeply freeze its identified message
|
|
|
+ * without copying the event. The caller transfers an object graph that no
|
|
|
+ * producer retains and that shares no mutable children with another event.
|
|
|
+ * Use {@link snapshotSessionEvent} when exclusive ownership is not guaranteed.
|
|
|
+ * @param event - exclusively owned event imported across a trusted boundary.
|
|
|
+ * @returns the same event object with a validated, deeply frozen message.
|
|
|
*/
|
|
|
-export function snapshotSessionEvent<T extends SessionEvent>(event: T): T {
|
|
|
- const snapshot = structuredClone(event)
|
|
|
+export function adoptSessionEvent<T extends SessionEvent>(event: T): T {
|
|
|
assertMessageEventShape(
|
|
|
- snapshot,
|
|
|
- `session event at seq ${snapshot.seq}`,
|
|
|
+ event,
|
|
|
+ `session event at seq ${event.seq}`,
|
|
|
)
|
|
|
- switch (snapshot.type) {
|
|
|
+ switch (event.type) {
|
|
|
case 'user/message':
|
|
|
- deepFreeze(snapshot.data)
|
|
|
+ deepFreeze(event.data)
|
|
|
break
|
|
|
case 'assistant/message':
|
|
|
case 'tool/result':
|
|
|
case 'steering/message':
|
|
|
- deepFreeze(snapshot.data.message)
|
|
|
+ deepFreeze(event.data.message)
|
|
|
break
|
|
|
default:
|
|
|
// SessionEventMap is merge-extensible; plugin-owned events carry no core message.
|
|
|
break
|
|
|
}
|
|
|
- return snapshot
|
|
|
+ return event
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Detach one event while preserving deep immutability for its identified message.
|
|
|
+ * @param event - event imported across a query or persistence boundary.
|
|
|
+ * @returns a detached event snapshot with a validated, deeply frozen message.
|
|
|
+ */
|
|
|
+export function snapshotSessionEvent<T extends SessionEvent>(event: T): T {
|
|
|
+ return adoptSessionEvent(structuredClone(event))
|
|
|
}
|
|
|
|
|
|
/** Validate the fixed event envelope after one-pass JSON materialization. */
|