@deepseek-ai/dsh-session 0.2.2-alpha.3 → 0.2.2-rc.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.
Files changed (41) hide show
  1. package/README.i18n.yaml +4 -4
  2. package/README.md +3 -1
  3. package/README.zh.md +3 -1
  4. package/lib/index.js +151 -79
  5. package/lib/types/fork.d.ts.map +1 -0
  6. package/lib/types/fork.js.map +1 -0
  7. package/lib/types/index.d.ts +17 -1
  8. package/lib/types/index.d.ts.map +1 -0
  9. package/lib/types/index.js +82 -63
  10. package/lib/types/index.js.map +1 -0
  11. package/lib/types/known-event-types.d.ts.map +1 -0
  12. package/lib/types/known-event-types.js +1 -0
  13. package/lib/types/known-event-types.js.map +1 -0
  14. package/lib/types/preparation.d.ts.map +1 -0
  15. package/lib/types/preparation.js.map +1 -0
  16. package/lib/types/repair.d.ts.map +1 -0
  17. package/lib/types/repair.js.map +1 -0
  18. package/lib/types/request-header.d.ts.map +1 -0
  19. package/lib/types/request-header.js.map +1 -0
  20. package/lib/types/seq-ranges.d.ts.map +1 -0
  21. package/lib/types/seq-ranges.js.map +1 -0
  22. package/lib/types/surface.d.ts +6 -0
  23. package/lib/types/surface.d.ts.map +1 -0
  24. package/lib/types/surface.js +57 -11
  25. package/lib/types/surface.js.map +1 -0
  26. package/lib/types/tool-history.d.ts.map +1 -0
  27. package/lib/types/tool-history.js.map +1 -0
  28. package/lib/types/types.d.ts +4 -0
  29. package/lib/types/types.d.ts.map +1 -0
  30. package/lib/types/types.js.map +1 -0
  31. package/package.json +14 -11
  32. package/src/fork.ts +30 -0
  33. package/src/index.ts +1555 -0
  34. package/src/known-event-types.ts +91 -0
  35. package/src/preparation.ts +49 -0
  36. package/src/repair.ts +211 -0
  37. package/src/request-header.ts +69 -0
  38. package/src/seq-ranges.ts +73 -0
  39. package/src/surface.ts +758 -0
  40. package/src/tool-history.ts +75 -0
  41. package/src/types.ts +574 -0
package/README.i18n.yaml CHANGED
@@ -27,11 +27,11 @@
27
27
  en: c4bf3103b9762b6c
28
28
  zh: 369c734bb20de2ea
29
29
  /deepseek-ai-dsh-session/use-this-package/write-experimental-plugin-records:
30
- en: 6c758f98535b734c
31
- zh: ac54342937477f37
30
+ en: 002b8240caa0c57c
31
+ zh: e0954dd62913a944
32
32
  /deepseek-ai-dsh-session/use-this-package/fork-a-session:
33
- en: 1c40d17a786059d3
34
- zh: 81320ce118977c65
33
+ en: 4fcc9eb5c0278b51
34
+ zh: 2a26e81ebffbd9a6
35
35
  /deepseek-ai-dsh-session/use-this-package/flush-durable-state:
36
36
  en: 70a7673145c851ad
37
37
  zh: 0dd1667d9518993d
package/README.md CHANGED
@@ -71,7 +71,7 @@ Session log positions use two numeric types. `SessionSeq` identifies an existing
71
71
 
72
72
  `appendPluginRecord(session, type, data)` appends one plugin record for a package under `packages/experimental/`; the `verify-plugin-record-callers` check rejects any other production caller in this repository. Plugins outside this repository must not call it either, although no repository check can inspect them.
73
73
 
74
- Declare each record in `PluginRecordMap` through `@deepseek-ai/dsh-session/types`, with a description and an explicit payload type annotation in the owning experimental package's `src/`. Names use `plugin:<owner>/<record>`, where `owner` is the suffix of the package name `@deepseek-ai/dsh-experimental-<owner>`; for example, `plugin:pi-extensions/entry`. TypeScript checks each name and payload against its declaration; the writer's runtime JSON snapshot rejects values that cannot be preserved losslessly. Put extension-defined or other dynamic names inside the payload of a declared record.
74
+ Declare each record in `PluginRecordMap` through `@deepseek-ai/dsh-session/types`, with a description and an explicit payload type annotation in the owning experimental package's `src/`. Names use `plugin:<owner>/<record>`, where `owner` is the package name after removing the legacy `@deepseek-ai/dsh-experimental-` prefix when present, otherwise `@deepseek-ai/dsh-`; for example, `plugin:pi-extensions/entry`. TypeScript checks each name and payload against its declaration; the writer's runtime JSON snapshot rejects values that cannot be preserved losslessly. Put extension-defined or other dynamic names inside the payload of a declared record.
75
75
 
76
76
  The [experimental persistence catalog](../../../docs/experimental-persistence-catalog.md) lists current plugin-record declarations with their owners, descriptions, payload type annotations, and source files. These declarations remain separate from `SessionEventMap`, expanded persistence schemas, released type history, and `KNOWN_SESSION_EVENT_TYPES`. Records carry `ignorable: true`: a build that does not recognize one retains and skips it on read. Records never enter the model-visible surface. Resume and fork carry them with the rest of the log; Session format migration keeps them on a best-effort basis.
77
77
 
