@deepseek-ai/dsh-session 0.2.0-rc.2 → 0.2.1-alpha.2
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/README.i18n.yaml +15 -12
- package/README.md +16 -5
- package/README.zh.md +16 -5
- package/lib/index.js +136 -20
- package/lib/types/index.d.ts +51 -7
- package/lib/types/index.js +122 -7
- package/lib/types/known-event-types.js +1 -0
- package/lib/types/surface.js +2 -8
- package/lib/types/types.d.ts +47 -16
- package/lib/types/types.js +14 -14
- package/package.json +10 -16
- package/lib/invariant.js +0 -174
- package/lib/types/invariant.d.ts +0 -18
- package/lib/types/invariant.js +0 -207
package/lib/types/index.js
CHANGED
|
@@ -324,6 +324,77 @@ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
|
|
|
324
324
|
}
|
|
325
325
|
/** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
|
|
326
326
|
const attachments = new WeakMap();
|
|
327
|
+
/**
|
|
328
|
+
* Grammar of a {@link PluginRecordType}: `plugin:` followed by
|
|
329
|
+
* slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
|
|
330
|
+
* each segment starting with a letter or digit.
|
|
331
|
+
*/
|
|
332
|
+
const PLUGIN_RECORD_TYPE = /^plugin:[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/u;
|
|
333
|
+
/**
|
|
334
|
+
* Whether one event type name follows the plugin record grammar.
|
|
335
|
+
* @param type - an event type name.
|
|
336
|
+
* @returns whether the name is a {@link PluginRecordType}.
|
|
337
|
+
*/
|
|
338
|
+
function isPluginRecordType(type) {
|
|
339
|
+
return PLUGIN_RECORD_TYPE.test(type);
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Type one constructed plugin record as a log event after checking its type
|
|
343
|
+
* against the plugin record grammar. A restored Session already holds events
|
|
344
|
+
* whose types are outside `SessionEventMap` when they are marked ignorable;
|
|
345
|
+
* a plugin record is such an event.
|
|
346
|
+
* @param value - the frozen record envelope built by {@link Session}.
|
|
347
|
+
* @throws when the record type is outside the plugin record grammar.
|
|
348
|
+
*/
|
|
349
|
+
function assertPluginRecordEvent(value) {
|
|
350
|
+
const { type } = value;
|
|
351
|
+
if (!isPluginRecordType(type)) {
|
|
352
|
+
throw new Error(`plugin record type "${type}" must be "plugin:" followed by lowercase slash-separated segments`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
/** Module-private access to {@link Session}'s record commit, assigned once by its static block. */
|
|
356
|
+
let commitPluginRecord;
|
|
357
|
+
/**
|
|
358
|
+
* Append one experimental plugin record: an event marked `ignorable` whose
|
|
359
|
+
* type is declared in {@link PluginRecordMap}, outside {@link SessionEventMap}.
|
|
360
|
+
* The plugin record catalog lists it separately from released event schemas.
|
|
361
|
+
* A reader that does not recognize the type retains and skips the record, which never
|
|
362
|
+
* enters the model-visible surface, and fork and resume carry it with the rest
|
|
363
|
+
* of the log. A Session format migration retains records on a best-effort
|
|
364
|
+
* basis, so a record holds plugin-owned state that its owner can lose without
|
|
365
|
+
* changing how the rest of the log is interpreted.
|
|
366
|
+
*
|
|
367
|
+
* Only production source under `packages/experimental/` may call this
|
|
368
|
+
* function; the `verify-plugin-record-callers` gate rejects any other
|
|
369
|
+
* production caller in this repository. Read records back with
|
|
370
|
+
* {@link pluginRecordOf}.
|
|
371
|
+
* @param session - the Session whose log receives the record.
|
|
372
|
+
* @param type - declared record name: `plugin:` followed by slash-separated
|
|
373
|
+
* segments of lowercase letters, digits, `.`, `_`, and `-`, each starting
|
|
374
|
+
* with a letter or digit.
|
|
375
|
+
* @param data - JSON payload, snapshotted before it enters the log.
|
|
376
|
+
* @returns the sequence number of the committed record.
|
|
377
|
+
* @throws when `type` is outside that grammar, when `data` is not losslessly
|
|
378
|
+
* JSON-serializable, or when the call reenters another append's publication;
|
|
379
|
+
* a rejected record does not change the log.
|
|
380
|
+
*/
|
|
381
|
+
export function appendPluginRecord(session, type, data) {
|
|
382
|
+
return commitPluginRecord(session, type, data);
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* Read one committed event as a plugin record. The V3-to-V4 format edge also
|
|
386
|
+
* renames each unknown ignorable V3 event into the `plugin:` namespace, so an
|
|
387
|
+
* owner names its records under its own package name and validates `data`.
|
|
388
|
+
* @param event - any committed Session event, live or restored.
|
|
389
|
+
* @returns the record, or undefined when the event is not an ignorable event
|
|
390
|
+
* whose type follows the plugin record grammar.
|
|
391
|
+
*/
|
|
392
|
+
export function pluginRecordOf(event) {
|
|
393
|
+
const type = event.type;
|
|
394
|
+
if (event.ignorable !== true || !isPluginRecordType(type))
|
|
395
|
+
return undefined;
|
|
396
|
+
return { type, seq: event.seq, time: event.time, data: event.data };
|
|
397
|
+
}
|
|
327
398
|
/**
|
|
328
399
|
* An event-sourced session: an append-only log of {@link SessionEvent}s.
|
|
329
400
|
*
|
|
@@ -578,10 +649,7 @@ export class Session {
|
|
|
578
649
|
if (surfaceMetadataSnapshot === undefined) {
|
|
579
650
|
throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
|
|
580
651
|
}
|
|
581
|
-
const entry =
|
|
582
|
-
if (entry?.appending) {
|
|
583
|
-
throw new Error('session append cannot reenter while another append is being published');
|
|
584
|
-
}
|
|
652
|
+
const entry = this.publicationEntry();
|
|
585
653
|
const event = deepFreeze({
|
|
586
654
|
type,
|
|
587
655
|
seq: SessionSeq(this.log.length),
|
|
@@ -590,6 +658,54 @@ export class Session {
|
|
|
590
658
|
...surfaceMetadataSnapshot,
|
|
591
659
|
});
|
|
592
660
|
validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
|
|
661
|
+
this.commit(event, entry);
|
|
662
|
+
return event;
|
|
663
|
+
}
|
|
664
|
+
static {
|
|
665
|
+
commitPluginRecord = (session, type, data) => session.#appendRecord(type, data);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Commit one plugin record; {@link appendPluginRecord} is the only caller.
|
|
669
|
+
* @param type - record type, checked against the plugin record grammar.
|
|
670
|
+
* @param data - JSON payload, snapshotted before it enters the log.
|
|
671
|
+
* @returns the record's sequence number.
|
|
672
|
+
*/
|
|
673
|
+
#appendRecord(type, data) {
|
|
674
|
+
const dataSnapshot = snapshotJsonValue(data);
|
|
675
|
+
if (dataSnapshot === undefined) {
|
|
676
|
+
throw new Error(`plugin record "${type}" carries non-JSON-serializable data`);
|
|
677
|
+
}
|
|
678
|
+
const entry = this.publicationEntry();
|
|
679
|
+
const record = deepFreeze({
|
|
680
|
+
type,
|
|
681
|
+
seq: SessionSeq(this.log.length),
|
|
682
|
+
time: Date.now(),
|
|
683
|
+
data: dataSnapshot,
|
|
684
|
+
ignorable: true,
|
|
685
|
+
});
|
|
686
|
+
assertPluginRecordEvent(record);
|
|
687
|
+
this.commit(record, entry);
|
|
688
|
+
return record.seq;
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Read the store attachment for one append, refusing an append that
|
|
692
|
+
* reenters while another append is being published.
|
|
693
|
+
* @returns the attachment, or undefined for a detached Session.
|
|
694
|
+
*/
|
|
695
|
+
publicationEntry() {
|
|
696
|
+
const entry = attachments.get(this);
|
|
697
|
+
if (entry?.appending) {
|
|
698
|
+
throw new Error('session append cannot reenter while another append is being published');
|
|
699
|
+
}
|
|
700
|
+
return entry;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Accept one frozen candidate at the next sequence number, enter it into the
|
|
704
|
+
* log, and notify `session/event` observers with per-listener containment.
|
|
705
|
+
* @param event - the candidate built at the current log length.
|
|
706
|
+
* @param entry - the attachment read before the candidate was built.
|
|
707
|
+
*/
|
|
708
|
+
commit(event, entry) {
|
|
593
709
|
this.surfaceManager.validateNext(event);
|
|
594
710
|
if (entry !== undefined)
|
|
595
711
|
entry.appending = true;
|
|
@@ -604,7 +720,6 @@ export class Session {
|
|
|
604
720
|
if (callbacks !== undefined && entry !== undefined) {
|
|
605
721
|
invokeContainedSessionObservers(entry.emitCtx, 'session/event', entry.id, callbackArgs, callbacks);
|
|
606
722
|
}
|
|
607
|
-
return event;
|
|
608
723
|
}
|
|
609
724
|
finally {
|
|
610
725
|
if (entry !== undefined) {
|
|
@@ -999,8 +1114,8 @@ export class SessionStore extends Service {
|
|
|
999
1114
|
* store owns the carrier, so callers (the checkpoint policy's per-request
|
|
1000
1115
|
* barrier, goal-round-driver's idle checkpoint, teardown drains, and consumers
|
|
1001
1116
|
* that flush themselves before reading storage) must come through here
|
|
1002
|
-
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1003
|
-
* one spelling
|
|
1117
|
+
* rather than dispatch a raw `ctx.parallel('session/flush', …)` — one owner
|
|
1118
|
+
* and one spelling.
|
|
1004
1119
|
* @param session - the session whose buffered events must reach durable storage.
|
|
1005
1120
|
* @returns whether at least one durability listener participated, after every
|
|
1006
1121
|
* listener has settled successfully.
|
|
@@ -77,6 +77,7 @@ export const KNOWN_SESSION_EVENT_TYPES = new Set([
|
|
|
77
77
|
'turn/start',
|
|
78
78
|
'user/message',
|
|
79
79
|
'web/deepseek-search-llm-request',
|
|
80
|
+
'working-directory/change',
|
|
80
81
|
'workspace/changes',
|
|
81
82
|
]);
|
|
82
83
|
/** Event types whose model-visible effects require an explicit pure interpreter. */
|
package/lib/types/surface.js
CHANGED
|
@@ -80,14 +80,8 @@ export function deriveEventMessage(event, projectedMessages) {
|
|
|
80
80
|
// history; turn/step boundaries, failed attempts, and errors are trace/replay
|
|
81
81
|
// data.
|
|
82
82
|
switch (event.type) {
|
|
83
|
-
// Ordinary prompts and injected context
|
|
84
|
-
//
|
|
85
|
-
// (e.g. `<context>`) here: framing is caller-owned — a producer bakes it
|
|
86
|
-
// into `content`, as agent-instructions does with `<system-reminder>` — or,
|
|
87
|
-
// if reintroduced, must be driven by the event `meta` map and a dedicated
|
|
88
|
-
// renderer, keeping this projection a verbatim pass-through. See the
|
|
89
|
-
// deferred design note in
|
|
90
|
-
// ../../../../.agents/notes/implemented/simplification/2026-07-20-unwrap-injected-content-envelopes.md
|
|
83
|
+
// Ordinary prompts and injected context retain producer-owned framing.
|
|
84
|
+
// See Derived history in packages/core/session/README.md.
|
|
91
85
|
case 'user/message': {
|
|
92
86
|
return event.data;
|
|
93
87
|
}
|
package/lib/types/types.d.ts
CHANGED
|
@@ -36,20 +36,20 @@ export type OptionalSessionSeq = SessionSeq | null;
|
|
|
36
36
|
* event-body read composes the build-static adjacent chain and publishes only
|
|
37
37
|
* this final generation before constructing a Session.
|
|
38
38
|
*
|
|
39
|
-
* The version is a single monotonic integer with no major/minor split.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `.agents/notes/implemented/
|
|
39
|
+
* The version is a single monotonic integer with no major/minor split. Retain
|
|
40
|
+
* it when older readers resume correctly, preserve safely ignorable records,
|
|
41
|
+
* or refuse required features before execution, mutation, or tail repair.
|
|
42
|
+
* New readers must preserve historical meanings, including logs mixing old
|
|
43
|
+
* and new record representations. Bump when an existing discriminator cannot
|
|
44
|
+
* prevent unsafe interpretation; a structural difference alone is insufficient.
|
|
45
|
+
* Unknown fields and values require evidence of the released reader's actual
|
|
46
|
+
* admission behavior. The per-event {@link SessionEvent.ignorable} marker
|
|
47
|
+
* governs unknown event types, not arbitrary nested payload changes.
|
|
48
|
+
* Same-version readers retain earlier representations: the adjacent migration
|
|
49
|
+
* chain does not run when stored and current versions match. A subsequent
|
|
50
|
+
* migration must account for all supported representations of its source.
|
|
51
|
+
* Compatibility review and the retained migration obligations are recorded in
|
|
52
|
+
* `.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md`.
|
|
53
53
|
*/
|
|
54
54
|
export declare const SESSION_FORMAT_VERSION = 4;
|
|
55
55
|
/**
|
|
@@ -417,8 +417,8 @@ export interface SessionEventMap {
|
|
|
417
417
|
* keep ordinary restore and replay lifecycle boundaries.
|
|
418
418
|
*
|
|
419
419
|
* Only the `Session` constructor and `buildForkSeed` may create this marker.
|
|
420
|
-
*
|
|
421
|
-
*
|
|
420
|
+
* Session append does not reject other writers, so a plugin appending one
|
|
421
|
+
* would silently classify every live bracket before it as seed history.
|
|
422
422
|
*
|
|
423
423
|
* An owner of a standalone open/close bracket (`compaction/start` …
|
|
424
424
|
* `compaction/end`) reads it because seed history and live work are otherwise
|
|
@@ -510,6 +510,37 @@ export type SessionEvent<T extends SessionEventType = SessionEventType> = {
|
|
|
510
510
|
sourceEventSeqs?: never;
|
|
511
511
|
});
|
|
512
512
|
}[T];
|
|
513
|
+
/**
|
|
514
|
+
* Event type of an experimental plugin record: `plugin:` followed by
|
|
515
|
+
* slash-separated segments of lowercase letters, digits, `.`, `_`, and `-`,
|
|
516
|
+
* each starting with a letter or digit. A record type is never a
|
|
517
|
+
* {@link SessionEventMap} member.
|
|
518
|
+
*/
|
|
519
|
+
export type PluginRecordType = `plugin:${string}`;
|
|
520
|
+
/**
|
|
521
|
+
* Payloads written by experimental packages, keyed by their `plugin:` record
|
|
522
|
+
* names. Packages augment this map to type writes and enter the current plugin
|
|
523
|
+
* record catalog; these declarations do not enter {@link SessionEventMap} or
|
|
524
|
+
* released persistence schemas. Stored records still require owner validation.
|
|
525
|
+
*/
|
|
526
|
+
export interface PluginRecordMap {
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* One committed experimental plugin record, read from the log by
|
|
530
|
+
* `pluginRecordOf`. The record's owner validates `data` before use, because a
|
|
531
|
+
* restored record carries whatever JSON an earlier build of its owner wrote,
|
|
532
|
+
* or a V3 event that a format migration renamed into the `plugin:` namespace held.
|
|
533
|
+
*/
|
|
534
|
+
export interface PluginRecord {
|
|
535
|
+
/** The record type, chosen by the owning plugin. */
|
|
536
|
+
readonly type: PluginRecordType;
|
|
537
|
+
/** The record's position in the Session log. */
|
|
538
|
+
readonly seq: SessionSeq;
|
|
539
|
+
/** Unix epoch milliseconds at append. */
|
|
540
|
+
readonly time: number;
|
|
541
|
+
/** The JSON payload as committed. */
|
|
542
|
+
readonly data: unknown;
|
|
543
|
+
}
|
|
513
544
|
declare module '@deepseek-ai/dsh-typert-protocol' {
|
|
514
545
|
interface RemoteErrorDetailsMap {
|
|
515
546
|
/** The named Session does not exist; produced by every layer that resolves a SessionId. */
|
package/lib/types/types.js
CHANGED
|
@@ -36,20 +36,20 @@ export function SessionLogOffset(value) {
|
|
|
36
36
|
* event-body read composes the build-static adjacent chain and publishes only
|
|
37
37
|
* this final generation before constructing a Session.
|
|
38
38
|
*
|
|
39
|
-
* The version is a single monotonic integer with no major/minor split.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* `.agents/notes/implemented/
|
|
39
|
+
* The version is a single monotonic integer with no major/minor split. Retain
|
|
40
|
+
* it when older readers resume correctly, preserve safely ignorable records,
|
|
41
|
+
* or refuse required features before execution, mutation, or tail repair.
|
|
42
|
+
* New readers must preserve historical meanings, including logs mixing old
|
|
43
|
+
* and new record representations. Bump when an existing discriminator cannot
|
|
44
|
+
* prevent unsafe interpretation; a structural difference alone is insufficient.
|
|
45
|
+
* Unknown fields and values require evidence of the released reader's actual
|
|
46
|
+
* admission behavior. The per-event {@link SessionEvent.ignorable} marker
|
|
47
|
+
* governs unknown event types, not arbitrary nested payload changes.
|
|
48
|
+
* Same-version readers retain earlier representations: the adjacent migration
|
|
49
|
+
* chain does not run when stored and current versions match. A subsequent
|
|
50
|
+
* migration must account for all supported representations of its source.
|
|
51
|
+
* Compatibility review and the retained migration obligations are recorded in
|
|
52
|
+
* `.agents/notes/implemented/process/2026-10-08-session-reader-compatibility-review.md`.
|
|
53
53
|
*/
|
|
54
54
|
export const SESSION_FORMAT_VERSION = 4;
|
|
55
55
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-session",
|
|
3
3
|
"description": "Event-sourced session store for the DeepSeek Harness",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -18,10 +18,6 @@
|
|
|
18
18
|
"types": "./lib/types/index.d.ts",
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
|
-
"./invariant": {
|
|
22
|
-
"types": "./lib/types/invariant.d.ts",
|
|
23
|
-
"default": "./lib/invariant.js"
|
|
24
|
-
},
|
|
25
21
|
"./types": {
|
|
26
22
|
"types": "./lib/types/types.d.ts",
|
|
27
23
|
"default": "./lib/types/types.js"
|
|
@@ -39,25 +35,23 @@
|
|
|
39
35
|
},
|
|
40
36
|
"files": [
|
|
41
37
|
"lib/index.js",
|
|
42
|
-
"lib/invariant.js",
|
|
43
38
|
"lib/types/**/*.js",
|
|
44
39
|
"lib/types/**/*.d.ts"
|
|
45
40
|
],
|
|
46
41
|
"license": "MIT",
|
|
47
42
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/
|
|
49
|
-
"@deepseek-ai/
|
|
43
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
|
|
44
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
50
45
|
},
|
|
51
46
|
"devDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/cordis": "~4.0.
|
|
56
|
-
"@deepseek-ai/dsh-typert-registry": "0.2.0-rc.2"
|
|
47
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.2",
|
|
48
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.2",
|
|
49
|
+
"@deepseek-ai/dsh-typert-registry": "0.2.1-alpha.2",
|
|
50
|
+
"@deepseek-ai/cordis": "~4.0.5-alpha.1"
|
|
57
51
|
},
|
|
58
52
|
"dependencies": {
|
|
59
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
60
|
-
"@deepseek-ai/dsh-brand": "0.2.
|
|
61
|
-
"@deepseek-ai/dsh-util-values": "0.2.
|
|
53
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-brand": "0.2.1-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-util-values": "0.2.1-alpha.2"
|
|
62
56
|
}
|
|
63
57
|
}
|
package/lib/invariant.js
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
2
|
-
import "@deepseek-ai/dsh-brand";
|
|
3
|
-
//#endregion
|
|
4
|
-
//#region lib/types/invariant.js
|
|
5
|
-
/**
|
|
6
|
-
* Package-owned relational invariants for the session event log. Load this
|
|
7
|
-
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
8
|
-
*
|
|
9
|
-
* @module @deepseek-ai/dsh-session/invariant
|
|
10
|
-
*/
|
|
11
|
-
const PACKAGE_NAME = "@deepseek-ai/dsh-session";
|
|
12
|
-
/** Cordis companion plugin name. */
|
|
13
|
-
const name = "session-invariant";
|
|
14
|
-
/** Service required before the companion can reserve package ownership. */
|
|
15
|
-
const inject = ["invariants"];
|
|
16
|
-
/** Assert that a step-scoped event names the currently open turn and step. */
|
|
17
|
-
function requireOpenStep(trace, kind, turn, step, fail) {
|
|
18
|
-
if (trace.openTurn !== turn || trace.openStep !== step) fail(`${kind} names turn ${turn}/step ${step} but open is turn ${trace.openTurn}/step ${trace.openStep}`);
|
|
19
|
-
}
|
|
20
|
-
/** Validate one candidate event without mutating the committed trace. */
|
|
21
|
-
function validateEvent(trace, event, fail) {
|
|
22
|
-
if (event.seq <= trace.lastSeq) fail(`seq must strictly increase: saw ${event.seq} after ${trace.lastSeq}`);
|
|
23
|
-
let openTurn = trace.openTurn;
|
|
24
|
-
let openStep = trace.openStep;
|
|
25
|
-
let nextTurn = trace.nextTurn;
|
|
26
|
-
let nextStep = trace.nextStep;
|
|
27
|
-
let pendingCalls = { kind: "none" };
|
|
28
|
-
switch (event.type) {
|
|
29
|
-
case "turn/start":
|
|
30
|
-
if (trace.openTurn !== null) fail(`turn/start ${event.data.turn} while turn ${trace.openTurn} is still open`);
|
|
31
|
-
if (event.data.turn !== trace.nextTurn) fail(`turn/start expected turn ${trace.nextTurn}, got ${event.data.turn}`);
|
|
32
|
-
openTurn = event.data.turn;
|
|
33
|
-
nextStep = 1;
|
|
34
|
-
break;
|
|
35
|
-
case "turn/end":
|
|
36
|
-
if (trace.openTurn !== event.data.turn) fail(`turn/end ${event.data.turn} does not match open turn ${trace.openTurn}`);
|
|
37
|
-
if (trace.openStep !== null) fail(`turn/end ${event.data.turn} while step ${trace.openStep} is still open`);
|
|
38
|
-
openTurn = null;
|
|
39
|
-
nextTurn += 1;
|
|
40
|
-
break;
|
|
41
|
-
case "step/start":
|
|
42
|
-
if (trace.openTurn !== event.data.turn) fail(`step/start in turn ${event.data.turn} but open turn is ${trace.openTurn}`);
|
|
43
|
-
if (trace.openStep !== null) fail(`step/start ${event.data.step} while step ${trace.openStep} is still open`);
|
|
44
|
-
if (event.data.step !== trace.nextStep) fail(`step/start expected step ${trace.nextStep} in turn ${event.data.turn}, got ${event.data.step}`);
|
|
45
|
-
openStep = event.data.step;
|
|
46
|
-
break;
|
|
47
|
-
case "developer/message":
|
|
48
|
-
requireOpenStep(trace, "developer/message", event.data.turn, event.data.step, fail);
|
|
49
|
-
break;
|
|
50
|
-
case "step/end":
|
|
51
|
-
requireOpenStep(trace, "step/end", event.data.turn, event.data.step, fail);
|
|
52
|
-
pendingCalls = { kind: "clear" };
|
|
53
|
-
openStep = null;
|
|
54
|
-
nextStep += 1;
|
|
55
|
-
break;
|
|
56
|
-
case "assistant/attempt":
|
|
57
|
-
requireOpenStep(trace, "assistant/attempt", event.data.turn, event.data.step, fail);
|
|
58
|
-
break;
|
|
59
|
-
case "assistant/message":
|
|
60
|
-
requireOpenStep(trace, "assistant/message", event.data.turn, event.data.step, fail);
|
|
61
|
-
break;
|
|
62
|
-
case "tool/call":
|
|
63
|
-
requireOpenStep(trace, "tool/call", event.data.turn, event.data.step, fail);
|
|
64
|
-
pendingCalls = {
|
|
65
|
-
kind: "add",
|
|
66
|
-
callId: event.data.callId
|
|
67
|
-
};
|
|
68
|
-
break;
|
|
69
|
-
case "tool/result": {
|
|
70
|
-
if (event.surfaceOp !== "append") {
|
|
71
|
-
if (trace.openTurn === null) fail("tool/result surface replacement appended outside any open turn");
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
requireOpenStep(trace, "tool/result", event.data.turn, event.data.step, fail);
|
|
75
|
-
const callId = event.data.message.source.callId;
|
|
76
|
-
const syntheticNotStarted = event.data.message.isError === true && event.data.error?.code === "TOOL_NOT_STARTED";
|
|
77
|
-
if (!trace.pendingCalls.has(callId) && !syntheticNotStarted) fail(`tool/result for ${callId} with no prior tool/call in this step`);
|
|
78
|
-
pendingCalls = {
|
|
79
|
-
kind: "delete",
|
|
80
|
-
callId
|
|
81
|
-
};
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
case "system/message":
|
|
85
|
-
requireOpenStep(trace, "system/message", event.data.turn, event.data.step, fail);
|
|
86
|
-
break;
|
|
87
|
-
case "user/message": break;
|
|
88
|
-
case "session/end-seed": break;
|
|
89
|
-
case "request/header":
|
|
90
|
-
case "request/context":
|
|
91
|
-
if (trace.openTurn === null) fail(`${event.type} appended outside any open turn (core execution events must be turn-enclosed)`);
|
|
92
|
-
break;
|
|
93
|
-
default: break;
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
scalars: {
|
|
97
|
-
lastSeq: event.seq,
|
|
98
|
-
openTurn,
|
|
99
|
-
openStep,
|
|
100
|
-
nextTurn,
|
|
101
|
-
nextStep
|
|
102
|
-
},
|
|
103
|
-
pendingCalls
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/** Apply one already-validated transition after its event commits. */
|
|
107
|
-
function applyTransition(trace, transition) {
|
|
108
|
-
Object.assign(trace, transition.scalars);
|
|
109
|
-
switch (transition.pendingCalls.kind) {
|
|
110
|
-
case "none": break;
|
|
111
|
-
case "add":
|
|
112
|
-
trace.pendingCalls.add(transition.pendingCalls.callId);
|
|
113
|
-
break;
|
|
114
|
-
case "delete":
|
|
115
|
-
trace.pendingCalls.delete(transition.pendingCalls.callId);
|
|
116
|
-
break;
|
|
117
|
-
case "clear":
|
|
118
|
-
trace.pendingCalls.clear();
|
|
119
|
-
break;
|
|
120
|
-
/* v8 ignore next -- validateEvent produces this closed transition union */
|
|
121
|
-
default: assertNever(transition.pendingCalls, "session trace pending-call transition");
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
/** Install the session contribution into its child registration fiber. */
|
|
125
|
-
const install = Object.assign((ctx, fail) => {
|
|
126
|
-
const traces = /* @__PURE__ */ new WeakMap();
|
|
127
|
-
const stagedTransitions = /* @__PURE__ */ new WeakMap();
|
|
128
|
-
const freshTrace = () => ({
|
|
129
|
-
lastSeq: -1,
|
|
130
|
-
openTurn: null,
|
|
131
|
-
openStep: null,
|
|
132
|
-
nextTurn: 1,
|
|
133
|
-
nextStep: 1,
|
|
134
|
-
pendingCalls: /* @__PURE__ */ new Set()
|
|
135
|
-
});
|
|
136
|
-
const seedSession = (session) => {
|
|
137
|
-
const trace = freshTrace();
|
|
138
|
-
traces.set(session, trace);
|
|
139
|
-
for (const event of session.snapshotEvents()) applyTransition(trace, validateEvent(trace, event, fail));
|
|
140
|
-
return trace;
|
|
141
|
-
};
|
|
142
|
-
/* v8 ignore next -- session/event always follows list() or session/created seeding */
|
|
143
|
-
const traceFor = (session) => traces.get(session) ?? seedSession(session);
|
|
144
|
-
for (const session of ctx.sessions.list()) seedSession(session);
|
|
145
|
-
ctx.on("session/created", (session) => {
|
|
146
|
-
seedSession(session);
|
|
147
|
-
}, { global: true });
|
|
148
|
-
ctx.on("session/event", (session, event) => {
|
|
149
|
-
const staged = stagedTransitions.get(event);
|
|
150
|
-
/* v8 ignore next 2 -- internal/dispatch stages the exact callback arguments */
|
|
151
|
-
if (staged === void 0 || staged.session !== session) return fail("session/event reached publication without matching pre-commit validation");
|
|
152
|
-
stagedTransitions.delete(event);
|
|
153
|
-
applyTransition(staged.trace, staged.transition);
|
|
154
|
-
}, { global: true });
|
|
155
|
-
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
156
|
-
if (eventName !== "session/event") return;
|
|
157
|
-
const [session, event] = args;
|
|
158
|
-
const trace = traceFor(session);
|
|
159
|
-
const transition = validateEvent(trace, event, fail);
|
|
160
|
-
stagedTransitions.set(event, {
|
|
161
|
-
session,
|
|
162
|
-
trace,
|
|
163
|
-
transition
|
|
164
|
-
});
|
|
165
|
-
}, { global: true });
|
|
166
|
-
}, { inject: ["sessions"] });
|
|
167
|
-
/**
|
|
168
|
-
* Register the session invariant companion.
|
|
169
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
170
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
171
|
-
*/
|
|
172
|
-
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
173
|
-
//#endregion
|
|
174
|
-
export { apply, inject, name };
|
package/lib/types/invariant.d.ts
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package-owned relational invariants for the session event log. Load this
|
|
3
|
-
* companion beside `@deepseek-ai/dsh-invariants` to enable the checks.
|
|
4
|
-
*
|
|
5
|
-
* @module @deepseek-ai/dsh-session/invariant
|
|
6
|
-
*/
|
|
7
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
8
|
-
/** Cordis companion plugin name. */
|
|
9
|
-
export declare const name = "session-invariant";
|
|
10
|
-
/** Service required before the companion can reserve package ownership. */
|
|
11
|
-
export declare const inject: string[];
|
|
12
|
-
/**
|
|
13
|
-
* Register the session invariant companion.
|
|
14
|
-
* @param ctx - Cordis context carrying the invariant service.
|
|
15
|
-
* @returns the installed registration's disposer after setup succeeds.
|
|
16
|
-
*/
|
|
17
|
-
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
18
|
-
//# sourceMappingURL=invariant.d.ts.map
|