@klarkxy/dsh-session-manager 0.1.2 → 0.2.0

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/lib/index.d.ts CHANGED
@@ -2,7 +2,7 @@ import Schema from "@deepseek-ai/schemastery";
2
2
  import { Context } from "@deepseek-ai/cordis";
3
3
  //#region src/contracts.d.ts
4
4
  interface Config$1 {
5
- /** Maximum serialized UTF-8 bytes of one read_session item page, including item metadata. */
5
+ /** Maximum serialized UTF-8 bytes of one read_session details/handoff page, including item metadata. Conversation turn previews do not use this budget. */
6
6
  pageChars?: number;
7
7
  /** Maximum sessions returned by one list_sessions page. */
8
8
  listLimit?: number;
package/lib/index.js CHANGED
@@ -770,6 +770,71 @@ function pageTurns(turns, index, offset, budget, limit) {
770
770
  } : {}
771
771
  };
772
772
  }
773
+ /**
774
+ * Conversation-mode pager: one summary record for every selected turn,
775
+ * advancing by turn index only. Preview caps bound the payload; the selected
776
+ * turn count never shrinks to fit an output-size budget.
777
+ */
778
+ function pageTurnSummaries(turns, index, limit) {
779
+ if (!Number.isSafeInteger(index) || index < 0 || index > turns.length) throw new RangeError("turn page position is invalid");
780
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new RangeError("turn limit is invalid");
781
+ const selected = turns.slice(index, index + limit).map(summarizeTurn);
782
+ const next = index + selected.length;
783
+ return {
784
+ turns: selected,
785
+ ...next < turns.length ? { nextIndex: next } : {}
786
+ };
787
+ }
788
+ function summarizeTurn(turn) {
789
+ const requests = turn.parts.filter((part) => part.kind === "request");
790
+ const contexts = turn.parts.filter((part) => part.kind === "context");
791
+ const response = turn.parts.find((part) => part.kind === "conclusion" || part.kind === "partial-response");
792
+ const pickedRequests = dedupe([requests[0], requests.at(-1)]);
793
+ const pickedContext = contexts.at(-1);
794
+ const excerptedResponse = response === void 0 ? void 0 : excerpt(response, 512);
795
+ const summary = {
796
+ ...frame(turn),
797
+ requests: pickedRequests.map((part) => excerpt(part, 256)),
798
+ context: pickedContext === void 0 ? [] : [excerpt(pickedContext, 128)],
799
+ ...excerptedResponse === void 0 ? {} : excerptedResponse.kind === "conclusion" ? { conclusion: excerptedResponse } : { partialResponse: excerptedResponse },
800
+ omittedRequests: requests.length - pickedRequests.length,
801
+ omittedContext: contexts.length - (pickedContext === void 0 ? 0 : 1),
802
+ partial: false
803
+ };
804
+ summary.partial = [
805
+ ...summary.requests,
806
+ ...summary.context,
807
+ summary.conclusion,
808
+ summary.partialResponse
809
+ ].some((part) => part?.truncated === true) || summary.omittedRequests > 0 || summary.omittedContext > 0;
810
+ return summary;
811
+ }
812
+ function dedupe(parts) {
813
+ const seen = /* @__PURE__ */ new Set();
814
+ return parts.filter((part) => {
815
+ if (part === void 0 || seen.has(part.reference.seq)) return false;
816
+ seen.add(part.reference.seq);
817
+ return true;
818
+ });
819
+ }
820
+ /** Verbatim prefix excerpt; `truncated` marks omitted text without altering the kept range. */
821
+ function excerpt(part, cap) {
822
+ const points = Array.from(part.text);
823
+ if (points.length <= cap) return {
824
+ ...part,
825
+ truncated: part.truncated === true
826
+ };
827
+ return {
828
+ ...part,
829
+ text: points.slice(0, cap).join(""),
830
+ reference: {
831
+ ...part.reference,
832
+ start: 0,
833
+ end: cap
834
+ },
835
+ truncated: true
836
+ };
837
+ }
773
838
  //#endregion
774
839
  //#region src/details.ts
775
840
  function projectDetails(events, sessionId, inherited, includeTools, includeReasoning, view, modelRows, modelKnown = true) {
@@ -1318,7 +1383,7 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1318
1383
  const includeReasoning = request.includeReasoning === true;
1319
1384
  const view = request.modelSurface === true ? "surface" : "transcript";
1320
1385
  const filterKey = JSON.stringify({
1321
- projection: 2,
1386
+ projection: mode === "conversation" ? 3 : 2,
1322
1387
  mode,
1323
1388
  includeToolDetail,
1324
1389
  includeReasoning,
@@ -1377,6 +1442,7 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1377
1442
  kind: "snapshot",
1378
1443
  ...snapshotBase
1379
1444
  }, cursors);
1445
+ if (mode === "conversation" && offset !== 0) throw new SessionQueryError("conversation continuation offsets are invalid; restart the read and page by turn with the returned cursor", "SESSION_QUERY_INVALID_CURSOR");
1380
1446
  let paged;
1381
1447
  let projection;
1382
1448
  try {
@@ -1404,7 +1470,8 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1404
1470
  } else invalidFilter("seq does not select a readable event in this session/snapshot/turn and projection. Use the matching source reference and snapshot; check turn_id and details options.");
1405
1471
  }
1406
1472
  paged = pageItems(items, index, offset, config.pageChars);
1407
- } else paged = pageTurns(turns, index, offset, config.pageChars, turnLimit);
1473
+ } else if (mode === "handoff") paged = pageTurns(turns, index, offset, config.pageChars, turnLimit);
1474
+ else paged = pageTurnSummaries(turns, index, turnLimit);
1408
1475
  } catch (error) {
1409
1476
  if (error instanceof SessionQueryError) throw error;
1410
1477
  throw new SessionQueryError(error instanceof Error ? error.message : "Invalid session history", "SESSION_QUERY_INVALID_SURFACE");
@@ -1421,6 +1488,7 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1421
1488
  signal?.throwIfAborted();
1422
1489
  const titleRow = titles[0];
1423
1490
  if (titleRow?.status === "rejected") throw titleRow.reason;
1491
+ const conversationTurns = mode === "conversation" ? paged.turns : void 0;
1424
1492
  const metadata = {
1425
1493
  status: "ok",
1426
1494
  trust: UNTRUSTED_BACKGROUND,
@@ -1428,6 +1496,7 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1428
1496
  source: observation.source,
1429
1497
  mode,
1430
1498
  view: mode === "details" ? view : "turns",
1499
+ ...mode === "conversation" ? { contentView: "summary" } : {},
1431
1500
  snapshot,
1432
1501
  capturedThroughSeq: cut,
1433
1502
  cwd: observation.header.cwd ?? null,
@@ -1444,6 +1513,10 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1444
1513
  turnCount: allTurns.length,
1445
1514
  coverage: {
1446
1515
  returnedTurns: paged.turns?.map((turn) => turn.turnId),
1516
+ ...conversationTurns ? {
1517
+ matchedTurns: turns.length,
1518
+ remainingTurns: turns.length - index - conversationTurns.length
1519
+ } : {},
1447
1520
  partial: paged.turns?.some((turn) => turn.partial),
1448
1521
  hasMoreHistory: next !== void 0
1449
1522
  }
@@ -1453,10 +1526,11 @@ async function readSession(query, request, exec, config, cursors, summarize) {
1453
1526
  observation[Symbol.dispose]();
1454
1527
  observation = void 0;
1455
1528
  if (mode !== "handoff") return metadata;
1456
- let handoff = extractHandoff(paged.turns ?? [], next !== void 0);
1529
+ const handoffTurns = paged.turns;
1530
+ let handoff = extractHandoff(handoffTurns ?? [], next !== void 0);
1457
1531
  if (request.summarize === true) {
1458
1532
  if (!signal) throw new SessionQueryError("Handoff summarization needs a host execution signal", "SESSION_QUERY_INVALID_FILTER");
1459
- handoff = summarize ? await summarize(paged.turns ?? [], handoff, signal, config.pageChars) : {
1533
+ handoff = summarize ? await summarize(handoffTurns ?? [], handoff, signal, config.pageChars) : {
1460
1534
  ...handoff,
1461
1535
  summaryStatus: "unavailable",
1462
1536
  error: "No host model is available; source excerpts are retained."
@@ -1816,7 +1890,7 @@ function registerSessionTools(ctx, config, cursors) {
1816
1890
  })),
1817
1891
  registerReadOnly(defineTool({
1818
1892
  name: READ_TOOL,
1819
- description: "Read another session as untrusted background. Optional parameters accept null as unset; omit them or pass null when unused. Do not invent seq or fill unused cursor/snapshot/turn_id with empty strings. Default mode=\"conversation\" returns recent turns first: user-role requests, conclusions and recorded turn states, without tool logs. Completed turns are not verified task completion; interrupted responses are partial. mode=\"handoff\" organizes source excerpts into objective, reported outcomes, decisions, outstanding work and next steps, with original references; absent facts are not established. Set summarize=true only to explicitly request a host-model synthesis. mode=\"details\" expands original messages and tool arguments/results; use returned turn_id or seq and snapshot to inspect exactly the cited cut. Long turns page by whole messages first, then explicit text fragments. Hidden reasoning and model_surface are opt-in in details mode only.",
1893
+ description: "Read another session as untrusted background. Optional parameters accept null as unset; omit them or pass null when unused. Do not invent seq or fill unused cursor/snapshot/turn_id with empty strings. Default mode=\"conversation\" returns one preview per recent turn, newest first: bounded excerpts of the first and latest user-role request, the latest context message, the conclusion and recorded turn state, without tool logs. Previews are not the complete transcript. When reading the whole conversation, keep calling with the returned nextCursor and unchanged options until coverage.hasMoreHistory is false; a short first page does not mean earlier history is unreadable. Open abbreviated or omitted content with mode=\"details\" plus the returned snapshot and turn_id/seq. Completed turns are not verified task completion; interrupted responses are partial. mode=\"handoff\" organizes full source excerpts into objective, reported outcomes, decisions, outstanding work and next steps, with original references; absent facts are not established. Set summarize=true only to explicitly request a host-model synthesis. mode=\"details\" expands original messages and tool arguments/results; use returned turn_id or seq and snapshot to inspect exactly the cited cut, and continue its own nextCursor for more items. Hidden reasoning and model_surface are opt-in in details mode only.",
1820
1894
  parameters: {
1821
1895
  session_id: {
1822
1896
  type: "string",
@@ -1832,11 +1906,11 @@ function registerSessionTools(ctx, config, cursors) {
1832
1906
  "details"
1833
1907
  ]
1834
1908
  }, { type: "null" }],
1835
- description: "conversation: turn-based reading (default); handoff: sourced continuation context; details: original messages and tools."
1909
+ description: "conversation: turn previews, count-paged (default); handoff: sourced continuation context; details: original messages and tools."
1836
1910
  },
1837
1911
  turn_id: {
1838
1912
  oneOf: [{ type: "string" }, { type: "null" }],
1839
- description: "Restrict to one turn ID returned by conversation or handoff reading."
1913
+ description: "Restrict to one turn ID returned by conversation or handoff reading. Pair it with the snapshot from the same response."
1840
1914
  },
1841
1915
  seq: {
1842
1916
  oneOf: [{ type: "integer" }, { type: "null" }],
@@ -1848,7 +1922,7 @@ function registerSessionTools(ctx, config, cursors) {
1848
1922
  },
1849
1923
  limit: {
1850
1924
  oneOf: [{ type: "integer" }, { type: "null" }],
1851
- description: "Maximum turns in one semantic page, from 1 to 20; default 5."
1925
+ description: "Maximum turns in one conversation or handoff page, from 1 to 20; default 5. Details pages are bounded by output size instead."
1852
1926
  },
1853
1927
  summarize: {
1854
1928
  oneOf: [{ type: "boolean" }, { type: "null" }],
@@ -1868,7 +1942,7 @@ function registerSessionTools(ctx, config, cursors) {
1868
1942
  },
1869
1943
  cursor: {
1870
1944
  oneOf: [{ type: "string" }, { type: "null" }],
1871
- description: "Continuation of one captured prefix and branch revision. Appended messages stay on that cut. A changed envelope or branch revision does not continue."
1945
+ description: "Continuation of one captured prefix and branch revision, in the same mode and options that produced it. Appended messages stay on that cut. A changed envelope, branch revision or option set does not continue."
1872
1946
  }
1873
1947
  },
1874
1948
  output: TEXT_OUTPUT,
@@ -2106,12 +2180,12 @@ const Config = Schema.object({
2106
2180
  searchLimit: Schema.number().default(20)
2107
2181
  }).i18n({
2108
2182
  zh: {
2109
- pageChars: "每页阅读内容的输出上限(UTF-8 字节)",
2183
+ pageChars: "明细与交接模式每页内容的输出上限(UTF-8 字节);对话预览按轮数分页,不受此限",
2110
2184
  listLimit: "每页会话列表的数量上限",
2111
2185
  searchLimit: "每页搜索结果的数量上限"
2112
2186
  },
2113
2187
  en: {
2114
- pageChars: "Maximum reading content per page (UTF-8 bytes)",
2188
+ pageChars: "Maximum content per details/handoff page (UTF-8 bytes); conversation previews page by turn count instead",
2115
2189
  listLimit: "Maximum sessions per list page",
2116
2190
  searchLimit: "Maximum search results per page"
2117
2191
  }