@@ -83,6 +83,8 @@ The [experimental persistence catalog](../../../docs/experimental-persistence-ca
83
83
 
84
84
  The logical `SessionHeader.isSeeded` field reports whether fork history exists without exposing a positional integer. `Session.inheritedEventCount` retains the exact checked `SessionLogOffset`; `ownEvents()` returns events at and after that cut, and `isOwnSeq(seq)` accepts only an existing child-owned position. A low-level seeded constructor must supply an explicit `seed` and `inheritedEventCount` because the constructor seed can contain child-owned setup events after the inherited prefix.
85
85
 
86
+ `session.append(type, data, ...opts)` appends one event and returns that event. `session.append(inputs)` accepts a readonly array of positional `[type, data, ...opts]` tuples (`SessionEventInput`) and returns the committed events in input order; an empty array appends nothing. Array admission validates every candidate and synchronous dispatch before changing the log or message surface, so observers read the complete committed group. Persistence listeners retain their own durability rules; array admission alone does not guarantee crash-atomic storage.
87
+
86
88
  ### Flush durable state
87
89
 
88
90
  `ctx.sessions.flush(session)` dispatches the awaited durability checkpoint: every persistence listener flushes and the call settles after all of them. A producer that needs an immediate durability barrier awaits it instead of assuming the write-behind drained.
package/README.zh.md CHANGED
@@ -71,7 +71,7 @@ session.deriveMessages() // the derived model history
71
71
 
72
72
  `appendPluginRecord(session, type, data)` 为 `packages/experimental/` 下的包追加一条插件记录;`verify-plugin-record-callers` 检查拒绝本仓库中任何其他生产调用方。本仓库之外的插件同样不得调用它,尽管仓库检查无法检查它们。
73
73
 
74
- 在所属实验性包的 `src/` 中,通过 `@deepseek-ai/dsh-session/types` 的 `PluginRecordMap` 为每条记录声明描述与显式 payload 类型注解。名称使用 `plugin:<owner>/<record>`,其中 `owner` 是包名 `@deepseek-ai/dsh-experimental-<owner>` 的后缀;例如 `plugin:pi-extensions/entry`。TypeScript 按声明检查每个名称与 payload;写入方运行时的 JSON 快照拒绝无法无损保留的值。扩展自定义名称或其他动态名称放在已声明记录的 payload 中。
74
+ 在所属实验性包的 `src/` 中,通过 `@deepseek-ai/dsh-session/types` 的 `PluginRecordMap` 为每条记录声明描述与显式 payload 类型注解。名称使用 `plugin:<owner>/<record>`,其中 `owner` 是包名去掉前缀后的部分:带旧前缀时去掉 `@deepseek-ai/dsh-experimental-`,否则去掉 `@deepseek-ai/dsh-`;例如 `plugin:pi-extensions/entry`。TypeScript 按声明检查每个名称与 payload;写入方运行时的 JSON 快照拒绝无法无损保留的值。扩展自定义名称或其他动态名称放在已声明记录的 payload 中。
75
75
 
76
76
  [实验性持久化目录](../../../docs/experimental-persistence-catalog.zh.md)列出当前插件记录声明及其所属包、描述、payload 类型注解与源文件。这些声明与 `SessionEventMap`、展开的持久化 schema、已发布类型历史及 `KNOWN_SESSION_EVENT_TYPES` 分开。记录带有 `ignorable: true`:不认识某条记录的构建在读取时保留并跳过它。记录从不进入模型可见的 surface。resume 与 fork 随日志其余部分一起携带它们;Session 格式迁移以尽力而为的方式保留它们。
77
77
 
@@ -83,6 +83,8 @@ session.deriveMessages() // the derived model history
83
83
 
84
84
  逻辑 `SessionHeader.isSeeded` 字段报告是否存在 fork 历史,而不公开位置整数。`Session.inheritedEventCount` 保留经过校验的精确 `SessionLogOffset`;`ownEvents()` 返回从该切点开始的事件,`isOwnSeq(seq)` 只接受已存在且由子会话拥有的位置。底层带 seed 构造必须显式提供 `seed` 与 `inheritedEventCount`,因为构造 seed 可以在继承前缀之后包含子会话自有的设置事件。
85
85
 
86
+ `session.append(type, data, ...opts)` 追加一个事件并返回该事件。`session.append(inputs)` 接受位置参数元组 `[type, data, ...opts]`(`SessionEventInput`)的只读数组,并按输入顺序返回已提交事件;空数组不追加任何内容。数组准入会在改变日志或surface前验证所有候选和同步分发,因此观察者读取完整的已提交组。持久化监听器保留各自的持久性规则;仅数组准入不保证进程崩溃时整组存储的原子性。
87
+
86
88
  ### 刷新持久状态
87
89
 
88
90
  `ctx.sessions.flush(session)` 分发需等待完成的持久性检查点:每个持久化监听器都会刷新,调用在所有监听器结算后完成。需要立即持久性屏障的生产方应等待它,而不是假定延后写入已经排空。
package/lib/index.js CHANGED
@@ -123,6 +123,7 @@ const KNOWN_SESSION_EVENT_TYPES = new Set([
123
123
  "team/member",
124
124
  "team/message/delivered",
125
125
  "team/message/queued",
126
+ "team/mode",
126
127
  "team/task",
127
128
  "todo/write",
128
129
  "tool-workflow/agent-end",
@@ -331,12 +332,12 @@ function assertSourceEventReferences(event, shadowedSeqs) {
331
332
  if (missing.length > 0) throw new Error(`surface replace: sourceEventSeqs must include every shadowed surface node; missing ${missing.join(", ")}`);
332
333
  }
333
334
  /** Resolve tool additions against their immutable historical request header. */
334
- function assertDeveloperHeader(event, events, baseSeq) {
335
+ function assertDeveloperHeader(event, read) {
335
336
  if (event.type !== "developer/message") return;
336
337
  validateSessionEventData(event, `developer/message at seq ${event.seq}`);
337
338
  if (event.data.headerSeq === void 0) return;
338
339
  const headerSeq = event.data.headerSeq;
339
- const headerEvent = events[headerSeq - baseSeq];
340
+ const headerEvent = read(headerSeq);
340
341
  if (headerSeq >= event.seq || headerEvent?.type !== "request/header") throw new Error("developer/message headerSeq must reference an earlier request/header");
341
342
  for (const block of event.data.message.content) {
342
343
  if (block.type !== "tool-addition") continue;
@@ -391,11 +392,11 @@ function isDeepEqualJson(a, b) {
391
392
  return aKeys.every((key) => Object.hasOwn(b, key) && isDeepEqualJson(a[key], bRecord[key]));
392
393
  }
393
394
  /** Restrict a tool-result replacement to one current result's content. */
394
- function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
395
+ function assertToolResultRewrite(event, shadowedSeqs, read) {
395
396
  if (event.type !== "tool/result") return;
396
397
  if (shadowedSeqs.length !== 1) throw new Error("tool/result surface replacement must rewrite exactly one current node");
397
398
  for (const originalSeq of shadowedSeqs) {
398
- const original = events[originalSeq - baseSeq];
399
+ const original = read(originalSeq);
399
400
  if (original?.type !== "tool/result") throw new Error("tool/result surface replacement must target a current tool/result");
400
401
  const originalRest = { ...original.data };
401
402
  const replacementRest = { ...event.data };
@@ -416,27 +417,36 @@ function assertToolResultRewrite(event, shadowedSeqs, events, baseSeq) {
416
417
  * exactly that node; later system nodes carry no protection and a compaction
417
418
  * range may shadow them.
418
419
  */
419
- function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, events, baseSeq) {
420
+ function assertSystemHeadRewrite(event, state, startIdx, shadowedSeqs, read) {
420
421
  if (startIdx !== 0) return;
421
- if (events[state.nodes[0] - baseSeq]?.type !== "system/message") return;
422
+ if (read(state.nodes[0])?.type !== "system/message") return;
422
423
  if (event.type !== "system/message" || shadowedSeqs.length !== 1) throw new Error("surface replace: node 0 holds the system prompt and may be rewritten only by a system/message over exactly that node");
423
424
  }
424
425
  /** Validate one event at its replay boundary and prepare its atomic fold transition. */
425
- function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projections) {
426
+ function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projections, candidates) {
426
427
  if (event.seq !== expectedSeq) throw new Error(`session event seq ${event.seq} is not contiguous; expected ${expectedSeq}`);
427
428
  const surfaceOp = validateSurfaceMetadata(event);
428
- assertDeveloperHeader(event, events, baseSeq);
429
- const projection = projections.find((item) => item.type === event.type);
430
- if (projection !== void 0) return {
431
- kind: "project",
432
- projection,
433
- messages: projection.project(event, {
434
- nodes: state.nodes,
435
- events,
436
- baseSeq,
437
- messages: state.projectedMessages
438
- })
429
+ const read = (seq) => {
430
+ const index = seq - baseSeq;
431
+ return index < events.length ? events[index] : candidates?.[index - events.length];
439
432
  };
433
+ assertDeveloperHeader(event, read);
434
+ const projection = projections.find((item) => item.type === event.type);
435
+ if (projection !== void 0) {
436
+ let projectionEvents;
437
+ return {
438
+ kind: "project",
439
+ projection,
440
+ messages: projection.project(event, {
441
+ nodes: state.nodes,
442
+ get events() {
443
+ return candidates === void 0 ? events : projectionEvents ??= [...events, ...candidates];
444
+ },
445
+ baseSeq,
446
+ messages: state.projectedMessages
447
+ })
448
+ };
449
+ }
440
450
  if (MESSAGE_PROJECTION_EVENT_TYPES.has(event.type)) throw new Error(`session event "${event.type}" requires a message projection; load its owning plugin or supply its projection definition`);
441
451
  if (surfaceOp === void 0) return;
442
452
  if (surfaceOp === "append") return {
@@ -445,8 +455,8 @@ function planSurfaceEvent(state, event, expectedSeq, events, baseSeq, projection
445
455
  };
446
456
  const range = replacementRange(state, surfaceOp);
447
457
  assertSourceEventReferences(event, range.shadowedSeqs);
448
- assertToolResultRewrite(event, range.shadowedSeqs, events, baseSeq);
449
- assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, events, baseSeq);
458
+ assertToolResultRewrite(event, range.shadowedSeqs, read);
459
+ assertSystemHeadRewrite(event, state, range.startIdx, range.shadowedSeqs, read);
450
460
  return {
451
461
  kind: "replace",
452
462
  seq: event.seq,
@@ -535,6 +545,40 @@ var SurfaceManager = class {
535
545
  plan: planSurfaceEvent(this._state, event, expectedSeq, this.log, this.baseSeq, this.projections)
536
546
  };
537
547
  }
548
+ /**
549
+ * Validate consecutive candidates against an isolated surface fold.
550
+ * @param events - candidates immediately following the committed log.
551
+ * @returns a callback adopting the validated fold after all candidates enter the log.
552
+ */
553
+ prepareBatch(events) {
554
+ this._assertProjections();
555
+ if (this._lastProcessedSeq < this.baseSeq + this.log.length - 1) this._processDelta();
556
+ const lastProcessedSeq = SessionSeq(this.baseSeq + this.log.length + events.length - 1);
557
+ if (events.every((event) => (event.surfaceOp === void 0 || event.surfaceOp === "append") && !this.projections.some((projection) => projection.type === event.type))) {
558
+ const plans = events.map((event, index) => planSurfaceEvent(this._state, event, SessionSeq(this.baseSeq + this.log.length + index), this.log, this.baseSeq, this.projections, events));
559
+ return () => {
560
+ for (const plan of plans) applySurfacePlan(this._state, plan);
561
+ this._lastProcessedSeq = lastProcessedSeq;
562
+ this._pendingPlan = void 0;
563
+ };
564
+ }
565
+ const state = {
566
+ ...this._state,
567
+ nodes: [...this._state.nodes],
568
+ projectedMessages: new Map(this._state.projectedMessages),
569
+ projections: new Set(this._state.projections)
570
+ };
571
+ const plans = events.map((event, index) => {
572
+ const plan = planSurfaceEvent(state, event, SessionSeq(this.baseSeq + this.log.length + index), this.log, this.baseSeq, this.projections, events);
573
+ applySurfacePlan(state, plan);
574
+ return plan;
575
+ });
576
+ return () => {
577
+ for (const plan of plans) applySurfacePlan(this._state, plan);
578
+ this._lastProcessedSeq = lastProcessedSeq;
579
+ this._pendingPlan = void 0;
580
+ };
581
+ }
538
582
  /** Monotonic count of folded positional replacements. */
539
583
  get replaceGeneration() {
540
584
  this._assertProjections();
@@ -1315,6 +1359,36 @@ function pluginRecordOf(event) {
1315
1359
  data: event.data
1316
1360
  };
1317
1361
  }
1362
+ /** Copy live append inputs before sequence assignment. */
1363
+ function snapshotAppend(type, data, opts, subject) {
1364
+ const surfaceOp = opts?.surfaceOp;
1365
+ const sourceEventSeqs = opts?.sourceEventSeqs;
1366
+ const dataSnapshot = snapshotJsonValue(data);
1367
+ if (dataSnapshot === void 0) throw new Error(`${subject} carries non-JSON-serializable data`);
1368
+ const surface = snapshotJsonValue({
1369
+ ...sourceEventSeqs === void 0 ? {} : { sourceEventSeqs },
1370
+ ...surfaceOp === void 0 ? {} : { surfaceOp }
1371
+ });
1372
+ if (surface === void 0) throw new Error(`${subject} carries non-JSON-serializable surface metadata`);
1373
+ return {
1374
+ type,
1375
+ data: dataSnapshot,
1376
+ surface
1377
+ };
1378
+ }
1379
+ /** Construct and validate one live event from detached inputs. */
1380
+ function createAppendEvent(snapshot, seq) {
1381
+ const { type, data, surface } = snapshot;
1382
+ const event = deepFreeze({
1383
+ type,
1384
+ seq,
1385
+ time: Date.now(),
1386
+ data,
1387
+ ...surface
1388
+ });
1389
+ validateSessionEventData(event, `session event "${type}" at seq ${seq}`);
1390
+ return event;
1391
+ }
1318
1392
  /**
1319
1393
  * An event-sourced session: an append-only log of {@link SessionEvent}s.
1320
1394
  *
@@ -1325,6 +1399,7 @@ function pluginRecordOf(event) {
1325
1399
  */
1326
1400
  var Session = class Session {
1327
1401
  log = [];
1402
+ batchPublication;
1328
1403
  /** Single incremental owner of surface acceptance and projection state. */
1329
1404
  surfaceManager;
1330
1405
  /** The ordered surface over this session's event log. */
@@ -1479,71 +1554,68 @@ var Session = class Session {
1479
1554
  get seq() {
1480
1555
  return SessionLogOffset(this.log.length);
1481
1556
  }
1482
- /**
1483
- * Append one typed event to the log and synchronously notify observers via
1484
- * the store-owned, module-private publication hooks. The hot path never blocks
1485
- * on I/O — persistence plugins buffer asynchronously. Once the event enters
1486
- * the log, the append is committed: observer failures are logged and
1487
- * contained per listener, so they do not change the return value or prevent
1488
- * later listeners from observing the same accepted event.
1489
- * Independent Sessions append without notifying store observers. Once a store
1490
- * prepares or enters the instance, writes require a completed creation
1491
- * announcement and remain disabled after removal.
1492
- *
1493
- * @param type - The event type (key of {@link SessionEventMap}).
1494
- * @param data - The event payload; must be JSON-serializable.
1495
- * @param opts - Surface metadata: `surfaceOp` controls how the event enters
1496
- * the ordered surface; `sourceEventSeqs` lists the seq numbers of earlier
1497
- * events this one derives from. REQUIRED for
1498
- * {@link SurfaceEventType} events (every message-producing event must
1499
- * declare how it joins the surface, the sole source of derived model
1500
- * history) and
1501
- * rejected by the compiler for non-surface types like `turn/start` or
1502
- * `assistant/attempt`. Assistant messages embed their exact provider
1503
- * stream and cannot cite top-level source events.
1504
- * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
1505
- * `data` that entered the log, so reading `event.data` back sees the logged
1506
- * value, never the caller's still-mutable input.
1507
- * @throws if a store-owned Session is unentered or its creation announcement
1508
- * has not completed, or if `data` or surface
1509
- * metadata is not losslessly JSON-serializable
1510
- * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1511
- * circular reference, sparse array, or an exotic object such as
1512
- * Map/Set/Date/class instance), or when the candidate violates the
1513
- * request-header empty-field or tool-error consistency rules, or the
1514
- * canonical surface contract (marker shape and eligibility, unique
1515
- * earlier source-event references, positional replacement validity, and complete
1516
- * shadowed-node coverage). One iterative pass reads, validates, and
1517
- * copies each nested value once, so a stateful getter cannot supply one value
1518
- * to validation and another to storage. The event log is the durable source
1519
- * of truth, so a bad event fails at the append site rather than later during
1520
- * a backend flush. A synchronous internal dispatch validation failure or an
1521
- * append reentered while this acceptance/publication boundary is open also
1522
- * rejects before the log changes.
1523
- */
1557
+ /** Current event while synchronously notifying a batch's observers. */
1558
+ get currentBatchPublication() {
1559
+ return this.batchPublication;
1560
+ }
1524
1561
  append(type, data, ...opts) {
1562
+ if (typeof type !== "string") return this.#appendEvents(type);
1525
1563
  this.publicationEntry();
1526
- const surfaceOpts = opts[0];
1527
- const surfaceMetadata = {
1528
- ...surfaceOpts?.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
1529
- ...surfaceOpts?.surfaceOp === void 0 ? {} : { surfaceOp: surfaceOpts.surfaceOp }
1530
- };
1531
- const dataSnapshot = snapshotJsonValue(data);
1532
- if (dataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable data`);
1533
- const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
1534
- if (surfaceMetadataSnapshot === void 0) throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
1564
+ const snapshot = snapshotAppend(type, data, opts[0], `session event "${type}"`);
1535
1565
  const entry = this.publicationEntry();
1536
- const event = deepFreeze({
1537
- type,
1538
- seq: SessionSeq(this.log.length),
1539
- time: Date.now(),
1540
- data: dataSnapshot,
1541
- ...surfaceMetadataSnapshot
1542
- });
1543
- validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
1566
+ const event = createAppendEvent(snapshot, SessionSeq(this.log.length));
1544
1567
  this.commit(event, entry);
1545
1568
  return event;
1546
1569
  }
1570
+ /**
1571
+ * Validate and publish consecutive events together. Observers see the complete log.
1572
+ * @param inputs - event payloads and surface intents in commit order.
1573
+ * @returns immutable committed events in the same order.
1574
+ * @throws before changing the log if any candidate or synchronous dispatch validation fails.
1575
+ */
1576
+ #appendEvents(inputs) {
1577
+ this.publicationEntry();
1578
+ if (inputs.length === 0) return [];
1579
+ const snapshots = Array.from({ length: inputs.length }, (_, index) => {
1580
+ const input = inputs.at(index);
1581
+ if (input === void 0) throw new Error(`session batch has an empty member at index ${index}`);
1582
+ const [type, data, surfaceOpts] = input;
1583
+ return snapshotAppend(type, data, surfaceOpts, `session batch member ${index} ("${type}")`);
1584
+ });
1585
+ const entry = this.publicationEntry();
1586
+ const startSeq = this.log.length;
1587
+ const events = snapshots.map((snapshot, index) => createAppendEvent(snapshot, SessionSeq(startSeq + index)));
1588
+ const acceptSurface = this.surfaceManager.prepareBatch(events);
1589
+ if (entry !== void 0) entry.appending = true;
1590
+ try {
1591
+ const publications = entry === void 0 ? [] : events.map((event) => {
1592
+ const args = [this, event];
1593
+ return {
1594
+ event,
1595
+ args,
1596
+ callbacks: collectSessionCallbacks(entry.emitCtx, [
1597
+ entry.carrier,
1598
+ "session/event",
1599
+ ...args
1600
+ ])
1601
+ };
1602
+ });
1603
+ for (const event of events) this.log.push(event);
1604
+ this.eventsSnapshot = void 0;
1605
+ acceptSurface();
1606
+ if (entry !== void 0) for (const { event, args, callbacks } of publications) {
1607
+ this.batchPublication = event;
1608
+ invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, args, callbacks);
1609
+ }
1610
+ } finally {
1611
+ this.batchPublication = void 0;
1612
+ if (entry !== void 0) {
1613
+ entry.appending = false;
1614
+ if (entry.detachRequested) entry.detach();
1615
+ }
1616
+ }
1617
+ return events;
1618
+ }
1547
1619
  static {
1548
1620
  commitPluginRecord = (session, type, data) => session.#appendRecord(type, data);
1549
1621
  }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../../src/fork.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,YAAY,CAAA;AAE5E;;;;;;;;;;GAUG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,YAAY,EAAE,EAAE,QAAQ,EAAE,cAAc,GAAG,YAAY,EAAE,CASvG"}
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fork.js","sourceRoot":"","sources":["../../src/fork.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAGvC;;;;;;;;;;GAUG;AACH,MAAM,UAAU,aAAa,CAAC,MAA+B,EAAE,QAAwB;IACrF,MAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,CAAC,CAAC,CAAA;IAC5C,MAAM,CAAC,IAAI,CAAC;QACV,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,UAAU,CAAC,QAAQ,GAAG,CAAC,CAAC;QACvD,6GAA6G;QAC7G,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAE,CAAC,IAAI;QAC5B,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE;KAC1B,CAAC,CAAA;IACF,OAAO,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAA;AACnE,CAAC"}
@@ -10,7 +10,7 @@ import type { Scoped } from '@deepseek-ai/dsh-scope';
10
10
  import type { Message } from '@deepseek-ai/dsh-llm';
11
11
  import { SessionLogOffset, SessionSeq } from './types.ts';
12
12
  import type { TypertLookup } from '@deepseek-ai/dsh-typert-protocol';
13
- import type { CreateSessionOptions, EpochHeader, PluginRecord, PluginRecordMap, PluginRecordType, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
13
+ import type { CreateSessionOptions, EpochHeader, PluginRecord, PluginRecordMap, PluginRecordType, PrepareSessionOptions, RequestContext, SessionEvent, SessionEventInput, SessionEventMap, SessionEventType, SessionHeader, SessionId, SessionSeedEventState, SurfaceIntent, SurfaceEventType } from './types.ts';
14
14
  import type { SessionSurface, SessionMessageProjection } from './surface.ts';
15
15
  import type { ToolHistory } from '@deepseek-ai/dsh-llm';
16
16
  export { buildForkSeed } from './fork.ts';
@@ -54,6 +54,9 @@ declare module '@deepseek-ai/cordis' {
54
54
  * Post-commit, fire-and-forget append feed. The listener snapshot resolves
55
55
  * before the log push, but callbacks run after it; observer failures are
56
56
  * logged and contained without making the committed append fail.
57
+ * Array append commits every member before publishing them in order;
58
+ * `session.seq` and `session.surface` already include the complete batch.
59
+ * `session.currentBatchPublication` identifies the member being delivered.
57
60
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
58
61
  * receive only events from sessions entered through that agent's context.
59
62
  * @param session - the session whose log grew.
@@ -139,6 +142,7 @@ export declare function pluginRecordOf(event: SessionEvent): PluginRecord | unde
139
142
  export declare class Session {
140
143
  #private;
141
144
  private log;
145
+ private batchPublication;
142
146
  /** Single incremental owner of surface acceptance and projection state. */
143
147
  private readonly surfaceManager;
144
148
  /** The ordered surface over this session's event log. */
@@ -241,6 +245,18 @@ export declare class Session {
241
245
  isOwnSeq(seq: SessionSeq): boolean;
242
246
  /** The next event's sequence number — always the log length (the `seq = log.length` contiguity contract). */
243
247
  get seq(): SessionLogOffset;
248
+ /** Current event while synchronously notifying a batch's observers. */
249
+ get currentBatchPublication(): SessionEvent | undefined;
250
+ /**
251
+ * Append consecutive events together after validating every candidate and dispatch.
252
+ * Observers read the complete committed log and final message surface.
253
+ * Listener registration changes during publication take effect on the next append.
254
+ * Persistence listeners retain their own durability rules.
255
+ * @param inputs - event payloads and surface intents in commit order. An empty array records no events.
256
+ * @returns immutable committed events in the same order.
257
+ * @throws if the Session cannot accept writes, including an empty append, or any candidate or synchronous dispatch validation fails.
258
+ */
259
+ append(inputs: readonly SessionEventInput[]): readonly SessionEvent[];
244
260
  /**
245
261
  * Append one typed event to the log and synchronously notify observers via
246
262
  * the store-owned, module-private publication hooks. The hot path never blocks
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKtD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,EAA0B,gBAAgB,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AACjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AACpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,cAAc,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,gBAAgB,EAAE,aAAa,EAAE,SAAS,EAAE,qBAAqB,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAEjT,OAAO,KAAK,EAAE,cAAc,EAAE,wBAAwB,EAAE,MAAM,cAAc,CAAA;AAG5E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAIvD,OAAO,EAAE,aAAa,EAAE,MAAM,WAAW,CAAA;AACzC,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACrD,YAAY,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AACjE,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AAC7H,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAC9G,YAAY,EAAE,cAAc,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,wBAAwB,EAAE,+BAA+B,EAAE,MAAM,cAAc,CAAA;AACxJ,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,oBAAoB,EAAE,yBAAyB,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAA;AACtJ,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AACtF,OAAO,EAAE,yBAAyB,EAAE,MAAM,wBAAwB,CAAA;AAElE,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QACf,QAAQ,EAAE,YAAY,CAAA;KACvB;IAED,UAAU,MAAM;QACd;;;;;;;;;;;WAWG;QACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;QAChE;;;;;;;WAOG;QACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;QACjE;;;;;;;;;;;;WAYG;QACH,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,GAAG,IAAI,CAAA;QACnF;;;;;;WAMG;QACH,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;KAC/E;CACF;AAED,OAAO,QAAQ,kCAAkC,CAAC;IAChD,UAAU,eAAe;QACvB,OAAO,EAAE,YAAY,CAAC,OAAO,EAAE,SAAS,CAAC,CAAA;KAC1C;CACF;AAoED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,YAAY,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAsBrE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,YAAY,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC,CAExE;AA6QD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,SAAS,OAAO,CAAC,MAAM,eAAe,EAAE,gBAAgB,CAAC,EAC3F,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,OAAO,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,GAChC,UAAU,CAEZ;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,YAAY,GAAG,YAAY,GAAG,SAAS,CAI5E;AA6BD;;;;;;;GAOG;AACH,qBAAa,OAAO;;IAClB,OAAO,CAAC,GAAG,CAAqB;IAChC,OAAO,CAAC,gBAAgB,CAA0B;IAClD,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgB;IAE/C,yDAAyD;IACzD,IAAI,OAAO,IAAI,cAAc,CAE5B;IAED;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;IAE9B,0EAA0E;IAC1E,QAAQ,CAAC,mBAAmB,EAAE,gBAAgB,CAAA;IAE9C,2EAA2E;IAC3E,IAAI,EAAE,IAAI,SAAS,CAElB;IAED;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,YAAY,EAAE,gBAAgB,CAAA;IAEvC;;;;OAIG;IACH,QAAQ,CAAC,iBAAiB,EAAE,gBAAgB,CAAA;IAE5C;;;;;;;;;;OAUG;IACH,MAAM,CAAC,MAAM,CACX,EAAE,EAAE,SAAS,EACb,IAAI,CAAC,EAAE,SAAS,YAAY,EAAE,EAC9B,MAAM,CAAC,EAAE,aAAa,EACtB,mBAAmB,CAAC,EAAE,gBAAgB,EACtC,WAAW,CAAC,EAAE,SAAS,wBAAwB,EAAE,GAChD,OAAO;IAIV;;;;;;;;;;;;;;OAcG;IACH,MAAM,CAAC,WAAW,CAChB,EAAE,EAAE,SAAS,EACb,IAAI,EAAE,SAAS,YAAY,EAAE,EAC7B,MAAM,EAAE,aAAa,EACrB,mBAAmB,EAAE,gBAAgB,EACrC,UAAU,EAAE,qBAAqB,EACjC,WAAW,CAAC,EAAE,SAAS,wBAAwB,EAAE,GAChD,OAAO;IAWV,OAAO;IA2EP,qEAAqE;IACrE,OAAO,CAAC,cAAc,CAAqC;IAE3D;;;;;;OAMG;IACH,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,YAAY,GAAG,SAAS;IAIlD;;;;;;;;;OASG;IACH,cAAc,CACZ,OAAO,GAAE,gBAAsC,EAC/C,cAAc,GAAE,gBAA2B,GAC1C,SAAS,YAAY,EAAE;IAQ1B;;;;;OAKG;IACH,SAAS,IAAI,SAAS,YAAY,EAAE;IAKpC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO;IAIlC,6GAA6G;IAC7G,IAAI,GAAG,IAAI,gBAAgB,CAE1B;IAED,uEAAuE;IACvE,IAAI,uBAAuB,IAAI,YAAY,GAAG,SAAS,CAEtD;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,MAAM,EAAE,SAAS,iBAAiB,EAAE,GAAG,SAAS,YAAY,EAAE;IAErE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAyCG;IACH,MAAM,CAAC,CAAC,SAAS,gBAAgB,EAC/B,IAAI,EAAE,CAAC,EACP,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC,EACxB,GAAG,IAAI,EAAE,CAAC,SAAS,gBAAgB,GAAG,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC,GAAG,EAAE,GAClE,YAAY,CAAC,CAAC,CAAC;IA2FlB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;;OAKG;IACH,OAAO,CAAC,MAAM;IAsBd,4EAA4E;IAC5E,OAAO,CAAC,UAAU,CAAyB;IAC3C,kEAAkE;IAClE,OAAO,CAAC,aAAa,CAAI;IAEzB;;;;;;;OAOG;IACH,aAAa,IAAI,WAAW,GAAG,SAAS;IAYxC,+CAA+C;IAC/C,OAAO,CAAC,WAAW,CAA4B;IAC/C,OAAO,CAAC,cAAc,CAAI;IAE1B;;;;OAIG;IACH,cAAc,IAAI,cAAc,GAAG,SAAS;IAU5C,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAA8B;IACpE,mFAAmF;IACnF,OAAO,CAAC,cAAc,CAAI;IAE1B;;;;OAIG;IACH,WAAW,IAAI,WAAW;IAM1B,+EAA+E;IAC/E,OAAO,CAAC,OAAO,CAAgB;IAC/B,gEAAgE;IAChE,OAAO,CAAC,YAAY,CAAI;IACxB,0EAA0E;IAC1E,OAAO,CAAC,iBAAiB,CAAI;IAE7B;;;;;;;;;;;;;;;;;OAiBG;IACH,cAAc,IAAI,OAAO,EAAE;IAuB3B;;;;;OAKG;IACH,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,GAAG,IAAI;CAGxD;AAED,0EAA0E;AAC1E,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,SAAS,CAAA;AAEnD;;;;;;GAMG;AACH,MAAM,MAAM,oBAAoB,GAC5B,mBAAmB,GACnB,kBAAkB,GAClB,wBAAwB,GACxB,kBAAkB,CAAA;AAEtB,+CAA+C;AAC/C,qBAAa,gBAAiB,SAAQ,KAAK;aACI,IAAI,EAAE,oBAAoB;gBAA3D,OAAO,EAAE,MAAM,EAAkB,IAAI,EAAE,oBAAoB;CAIxE;AAED;;;;;;GAMG;AACH,qBAAa,YAAa,SAAQ,OAAO;IACvC,OAAO,CAAC,KAAK,CAAqC;IAClD,OAAO,CAAC,OAAO,CAAI;IACnB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAiC;IAE7D,0GAA0G;IAC1G,IAAI,kBAAkB,IAAI,SAAS,wBAAwB,EAAE,CAE5D;IAED;;;;;;OAMG;IACH,yBAAyB,CAAC,UAAU,EAAE,wBAAwB,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC;gBAUxE,GAAG,EAAE,OAAO;IAcxB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO;IAa/D;;;;;;;;;;;;;;;;;;OAkBG;IACH,OAAO,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO;IAkDjE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,IAAI;IAmCnC,oFAAoF;IACpF,OAAO,CAAC,aAAa;IAWrB;;;;;;;;gEAQ4D;IAC5D,QAAQ,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IA+BhC,2EAA2E;IAC3E,OAAO,CAAC,YAAY;IAUpB;;;;;;;;;;;;OAYG;IACG,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB/C,qEAAqE;IACrE,OAAO,CAAC,YAAY;IAQpB;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS;IAIvC;;;OAGG;IACH,IAAI,IAAI,OAAO,EAAE;IAIjB;;;;;;;;;;;;;;;OAeG;IACH,IAAI,CAAC,MAAM,EAAE,iBAAiB,EAAE,QAAQ,CAAC,EAAE,UAAU,EAAE,cAAc,CAAC,EAAE,SAAS,GAAG,OAAO;IAmB3F,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,kBAAkB;CAe3B;AAED,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAClE,eAAe,YAAY,CAAA"}
@@ -397,6 +397,28 @@ export function pluginRecordOf(event) {
397
397
  return undefined;
398
398
  return { type, seq: event.seq, time: event.time, data: event.data };
399
399
  }
400
+ /** Copy live append inputs before sequence assignment. */
401
+ function snapshotAppend(type, data, opts, subject) {
402
+ const surfaceOp = opts?.surfaceOp;
403
+ const sourceEventSeqs = opts?.sourceEventSeqs;
404
+ const dataSnapshot = snapshotJsonValue(data);
405
+ if (dataSnapshot === undefined)
406
+ throw new Error(`${subject} carries non-JSON-serializable data`);
407
+ const surface = snapshotJsonValue({
408
+ ...sourceEventSeqs === undefined ? {} : { sourceEventSeqs },
409
+ ...surfaceOp === undefined ? {} : { surfaceOp },
410
+ });
411
+ if (surface === undefined)
412
+ throw new Error(`${subject} carries non-JSON-serializable surface metadata`);
413
+ return { type, data: dataSnapshot, surface };
414
+ }
415
+ /** Construct and validate one live event from detached inputs. */
416
+ function createAppendEvent(snapshot, seq) {
417
+ const { type, data, surface } = snapshot;
418
+ const event = deepFreeze({ type, seq, time: Date.now(), data, ...surface });
419
+ validateSessionEventData(event, `session event "${type}" at seq ${seq}`);
420
+ return event;
421
+ }
400
422
  /**
401
423
  * An event-sourced session: an append-only log of {@link SessionEvent}s.
402
424
  *
@@ -407,6 +429,7 @@ export function pluginRecordOf(event) {
407
429
  */
408
430
  export class Session {
409
431
  log = [];
432
+ batchPublication;
410
433
  /** Single incremental owner of surface acceptance and projection state. */
411
434
  surfaceManager;
412
435
  /** The ordered surface over this session's event log. */
@@ -600,76 +623,72 @@ export class Session {
600
623
  get seq() {
601
624
  return SessionLogOffset(this.log.length);
602
625
  }
603
- /**
604
- * Append one typed event to the log and synchronously notify observers via
605
- * the store-owned, module-private publication hooks. The hot path never blocks
606
- * on I/O — persistence plugins buffer asynchronously. Once the event enters
607
- * the log, the append is committed: observer failures are logged and
608
- * contained per listener, so they do not change the return value or prevent
609
- * later listeners from observing the same accepted event.
610
- * Independent Sessions append without notifying store observers. Once a store
611
- * prepares or enters the instance, writes require a completed creation
612
- * announcement and remain disabled after removal.
613
- *
614
- * @param type - The event type (key of {@link SessionEventMap}).
615
- * @param data - The event payload; must be JSON-serializable.
616
- * @param opts - Surface metadata: `surfaceOp` controls how the event enters
617
- * the ordered surface; `sourceEventSeqs` lists the seq numbers of earlier
618
- * events this one derives from. REQUIRED for
619
- * {@link SurfaceEventType} events (every message-producing event must
620
- * declare how it joins the surface, the sole source of derived model
621
- * history) and
622
- * rejected by the compiler for non-surface types like `turn/start` or
623
- * `assistant/attempt`. Assistant messages embed their exact provider
624
- * stream and cannot cite top-level source events.
625
- * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
626
- * `data` that entered the log, so reading `event.data` back sees the logged
627
- * value, never the caller's still-mutable input.
628
- * @throws if a store-owned Session is unentered or its creation announcement
629
- * has not completed, or if `data` or surface
630
- * metadata is not losslessly JSON-serializable
631
- * (BigInt, function, symbol, undefined, negative zero, non-finite number,
632
- * circular reference, sparse array, or an exotic object such as
633
- * Map/Set/Date/class instance), or when the candidate violates the
634
- * request-header empty-field or tool-error consistency rules, or the
635
- * canonical surface contract (marker shape and eligibility, unique
636
- * earlier source-event references, positional replacement validity, and complete
637
- * shadowed-node coverage). One iterative pass reads, validates, and
638
- * copies each nested value once, so a stateful getter cannot supply one value
639
- * to validation and another to storage. The event log is the durable source
640
- * of truth, so a bad event fails at the append site rather than later during
641
- * a backend flush. A synchronous internal dispatch validation failure or an
642
- * append reentered while this acceptance/publication boundary is open also
643
- * rejects before the log changes.
644
- */
626
+ /** Current event while synchronously notifying a batch's observers. */
627
+ get currentBatchPublication() {
628
+ return this.batchPublication;
629
+ }
645
630
  append(type, data, ...opts) {
631
+ if (typeof type !== 'string')
632
+ return this.#appendEvents(type);
646
633
  this.publicationEntry();
647
- const surfaceOpts = opts[0];
648
- const surfaceMetadata = {
649
- ...surfaceOpts?.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
650
- ...surfaceOpts?.surfaceOp === undefined ? {} : { surfaceOp: surfaceOpts.surfaceOp },
651
- };
652
- const dataSnapshot = snapshotJsonValue(data);
653
- if (dataSnapshot === undefined) {
654
- throw new Error(`session event "${type}" carries non-JSON-serializable data`);
655
- }
656
- const surfaceMetadataSnapshot = snapshotJsonValue(surfaceMetadata);
657
- if (surfaceMetadataSnapshot === undefined) {
658
- throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
659
- }
634
+ const snapshot = snapshotAppend(type, data, opts[0], `session event "${type}"`);
660
635
  // Payload getters can detach the Session while the snapshots are read.
661
636
  const entry = this.publicationEntry();
662
- const event = deepFreeze({
663
- type,
664
- seq: SessionSeq(this.log.length),
665
- time: Date.now(),
666
- data: dataSnapshot,
667
- ...surfaceMetadataSnapshot,
668
- });
669
- validateSessionEventData(event, `session event "${type}" at seq ${event.seq}`);
637
+ const event = createAppendEvent(snapshot, SessionSeq(this.log.length));
670
638
  this.commit(event, entry);
671
639
  return event;
672
640
  }
641
+ /**
642
+ * Validate and publish consecutive events together. Observers see the complete log.
643
+ * @param inputs - event payloads and surface intents in commit order.
644
+ * @returns immutable committed events in the same order.
645
+ * @throws before changing the log if any candidate or synchronous dispatch validation fails.
646
+ */
647
+ #appendEvents(inputs) {
648
+ this.publicationEntry();
649
+ if (inputs.length === 0)
650
+ return [];
651
+ const snapshots = Array.from({ length: inputs.length }, (_, index) => {
652
+ const input = inputs.at(index);
653
+ if (input === undefined)
654
+ throw new Error(`session batch has an empty member at index ${index}`);
655
+ const [type, data, surfaceOpts] = input;
656
+ return snapshotAppend(type, data, surfaceOpts, `session batch member ${index} ("${type}")`);
657
+ });
658
+ // Payload getters may append to or detach the Session.
659
+ const entry = this.publicationEntry();
660
+ const startSeq = this.log.length;
661
+ const events = snapshots.map((snapshot, index) => createAppendEvent(snapshot, SessionSeq(startSeq + index)));
662
+ const acceptSurface = this.surfaceManager.prepareBatch(events);
663
+ if (entry !== undefined)
664
+ entry.appending = true;
665
+ try {
666
+ const publications = entry === undefined ? [] : events.map((event) => {
667
+ const args = [this, event];
668
+ const callbacks = collectSessionCallbacks(entry.emitCtx, [entry.carrier, 'session/event', ...args]);
669
+ return { event, args, callbacks };
670
+ });
671
+ for (const event of events)
672
+ this.log.push(event);
673
+ this.eventsSnapshot = undefined;
674
+ acceptSurface();
675
+ if (entry !== undefined) {
676
+ for (const { event, args, callbacks } of publications) {
677
+ this.batchPublication = event;
678
+ invokeContainedSessionObservers(entry.emitCtx, 'session/event', entry.id, args, callbacks);
679
+ }
680
+ }
681
+ }
682
+ finally {
683
+ this.batchPublication = undefined;
684
+ if (entry !== undefined) {
685
+ entry.appending = false;
686
+ if (entry.detachRequested)
687
+ entry.detach();
688
+ }
689
+ }
690
+ return events;
691
+ }
673
692
  static {
674
693
  commitPluginRecord = (session, type, data) => session.#appendRecord(type, data);
675
694
  }