dsh-deja 0.21.6 → 0.22.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 +7 -1
- package/docs/zh.md +3 -1
- package/index.js +189 -8
- package/lib.js +23 -0
- package/package.json +3 -3
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
|
|
11
|
+
deja indexes the session files that forty 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
|
|
@@ -58,6 +58,12 @@ whether this machine's history answers the prompt and adds the answer to the
|
|
|
58
58
|
runtime context. Silence is the common case — it speaks only when there is
|
|
59
59
|
something to say.
|
|
60
60
|
|
|
61
|
+
The same recall also wires what `deja install dsh-auto` wires: after a read,
|
|
62
|
+
edit or write the model gets what past sessions said about that file, after a
|
|
63
|
+
command that failed it gets what this machine ran after the same error before,
|
|
64
|
+
a compaction keeps what the session was in the middle of, and a session that
|
|
65
|
+
ends drops its live stamp.
|
|
66
|
+
|
|
61
67
|
```yaml
|
|
62
68
|
- insert:
|
|
63
69
|
- id: deja
|
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
|
|
9
|
+
deja 索引 Claude Code、Codex、Cursor、opencode、Antigravity、Grok Build、Kimi、Cline、Zed 等其他四十个智能体本来就写在磁盘上的会话文件。不需要提前录制:历史已经在那里,包括安装 deja 之前的那些月份。如果你上周才转到 dsh,上周之前做过的事仍然可以在 dsh 里查到。
|
|
10
10
|
|
|
11
11
|
## 安装
|
|
12
12
|
|
|
@@ -42,6 +42,8 @@ dsh plugin --profile web add github:vshulcz/deja-vu#path:extensions/dsh
|
|
|
42
42
|
|
|
43
43
|
以及默认开启的自动召回:每个会话一次,插件把这个项目里以前的会话定下来的事加进来;每一步之前,插件会问 deja 这台机器的历史能否回答当前提问,并把结果加入运行时上下文。多数情况下它保持沉默,只有确实有内容时才开口。
|
|
44
44
|
|
|
45
|
+
同一套召回也接上了 `deja install dsh-auto` 接的那些:读、改、写一个文件之后,模型会拿到以前的会话对这个文件说过什么;命令失败之后,会拿到这台机器上次遇到同样错误后跑过的命令;压缩时保留会话正在做的事;会话结束时撤掉它的在线标记。
|
|
46
|
+
|
|
45
47
|
```yaml
|
|
46
48
|
- insert:
|
|
47
49
|
- id: deja
|
package/index.js
CHANGED
|
@@ -2,16 +2,17 @@
|
|
|
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-
|
|
5
|
+
// in Claude Code, Codex, Cursor, opencode and thirty-seven 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";
|
|
9
|
-
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
10
11
|
import { existsSync } from "node:fs";
|
|
11
12
|
import { homedir } from "node:os";
|
|
12
13
|
import { join } from "node:path";
|
|
13
14
|
|
|
14
|
-
import { argv, contributions, guarded } from "./lib.js";
|
|
15
|
+
import { argv, contributions, exitStatus, guarded, resultText } from "./lib.js";
|
|
15
16
|
|
|
16
17
|
const require = createRequire(import.meta.url);
|
|
17
18
|
|
|
@@ -105,7 +106,7 @@ function tools(ctx) {
|
|
|
105
106
|
guarded(() => ctx.tools.register(defineTool({
|
|
106
107
|
name: "deja_recall",
|
|
107
108
|
description:
|
|
108
|
-
"Search this machine's own past AI coding sessions — every agent used on it, including months before deja was installed. Use before debugging an error or re-implementing anything that may already exist. Match on the most specific token available: an exact error string, function name, file path or flag.",
|
|
109
|
+
"Search this machine's own past AI coding sessions — every agent used on it, including months before deja was installed. Use before debugging an error or re-implementing anything that may already exist, and before the first edit in a task and before calling a change done or ready to merge. Match on the most specific token available: an exact error string, function name, file path or flag.",
|
|
109
110
|
parameters: {
|
|
110
111
|
query: {
|
|
111
112
|
type: "string",
|
|
@@ -291,8 +292,14 @@ function autoDigest(ctx) {
|
|
|
291
292
|
);
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
// dsh names a session "session-<uuid>" and its log's folder the same; deja
|
|
296
|
+
// indexes it by the uuid, so that is the id every hook is given.
|
|
297
|
+
function bareSessionId(id) {
|
|
298
|
+
return String(id || "").replace(/^session-/, "");
|
|
299
|
+
}
|
|
300
|
+
|
|
294
301
|
function sessionId(agent) {
|
|
295
|
-
return (agent && (agent.sessionId || (agent.session && agent.session.id))) || "";
|
|
302
|
+
return bareSessionId((agent && (agent.sessionId || (agent.session && agent.session.id))) || "");
|
|
296
303
|
}
|
|
297
304
|
|
|
298
305
|
// The workspace a session belongs to. One web or tui process serves sessions
|
|
@@ -307,7 +314,10 @@ function sessionCwd(agent) {
|
|
|
307
314
|
function autoRecall(ctx) {
|
|
308
315
|
let asked = "";
|
|
309
316
|
let askedIn = "";
|
|
317
|
+
let askedBy = "";
|
|
310
318
|
let recalled = "";
|
|
319
|
+
const compacted = new Set();
|
|
320
|
+
watchCompaction(ctx, compacted);
|
|
311
321
|
|
|
312
322
|
guarded(() =>
|
|
313
323
|
ctx.systemPrompt.context({
|
|
@@ -319,11 +329,17 @@ function autoRecall(ctx) {
|
|
|
319
329
|
const prompt = lastHumanText(agent);
|
|
320
330
|
if (!prompt) return "";
|
|
321
331
|
const cwd = sessionCwd(agent);
|
|
322
|
-
|
|
323
|
-
|
|
332
|
+
const sid = sessionId(agent);
|
|
333
|
+
const afterCompaction = compacted.delete(sid);
|
|
334
|
+
// The same question asked in another workspace is a different question,
|
|
335
|
+
// and so is one asked again after its session compacted: deja then
|
|
336
|
+
// answers with the recovery packet, which only a prompt naming the
|
|
337
|
+
// session gets back.
|
|
338
|
+
if (prompt !== asked || cwd !== askedIn || sid !== askedBy || afterCompaction) {
|
|
324
339
|
asked = prompt;
|
|
325
340
|
askedIn = cwd;
|
|
326
|
-
|
|
341
|
+
askedBy = sid;
|
|
342
|
+
recalled = run(["hook-prompt", "--plain"], JSON.stringify({ prompt, cwd, session_id: sid }));
|
|
327
343
|
}
|
|
328
344
|
// Silence is the common case: this speaks only when the history answers.
|
|
329
345
|
return recalled;
|
|
@@ -332,6 +348,169 @@ function autoRecall(ctx) {
|
|
|
332
348
|
);
|
|
333
349
|
}
|
|
334
350
|
|
|
351
|
+
// runAsync is run for a seam dsh awaits, so the event loop goes on while deja
|
|
352
|
+
// answers. It never rejects.
|
|
353
|
+
function runAsync(args, input) {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
try {
|
|
356
|
+
const child = execFile(DEJA, args, { encoding: "utf8", timeout: 20000, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) =>
|
|
357
|
+
resolve(err ? "" : String(stdout || "").trim()),
|
|
358
|
+
);
|
|
359
|
+
child.on("error", () => resolve(""));
|
|
360
|
+
child.stdin.on("error", () => {});
|
|
361
|
+
child.stdin.end(input);
|
|
362
|
+
} catch {
|
|
363
|
+
resolve("");
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Compaction shadows the turns it summarises but leaves them in the session
|
|
369
|
+
// log, so deja reads the session there as compaction/start lands and keeps a
|
|
370
|
+
// recovery packet: the task, each command with how it went, what was still
|
|
371
|
+
// open. The log is written behind the session, so it is flushed first. dsh
|
|
372
|
+
// assembles a step's context before its agent/pre-step waterfall, where the
|
|
373
|
+
// compaction runs, so the step that compacts waits there for the capture and
|
|
374
|
+
// the packet rides the recall of the next step, or of the next turn.
|
|
375
|
+
function watchCompaction(ctx, compacted) {
|
|
376
|
+
if (typeof ctx.on !== "function") return;
|
|
377
|
+
const capturing = new Map();
|
|
378
|
+
const listen = (c) =>
|
|
379
|
+
c.on("session/event", (session, event) => {
|
|
380
|
+
if (!session || !event || event.type !== "compaction/start") return;
|
|
381
|
+
const sid = bareSessionId(session.id || (session.header && session.header.id));
|
|
382
|
+
if (!sid) return;
|
|
383
|
+
const cwd = (session.header && session.header.cwd) || process.cwd();
|
|
384
|
+
// One compaction can follow another inside a step; two captures at once
|
|
385
|
+
// contend for the index and the loser marks the packet unavailable, so
|
|
386
|
+
// each waits for the one before it.
|
|
387
|
+
const before = capturing.get(sid) || Promise.resolve();
|
|
388
|
+
const capture = before.then(async () => {
|
|
389
|
+
try {
|
|
390
|
+
if (c.sessions && typeof c.sessions.flush === "function") await c.sessions.flush(session);
|
|
391
|
+
} catch {}
|
|
392
|
+
await runAsync(["hook-precompact"], JSON.stringify({ session_id: sid, cwd, harness: "deepseek" }));
|
|
393
|
+
compacted.add(sid);
|
|
394
|
+
});
|
|
395
|
+
capturing.set(sid, capture);
|
|
396
|
+
capture.then(() => {
|
|
397
|
+
if (capturing.get(sid) === capture) capturing.delete(sid);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
try {
|
|
401
|
+
if (typeof ctx.inject === "function") ctx.inject(["sessions"], listen);
|
|
402
|
+
else listen(ctx);
|
|
403
|
+
} catch {}
|
|
404
|
+
try {
|
|
405
|
+
ctx.on("agent/pre-step", async (step, next) => {
|
|
406
|
+
const sid = sessionId(step && step.agent);
|
|
407
|
+
if (capturing.has(sid)) await capturing.get(sid);
|
|
408
|
+
const decision = await next();
|
|
409
|
+
if (capturing.has(sid)) await capturing.get(sid);
|
|
410
|
+
return decision;
|
|
411
|
+
});
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// The point of action, as `deja install dsh-auto` wires it. tools/post-execute
|
|
416
|
+
// runs on every tool result, and an accept decision's additionalContexts reach
|
|
417
|
+
// the model on the next step: a read, edit or write gets what deja knows about
|
|
418
|
+
// the file, a bash that exited non-zero gets the fix this machine found for
|
|
419
|
+
// that error before. The before-seam, tools/execute, has no channel to the
|
|
420
|
+
// model.
|
|
421
|
+
function toolNotes(ctx) {
|
|
422
|
+
if (typeof ctx.on !== "function") return;
|
|
423
|
+
try {
|
|
424
|
+
ctx.on("tools/post-execute", async (exec, result, next) => {
|
|
425
|
+
const decision = await next();
|
|
426
|
+
try {
|
|
427
|
+
if (!decision || decision.kind !== "accept" || !exec) return decision;
|
|
428
|
+
const note = await toolNote(exec, result);
|
|
429
|
+
if (!note) return decision;
|
|
430
|
+
return {
|
|
431
|
+
...decision,
|
|
432
|
+
additionalContexts: [...(decision.additionalContexts || []), noteMessage(note)],
|
|
433
|
+
};
|
|
434
|
+
} catch {
|
|
435
|
+
return decision;
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
} catch {}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// A session is over when dsh disposes of it, or when the process goes: on
|
|
442
|
+
// 0.1.1-rc.2 the headless profile exits without disposing anything, and only
|
|
443
|
+
// process "exit" runs. The digest and the recall stamp a session live, which
|
|
444
|
+
// keeps it out of its own MCP recall; dropping the stamp lets the next session
|
|
445
|
+
// be answered with it now rather than twenty minutes from now.
|
|
446
|
+
function sessionEnds(ctx) {
|
|
447
|
+
if (typeof ctx.on !== "function") return;
|
|
448
|
+
const live = new Set();
|
|
449
|
+
const end = (sid) => {
|
|
450
|
+
if (!sid || !live.has(sid)) return;
|
|
451
|
+
live.delete(sid);
|
|
452
|
+
run(["hook-session-end"], JSON.stringify({ session_id: sid }));
|
|
453
|
+
};
|
|
454
|
+
try {
|
|
455
|
+
ctx.on("session/created", (session) => {
|
|
456
|
+
const sid = bareSessionId(session && session.id);
|
|
457
|
+
if (sid) live.add(sid);
|
|
458
|
+
});
|
|
459
|
+
ctx.on("session/disposed", (session) => end(bareSessionId(session && session.id)));
|
|
460
|
+
process.on("exit", () => {
|
|
461
|
+
for (const sid of [...live]) end(sid);
|
|
462
|
+
});
|
|
463
|
+
} catch {}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function toolNote(exec, result) {
|
|
467
|
+
const args = exec.arguments || {};
|
|
468
|
+
const base = { session_id: sessionId(exec.agent), cwd: sessionCwd(exec.agent) };
|
|
469
|
+
if ((exec.name === "read" || exec.name === "edit" || exec.name === "write") && args.file_path) {
|
|
470
|
+
return runAsync(["hook-tool", "--plain"], JSON.stringify({
|
|
471
|
+
tool_name: exec.name,
|
|
472
|
+
tool_input: { file_path: String(args.file_path) },
|
|
473
|
+
...base,
|
|
474
|
+
}));
|
|
475
|
+
}
|
|
476
|
+
// The profile's other editor names the file "path"; view is its read.
|
|
477
|
+
if (exec.name === "str_replace_editor" && args.path) {
|
|
478
|
+
return runAsync(["hook-tool", "--plain"], JSON.stringify({
|
|
479
|
+
tool_name: args.command === "view" ? "read" : "edit",
|
|
480
|
+
tool_input: { file_path: String(args.path) },
|
|
481
|
+
...base,
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
// pwsh is the Windows shell, with the same markers.
|
|
485
|
+
if (exec.name === "bash" || exec.name === "pwsh") {
|
|
486
|
+
const status = exitStatus(resultText(result));
|
|
487
|
+
if (!(result && result.isError) && !status.failed) return "";
|
|
488
|
+
return runAsync(["hook-tool-after", "--plain"], JSON.stringify({
|
|
489
|
+
tool_name: exec.name === "pwsh" ? "powershell" : "bash",
|
|
490
|
+
tool_input: { command: String(args.command || "") },
|
|
491
|
+
tool_response: status.body,
|
|
492
|
+
...base,
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// A context message the way dsh's own plugins build one: user role, a plugin
|
|
499
|
+
// source so it is not read back as something the person typed, a fresh id,
|
|
500
|
+
// frozen.
|
|
501
|
+
function noteMessage(text) {
|
|
502
|
+
const message = {
|
|
503
|
+
role: "user",
|
|
504
|
+
content: [{ type: "text", text }],
|
|
505
|
+
source: { kind: "plugin", plugin: "deja", form: "notice" },
|
|
506
|
+
id: randomUUID(),
|
|
507
|
+
};
|
|
508
|
+
Object.freeze(message.content[0]);
|
|
509
|
+
Object.freeze(message.content);
|
|
510
|
+
Object.freeze(message.source);
|
|
511
|
+
return Object.freeze(message);
|
|
512
|
+
}
|
|
513
|
+
|
|
335
514
|
// lastHumanText is the newest thing the person actually typed. At assembly
|
|
336
515
|
// time the message has already left the inbox and has not been appended as a
|
|
337
516
|
// "user/message" yet — the only durable record of it is the inbox splice that
|
|
@@ -392,6 +571,8 @@ function apply(ctx, config) {
|
|
|
392
571
|
if (adds.recall) {
|
|
393
572
|
autoDigest(ctx);
|
|
394
573
|
autoRecall(ctx);
|
|
574
|
+
toolNotes(ctx);
|
|
575
|
+
sessionEnds(ctx);
|
|
395
576
|
}
|
|
396
577
|
}
|
|
397
578
|
|
package/lib.js
CHANGED
|
@@ -39,6 +39,29 @@ export function guarded(register) {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// resultText is the text a tool handed back to the model.
|
|
43
|
+
export function resultText(result) {
|
|
44
|
+
const content = result && result.content;
|
|
45
|
+
if (!Array.isArray(content)) return "";
|
|
46
|
+
return content
|
|
47
|
+
.filter((part) => part && part.type === "text" && typeof part.text === "string")
|
|
48
|
+
.map((part) => part.text)
|
|
49
|
+
.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// exitStatus reads how a dsh command ended. dsh reports it in the text rather
|
|
53
|
+
// than as an error result: a last line of "[exit code: N]" for a non-zero exit,
|
|
54
|
+
// or "[killed by signal: X]". Only the last line counts, since output may quote
|
|
55
|
+
// a marker anywhere else. The marker is cut from the body before the lookup:
|
|
56
|
+
// an error whose last line is "[exit code: 1]" matches nothing on file.
|
|
57
|
+
export function exitStatus(text) {
|
|
58
|
+
const signal = /\n\[killed by signal: [^\]\n]+\]$/.exec(text);
|
|
59
|
+
if (signal) return { body: text.slice(0, signal.index), failed: true };
|
|
60
|
+
const exit = /\n\[exit code: (\d+)\]$/.exec(text);
|
|
61
|
+
if (exit) return { body: text.slice(0, exit.index), failed: exit[1] !== "0" };
|
|
62
|
+
return { body: text, failed: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
42
65
|
// contributions says what this package adds, given what `deja install` already
|
|
43
66
|
// wrote into DSH_HOME and what the profile turned off. The rule is the same
|
|
44
67
|
// 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.
|
|
4
|
-
"description": "deja-vu memory for DeepSeek Harness: the session history of
|
|
3
|
+
"version": "0.22.0",
|
|
4
|
+
"description": "deja-vu memory for DeepSeek Harness: the session history of forty 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": {
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@vshulcz/deja-vu": "^0.
|
|
65
|
+
"@vshulcz/deja-vu": "^0.22.0"
|
|
66
66
|
},
|
|
67
67
|
"scripts": {
|
|
68
68
|
"test": "node --test test/*.test.js"
|