dsh-session-recall 0.5.1 → 0.7.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/README.md +1 -1
- package/lib/index.d.ts +16 -1
- package/lib/index.js +80 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ If you need agent memory orchestration, use a memory framework; if you need boun
|
|
|
21
21
|
| Capability focus | Memory frameworks | Generic transcript search | `dsh-session-recall` |
|
|
22
22
|
|---|---|---|---|
|
|
23
23
|
| Retrieval target | Derived memory objects | Varies by implementation | **Original session transcript events** |
|
|
24
|
-
| Scope control | Framework-specific | Often coarse | **cwd-scoped default + explicit `all_projects` gate** |
|
|
24
|
+
| Scope control | Framework-specific | Often coarse | **cwd-scoped default + explicit `all_projects`, `since_days`, `tools`, `errors_only` gate** |
|
|
25
25
|
| CJK behavior | Framework-specific | Often tokenizer-limited | **FTS + CJK zero-hit substring fallback** |
|
|
26
26
|
| Output contract | Usually framework-native | Varies | **Typed `recall` result with stable fields/hints** |
|
|
27
27
|
|
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;
|
|
@@ -129,6 +136,12 @@ interface RecallArgs {
|
|
|
129
136
|
all_projects?: boolean;
|
|
130
137
|
limit?: number;
|
|
131
138
|
cursor?: string;
|
|
139
|
+
/** Only return sessions/entries newer than this many days. */
|
|
140
|
+
since_days?: number;
|
|
141
|
+
/** Only return tool-related events whose tool name matches one of these (substring, case-insensitive). */
|
|
142
|
+
tools?: readonly string[];
|
|
143
|
+
/** Only return failed tool calls (tool results carrying an error). */
|
|
144
|
+
errors_only?: boolean;
|
|
132
145
|
}
|
|
133
146
|
//#endregion
|
|
134
147
|
//#region src/rank.d.ts
|
|
@@ -186,6 +199,8 @@ interface RecallQueryEngine {
|
|
|
186
199
|
readTitleSnapshots(sessionIds: readonly string[], signal?: AbortSignal): Promise<SessionTitleObservationResult[]>;
|
|
187
200
|
listSessions(signal?: AbortSignal): Promise<SessionRecord[]>;
|
|
188
201
|
filterEvents(sessionId: SessionId, filters: readonly SessionEventResultFilter[]): Promise<SessionEventSearchDocument[]>;
|
|
202
|
+
/** Trace the caller's session lineage for caller-authorization gating. */
|
|
203
|
+
traceSession(sessionId: SessionId, signal?: AbortSignal): Promise<SessionLineageTrace>;
|
|
189
204
|
}
|
|
190
205
|
declare const RECALL_TOOL_DESCRIPTION: string;
|
|
191
206
|
/** 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,13 +297,55 @@ 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
|
+
/** Whether a best-match event type is a tool result (vs user/assistant message). */
|
|
301
|
+
function isToolResultType(type) {
|
|
302
|
+
return type === "tool/result" || type.startsWith("tool/");
|
|
303
|
+
}
|
|
304
|
+
/** Apply the dimensional post-filters (sinceDays / tools / errorsOnly) to recall items. */
|
|
305
|
+
function applyDimensionFilters(items, args, now = Date.now()) {
|
|
306
|
+
let out = [...items];
|
|
307
|
+
if (args.since_days !== void 0 && args.since_days > 0) {
|
|
308
|
+
const cutoff = now - args.since_days * 864e5;
|
|
309
|
+
out = out.filter((item) => {
|
|
310
|
+
return Math.max(item.createdAt ?? 0, item.bestMatch?.time ?? 0) >= cutoff;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (args.tools !== void 0 && args.tools.length > 0) {
|
|
314
|
+
const needles = args.tools.map((t) => t.toLowerCase());
|
|
315
|
+
out = out.filter((item) => {
|
|
316
|
+
if (!isToolResultType(item.bestMatch?.type ?? "")) return false;
|
|
317
|
+
const snippet = (item.bestMatch?.snippet ?? "").toLowerCase();
|
|
318
|
+
return needles.some((n) => snippet.includes(n));
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
if (args.errors_only === true) out = out.filter((item) => {
|
|
322
|
+
if ((item.bestMatch?.type ?? "") !== "tool/result") return false;
|
|
323
|
+
const snippet = item.bestMatch?.snippet ?? "";
|
|
324
|
+
return snippet.includes("[error]") || /\berror\b|\bfailed\b/i.test(snippet);
|
|
325
|
+
});
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
/** Walk a lineage trace and collect every session id in the tree. */
|
|
329
|
+
function collectLineageIds(trace) {
|
|
330
|
+
const ids = /* @__PURE__ */ new Set();
|
|
331
|
+
ids.add(trace.target.header.id);
|
|
332
|
+
for (const ancestor of trace.ancestors) ids.add(ancestor.header.id);
|
|
333
|
+
const walk = (nodes) => {
|
|
334
|
+
for (const node of nodes) {
|
|
335
|
+
ids.add(node.session.header.id);
|
|
336
|
+
walk(node.descendants);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
walk(trace.descendants);
|
|
340
|
+
return ids;
|
|
341
|
+
}
|
|
299
342
|
const RECALL_TOOL_DESCRIPTION = [
|
|
300
343
|
"Search the FULL TEXT of past and current session transcripts on this machine (your own conversation history with this user).",
|
|
301
344
|
"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
345
|
"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
346
|
"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.",
|
|
304
347
|
"To save a full evidence report of a hit session, call the transcript_export tool (from dsh-session-export) with its sessionId when available.",
|
|
305
|
-
"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).",
|
|
348
|
+
"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). Narrow further with since_days (only sessions newer than N days), tools (only tool events whose tool name matches, e.g. tools=[\"bash\"]), or errors_only=true (only failed tool calls).",
|
|
306
349
|
"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.",
|
|
307
350
|
"The first search after startup may be slow while the index builds."
|
|
308
351
|
].join(" ");
|
|
@@ -585,6 +628,19 @@ function createRecallTool(config, engine, approver) {
|
|
|
585
628
|
cursor: {
|
|
586
629
|
type: "string",
|
|
587
630
|
description: "Opaque continuation cursor from a previous recall result with hasMore=true."
|
|
631
|
+
},
|
|
632
|
+
since_days: {
|
|
633
|
+
type: "integer",
|
|
634
|
+
description: "Only return sessions newer than this many days (0 = no time filter)."
|
|
635
|
+
},
|
|
636
|
+
tools: {
|
|
637
|
+
type: "array",
|
|
638
|
+
items: { type: "string" },
|
|
639
|
+
description: "Only return tool events whose tool name contains one of these substrings (case-insensitive), e.g. [\"bash\",\"read\"]."
|
|
640
|
+
},
|
|
641
|
+
errors_only: {
|
|
642
|
+
type: "boolean",
|
|
643
|
+
description: "Only return failed tool calls (tool results carrying an error)."
|
|
588
644
|
}
|
|
589
645
|
},
|
|
590
646
|
output: {
|
|
@@ -598,6 +654,11 @@ function createRecallTool(config, engine, approver) {
|
|
|
598
654
|
if (query === "") return recallError(query, Object.assign(/* @__PURE__ */ new Error("empty query"), { code: "SESSION_QUERY_INVALID_QUERY" }));
|
|
599
655
|
const limit = clamp(Math.trunc(args.limit ?? cfg.defaultLimit), 1, cfg.maxLimit);
|
|
600
656
|
const agentCwd = exec.agent?.session.header?.cwd ?? null;
|
|
657
|
+
const callerSessionId = exec.agent?.session?.id;
|
|
658
|
+
let allowedIds = null;
|
|
659
|
+
if (cfg.callerTreeOnly && callerSessionId != null) try {
|
|
660
|
+
allowedIds = collectLineageIds(await engine.traceSession(SessionId(callerSessionId), exec.signal));
|
|
661
|
+
} catch {}
|
|
601
662
|
const scopeHints = [];
|
|
602
663
|
let wantAll = false;
|
|
603
664
|
if (args.session_id == null && args.all_projects === true) {
|
|
@@ -633,6 +694,17 @@ function createRecallTool(config, engine, approver) {
|
|
|
633
694
|
limit,
|
|
634
695
|
cursor: brand(args.cursor)
|
|
635
696
|
}, { signal: exec.signal });
|
|
697
|
+
if (allowedIds != null && !allowedIds.has(args.session_id)) return {
|
|
698
|
+
query,
|
|
699
|
+
scope,
|
|
700
|
+
count: 0,
|
|
701
|
+
hasMore: false,
|
|
702
|
+
items: [],
|
|
703
|
+
nextCursor: null,
|
|
704
|
+
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.",
|
|
705
|
+
redacted: 0,
|
|
706
|
+
diagnostics: null
|
|
707
|
+
};
|
|
636
708
|
if (!cwdAllowed(page.session.cwd ?? null, cfg)) return {
|
|
637
709
|
query,
|
|
638
710
|
scope,
|
|
@@ -644,7 +716,7 @@ function createRecallTool(config, engine, approver) {
|
|
|
644
716
|
redacted: 0,
|
|
645
717
|
diagnostics: null
|
|
646
718
|
};
|
|
647
|
-
let items = eventItems(page, sessionId);
|
|
719
|
+
let items = applyDimensionFilters(eventItems(page, sessionId), args);
|
|
648
720
|
let hint = null;
|
|
649
721
|
let diagnostics = {
|
|
650
722
|
source: "fts",
|
|
@@ -659,7 +731,7 @@ function createRecallTool(config, engine, approver) {
|
|
|
659
731
|
scanBudget: cfg.cjkFallbackScanMax,
|
|
660
732
|
ranked: false
|
|
661
733
|
};
|
|
662
|
-
if (docs.length > 0) items = docs.slice(0, limit).map((doc) => ({
|
|
734
|
+
if (docs.length > 0) items = applyDimensionFilters(docs.slice(0, limit).map((doc) => ({
|
|
663
735
|
sessionId,
|
|
664
736
|
id8: id8(sessionId),
|
|
665
737
|
title: null,
|
|
@@ -673,7 +745,7 @@ function createRecallTool(config, engine, approver) {
|
|
|
673
745
|
time: doc.time,
|
|
674
746
|
snippet: snippetAround(doc.text, highlight, CJK_SNIPPET_CHARS)
|
|
675
747
|
}
|
|
676
|
-
}));
|
|
748
|
+
})), args);
|
|
677
749
|
hint = items.length > 0 ? cjkFallbackHint(items.length, cfg.cjkHint) : cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
678
750
|
} else if (items.length === 0) hint = cjkZeroHitHint(query, true, cfg.cjkHint);
|
|
679
751
|
const red = applyRedaction(items);
|
|
@@ -706,6 +778,8 @@ function createRecallTool(config, engine, approver) {
|
|
|
706
778
|
};
|
|
707
779
|
const ranked = rankingActive(rankingOptions);
|
|
708
780
|
let items = rankItems(toItems(page.items, titles).filter((item) => cwdAllowed(item.cwd, cfg)), rankingOptions);
|
|
781
|
+
if (allowedIds != null) items = items.filter((item) => allowedIds.has(item.sessionId));
|
|
782
|
+
items = applyDimensionFilters(items, args);
|
|
709
783
|
let hint = null;
|
|
710
784
|
let fallbackRan = false;
|
|
711
785
|
let diagnostics = {
|
|
@@ -716,7 +790,7 @@ function createRecallTool(config, engine, approver) {
|
|
|
716
790
|
fallbackRan = true;
|
|
717
791
|
const scan = await cjkScanSessions(engine, query, agentCwd, wantAll, cfg, cfg.cjkFallbackScanMax, limit, exec.signal);
|
|
718
792
|
const scanTitles = await titlesFor(engine, scan.items.map((item) => item.sessionId), exec.signal);
|
|
719
|
-
items = rankItems(scan.items.map((item) => ({
|
|
793
|
+
items = rankItems(scan.items.filter((item) => allowedIds == null || allowedIds.has(item.sessionId)).map((item) => ({
|
|
720
794
|
...item,
|
|
721
795
|
title: scanTitles.get(item.sessionId) ?? null
|
|
722
796
|
})), 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.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|