@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.
@@ -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 = attachments.get(this);
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, and the scoped-dispatch invariant can pin it.
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. */
@@ -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 project in user role: the event's
84
- // model-facing content stays verbatim. Do NOT re-add per-type framing
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
  }
@@ -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. Whether
40
- * a bump is needed is decided by what the WRITER emits, never by what a newer
41
- * reader can accept: bump exactly when an older runtime could no longer handle
42
- * a new log with full semantic correctness ("parses without error" is not
43
- * correctness — silently skipping content that shapes reconstruction is a
44
- * wrong read). Only structural changes reach that bar: the header shape, the
45
- * {@link SessionEvent} envelope, core event semantics, or the surface
46
- * mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).
47
- * Adding an ordinary event type does not bump — the per-event
48
- * {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
49
- * in doubt, bump: a near-identity upgrade step is almost free, a missed bump
50
- * makes older runtimes read new logs wrong silently. The released migration,
51
- * immutable prior-generation, and current fast-path rules are recorded in
52
- * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
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
- * The invariant companion deliberately constrains nothing here, so a plugin
421
- * appending one would silently classify every live bracket before it as seed history.
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. */
@@ -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. Whether
40
- * a bump is needed is decided by what the WRITER emits, never by what a newer
41
- * reader can accept: bump exactly when an older runtime could no longer handle
42
- * a new log with full semantic correctness ("parses without error" is not
43
- * correctness — silently skipping content that shapes reconstruction is a
44
- * wrong read). Only structural changes reach that bar: the header shape, the
45
- * {@link SessionEvent} envelope, core event semantics, or the surface
46
- * mechanism (the {@link SurfaceEventType} set and {@link SurfaceOp} variants).
47
- * Adding an ordinary event type does not bump — the per-event
48
- * {@link SessionEvent.ignorable} guard covers vocabulary growth instead. When
49
- * in doubt, bump: a near-identity upgrade step is almost free, a missed bump
50
- * makes older runtimes read new logs wrong silently. The released migration,
51
- * immutable prior-generation, and current fast-path rules are recorded in
52
- * `.agents/notes/implemented/architecture/2026-08-31-released-session-format-migrations.md`.
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.0-rc.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/cordis": "~4.0.4",
49
- "@deepseek-ai/dsh-scope": "0.2.0-rc.2"
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-invariants": "0.2.0-rc.2",
53
- "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
54
- "@deepseek-ai/dsh-scope": "0.2.0-rc.2",
55
- "@deepseek-ai/cordis": "~4.0.4",
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.0-rc.2",
60
- "@deepseek-ai/dsh-brand": "0.2.0-rc.2",
61
- "@deepseek-ai/dsh-util-values": "0.2.0-rc.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 };
@@ -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