dsh-session-recall 0.5.1 → 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,6 +297,20 @@ 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.",
@@ -598,6 +613,11 @@ function createRecallTool(config, engine, approver) {
598
613
  if (query === "") return recallError(query, Object.assign(/* @__PURE__ */ new Error("empty query"), { code: "SESSION_QUERY_INVALID_QUERY" }));
599
614
  const limit = clamp(Math.trunc(args.limit ?? cfg.defaultLimit), 1, cfg.maxLimit);
600
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 {}
601
621
  const scopeHints = [];
602
622
  let wantAll = false;
603
623
  if (args.session_id == null && args.all_projects === true) {
@@ -633,6 +653,17 @@ function createRecallTool(config, engine, approver) {
633
653
  limit,
634
654
  cursor: brand(args.cursor)
635
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
+ };
636
667
  if (!cwdAllowed(page.session.cwd ?? null, cfg)) return {
637
668
  query,
638
669
  scope,
@@ -706,6 +737,7 @@ function createRecallTool(config, engine, approver) {
706
737
  };
707
738
  const ranked = rankingActive(rankingOptions);
708
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));
709
741
  let hint = null;
710
742
  let fallbackRan = false;
711
743
  let diagnostics = {
@@ -716,7 +748,7 @@ function createRecallTool(config, engine, approver) {
716
748
  fallbackRan = true;
717
749
  const scan = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
718
750
  const scanTitles = await titlesFor(engine, scan.items.map((item) => item.sessionId), exec.signal);
719
- items = rankItems(scan.items.map((item) => ({
751
+ items = rankItems(scan.items.filter((item) => allowedIds == null || allowedIds.has(item.sessionId)).map((item) => ({
720
752
  ...item,
721
753
  title: scanTitles.get(item.sessionId) ?? null
722
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.1",
4
+ "version": "0.6.0",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },