dsh-session-recall 0.5.0 → 0.6.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
@@ -1,5 +1,5 @@
1
1
  import { ToolDefinition, ToolRunContext } from "@deepseek-ai/dsh-tools";
2
- import { SessionEventResultFilter, SessionEventSearchDocument, SessionEventSearchPage, SessionEventSearchRequest, SessionRecord, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
2
+ import { SessionEventResultFilter, SessionEventSearchDocument, SessionEventSearchPage, SessionEventSearchRequest, SessionLineageTrace, SessionRecord, SessionSearchExecContext, SessionSearchHit, SessionSearchPage, SessionSearchRequest, SessionTitleObservationResult } from "@deepseek-ai/dsh-session-query";
3
3
  import { JsonValue, SessionId } from "@deepseek-ai/dsh-session";
4
4
  import { Context } from "@deepseek-ai/cordis";
5
5
  import { ContentBlock } from "@deepseek-ai/dsh-llm";
@@ -50,6 +50,12 @@ interface RecallConfig {
50
50
  recencyHalfLifeDays?: number;
51
51
  /** Sessions started in these project directories rank first. Default: none. */
52
52
  pinnedCwds?: readonly string[];
53
+ /**
54
+ * Restrict recall results to sessions in the calling agent's own lineage
55
+ * (ancestors + self + descendants). Default `true` — other agents in the
56
+ * same project are invisible unless this is set to `false`.
57
+ */
58
+ callerTreeOnly?: boolean;
53
59
  }
54
60
  /** Validated, fully defaulted configuration. */
55
61
  interface NormalizedRecallConfig {
@@ -65,6 +71,7 @@ interface NormalizedRecallConfig {
65
71
  readonly allProjectsPolicy: AllProjectsPolicy;
66
72
  readonly recencyHalfLifeDays: number | undefined;
67
73
  readonly pinnedCwds: readonly string[];
74
+ readonly callerTreeOnly: boolean;
68
75
  }
69
76
  /** Default, clamp, and cross-check every optional field. */
70
77
  declare function normalizeRecallConfig(config?: RecallConfig): NormalizedRecallConfig;
@@ -186,6 +193,8 @@ interface RecallQueryEngine {
186
193
  readTitleSnapshots(sessionIds: readonly string[], signal?: AbortSignal): Promise<SessionTitleObservationResult[]>;
187
194
  listSessions(signal?: AbortSignal): Promise<SessionRecord[]>;
188
195
  filterEvents(sessionId: SessionId, filters: readonly SessionEventResultFilter[]): Promise<SessionEventSearchDocument[]>;
196
+ /** Trace the caller's session lineage for caller-authorization gating. */
197
+ traceSession(sessionId: SessionId, signal?: AbortSignal): Promise<SessionLineageTrace>;
189
198
  }
190
199
  declare const RECALL_TOOL_DESCRIPTION: string;
191
200
  /** The approval verdict vocabulary mirrored from `@deepseek-ai/dsh-user-approval`. */
package/lib/index.js CHANGED
@@ -97,7 +97,8 @@ function normalizeRecallConfig(config) {
97
97
  cwdDenylist: stringList(config?.cwdDenylist),
98
98
  allProjectsPolicy: normalizePolicy(config?.allProjectsPolicy),
99
99
  recencyHalfLifeDays: typeof config?.recencyHalfLifeDays === "number" && Number.isFinite(config.recencyHalfLifeDays) && config.recencyHalfLifeDays > 0 ? Math.min(3650, Math.trunc(config.recencyHalfLifeDays)) : void 0,
100
- pinnedCwds: stringList(config?.pinnedCwds)
100
+ pinnedCwds: stringList(config?.pinnedCwds),
101
+ callerTreeOnly: config?.callerTreeOnly !== false
101
102
  };
102
103
  }
103
104
  /** Whether a session cwd is searchable under the allowlist/denylist policy. */
@@ -296,11 +297,26 @@ function cjkZeroHitHint(query, zeroHits, enabled) {
296
297
  * calling agent's own project cwd unless the model explicitly widens the
297
298
  * scope and the deployment allows it.
298
299
  */
300
+ /** Walk a lineage trace and collect every session id in the tree. */
301
+ function collectLineageIds(trace) {
302
+ const ids = /* @__PURE__ */ new Set();
303
+ ids.add(trace.target.header.id);
304
+ for (const ancestor of trace.ancestors) ids.add(ancestor.header.id);
305
+ const walk = (nodes) => {
306
+ for (const node of nodes) {
307
+ ids.add(node.session.header.id);
308
+ walk(node.descendants);
309
+ }
310
+ };
311
+ walk(trace.descendants);
312
+ return ids;
313
+ }
299
314
  const RECALL_TOOL_DESCRIPTION = [
300
315
  "Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
301
316
  "Use it when the user refers to earlier work (\"that bug we fixed last week\", \"the font we chose for my resume\") or when prior context was compacted away.",
302
317
  "Matches whole words/phrases for English and code identifiers; a zero-hit Chinese (CJK) query automatically falls back to a substring scan in which every whitespace-separated term must match. Returns the best-matching event snippet per session plus the session id.",
303
318
  "Then use the read tool on files, or ask the user, to go deeper — this tool only points at history, it does not resume sessions.",
319
+ "To save a full evidence report of a hit session, call the transcript_export tool (from dsh-session-export) with its sessionId when available.",
304
320
  "Scoping: by default only sessions started in the current project directory; pass all_projects=true to search everywhere (the deployment may ignore it or require user approval).",
305
321
  "When the deployment enables redaction, secret-looking text in snippets appears as [REDACTED] or a #hash marker — treat it as removed; do not try to reconstruct or echo it.",
306
322
  "The first search after startup may be slow while the index builds."
@@ -597,6 +613,11 @@ function createRecallTool(config, engine, approver) {
597
613
  if (query === "") return recallError(query, Object.assign(/* @__PURE__ */ new Error("empty query"), { code: "SESSION_QUERY_INVALID_QUERY" }));
598
614
  const limit = clamp(Math.trunc(args.limit ?? cfg.defaultLimit), 1, cfg.maxLimit);
599
615
  const agentCwd = exec.agent?.session.header?.cwd ?? null;
616
+ const callerSessionId = exec.agent?.session?.id;
617
+ let allowedIds = null;
618
+ if (cfg.callerTreeOnly && callerSessionId != null) try {
619
+ allowedIds = collectLineageIds(await engine.traceSession(SessionId(callerSessionId), exec.signal));
620
+ } catch {}
600
621
  const scopeHints = [];
601
622
  let wantAll = false;
602
623
  if (args.session_id == null && args.all_projects === true) {
@@ -632,6 +653,17 @@ function createRecallTool(config, engine, approver) {
632
653
  limit,
633
654
  cursor: brand(args.cursor)
634
655
  }, { signal: exec.signal });
656
+ if (allowedIds != null && !allowedIds.has(args.session_id)) return {
657
+ query,
658
+ scope,
659
+ count: 0,
660
+ hasMore: false,
661
+ items: [],
662
+ nextCursor: null,
663
+ hint: "that session is outside your conversation lineage. Search without session_id to find sessions in your own tree, or ask the user to disable callerTreeOnly.",
664
+ redacted: 0,
665
+ diagnostics: null
666
+ };
635
667
  if (!cwdAllowed(page.session.cwd ?? null, cfg)) return {
636
668
  query,
637
669
  scope,
@@ -705,6 +737,7 @@ function createRecallTool(config, engine, approver) {
705
737
  };
706
738
  const ranked = rankingActive(rankingOptions);
707
739
  let items = rankItems(toItems(page.items, titles).filter((item) => cwdAllowed(item.cwd, cfg)), rankingOptions);
740
+ if (allowedIds != null) items = items.filter((item) => allowedIds.has(item.sessionId));
708
741
  let hint = null;
709
742
  let fallbackRan = false;
710
743
  let diagnostics = {
@@ -715,7 +748,7 @@ function createRecallTool(config, engine, approver) {
715
748
  fallbackRan = true;
716
749
  const scan = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
717
750
  const scanTitles = await titlesFor(engine, scan.items.map((item) => item.sessionId), exec.signal);
718
- items = rankItems(scan.items.map((item) => ({
751
+ items = rankItems(scan.items.filter((item) => allowedIds == null || allowedIds.has(item.sessionId)).map((item) => ({
719
752
  ...item,
720
753
  title: scanTitles.get(item.sessionId) ?? null
721
754
  })), rankingOptions);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-session-recall",
3
3
  "description": "Deterministic cross-session transcript retrieval for DeepSeek Harness: the model-facing `recall` tool searches past session logs with explicit scope control",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },