dsh-deja 0.22.2 → 0.23.1

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 CHANGED
@@ -8,7 +8,7 @@ DeepSeek Harness can already search its own sessions — that is what the built-
8
8
  `session-query` subsystem does. This plugin answers the other question: what you
9
9
  did in the *other* agents on this machine.
10
10
 
11
- deja indexes the session files that forty-one other coding agents — Claude Code,
11
+ deja indexes the session files that forty-two other coding agents — Claude Code,
12
12
  Codex, Cursor, opencode, Antigravity, Grok Build, Kimi, Cline and Zed among
13
13
  them — already write to disk. Nothing has to have been recorded ahead of time: the history is already
14
14
  there, including the months before deja was installed. If you moved to dsh last
package/docs/zh.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  DeepSeek Harness 自带的 `session-query` 已经可以检索它自己的会话。这个插件回答的是另一个问题:你在这台机器上**其他**智能体里做过什么。
8
8
 
9
- deja 索引 Claude Code、Codex、Cursor、opencode、Antigravity、Grok Build、Kimi、Cline、Zed 等其他四十一个智能体本来就写在磁盘上的会话文件。不需要提前录制:历史已经在那里,包括安装 deja 之前的那些月份。如果你上周才转到 dsh,上周之前做过的事仍然可以在 dsh 里查到。
9
+ deja 索引 Claude Code、Codex、Cursor、opencode、Antigravity、Grok Build、Kimi、Cline、Zed 等其他四十二个智能体本来就写在磁盘上的会话文件。不需要提前录制:历史已经在那里,包括安装 deja 之前的那些月份。如果你上周才转到 dsh,上周之前做过的事仍然可以在 dsh 里查到。
10
10
 
11
11
  ## 安装
12
12
 
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // dsh answers questions about its own sessions through the built-in
4
4
  // session-query subsystem. This plugin answers the other question: what you did
5
- // in Claude Code, Codex, Cursor, opencode and thirty-eight more agents, on this
5
+ // in Claude Code, Codex, Cursor, opencode and thirty-nine more agents, on this
6
6
  // machine, before dsh existed. The index is deja's; this file is the seam.
7
7
 
8
8
  import { createRequire } from "node:module";
