@qilin-agent/session-projection 0.0.0-stage → 3.1.3

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.
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Service Definition and drive registry for the session-projection capability seam: the merge-extensible state and client-view type
3
+ * tables, the `ProjectionDefinition` state-driven computation unit contract,
4
+ * and the `ctx.sessionProjections` registry that DRIVES every registered unit
5
+ * forward eagerly over committed session events. Domain host plugins
6
+ * contribute pure folds and optional client views; the framework owns the
7
+ * subscription, the per-session watermark cache, and change notification;
8
+ * carriers consume the snapshot read face and the change feed. Neither side
9
+ * knows the other
10
+ * (capability-seam three-way split). Design authority: the session-projection
11
+ * RFC (.agents/notes/proposed/architecture/2026-07-27-session-projection-and-command-log.md).
12
+ *
13
+ * Whole-value event rule (load-bearing): a state-carrying log event MUST
14
+ * carry the complete post-change state, never a bare delta — it keeps every
15
+ * unit's transition trivially cheap and every served value self-describing.
16
+ *
17
+ * @module @qilin-agent/session-projection
18
+ */
19
+ import { Service } from '@qilin-agent/kylin';
20
+ import { SessionLogOffset, SessionSeq } from '@qilin-agent/session';
21
+ /** Convert a log offset to the inclusive cursor immediately before it. */
22
+ function cursorBefore(offset) {
23
+ return offset === 0 ? -1 : SessionSeq(offset - 1);
24
+ }
25
+ /**
26
+ * `ctx.sessionProjections`: the projection unit table and its drive. The
27
+ * service subscribes to `session/event` once; every committed event passes
28
+ * every registered unit's `apply` (eager drive). A changed state reference
29
+ * computes the next client view; the change feed is notified only when its
30
+ * raw result changes by `Object.is`.
31
+ * Cells build lazily — a unit registered after events flowed, or a session
32
+ * older than the registry, folds `init` over the in-memory log on first
33
+ * touch (event or read). Registration is an effect (disposer rides the
34
+ * calling fiber): an unloaded domain plugin's key disappears from snapshots
35
+ * and clients read it as capability absence. A host reader either declares
36
+ * `sessionProjections` in its plugin `inject` or fails explicitly when the
37
+ * registry or required key is absent. Contributors may preserve optional
38
+ * registration through `ctx.inject(['sessionProjections'], ...)`. Registrants sharing a key
39
+ * share one unit and are counted: the same tool package mounted in N agent
40
+ * presets registers N times, and the key survives until the last one
41
+ * unloads.
42
+ */
43
+ export class SessionProjectionRegistry extends Service {
44
+ registrations = new Map();
45
+ listeners = new Set();
46
+ /**
47
+ * Create and install the registry as `ctx.sessionProjections`.
48
+ * @param ctx - Cordis context that owns the service.
49
+ */
50
+ constructor(ctx) {
51
+ super(ctx, 'sessionProjections');
52
+ ctx.on('session/created', (session) => {
53
+ if (session.seq !== 0)
54
+ return;
55
+ for (const registration of this.registrations.values()) {
56
+ if (registration.cells.has(session))
57
+ continue;
58
+ registration.cells.set(session, {
59
+ state: registration.def.init(session.header, session.inheritedEventCount),
60
+ observedSeq: -1,
61
+ views: [undefined, undefined],
62
+ });
63
+ }
64
+ });
65
+ ctx.on('session/event', (session, event) => {
66
+ this.drive(session, event);
67
+ });
68
+ }
69
+ register(definition) {
70
+ const wire = definition.wire;
71
+ const erased = {
72
+ key: definition.key,
73
+ stateSchema: definition.stateSchema,
74
+ init: (header, inheritedEventCount) => definition.init(header, inheritedEventCount),
75
+ apply: (state, event) => definition.apply(state, event),
76
+ wire: wire === undefined
77
+ ? undefined
78
+ : { viewSchema: wire.viewSchema, view: state => wire.view(state) },
79
+ stateVersion: definition.stateVersion,
80
+ };
81
+ if (!Number.isSafeInteger(definition.stateVersion) || definition.stateVersion < 0) {
82
+ throw new Error(`session projection ${JSON.stringify(definition.key)} stateVersion must be a non-negative integer, got ${String(definition.stateVersion)}`);
83
+ }
84
+ const dispose = this.ctx.effect(function* () {
85
+ const key = erased.key;
86
+ const existing = this.registrations.get(key);
87
+ if (existing === undefined) {
88
+ this.registrations.set(key, { def: erased, cells: new WeakMap(), refs: 1 });
89
+ }
90
+ else {
91
+ if (existing.def.stateVersion !== erased.stateVersion) {
92
+ throw new Error(`session projection key ${JSON.stringify(key)} is already registered at stateVersion ${String(existing.def.stateVersion)}; refusing to share it with stateVersion ${String(erased.stateVersion)}`);
93
+ }
94
+ existing.refs += 1;
95
+ }
96
+ yield () => {
97
+ const live = this.registrations.get(key);
98
+ /* v8 ignore next -- the disposer runs once per successful registration, so the entry it counted is still here */
99
+ if (live === undefined)
100
+ return;
101
+ live.refs -= 1;
102
+ if (live.refs === 0)
103
+ this.registrations.delete(key);
104
+ };
105
+ }.bind(this), 'sessionProjections.register()');
106
+ return () => void dispose();
107
+ }
108
+ /**
109
+ * Subscribe to the change feed. The registration is an effect on the
110
+ * calling context's fiber.
111
+ * @param listener - called once per client-visible unit whose raw view changed by `Object.is`, per committed event.
112
+ * @returns the exact disposer that unsubscribes.
113
+ */
114
+ onChanged(listener) {
115
+ const dispose = this.ctx.effect(() => {
116
+ this.listeners.add(listener);
117
+ return () => {
118
+ this.listeners.delete(listener);
119
+ };
120
+ }, 'sessionProjections.onChanged()');
121
+ return () => void dispose();
122
+ }
123
+ /**
124
+ * Read one unit's current host state after materializing every registered
125
+ * unit at the Session cursor. Unrelated wire views are not produced.
126
+ * The returned value is live; callers must not mutate it.
127
+ * @param session - the session whose state is read.
128
+ * @param key - the registered unit key.
129
+ * @returns current state, or `undefined` when the key is not registered.
130
+ */
131
+ stateOf(session, key) {
132
+ const registration = this.registrations.get(key);
133
+ if (registration === undefined)
134
+ return undefined;
135
+ this.materializeCells(session);
136
+ return this.cellFor(registration, session).state;
137
+ }
138
+ /**
139
+ * One consistent cut over every registered client-visible unit for one session, read from
140
+ * the watermark cache (missing cells fold lazily over the in-memory log).
141
+ * Fully synchronous — every value and `asOfSeq` reflect the same log
142
+ * position. Each value passes its unit's `viewSchema` before leaving.
143
+ * @param session - the session whose projection values are read.
144
+ * @param keys - optional client-visible outputs; state materialization remains complete.
145
+ * @returns the snapshot; `values` is empty when no selected client-visible unit is registered.
146
+ */
147
+ snapshot(session, keys) {
148
+ const values = {};
149
+ const selected = keys === undefined ? undefined : new Set(keys);
150
+ this.materializeCells(session);
151
+ for (const registration of this.registrations.values()) {
152
+ if (registration.def.wire === undefined)
153
+ continue;
154
+ if (selected !== undefined && !selected.has(registration.def.key))
155
+ continue;
156
+ const cell = this.cellFor(registration, session);
157
+ values[registration.def.key] = this.viewCell(registration, cell);
158
+ }
159
+ return { asOfSeq: cursorBefore(session.seq), values };
160
+ }
161
+ /**
162
+ * Read only already-materialized client-visible cells without folding history.
163
+ * Values may trail the live Session and are therefore hints, not a complete
164
+ * baseline. Missing cells are omitted.
165
+ * @param session - attached Session whose cached cells are inspected.
166
+ * @param keys - optional wire keys to view.
167
+ * @returns the lowest common cached cut, or `undefined` when no wire cell exists.
168
+ */
169
+ cachedSnapshot(session, keys) {
170
+ const values = {};
171
+ let asOfSeq;
172
+ const selected = keys === undefined ? undefined : new Set(keys);
173
+ for (const registration of this.registrations.values()) {
174
+ if (registration.def.wire === undefined)
175
+ continue;
176
+ if (selected !== undefined && !selected.has(registration.def.key))
177
+ continue;
178
+ const cell = registration.cells.get(session);
179
+ if (cell === undefined)
180
+ continue;
181
+ values[registration.def.key] = this.viewCell(registration, cell);
182
+ if (asOfSeq === undefined || cell.observedSeq < asOfSeq) {
183
+ asOfSeq = cell.observedSeq;
184
+ }
185
+ }
186
+ return asOfSeq === undefined ? undefined : { asOfSeq, values };
187
+ }
188
+ /**
189
+ * State-level checkpoint of every persisted unit for one session, read
190
+ * from the watermark cache (missing cells fold lazily over the in-memory
191
+ * log). This is the write side of the persisted projection cache: the
192
+ * returned rows are the `(key → {ver, seq, val})` part of the durable
193
+ * `(sessionId, key, ver, seq, val)`
194
+ * rows. Every `val` is a DETACHED structured clone — never the live
195
+ * cell reference: the watermark cache is this registry's authoritative
196
+ * mutable state, and a caller reaching the live reference could corrupt
197
+ * every subsequent snapshot and frame through it (plain JSON by the unit
198
+ * contract, so the clone is total).
199
+ * @param session - the session whose unit states are checkpointed.
200
+ * @returns one row per registered key.
201
+ */
202
+ checkpoint(session) {
203
+ const rows = {};
204
+ for (const registration of this.registrations.values()) {
205
+ const cell = this.cellFor(registration, session);
206
+ rows[registration.def.key] = {
207
+ ver: registration.def.stateVersion,
208
+ seq: cell.observedSeq,
209
+ val: structuredClone(cell.state),
210
+ };
211
+ }
212
+ return rows;
213
+ }
214
+ /**
215
+ * The stored seq a {@link restore} tail read over `checkpoint` must start
216
+ * at: one event BELOW the lowest usable watermark (a row is usable when
217
+ * its `ver` matches the live unit's `stateVersion`; an absent or mismatched row
218
+ * pulls the floor to `0` — that key must refold the full log). The
219
+ * one-below anchor is load-bearing: the tail then proves how far the
220
+ * stored log still extends, so {@link restore} can detect a log that
221
+ * shrank below a row's watermark (crash-repair truncation) instead of
222
+ * serving the stale row as current — an empty tail read from the anchor
223
+ * yields an end below every watermark and the restore rejects for a full
224
+ * re-read.
225
+ * @param checkpoint - persisted rows for one session (possibly stale or empty).
226
+ * @returns the offset for the stored-log suffix read (`SessionHandle.read`),
227
+ * or `undefined` when no unit is registered (no read needed —
228
+ * {@link restore} would serve empty values regardless).
229
+ */
230
+ restoreFloor(checkpoint) {
231
+ let floor;
232
+ for (const registration of this.registrations.values()) {
233
+ const row = checkpoint[registration.def.key];
234
+ const need = row !== undefined && row.ver === registration.def.stateVersion
235
+ ? Math.max(row.seq + 1, 0)
236
+ : 0;
237
+ floor = floor === undefined ? need : Math.min(floor, need);
238
+ }
239
+ return floor === undefined ? undefined : SessionLogOffset(Math.max(floor - 1, 0));
240
+ }
241
+ /**
242
+ * View a checkpoint's rows without any log read: for every registered
243
+ * client-visible unit whose row's `ver` matches, serve the schema-validated
244
+ * `view` of the schema-validated stored state; mismatched, malformed, or absent rows leave their key
245
+ * absent (a cold or listing consumer treats it as not-yet-available and a
246
+ * fuller read path refolds it). The zero-I/O rung of the read ladder —
247
+ * values are as stale as their rows, never wrong.
248
+ * @param checkpoint - persisted rows for one session (possibly stale or empty).
249
+ * @param keys - optional wire keys to view.
250
+ * @returns whole values per key with a usable row; empty when none.
251
+ */
252
+ viewCheckpoint(checkpoint, keys) {
253
+ const values = {};
254
+ const selected = keys === undefined ? undefined : new Set(keys);
255
+ for (const registration of this.registrations.values()) {
256
+ const def = registration.def;
257
+ if (def.wire === undefined)
258
+ continue;
259
+ if (selected !== undefined && !selected.has(def.key))
260
+ continue;
261
+ const row = checkpoint[def.key];
262
+ if (row === undefined || row.ver !== def.stateVersion)
263
+ continue;
264
+ let state;
265
+ try {
266
+ state = def.stateSchema.parse(row.val);
267
+ }
268
+ catch {
269
+ continue;
270
+ }
271
+ values[def.key] = def.wire.viewSchema.parse(def.wire.view(state));
272
+ }
273
+ return values;
274
+ }
275
+ /**
276
+ * Cold read: fold every persisted unit over a stored log suffix, seeding
277
+ * each from its checkpoint row when usable — the one read recipe (cached
278
+ * state + forward tail replay + `view`) applied without a live `Session`.
279
+ * Call with the stored events at or past `restoreFloor(checkpoint)` (a
280
+ * `SessionHandle.read` slice) and that same floor as
281
+ * `baseSeq`; the floor's one-below anchor makes the supplied end honest,
282
+ * so a shrunk log is detected here. A row is usable iff its
283
+ * `ver` matches the live unit's `stateVersion`, it does not predate `baseSeq`
284
+ * (`seq >= baseSeq - 1`), and it does not claim events past the
285
+ * supplied end (`seq <= endSeq`); an unusable row is discarded
286
+ * and its key refolds from `init` — which is only sound over the full
287
+ * log, so a discarded row with `baseSeq > 0` throws (the caller re-reads
288
+ * from seq 0, e.g. after a crash-repair truncation shrank the log below
289
+ * a row's watermark).
290
+ * @param checkpoint - persisted rows for one session (possibly stale or empty).
291
+ * @param events - the stored events with `seq >= baseSeq`, in seq order.
292
+ * @param baseSeq - the seq `events` starts at (its first event's seq when non-empty).
293
+ * @param header - immutable metadata for the Session being restored.
294
+ * @param inheritedEventCount - exact fork-inherited prefix length supplied to unit initialization.
295
+ * @returns the snapshot cut at the supplied log end (`asOfSeq` is the last
296
+ * supplied event's seq, `baseSeq - 1` for an empty tail) plus the
297
+ * refreshed checkpoint rows at that cut, ready for a durable write-back.
298
+ */
299
+ restore(checkpoint, events, baseSeq, header, inheritedEventCount) {
300
+ const endSeq = events.at(-1)?.seq ?? cursorBefore(baseSeq);
301
+ const beforeBase = cursorBefore(baseSeq);
302
+ const values = {};
303
+ const refreshed = {};
304
+ for (const registration of this.registrations.values()) {
305
+ const def = registration.def;
306
+ const row = checkpoint[def.key];
307
+ const usable = row !== undefined
308
+ && row.ver === def.stateVersion
309
+ && row.seq >= beforeBase
310
+ && row.seq <= endSeq;
311
+ if (!usable && baseSeq > 0) {
312
+ throw new Error(`session projection ${JSON.stringify(def.key)} cannot restore from seq ${baseSeq}: `
313
+ + 'its checkpoint row is missing, version-mismatched, or beyond the supplied log end; re-read from seq 0');
314
+ }
315
+ let state = usable
316
+ ? def.stateSchema.parse(row.val)
317
+ : def.init(header, inheritedEventCount);
318
+ const from = usable ? row.seq : beforeBase;
319
+ const startIndex = from - baseSeq + 1;
320
+ for (let index = startIndex; index < events.length; index++) {
321
+ const event = events[index];
322
+ const expectedSeq = SessionSeq(baseSeq + index);
323
+ if (event === undefined || event.seq !== expectedSeq) {
324
+ throw new Error(`session projection ${JSON.stringify(def.key)} cannot restore across missing seq ${String(expectedSeq)}`);
325
+ }
326
+ state = def.apply(state, event);
327
+ }
328
+ if (def.wire !== undefined)
329
+ values[def.key] = def.wire.viewSchema.parse(def.wire.view(state));
330
+ refreshed[def.key] = { ver: def.stateVersion, seq: endSeq, val: state };
331
+ }
332
+ return {
333
+ snapshot: { asOfSeq: endSeq, values: values },
334
+ checkpoint: refreshed,
335
+ };
336
+ }
337
+ /**
338
+ * Restore an exact cut and install its states on the supplied prepared Session.
339
+ * A later publication reuses these cells; ordinary live reads and event drive
340
+ * advance any constructor-owned suffix exactly once.
341
+ * @param session - exact prepared Session that owns the restored log prefix.
342
+ * @param checkpoint - persisted rows for this Session lifecycle.
343
+ * @param events - exact events at the observation cut.
344
+ * @param baseSeq - first supplied event sequence.
345
+ * @returns all projection values at the supplied cut.
346
+ */
347
+ hydrate(session, checkpoint, events, baseSeq) {
348
+ const endSeq = events.at(-1)?.seq ?? cursorBefore(baseSeq);
349
+ let complete = true;
350
+ for (const registration of this.registrations.values()) {
351
+ const current = registration.cells.get(session);
352
+ if (current?.observedSeq !== endSeq) {
353
+ complete = false;
354
+ break;
355
+ }
356
+ }
357
+ if (complete) {
358
+ const values = {};
359
+ for (const registration of this.registrations.values()) {
360
+ if (registration.def.wire === undefined)
361
+ continue;
362
+ const current = registration.cells.get(session);
363
+ values[registration.def.key] = this.viewCell(registration, current);
364
+ }
365
+ return { asOfSeq: endSeq, values };
366
+ }
367
+ const restored = this.restore(checkpoint, events, baseSeq, session.header, session.inheritedEventCount);
368
+ for (const registration of this.registrations.values()) {
369
+ const row = restored.checkpoint[registration.def.key];
370
+ if (row === undefined)
371
+ continue;
372
+ const current = registration.cells.get(session);
373
+ if (current !== undefined && current.observedSeq > row.seq)
374
+ continue;
375
+ registration.cells.set(session, {
376
+ state: row.val,
377
+ observedSeq: row.seq,
378
+ views: [undefined, undefined],
379
+ });
380
+ }
381
+ return restored.snapshot;
382
+ }
383
+ /** Materialize every registered unit cell at the Session's current cursor. */
384
+ materializeCells(session) {
385
+ for (const registration of this.registrations.values())
386
+ this.cellFor(registration, session);
387
+ }
388
+ /** Fold one unit from init over `events`, producing a cell watermarked at the last folded event. */
389
+ buildCell(def, header, inheritedEventCount, events) {
390
+ let state = def.init(header, inheritedEventCount);
391
+ for (const event of events)
392
+ state = def.apply(state, event);
393
+ return { state, observedSeq: (events.at(-1)?.seq ?? -1), views: [undefined, undefined] };
394
+ }
395
+ /** Read (or lazily build, folding the full in-memory log) one unit's cell. */
396
+ cellFor(registration, session) {
397
+ let cell = registration.cells.get(session);
398
+ if (cell === undefined) {
399
+ cell = this.buildCell(registration.def, session.header, session.inheritedEventCount,
400
+ // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
401
+ session.snapshotEvents());
402
+ registration.cells.set(session, cell);
403
+ }
404
+ else {
405
+ this.advanceCell(registration.def, cell, session, cursorBefore(session.seq));
406
+ }
407
+ return cell;
408
+ }
409
+ /** Advance one existing cell through a contiguous Session prefix. */
410
+ advanceCell(def, cell, session, throughSeq) {
411
+ if (cell.observedSeq >= throughSeq)
412
+ return;
413
+ for (let seq = cell.observedSeq + 1; seq <= throughSeq; seq++) {
414
+ // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
415
+ const event = session.eventAt(SessionSeq(seq));
416
+ if (event === undefined || event.seq !== seq) {
417
+ throw new Error(`session projection ${JSON.stringify(def.key)} cannot advance across missing seq ${String(seq)}`);
418
+ }
419
+ const next = def.apply(cell.state, event);
420
+ if (!Object.is(next, cell.state)) {
421
+ cell.views[0] = cell.views[1];
422
+ cell.views[1] = undefined;
423
+ }
424
+ cell.state = next;
425
+ cell.observedSeq = SessionSeq(seq);
426
+ }
427
+ }
428
+ /** Eager drive: pass one committed event through every unit; notify on changed raw view references. */
429
+ drive(session, event) {
430
+ for (const registration of this.registrations.values()) {
431
+ let cell = registration.cells.get(session);
432
+ if (cell !== undefined && cell.observedSeq >= event.seq)
433
+ continue;
434
+ if (cell === undefined) {
435
+ // Late build mid-stream: fold history before this event (seq = log
436
+ // index, so the prefix slice is exact), then take the normal gate.
437
+ cell = this.buildCell(registration.def, session.header, session.inheritedEventCount,
438
+ // oxlint-disable-next-line typescript/no-deprecated -- Existing Session history read; migration deferred.
439
+ session.snapshotEvents(SessionLogOffset(0), SessionLogOffset(event.seq)));
440
+ registration.cells.set(session, cell);
441
+ }
442
+ else {
443
+ this.advanceCell(registration.def, cell, session, event.seq === 0 ? -1 : SessionSeq(event.seq - 1));
444
+ }
445
+ const previousState = cell.state;
446
+ const next = registration.def.apply(previousState, event);
447
+ const changed = !Object.is(next, previousState);
448
+ cell.state = next;
449
+ cell.observedSeq = event.seq;
450
+ const wire = registration.def.wire;
451
+ if (changed && wire !== undefined) {
452
+ const views = cell.views;
453
+ views[0] = views[1];
454
+ if (this.listeners.size > 0) {
455
+ views[1] = wire.view(next);
456
+ if (!Object.is(views[0], views[1])) {
457
+ const value = wire.viewSchema.parse(views[1]);
458
+ for (const listener of this.listeners) {
459
+ listener(session, registration.def.key, value, event.seq);
460
+ }
461
+ }
462
+ }
463
+ else {
464
+ views[1] = undefined;
465
+ }
466
+ }
467
+ // An unchanged state keeps its current view as the valid comparison
468
+ // value for the next state change.
469
+ }
470
+ }
471
+ /** Return one schema-validated wire value. */
472
+ viewCell(registration, cell) {
473
+ const wire = registration.def.wire;
474
+ if (wire === undefined)
475
+ throw new Error(`session projection ${JSON.stringify(registration.def.key)} has no wire view`);
476
+ return wire.viewSchema.parse(wire.view(cell.state));
477
+ }
478
+ }
479
+ export default SessionProjectionRegistry;
480
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Pure-type outlet of the session-projection Service Definition: the one projection type
3
+ * table, importable from client aggregates without dragging the host-side
4
+ * cordis Context merges of the package root (qilin-agent → qilin-session). Domain
5
+ * packages may declare-merge through either the package root or this outlet —
6
+ * re-export preserves symbol identity, so both land on the same table.
7
+ *
8
+ * @module @qilin-agent/session-projection/types
9
+ */
10
+ /**
11
+ * The merge-extensible client projection table shared by wire blocks, client
12
+ * cells, and React hooks. Domain packages merge their client-visible key here;
13
+ * values are wire-JSON whole values. How a value is rendered is the slot
14
+ * system's business, never this layer's.
15
+ */
16
+ export interface SessionProjectionMap {
17
+ }
18
+ /**
19
+ * The merge-extensible host fold-state table. Each client-visible key also
20
+ * appears in {@link SessionProjectionMap}; host-only keys appear only here.
21
+ * Values must be plain JSON so the projection cache can persist them.
22
+ */
23
+ export interface SessionProjectionStateMap {
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Pure-type outlet of the session-projection Service Definition: the one projection type
3
+ * table, importable from client aggregates without dragging the host-side
4
+ * cordis Context merges of the package root (qilin-agent → qilin-session). Domain
5
+ * packages may declare-merge through either the package root or this outlet —
6
+ * re-export preserves symbol identity, so both land on the same table.
7
+ *
8
+ * @module @qilin-agent/session-projection/types
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "@qilin-agent/session-projection",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Session-projection seam: the merge-extensible projection type table, the provider contract, and the ctx.sessionProjections registry serving whole current values of log-derived per-session state",
4
+ "version": "3.1.3",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/kkutysllb/QiLin.git",
11
+ "directory": "packages/session/session-projection"
12
+ },
13
+ "engines": {
14
+ "node": "^22.19.0 || >=24.0.0"
15
+ },
16
+ "type": "module",
17
+ "main": "lib/index.js",
18
+ "types": "lib/types/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./lib/types/index.d.ts",
22
+ "default": "./lib/index.js"
23
+ },
24
+ "./types": {
25
+ "types": "./lib/types/types.d.ts",
26
+ "default": "./lib/types/types.js"
27
+ },
28
+ "./src/*": "./src/*",
29
+ "./package.json": "./package.json"
30
+ },
31
+ "files": [
32
+ "lib/index.js",
33
+ "lib/types/**/*.js",
34
+ "lib/types/**/*.d.ts"
35
+ ],
36
+ "license": "MIT",
37
+ "dependencies": {
38
+ "zod": "^4.4.3"
39
+ },
40
+ "peerDependencies": {
41
+ "@qilin-agent/session": "3.1.3",
42
+ "@qilin-agent/kylin": "~4.0.3"
43
+ },
44
+ "devDependencies": {
45
+ "@qilin-agent/session": "3.1.3",
46
+ "@qilin-agent/kylin": "~4.0.3"
47
+ }
6
48
  }