@deepseek-ai/dsh-session 0.2.1-rc.5 → 0.2.2-alpha.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.
package/README.i18n.yaml CHANGED
@@ -18,11 +18,11 @@
18
18
  en: 0882d4056c45aa75
19
19
  zh: fcee78e9845b78b3
20
20
  /deepseek-ai-dsh-session/use-this-package/create-and-inspect-sessions:
21
- en: f80dab583fa3939d
22
- zh: 5a9a79ebcef86119
21
+ en: 971e583b3d6c1af8
22
+ zh: c7da934ecb8f1e87
23
23
  /deepseek-ai-dsh-session/use-this-package/append-and-derive:
24
- en: aa5f1951ecf3ac0c
25
- zh: 62a599ace8f2bf15
24
+ en: c9ad40c8a8f2cd0b
25
+ zh: 0b76ae73f39bbb18
26
26
  /deepseek-ai-dsh-session/use-this-package/read-the-log:
27
27
  en: c4bf3103b9762b6c
28
28
  zh: 369c734bb20de2ea
package/README.md CHANGED
@@ -29,7 +29,9 @@ Mount `dsh-session` wherever a session must exist. It creates and holds event-so
29
29
 
30
30
  ### Create and inspect sessions
31
31
 
32
- `ctx.sessions.create()` builds a live session bound to the calling fiber; `get(id)` and `list()` find sessions, and `fork()` creates a child session from a stable prefix of a live one.
32
+ `ctx.sessions.create()` builds a live session bound to the calling fiber; `get(id)` and `list()` find sessions, and `fork()` creates an independent session from a stable prefix of a live one. It copies history, `cwd`, `isSeeded`, and the inherited event count without recording a source relationship. `parentSession` records a subagent’s direct creator; historical ordinary forks may retain their source there, so classification uses `origin`.
33
+
34
+ Independent `Session.create()` and `Session.fromRestore()` instances accept events and plugin records without sending `session/event`. A `SessionStore` takes ownership through `prepare()` or `enter()`; writes then require store membership and a completed synchronous `session/created` announcement. Creation listeners are read-only and see all existing history; entering an independent instance does not replay that history as `session/event`. Failed announcement or removal leaves writes disabled. Removal emits `session/disposed` only if `session/created` began, including a failed announcement; an entered but never announced instance emits neither notification.
33
35
 
34
36
  ```text
35
37
  const session = ctx.sessions.create(sessionId, { meta: { cwd: '/workspace' } })
@@ -39,7 +41,7 @@ ctx.sessions.list() // every live session, in creation order
39
41
 
40
42
  ### Append and derive
41
43
 
42
- `session.append(type, data, opts?)` commits one typed event — it snapshots and freezes the payload, validates it as lossless JSON, and notifies observers. `session.deriveMessages()` projects the log into the `Message[]` the model sees, incrementally and cached:
44
+ `session.append(type, data, opts?)` commits one typed event. It rejects read-only instances before reading payload fields, snapshots and freezes valid JSON, then rechecks write permission before committing and notifying observers. `session.deriveMessages()` projects the log into the `Message[]` the model sees, incrementally and cached:
43
45
 
44
46
  ```text
45
47
  session.append('user/message', { role: 'user', content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
package/README.zh.md CHANGED
@@ -29,7 +29,9 @@ kind: "package-reference"
29
29
 
30
30
  ### 创建与检查会话
31
31
 
32
- `ctx.sessions.create()` 构建绑定到调用方 fiber 的实时会话;`get(id)` 与 `list()` 查找会话,`fork()` 从实时会话的精确前缀创建子会话。
32
+ `ctx.sessions.create()` 构建绑定到调用方 fiber 的实时会话;`get(id)` 与 `list()` 查找会话,`fork()` 从实时会话的精确前缀创建独立会话。它复制历史、`cwd`、`isSeeded` 和继承事件数,不记录来源关系。`parentSession` 记录子代理的直接创建者;历史普通 fork 可能仍在此字段保留来源,因此分类使用 `origin`。
33
+
34
+ 独立的 `Session.create()` 和 `Session.fromRestore()` 实例接受事件和插件记录,但不发送 `session/event`。`SessionStore` 通过 `prepare()` 或 `enter()` 接管实例;此后只有实例仍在 Store 中且同步 `session/created` 宣告已完成时才能写入。创建监听器只能读取,并能看到全部已有历史;独立实例进入 Store 时不会把已有历史重放为 `session/event`。宣告失败或移除后仍禁止写入。只有开始过 `session/created` 宣告的实例在移除时发出 `session/disposed`,宣告失败也不例外;进入 Store 但从未宣告的实例不发出这两个通知。
33
35
 
34
36
  ```text
35
37
  const session = ctx.sessions.create(sessionId, { meta: { cwd: '/workspace' } })
@@ -39,7 +41,7 @@ ctx.sessions.list() // every live session, in creation order
39
41
 
40
42
  ### 追加与派生
41
43
 
42
- `session.append(type, data, opts?)` 提交一个类型化事件——它先快照并冻结载荷、校验其为无损 JSON,再通知观察者。`session.deriveMessages()` 把日志投影为模型看到的 `Message[]`,采用增量且有缓存的方式:
44
+ `session.append(type, data, opts?)` 提交一个类型化事件。它在读取载荷字段前拒绝只读实例,为有效 JSON 创建快照并冻结,再次检查写入权限后提交并通知观察者。`session.deriveMessages()` 把日志投影为模型看到的 `Message[]`,采用增量且有缓存的方式:
43
45
 
44
46
  ```text
45
47
  session.append('user/message', { role: 'user', content: [{ type: 'text', text: 'hello' }], source: { kind: 'user' } },
package/lib/index.js CHANGED
@@ -1238,7 +1238,7 @@ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
1238
1238
  ctx.logger.warn(`session "${id}": ${name} listener threw: ${String(error)}`);
1239
1239
  }
1240
1240
  }
1241
- /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
1241
+ /** Store-owned Sessions retain a null entry while unentered; absent keys are independent. */
1242
1242
  const attachments = /* @__PURE__ */ new WeakMap();
1243
1243
  /**
1244
1244
  * Grammar of a {@link PluginRecordType}: `plugin:` followed by
@@ -1288,7 +1288,9 @@ let commitPluginRecord;
1288
1288
  * with a letter or digit.
1289
1289
  * @param data - JSON payload, snapshotted before it enters the log.
1290
1290
  * @returns the sequence number of the committed record.
1291
- * @throws when `type` is outside that grammar, when `data` is not losslessly
1291
+ * @throws when a store-owned Session is unentered or its creation announcement
1292
+ * has not completed, when `type` is outside
1293
+ * that grammar, when `data` is not losslessly
1292
1294
  * JSON-serializable, or when the call reenters another append's publication;
1293
1295
  * a rejected record does not change the log.
1294
1296
  */
@@ -1484,6 +1486,9 @@ var Session = class Session {
1484
1486
  * the log, the append is committed: observer failures are logged and
1485
1487
  * contained per listener, so they do not change the return value or prevent
1486
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.
1487
1492
  *
1488
1493
  * @param type - The event type (key of {@link SessionEventMap}).
1489
1494
  * @param data - The event payload; must be JSON-serializable.
@@ -1499,7 +1504,9 @@ var Session = class Session {
1499
1504
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
1500
1505
  * `data` that entered the log, so reading `event.data` back sees the logged
1501
1506
  * value, never the caller's still-mutable input.
1502
- * @throws if `data` or surface metadata is not losslessly JSON-serializable
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
1503
1510
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
1504
1511
  * circular reference, sparse array, or an exotic object such as
1505
1512
  * Map/Set/Date/class instance), or when the candidate violates the
@@ -1515,6 +1522,7 @@ var Session = class Session {
1515
1522
  * rejects before the log changes.
1516
1523
  */
1517
1524
  append(type, data, ...opts) {
1525
+ this.publicationEntry();
1518
1526
  const surfaceOpts = opts[0];
1519
1527
  const surfaceMetadata = {
1520
1528
  ...surfaceOpts?.sourceEventSeqs === void 0 ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
@@ -1546,6 +1554,7 @@ var Session = class Session {
1546
1554
  * @returns the record's sequence number.
1547
1555
  */
1548
1556
  #appendRecord(type, data) {
1557
+ this.publicationEntry();
1549
1558
  const dataSnapshot = snapshotJsonValue(data);
1550
1559
  if (dataSnapshot === void 0) throw new Error(`plugin record "${type}" carries non-JSON-serializable data`);
1551
1560
  const entry = this.publicationEntry();
@@ -1561,12 +1570,13 @@ var Session = class Session {
1561
1570
  return record.seq;
1562
1571
  }
1563
1572
  /**
1564
- * Read the store attachment for one append, refusing an append that
1565
- * reenters while another append is being published.
1566
- * @returns the attachment, or undefined for a detached Session.
1573
+ * Require announced membership for store-owned Sessions and refuse reentrant publication.
1574
+ * @returns the current store attachment, or undefined for an independent Session.
1567
1575
  */
1568
1576
  publicationEntry() {
1569
1577
  const entry = attachments.get(this);
1578
+ if (entry === null) throw new Error(`session "${this.id}" cannot append outside a SessionStore`);
1579
+ if (entry !== void 0 && entry.announcement !== "complete") throw new Error(`session "${this.id}" cannot append before session/created completes`);
1570
1580
  if (entry?.appending) throw new Error("session append cannot reenter while another append is being published");
1571
1581
  return entry;
1572
1582
  }
@@ -1580,20 +1590,19 @@ var Session = class Session {
1580
1590
  this.surfaceManager.validateNext(event);
1581
1591
  if (entry !== void 0) entry.appending = true;
1582
1592
  try {
1583
- let callbacks;
1584
1593
  const callbackArgs = [this, event];
1585
- if (entry !== void 0) callbacks = collectSessionCallbacks(entry.emitCtx, [
1594
+ const callbacks = entry === void 0 ? void 0 : collectSessionCallbacks(entry.emitCtx, [
1586
1595
  entry.carrier,
1587
1596
  "session/event",
1588
1597
  ...callbackArgs
1589
1598
  ]);
1590
1599
  this.log.push(event);
1591
1600
  this.eventsSnapshot = void 0;
1592
- if (callbacks !== void 0 && entry !== void 0) invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
1601
+ if (entry !== void 0 && callbacks !== void 0) invokeContainedSessionObservers(entry.emitCtx, "session/event", entry.id, callbackArgs, callbacks);
1593
1602
  } finally {
1594
1603
  if (entry !== void 0) {
1595
1604
  entry.appending = false;
1596
- if (entry.detachRequested && !entry.announcing) entry.detach();
1605
+ if (entry.detachRequested && entry.announcement !== "dispatching") entry.detach();
1597
1606
  }
1598
1607
  }
1599
1608
  }
@@ -1791,7 +1800,7 @@ var SessionStore = class extends Service {
1791
1800
  * `eventState`, every seed event is either independently owned or any
1792
1801
  * shared value is deeply frozen; {@link Session.fromRestore} validates and
1793
1802
  * adopts those values without copying or freezing them.
1794
- * @returns the constructed session, NOT yet in the store.
1803
+ * @returns the store-owned session, not yet entered and read-only until announced.
1795
1804
  * @throws if a session with `id` already exists, metadata is not a plain
1796
1805
  * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
1797
1806
  * non-absolute path.
@@ -1807,7 +1816,11 @@ var SessionStore = class extends Service {
1807
1816
  const { eventState } = options;
1808
1817
  switch (eventState) {
1809
1818
  case "detached":
1810
- case "shared-frozen": return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState, this.projections);
1819
+ case "shared-frozen": {
1820
+ const session = Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState, this.projections);
1821
+ attachments.set(session, null);
1822
+ return session;
1823
+ }
1811
1824
  case void 0: break;
1812
1825
  /* v8 ignore next -- closed-union exhaustiveness guard */
1813
1826
  default: assertNever(eventState, "SessionStore.prepare event state");
@@ -1826,11 +1839,14 @@ var SessionStore = class extends Service {
1826
1839
  ...meta?.delegationDepth === void 0 ? {} : { delegationDepth: meta.delegationDepth },
1827
1840
  ...meta?.agentPreset === void 0 ? {} : { agentPreset: meta.agentPreset }
1828
1841
  };
1829
- return Session.create(sessionId, seed, header, options?.inheritedEventCount, this.projections);
1842
+ const session = Session.create(sessionId, seed, header, options?.inheritedEventCount, this.projections);
1843
+ attachments.set(session, null);
1844
+ return session;
1830
1845
  }
1831
1846
  /**
1832
- * Enter a {@link prepare}d session into the store: install the module-private
1833
- * append publication hooks and add it to the store. Returns the DETACH
1847
+ * Enter a session into the store: install the module-private
1848
+ * append publication hooks and add it to the store. Writes remain disabled
1849
+ * until {@link announce} completes successfully. Returns the DETACH
1834
1850
  * disposer (hooks + store removal). Does NOT emit `session/created` —
1835
1851
  * the caller yields this disposer inside its effect and THEN calls
1836
1852
  * {@link announce}, so a throwing `session/created` listener rolls the attach
@@ -1844,7 +1860,7 @@ var SessionStore = class extends Service {
1844
1860
  * the two back-to-back so they never trip this, but the public API cannot
1845
1861
  * assume that.
1846
1862
  *
1847
- * @param session - a {@link prepare}d session not yet in the store.
1863
+ * @param session - an unentered session; independent instances become store-owned.
1848
1864
  * @returns the detach disposer (publication hooks + store removal). When called from
1849
1865
  * a synchronous `session/created` listener, removal and disposal wait until
1850
1866
  * that creation dispatch unwinds.
@@ -1854,14 +1870,13 @@ var SessionStore = class extends Service {
1854
1870
  const id = session.id;
1855
1871
  const carrier = scopeTarget(session, scopeOf(this.ctx));
1856
1872
  if (this.store.has(id)) throw new Error(`session "${id}" already exists`);
1857
- if (attachments.has(session)) throw new Error(`session "${id}" is already attached to a store`);
1873
+ if (attachments.get(session) != null) throw new Error(`session "${id}" is already attached to a store`);
1858
1874
  const entry = {
1859
1875
  id,
1860
1876
  session,
1861
1877
  carrier,
1862
1878
  emitCtx: this.ctx,
1863
- announced: false,
1864
- announcing: false,
1879
+ announcement: "pending",
1865
1880
  appending: false,
1866
1881
  detachRequested: false,
1867
1882
  detach: () => {
@@ -1874,7 +1889,7 @@ var SessionStore = class extends Service {
1874
1889
  const detach = () => {
1875
1890
  if (!entered) return;
1876
1891
  entered = false;
1877
- if (entry.announcing || entry.appending) {
1892
+ if (entry.announcement === "dispatching" || entry.appending) {
1878
1893
  entry.detachRequested = true;
1879
1894
  return;
1880
1895
  }
@@ -1888,22 +1903,23 @@ var SessionStore = class extends Service {
1888
1903
  /* v8 ignore next -- enter() rejects replacement while this single-shot detach capability is live. */
1889
1904
  if (this.store.get(entry.id) !== entry) return;
1890
1905
  this.store.delete(entry.id);
1891
- attachments.delete(entry.session);
1892
- if (entry.announced) this.emitDisposed(entry);
1906
+ attachments.set(entry.session, null);
1907
+ if (entry.announcement !== "pending") this.emitDisposed(entry);
1893
1908
  }
1894
1909
  /** Emit `session/created` exactly once for an {@link enter}ed session (with
1895
1910
  * the carrier {@link enter} captured). Separate from {@link enter} so the
1896
1911
  * caller can yield the detach disposer first (rollback safety — see
1897
- * {@link enter}).
1912
+ * {@link enter}). The Session remains read-only throughout synchronous
1913
+ * dispatch; successful completion enables appends. A failed announcement
1914
+ * leaves it read-only and cannot be retried.
1898
1915
  * @param session - the entered session to announce to listeners.
1899
1916
  * @throws if the session is not live or its announcement already began,
1900
1917
  * including a reentrant call from a creation listener. */
1901
1918
  announce(session) {
1902
1919
  const entry = this.liveEntryFor(session);
1903
- if (entry.announced || entry.announcing) throw new Error(`session "${entry.id}" was already announced`);
1904
- entry.announced = true;
1920
+ if (entry.announcement !== "pending") throw new Error(`session "${entry.id}" was already announced`);
1905
1921
  const callbackArgs = [session];
1906
- entry.announcing = true;
1922
+ entry.announcement = "dispatching";
1907
1923
  try {
1908
1924
  const callbacks = collectSessionCallbacks(this.ctx, [
1909
1925
  entry.carrier,
@@ -1916,9 +1932,12 @@ var SessionStore = class extends Service {
1916
1932
  this.ctx.logger.warn(`session "${entry.id}": session/created listener rejected: ${String(error)}`);
1917
1933
  });
1918
1934
  }
1935
+ entry.announcement = "complete";
1936
+ } catch (error) {
1937
+ entry.announcement = "failed";
1938
+ throw error;
1919
1939
  } finally {
1920
- entry.announcing = false;
1921
- if (entry.detachRequested && !entry.appending) entry.detach();
1940
+ if (entry.detachRequested) entry.detach();
1922
1941
  }
1923
1942
  }
1924
1943
  /** Emit the paired teardown notification with per-listener containment. */
@@ -1969,7 +1988,7 @@ var SessionStore = class extends Service {
1969
1988
  /** Return the exact live entry; detached/prepared objects reject. */
1970
1989
  liveEntryFor(session) {
1971
1990
  const entry = attachments.get(session);
1972
- if (entry === void 0 || this.store.get(entry.id) !== entry) throw new Error(`session "${session.id}" is not live in this store`);
1991
+ if (entry == null || this.store.get(entry.id) !== entry) throw new Error(`session "${session.id}" is not live in this store`);
1973
1992
  return entry;
1974
1993
  }
1975
1994
  /**
@@ -1988,7 +2007,7 @@ var SessionStore = class extends Service {
1988
2007
  return [...this.store.values()].map((entry) => entry.session);
1989
2008
  }
1990
2009
  /**
1991
- * Create a live child session from an exact prefix of a live source.
2010
+ * Create an independent live session from an exact prefix of a live source.
1992
2011
  * `boundary` is an inclusive source event seq; omitted means the source's
1993
2012
  * current last event. An open tail receives synthetic tool results and
1994
2013
  * step/turn closers with the forked cause. Closed steps and turns remain
@@ -2014,7 +2033,6 @@ var SessionStore = class extends Service {
2014
2033
  inheritedEventCount: SessionLogOffset(resolved === void 0 ? 0 : resolved + 1),
2015
2034
  meta: {
2016
2035
  ...liveSource.header.cwd !== void 0 ? { cwd: liveSource.header.cwd } : {},
2017
- parentSession: liveSource.id,
2018
2036
  isSeeded: true
2019
2037
  }
2020
2038
  });
@@ -29,8 +29,10 @@ declare module '@deepseek-ai/cordis' {
29
29
  }
30
30
  interface Events {
31
31
  /**
32
- * Creation announcement during session publication. A synchronous throw vetoes and rolls
33
- * back with a paired disposal; detach requested during dispatch is deferred.
32
+ * Read-only creation announcement after store entry.
33
+ * New events reject until every synchronous listener has returned successfully.
34
+ * A synchronous throw vetoes creation and rolls back with a paired disposal;
35
+ * detach requested during dispatch is deferred.
34
36
  * A returned-promise rejection is logged but cannot retroactively veto this
35
37
  * synchronous boundary.
36
38
  * Scope-filtered dispatch (`@deepseek-ai/dsh-scope`): agent-scoped listeners
@@ -110,7 +112,9 @@ export declare function snapshotSessionEvent<T extends SessionEvent>(event: T):
110
112
  * with a letter or digit.
111
113
  * @param data - JSON payload, snapshotted before it enters the log.
112
114
  * @returns the sequence number of the committed record.
113
- * @throws when `type` is outside that grammar, when `data` is not losslessly
115
+ * @throws when a store-owned Session is unentered or its creation announcement
116
+ * has not completed, when `type` is outside
117
+ * that grammar, when `data` is not losslessly
114
118
  * JSON-serializable, or when the call reenters another append's publication;
115
119
  * a rejected record does not change the log.
116
120
  */
@@ -244,6 +248,9 @@ export declare class Session {
244
248
  * the log, the append is committed: observer failures are logged and
245
249
  * contained per listener, so they do not change the return value or prevent
246
250
  * later listeners from observing the same accepted event.
251
+ * Independent Sessions append without notifying store observers. Once a store
252
+ * prepares or enters the instance, writes require a completed creation
253
+ * announcement and remain disabled after removal.
247
254
  *
248
255
  * @param type - The event type (key of {@link SessionEventMap}).
249
256
  * @param data - The event payload; must be JSON-serializable.
@@ -259,7 +266,9 @@ export declare class Session {
259
266
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
260
267
  * `data` that entered the log, so reading `event.data` back sees the logged
261
268
  * value, never the caller's still-mutable input.
262
- * @throws if `data` or surface metadata is not losslessly JSON-serializable
269
+ * @throws if a store-owned Session is unentered or its creation announcement
270
+ * has not completed, or if `data` or surface
271
+ * metadata is not losslessly JSON-serializable
263
272
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
264
273
  * circular reference, sparse array, or an exotic object such as
265
274
  * Map/Set/Date/class instance), or when the candidate violates the
@@ -276,9 +285,8 @@ export declare class Session {
276
285
  */
277
286
  append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;
278
287
  /**
279
- * Read the store attachment for one append, refusing an append that
280
- * reenters while another append is being published.
281
- * @returns the attachment, or undefined for a detached Session.
288
+ * Require announced membership for store-owned Sessions and refuse reentrant publication.
289
+ * @returns the current store attachment, or undefined for an independent Session.
282
290
  */
283
291
  private publicationEntry;
284
292
  /**
@@ -426,15 +434,16 @@ export declare class SessionStore extends Service {
426
434
  * `eventState`, every seed event is either independently owned or any
427
435
  * shared value is deeply frozen; {@link Session.fromRestore} validates and
428
436
  * adopts those values without copying or freezing them.
429
- * @returns the constructed session, NOT yet in the store.
437
+ * @returns the store-owned session, not yet entered and read-only until announced.
430
438
  * @throws if a session with `id` already exists, metadata is not a plain
431
439
  * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
432
440
  * non-absolute path.
433
441
  */
434
442
  prepare(id?: SessionId, options?: PrepareSessionOptions): Session;
435
443
  /**
436
- * Enter a {@link prepare}d session into the store: install the module-private
437
- * append publication hooks and add it to the store. Returns the DETACH
444
+ * Enter a session into the store: install the module-private
445
+ * append publication hooks and add it to the store. Writes remain disabled
446
+ * until {@link announce} completes successfully. Returns the DETACH
438
447
  * disposer (hooks + store removal). Does NOT emit `session/created` —
439
448
  * the caller yields this disposer inside its effect and THEN calls
440
449
  * {@link announce}, so a throwing `session/created` listener rolls the attach
@@ -448,7 +457,7 @@ export declare class SessionStore extends Service {
448
457
  * the two back-to-back so they never trip this, but the public API cannot
449
458
  * assume that.
450
459
  *
451
- * @param session - a {@link prepare}d session not yet in the store.
460
+ * @param session - an unentered session; independent instances become store-owned.
452
461
  * @returns the detach disposer (publication hooks + store removal). When called from
453
462
  * a synchronous `session/created` listener, removal and disposal wait until
454
463
  * that creation dispatch unwinds.
@@ -460,7 +469,9 @@ export declare class SessionStore extends Service {
460
469
  /** Emit `session/created` exactly once for an {@link enter}ed session (with
461
470
  * the carrier {@link enter} captured). Separate from {@link enter} so the
462
471
  * caller can yield the detach disposer first (rollback safety — see
463
- * {@link enter}).
472
+ * {@link enter}). The Session remains read-only throughout synchronous
473
+ * dispatch; successful completion enables appends. A failed announcement
474
+ * leaves it read-only and cannot be retried.
464
475
  * @param session - the entered session to announce to listeners.
465
476
  * @throws if the session is not live or its announcement already began,
466
477
  * including a reentrant call from a creation listener. */
@@ -495,7 +506,7 @@ export declare class SessionStore extends Service {
495
506
  */
496
507
  list(): Session[];
497
508
  /**
498
- * Create a live child session from an exact prefix of a live source.
509
+ * Create an independent live session from an exact prefix of a live source.
499
510
  * `boundary` is an inclusive source event seq; omitted means the source's
500
511
  * current last event. An open tail receives synthetic tool results and
501
512
  * step/turn closers with the forked cause. Closed steps and turns remain
@@ -322,7 +322,7 @@ function invokeContainedSessionObservers(ctx, name, id, args, callbacks) {
322
322
  }
323
323
  }
324
324
  }
325
- /** Store attachment for the append path; module-private to keep Session store-agnostic publicly. */
325
+ /** Store-owned Sessions retain a null entry while unentered; absent keys are independent. */
326
326
  const attachments = new WeakMap();
327
327
  /**
328
328
  * Grammar of a {@link PluginRecordType}: `plugin:` followed by
@@ -374,7 +374,9 @@ let commitPluginRecord;
374
374
  * with a letter or digit.
375
375
  * @param data - JSON payload, snapshotted before it enters the log.
376
376
  * @returns the sequence number of the committed record.
377
- * @throws when `type` is outside that grammar, when `data` is not losslessly
377
+ * @throws when a store-owned Session is unentered or its creation announcement
378
+ * has not completed, when `type` is outside
379
+ * that grammar, when `data` is not losslessly
378
380
  * JSON-serializable, or when the call reenters another append's publication;
379
381
  * a rejected record does not change the log.
380
382
  */
@@ -605,6 +607,9 @@ export class Session {
605
607
  * the log, the append is committed: observer failures are logged and
606
608
  * contained per listener, so they do not change the return value or prevent
607
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.
608
613
  *
609
614
  * @param type - The event type (key of {@link SessionEventMap}).
610
615
  * @param data - The event payload; must be JSON-serializable.
@@ -620,7 +625,9 @@ export class Session {
620
625
  * @returns the logged event — its assigned `seq`/`time` plus the SNAPSHOT of
621
626
  * `data` that entered the log, so reading `event.data` back sees the logged
622
627
  * value, never the caller's still-mutable input.
623
- * @throws if `data` or surface metadata is not losslessly JSON-serializable
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
624
631
  * (BigInt, function, symbol, undefined, negative zero, non-finite number,
625
632
  * circular reference, sparse array, or an exotic object such as
626
633
  * Map/Set/Date/class instance), or when the candidate violates the
@@ -636,6 +643,7 @@ export class Session {
636
643
  * rejects before the log changes.
637
644
  */
638
645
  append(type, data, ...opts) {
646
+ this.publicationEntry();
639
647
  const surfaceOpts = opts[0];
640
648
  const surfaceMetadata = {
641
649
  ...surfaceOpts?.sourceEventSeqs === undefined ? {} : { sourceEventSeqs: surfaceOpts.sourceEventSeqs },
@@ -649,6 +657,7 @@ export class Session {
649
657
  if (surfaceMetadataSnapshot === undefined) {
650
658
  throw new Error(`session event "${type}" carries non-JSON-serializable surface metadata`);
651
659
  }
660
+ // Payload getters can detach the Session while the snapshots are read.
652
661
  const entry = this.publicationEntry();
653
662
  const event = deepFreeze({
654
663
  type,
@@ -671,6 +680,7 @@ export class Session {
671
680
  * @returns the record's sequence number.
672
681
  */
673
682
  #appendRecord(type, data) {
683
+ this.publicationEntry();
674
684
  const dataSnapshot = snapshotJsonValue(data);
675
685
  if (dataSnapshot === undefined) {
676
686
  throw new Error(`plugin record "${type}" carries non-JSON-serializable data`);
@@ -688,12 +698,17 @@ export class Session {
688
698
  return record.seq;
689
699
  }
690
700
  /**
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.
701
+ * Require announced membership for store-owned Sessions and refuse reentrant publication.
702
+ * @returns the current store attachment, or undefined for an independent Session.
694
703
  */
695
704
  publicationEntry() {
696
705
  const entry = attachments.get(this);
706
+ if (entry === null) {
707
+ throw new Error(`session "${this.id}" cannot append outside a SessionStore`);
708
+ }
709
+ if (entry !== undefined && entry.announcement !== 'complete') {
710
+ throw new Error(`session "${this.id}" cannot append before session/created completes`);
711
+ }
697
712
  if (entry?.appending) {
698
713
  throw new Error('session append cannot reenter while another append is being published');
699
714
  }
@@ -710,21 +725,20 @@ export class Session {
710
725
  if (entry !== undefined)
711
726
  entry.appending = true;
712
727
  try {
713
- let callbacks;
714
728
  const callbackArgs = [this, event];
715
- if (entry !== undefined) {
716
- callbacks = collectSessionCallbacks(entry.emitCtx, [entry.carrier, 'session/event', ...callbackArgs]);
717
- }
729
+ const callbacks = entry === undefined
730
+ ? undefined
731
+ : collectSessionCallbacks(entry.emitCtx, [entry.carrier, 'session/event', ...callbackArgs]);
718
732
  this.log.push(event);
719
733
  this.eventsSnapshot = undefined;
720
- if (callbacks !== undefined && entry !== undefined) {
734
+ if (entry !== undefined && callbacks !== undefined) {
721
735
  invokeContainedSessionObservers(entry.emitCtx, 'session/event', entry.id, callbackArgs, callbacks);
722
736
  }
723
737
  }
724
738
  finally {
725
739
  if (entry !== undefined) {
726
740
  entry.appending = false;
727
- if (entry.detachRequested && !entry.announcing)
741
+ if (entry.detachRequested && entry.announcement !== 'dispatching')
728
742
  entry.detach();
729
743
  }
730
744
  }
@@ -942,7 +956,7 @@ export class SessionStore extends Service {
942
956
  * `eventState`, every seed event is either independently owned or any
943
957
  * shared value is deeply frozen; {@link Session.fromRestore} validates and
944
958
  * adopts those values without copying or freezing them.
945
- * @returns the constructed session, NOT yet in the store.
959
+ * @returns the store-owned session, not yet entered and read-only until announced.
946
960
  * @throws if a session with `id` already exists, metadata is not a plain
947
961
  * lossless-JSON record with valid scalar fields, or `meta.cwd` is a
948
962
  * non-absolute path.
@@ -963,8 +977,11 @@ export class SessionStore extends Service {
963
977
  const { eventState } = options;
964
978
  switch (eventState) {
965
979
  case 'detached':
966
- case 'shared-frozen':
967
- return Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState, this.projections);
980
+ case 'shared-frozen': {
981
+ const session = Session.fromRestore(sessionId, options.seed, options.meta, options.inheritedEventCount, eventState, this.projections);
982
+ attachments.set(session, null);
983
+ return session;
984
+ }
968
985
  case undefined:
969
986
  break;
970
987
  /* v8 ignore next -- closed-union exhaustiveness guard */
@@ -985,11 +1002,14 @@ export class SessionStore extends Service {
985
1002
  ...meta?.delegationDepth === undefined ? {} : { delegationDepth: meta.delegationDepth },
986
1003
  ...meta?.agentPreset === undefined ? {} : { agentPreset: meta.agentPreset },
987
1004
  };
988
- return Session.create(sessionId, seed, header, options?.inheritedEventCount, this.projections);
1005
+ const session = Session.create(sessionId, seed, header, options?.inheritedEventCount, this.projections);
1006
+ attachments.set(session, null);
1007
+ return session;
989
1008
  }
990
1009
  /**
991
- * Enter a {@link prepare}d session into the store: install the module-private
992
- * append publication hooks and add it to the store. Returns the DETACH
1010
+ * Enter a session into the store: install the module-private
1011
+ * append publication hooks and add it to the store. Writes remain disabled
1012
+ * until {@link announce} completes successfully. Returns the DETACH
993
1013
  * disposer (hooks + store removal). Does NOT emit `session/created` —
994
1014
  * the caller yields this disposer inside its effect and THEN calls
995
1015
  * {@link announce}, so a throwing `session/created` listener rolls the attach
@@ -1003,7 +1023,7 @@ export class SessionStore extends Service {
1003
1023
  * the two back-to-back so they never trip this, but the public API cannot
1004
1024
  * assume that.
1005
1025
  *
1006
- * @param session - a {@link prepare}d session not yet in the store.
1026
+ * @param session - an unentered session; independent instances become store-owned.
1007
1027
  * @returns the detach disposer (publication hooks + store removal). When called from
1008
1028
  * a synchronous `session/created` listener, removal and disposal wait until
1009
1029
  * that creation dispatch unwinds.
@@ -1016,15 +1036,14 @@ export class SessionStore extends Service {
1016
1036
  // preparation. Only one exact same-id transaction can publish.
1017
1037
  if (this.store.has(id))
1018
1038
  throw new Error(`session "${id}" already exists`);
1019
- if (attachments.has(session))
1039
+ if (attachments.get(session) != null)
1020
1040
  throw new Error(`session "${id}" is already attached to a store`);
1021
1041
  const entry = {
1022
1042
  id,
1023
1043
  session,
1024
1044
  carrier,
1025
1045
  emitCtx: this.ctx,
1026
- announced: false,
1027
- announcing: false,
1046
+ announcement: 'pending',
1028
1047
  appending: false,
1029
1048
  detachRequested: false,
1030
1049
  detach: () => { this.detachEntered(entry); },
@@ -1039,7 +1058,7 @@ export class SessionStore extends Service {
1039
1058
  // A lifecycle listener may own the advanced detach capability. Keep the
1040
1059
  // entry and its publication hooks live until synchronous creation or append
1041
1060
  // publication unwinds, then publish the paired disposal edge.
1042
- if (entry.announcing || entry.appending) {
1061
+ if (entry.announcement === 'dispatching' || entry.appending) {
1043
1062
  entry.detachRequested = true;
1044
1063
  return;
1045
1064
  }
@@ -1056,28 +1075,28 @@ export class SessionStore extends Service {
1056
1075
  if (this.store.get(entry.id) !== entry)
1057
1076
  return;
1058
1077
  this.store.delete(entry.id);
1059
- attachments.delete(entry.session);
1060
- if (entry.announced)
1078
+ attachments.set(entry.session, null);
1079
+ if (entry.announcement !== 'pending')
1061
1080
  this.emitDisposed(entry);
1062
1081
  }
1063
1082
  /** Emit `session/created` exactly once for an {@link enter}ed session (with
1064
1083
  * the carrier {@link enter} captured). Separate from {@link enter} so the
1065
1084
  * caller can yield the detach disposer first (rollback safety — see
1066
- * {@link enter}).
1085
+ * {@link enter}). The Session remains read-only throughout synchronous
1086
+ * dispatch; successful completion enables appends. A failed announcement
1087
+ * leaves it read-only and cannot be retried.
1067
1088
  * @param session - the entered session to announce to listeners.
1068
1089
  * @throws if the session is not live or its announcement already began,
1069
1090
  * including a reentrant call from a creation listener. */
1070
1091
  announce(session) {
1071
1092
  const entry = this.liveEntryFor(session);
1072
- if (entry.announced || entry.announcing) {
1093
+ if (entry.announcement !== 'pending') {
1073
1094
  throw new Error(`session "${entry.id}" was already announced`);
1074
1095
  }
1075
- // Mark before emit: Cordis emit may deliver to earlier listeners and then
1076
- // throw. Rollback must still pair that partial creation with disposal, and
1077
- // a listener cannot recursively create a second lifecycle edge.
1078
- entry.announced = true;
1096
+ // Partial creation dispatch still requires disposal on rollback; listeners
1097
+ // cannot recursively begin another announcement.
1079
1098
  const callbackArgs = [session];
1080
- entry.announcing = true;
1099
+ entry.announcement = 'dispatching';
1081
1100
  try {
1082
1101
  const callbacks = collectSessionCallbacks(this.ctx, [entry.carrier, 'session/created', session]);
1083
1102
  for (const callback of callbacks) {
@@ -1091,10 +1110,14 @@ export class SessionStore extends Service {
1091
1110
  this.ctx.logger.warn(`session "${entry.id}": session/created listener rejected: ${String(error)}`);
1092
1111
  });
1093
1112
  }
1113
+ entry.announcement = 'complete';
1114
+ }
1115
+ catch (error) {
1116
+ entry.announcement = 'failed';
1117
+ throw error;
1094
1118
  }
1095
1119
  finally {
1096
- entry.announcing = false;
1097
- if (entry.detachRequested && !entry.appending)
1120
+ if (entry.detachRequested)
1098
1121
  entry.detach();
1099
1122
  }
1100
1123
  }
@@ -1145,7 +1168,7 @@ export class SessionStore extends Service {
1145
1168
  /** Return the exact live entry; detached/prepared objects reject. */
1146
1169
  liveEntryFor(session) {
1147
1170
  const entry = attachments.get(session);
1148
- if (entry === undefined || this.store.get(entry.id) !== entry) {
1171
+ if (entry == null || this.store.get(entry.id) !== entry) {
1149
1172
  throw new Error(`session "${session.id}" is not live in this store`);
1150
1173
  }
1151
1174
  return entry;
@@ -1166,7 +1189,7 @@ export class SessionStore extends Service {
1166
1189
  return [...this.store.values()].map(entry => entry.session);
1167
1190
  }
1168
1191
  /**
1169
- * Create a live child session from an exact prefix of a live source.
1192
+ * Create an independent live session from an exact prefix of a live source.
1170
1193
  * `boundary` is an inclusive source event seq; omitted means the source's
1171
1194
  * current last event. An open tail receives synthetic tool results and
1172
1195
  * step/turn closers with the forked cause. Closed steps and turns remain
@@ -1195,7 +1218,6 @@ export class SessionStore extends Service {
1195
1218
  inheritedEventCount: SessionLogOffset(resolved === undefined ? 0 : resolved + 1),
1196
1219
  meta: {
1197
1220
  ...liveSource.header.cwd !== undefined ? { cwd: liveSource.header.cwd } : {},
1198
- parentSession: liveSource.id,
1199
1221
  isSeeded: true,
1200
1222
  },
1201
1223
  });
@@ -68,7 +68,11 @@ export interface SessionHeader {
68
68
  readonly createdAt: number;
69
69
  /** Absolute working directory the session was created in (if any). */
70
70
  readonly cwd?: string;
71
- /** The session this one was forked from (seed lineage), if any. */
71
+ /**
72
+ * Direct creator of a subagent Session. Ordinary forks omit this field;
73
+ * historical ordinary forks may retain their source id here. Presence alone
74
+ * does not classify a Session as a subagent; use `origin` for that distinction.
75
+ */
72
76
  readonly parentSession?: SessionId;
73
77
  /**
74
78
  * Whether this Session contains a fork-inherited event prefix. The exact prefix
@@ -177,8 +181,9 @@ export interface TurnEndReasonMap {
177
181
  };
178
182
  /**
179
183
  * The turn failed. `error` is always a structured failure: the `LlmError`
180
- * facts verbatim, or `{ message: errorChain(error), code: 'UNKNOWN' }`
181
- * flattened from any other error.
184
+ * facts verbatim, or `{ message: errorChain(error), code }` flattened from
185
+ * any other error, where `code` is `TOOL_ACTIVATION_FAILED` for a failed
186
+ * tool condition and `UNKNOWN` otherwise.
182
187
  */
183
188
  error: {
184
189
  kind: 'error';
@@ -305,8 +310,10 @@ export interface SessionEventMap {
305
310
  * The rendered system prompt on the model-visible surface. The loop appends
306
311
  * the first one as surface node 0 before the step's first `user/message`.
307
312
  * A prepared in-history route can append nonempty changes in a continuing
308
- * series. An incapable route or new series normalizes text to the first system
309
- * node. Normalization empties nonempty later nodes, then rewrites the head if
313
+ * series. An incapable route, or a new series whose effective text differs,
314
+ * normalizes text to the first system node; a series start whose latest
315
+ * surviving node already holds the rendering leaves it in place.
316
+ * Normalization empties nonempty later nodes, then rewrites the head if
310
317
  * needed, through logged per-node replacements. An empty rendering always
311
318
  * clears all active system nodes, leaving no older instructions model-visible.
312
319
  * Empty later nodes are dormant and project to no message; an empty head with
package/locale/de.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Sitzung",
4
+ "description": "Protokollieren Sie Nachrichten, Toolaufrufe und führen Sie Ereignisse in einer Sitzung aus."
5
+ }
6
+ }
package/locale/es.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "sesión",
4
+ "description": "Registre mensajes, llamadas de herramientas y ejecute eventos en una sesión."
5
+ }
6
+ }
package/locale/fr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "séance",
4
+ "description": "Enregistrez les messages, les appels d'outils et exécutez des événements dans une session."
5
+ }
6
+ }
package/locale/ja.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "セッション記録",
4
+ "description": "セッション内のメッセージ、ツール呼び出し、実行イベントを記録します。"
5
+ }
6
+ }
package/locale/pt.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "sessão",
4
+ "description": "Registrar mensagens, chamadas de ferramentas e executar eventos em uma sessão."
5
+ }
6
+ }
package/locale/ru.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "сессия",
4
+ "description": "Регистрируйте сообщения, вызовы инструментов и запускайте события в сеансе."
5
+ }
6
+ }
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.1-rc.5",
4
+ "version": "0.2.2-alpha.3",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -42,18 +42,18 @@
42
42
  ],
43
43
  "license": "MIT",
44
44
  "peerDependencies": {
45
- "@deepseek-ai/dsh-scope": "0.2.1-rc.5",
46
- "@deepseek-ai/cordis": "~4.0.5"
45
+ "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
46
+ "@deepseek-ai/cordis": "~4.0.6-alpha.2"
47
47
  },
48
48
  "devDependencies": {
49
- "@deepseek-ai/dsh-scope": "0.2.1-rc.5",
50
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.5",
51
- "@deepseek-ai/dsh-typert-registry": "0.2.1-rc.5",
52
- "@deepseek-ai/cordis": "~4.0.5"
49
+ "@deepseek-ai/dsh-scope": "0.2.2-alpha.3",
50
+ "@deepseek-ai/dsh-typert-protocol": "0.2.2-alpha.3",
51
+ "@deepseek-ai/dsh-typert-registry": "0.2.2-alpha.3",
52
+ "@deepseek-ai/cordis": "~4.0.6-alpha.2"
53
53
  },
54
54
  "dependencies": {
55
- "@deepseek-ai/dsh-llm": "0.2.1-rc.5",
56
- "@deepseek-ai/dsh-brand": "0.2.1-rc.5",
57
- "@deepseek-ai/dsh-util-values": "0.2.1-rc.5"
55
+ "@deepseek-ai/dsh-llm": "0.2.2-alpha.3",
56
+ "@deepseek-ai/dsh-brand": "0.2.2-alpha.3",
57
+ "@deepseek-ai/dsh-util-values": "0.2.2-alpha.3"
58
58
  }
59
59
  }