@@ -12,7 +12,7 @@ import { existsSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { join } from "node:path";
14
14
 
15
- import { argv, contributions, exitStatus, guarded, newestEvents, noteBoard, resultText, splitHookReply } from "./lib.js";
15
+ import { argv, contributions, recallArgv, recallSilence, exitStatus, guarded, inert, newestEvents, noteBoard, resultText, splitHookReply } from "./lib.js";
16
16
 
17
17
  const require = createRequire(import.meta.url);
18
18
 
@@ -60,9 +60,10 @@ const DEJA = resolveDeja();
60
60
 
61
61
  // run returns stdout, or "" when deja is missing or says nothing. Memory is
62
62
  // optional everywhere: a throw here would end the turn.
63
- function run(args, input) {
63
+ function run(args, input, cwd) {
64
64
  try {
65
65
  return execFileSync(DEJA, args, {
66
+ cwd,
66
67
  encoding: "utf8",
67
68
  timeout: 20000,
68
69
  maxBuffer: 8 * 1024 * 1024,
@@ -95,8 +96,8 @@ function tools(ctx) {
95
96
  if (!defineTool) return;
96
97
 
97
98
  // Every tool here answers with the text deja printed, so one output
98
- // declaration serves them all. Printed with --framed: search, ctx, blame,
99
- // fix and how write for a person, and without the frame a tag planted in a
99
+ // declaration serves them all. Printed with --framed: ctx, blame, fix and
100
+ // how write for a person (recall frames its own page), and without the frame a tag planted in a
100
101
  // transcript reached the model as though deja had closed its own block. The schema is plain JSON Schema — a
101
102
  // schemastery instance is rejected as "schema must be a value schema object",
102
103
  // because the host validates that this is an ordinary JSON record.
@@ -119,14 +120,19 @@ function tools(ctx) {
119
120
  type: "number",
120
121
  description: "How many sessions to return. Default 5.",
121
122
  },
123
+ offset: {
124
+ type: "number",
125
+ description: "Sessions to skip, from the offset the previous page named. Default 0.",
126
+ },
122
127
  },
123
128
  output: TEXT_OUTPUT,
124
- execute(args) {
125
- // deja itself caps the window; asking for a hundred sessions would
126
- // spend the model's context on a tail nobody reads.
127
- const asked = Number.isFinite(args.limit) ? Math.trunc(args.limit) : 5;
128
- const limit = String(Math.min(20, Math.max(1, asked)));
129
- return Promise.resolve(answer(run(["--framed", ...argv("search", ["--json", "--limit", limit], args.query)])));
129
+ execute(args, exec) {
130
+ // The page the MCP recall tool serves, framed and budgeted by deja. It
131
+ // puts the project of its working directory first, so it runs in the
132
+ // calling session's workspace rather than wherever dsh was launched.
133
+ const call = recallArgv(args);
134
+ const out = run(call, undefined, sessionCwd(exec && exec.agent));
135
+ return Promise.resolve(answer(out || (INSTALLED ? recallSilence(call) : "")));
130
136
  },
131
137
  })));
132
138
 
@@ -282,19 +288,20 @@ function autoDigest(ctx, board) {
282
288
  ctx.systemPrompt.context({
283
289
  name: "deja:project",
284
290
  order: 110,
291
+ interpolate: false,
285
292
  text: (assembly) => {
286
293
  const agent = assembly && assembly.agent;
287
294
  if (!agent) return "";
288
295
  const sid = sessionId(agent);
289
296
  if (sid && seen.has(sid)) return "";
290
297
  if (sid) seen.add(sid);
291
- return askHook(
298
+ return inert(askHook(
292
299
  ["hook-context", "--plain"],
293
300
  ["hook-context"],
294
301
  JSON.stringify({ session_id: sid, cwd: sessionCwd(agent), source: "startup", deja_once: true }),
295
302
  sid,
296
303
  board,
297
- );
304
+ ));
298
305
  },
299
306
  }),
300
307
  );
@@ -344,6 +351,7 @@ function autoRecall(ctx, board) {
344
351
  ctx.systemPrompt.context({
345
352
  name: "deja:recall",
346
353
  order: 120,
354
+ interpolate: false,
347
355
  text: (assembly) => {
348
356
  const agent = assembly && assembly.agent;
349
357
  if (!agent) return "";
@@ -360,13 +368,13 @@ function autoRecall(ctx, board) {
360
368
  asked = prompt;
361
369
  askedIn = cwd;
362
370
  askedBy = sid;
363
- recalled = askHook(
371
+ recalled = inert(askHook(
364
372
  ["hook-prompt", "--plain"],
365
373
  ["hook-prompt"],
366
374
  JSON.stringify({ prompt, cwd, session_id: sid }),
367
375
  sid,
368
376
  board,
369
- );
377
+ ));
370
378
  }
371
379
  // Silence is the common case: this speaks only when the history answers.
372
380
  return recalled;
@@ -505,20 +513,23 @@ function toolNotes(ctx) {
505
513
  // be answered with it now rather than twenty minutes from now.
506
514
  function sessionEnds(ctx) {
507
515
  if (typeof ctx.on !== "function") return;
508
- const live = new Set();
516
+ // Each live session with the directory it opened in: the end refreshes that
517
+ // project's start digest, so the next session there sees this one.
518
+ const live = new Map();
509
519
  const end = (sid) => {
510
520
  if (!sid || !live.has(sid)) return;
521
+ const cwd = live.get(sid);
511
522
  live.delete(sid);
512
- run(["hook-session-end"], JSON.stringify({ session_id: sid }));
523
+ run(["hook-session-end"], JSON.stringify({ session_id: sid, cwd }));
513
524
  };
514
525
  try {
515
526
  ctx.on("session/created", (session) => {
516
527
  const sid = bareSessionId(session && session.id);
517
- if (sid) live.add(sid);
528
+ if (sid) live.set(sid, (session.header && session.header.cwd) || process.cwd());
518
529
  });
519
530
  ctx.on("session/disposed", (session) => end(bareSessionId(session && session.id)));
520
531
  process.on("exit", () => {
521
- for (const sid of [...live]) end(sid);
532
+ for (const sid of [...live.keys()]) end(sid);
522
533
  });
523
534
  } catch {}
524
535
  }
package/lib.js CHANGED
@@ -21,6 +21,35 @@ export function argv(cmd, flags, text) {
21
21
  return arg.startsWith("-") ? [cmd, ...flags, "--", arg] : [cmd, ...flags, arg];
22
22
  }
23
23
 
24
+ // recallArgv is the call behind the deja_recall tool: `deja recall`, the page
25
+ // the MCP recall tool serves, framed and held to its 4 KB budget by deja
26
+ // itself. The tool used to run `search --json`, which carries every hit's
27
+ // matched messages whole and handed a model 661,528 characters for five hits
28
+ // (#5568). The page ends with "call recall again with offset=N"; offset is
29
+ // how the tool asks for that, and it is sent only past the first page, so a
30
+ // deja too old to know --offset still answers the first one.
31
+ export function recallArgv(args) {
32
+ const count = (v, fallback) => {
33
+ const n = Math.trunc(Number(v));
34
+ return v == null || v === "" || !Number.isFinite(n) ? fallback : n;
35
+ };
36
+ const limit = Math.min(20, Math.max(1, count(args && args.limit, 5)));
37
+ const offset = Math.min(1e9, Math.max(0, count(args && args.offset, 0)));
38
+ const flags = ["--limit", String(limit)];
39
+ if (offset > 0) flags.push("--offset", String(offset));
40
+ return argv("recall", flags, args && args.query);
41
+ }
42
+
43
+ // recallSilence is the answer when `deja recall` printed nothing. Past the
44
+ // first page that is a deja older than --offset refusing the flag, not an
45
+ // empty history: recall always prints a line, "No more matches" included.
46
+ export function recallSilence(call) {
47
+ if (call.includes("--offset")) {
48
+ return "This deja cannot page recall. Run deja update, then ask again with the same offset.";
49
+ }
50
+ return "Nothing in this machine's history matches that.";
51
+ }
52
+
24
53
  // guarded runs one registration, and swallows the host's refusal of a name it
25
54
  // already holds.
26
55
  //
@@ -62,6 +91,18 @@ export function exitStatus(text) {
62
91
  return { body: text, failed: false };
63
92
  }
64
93
 
94
+ // inert keeps recalled text out of dsh's prompt templating. dsh reads every
95
+ // {{name}} in a context as a prompt variable and throws on one it does not
96
+ // know, which ends the turn before the model is called. Past sessions quote
97
+ // templates, and that very error, so one recall carrying {{cwd}} broke every
98
+ // turn after it (#5421). `interpolate: false` tells dsh 0.2.1-alpha.2 and later
99
+ // to leave the text alone; older hosts ignore it, and a listener on
100
+ // system-prompt/assemble may drop it, so the braces are split as well:
101
+ // { {cwd} } still reads as braces and holds no {{ to resolve.
102
+ export function inert(text) {
103
+ return String(text || "").replace(/\{(?=\{)/g, "{ ").replace(/\}(?=\})/g, "} ");
104
+ }
105
+
65
106
  // contributions says what this package adds, given what `deja install` already
66
107
  // wrote into DSH_HOME and what the profile turned off. The rule is the same
67
108
  // everywhere: fill the gaps, never repeat the installer.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-deja",
3
- "version": "0.22.2",
4
- "description": "deja-vu memory for DeepSeek Harness: the session history of forty-one other coding agents, searchable and recalled before each step — a local index, no LLM.",
3
+ "version": "0.23.1",
4
+ "description": "deja-vu memory for DeepSeek Harness: the session history of forty-two other coding agents, searchable and recalled before each step — a local index, no LLM.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "exports": {
@@ -68,7 +68,7 @@
68
68
  }
69
69
  },
70
70
  "dependencies": {
71
- "@vshulcz/deja-vu": "^0.22.2"
71
+ "@vshulcz/deja-vu": "^0.23.1"
72
72
  },
73
73
  "scripts": {
74
74
  "test": "node --test test/*.test.js"