@vshulcz/pi-deja 0.22.2 → 0.23.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.
Files changed (3) hide show
  1. package/index.ts +49 -7
  2. package/lib.mjs +30 -6
  3. package/package.json +2 -2
package/index.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  // deja-vu for pi: recall from the coding sessions already on this machine at
2
2
  // session start and on every prompt, and /deja to search them by hand.
3
3
  import { execFileSync } from "node:child_process";
4
- import { accessSync, constants, existsSync } from "node:fs";
4
+ import { accessSync, closeSync, constants, existsSync, openSync, readSync } from "node:fs";
5
5
  import { createRequire } from "node:module";
6
6
  import { homedir } from "node:os";
7
- import { join } from "node:path";
7
+ import { basename, join } from "node:path";
8
8
 
9
- import { argv, contextText, installerExtensionPaths, sessionKey } from "./lib.mjs";
9
+ import { argv, branchHoldsRecall, contextText, installerExtensionPaths, sessionKey } from "./lib.mjs";
10
10
 
11
11
  const require = createRequire(import.meta.url);
12
12
 
@@ -72,7 +72,7 @@ export default function (pi: any) {
72
72
  // it already does everything below, and two copies would inject the
73
73
  // recall twice and register /deja twice.
74
74
  try {
75
- if (installerExtensionPaths(homedir()).some((p) => existsSync(p))) return;
75
+ if (installerExtensionPaths(homedir(), process.env).some((p) => existsSync(p))) return;
76
76
  } catch {}
77
77
 
78
78
  // Once per session, not once per process: /new, a resume and a fork keep the
@@ -124,16 +124,58 @@ export default function (pi: any) {
124
124
  },
125
125
  });
126
126
 
127
+ // The session a fork was made from, named on its header by path, when the
128
+ // fork holds a copy of its turns: a fork at the first message copies
129
+ // nothing, and its source is no part of it. The id is on that file's own
130
+ // header; pi names the file <time>_<id>.jsonl.
131
+ const parents: Record<string, string> = {};
132
+ const parentID = (ctx: any): string => {
133
+ if (session in parents) return parents[session];
134
+ let id = "";
135
+ try {
136
+ const m = ctx.sessionManager;
137
+ const path = String((m.getHeader() || {}).parentSession || "");
138
+ const copied = (m.getBranch() || []).some((e: any) => e && e.type === "message");
139
+ if (path && copied) {
140
+ try {
141
+ const fd = openSync(path, "r");
142
+ const buf = Buffer.alloc(64 * 1024);
143
+ const n = readSync(fd, buf, 0, buf.length, 0);
144
+ closeSync(fd);
145
+ id = String(JSON.parse(buf.subarray(0, n).toString("utf8").split("\n", 1)[0]).id || "");
146
+ } catch {}
147
+ if (!id) {
148
+ const name = basename(path).replace(/\.jsonl$/, "");
149
+ id = name.slice(name.lastIndexOf("_") + 1);
150
+ }
151
+ }
152
+ } catch {}
153
+ if (session) parents[session] = id;
154
+ return id;
155
+ };
156
+
127
157
  pi.on("before_agent_start", async (event: any, ctx: any) => {
128
158
  try {
129
159
  const key = sessionKey(event, ctx);
130
160
  if (key) session = key;
161
+ // A resumed session holds the digest it opened with, and pi sends it to
162
+ // the model again with the rest of the session; a fork holds a copy of
163
+ // its source's. Asking for another put a second copy in front of the
164
+ // model, and in a fork one that led with the source itself.
165
+ const parent = parentID(ctx);
166
+ try {
167
+ if (!injected.has(session) && branchHoldsRecall(ctx.sessionManager.getBranch())) {
168
+ injected.add(session);
169
+ const stats = run(["statusline"], "");
170
+ if (stats) ctx.ui.setStatus("deja", stats);
171
+ }
172
+ } catch {}
131
173
  if (!injected.has(session)) {
132
174
  // The session goes with it: hook-context marks the one starting as
133
175
  // live, which keeps it out of its own MCP recall on this first turn
134
176
  // (#4394, as #4246 and #4273 did for Hermes and opencode).
135
177
  const { context: digest, receipt } = contextText(
136
- run(["hook-context"], JSON.stringify({ session_id: session, cwd: process.cwd() })),
178
+ run(["hook-context"], JSON.stringify({ session_id: session, parent_session_id: parent, cwd: process.cwd() })),
137
179
  );
138
180
  if (digest) {
139
181
  injected.add(session);
@@ -155,7 +197,7 @@ export default function (pi: any) {
155
197
  }
156
198
  return;
157
199
  }
158
- const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session }));
200
+ const raw = run(["hook-prompt"], JSON.stringify({ prompt: event.prompt || "", session_id: session, parent_session_id: parent }));
159
201
  if (!raw) return;
160
202
  const resp = JSON.parse(raw);
161
203
  if (resp && resp.systemMessage) ctx.ui.notify(resp.systemMessage, "info");
@@ -251,7 +293,7 @@ export default function (pi: any) {
251
293
  // on a switch ctx may already name the next session.
252
294
  pi.on("session_shutdown", async (_event: any) => {
253
295
  try {
254
- if (session) run(["hook-session-end"], JSON.stringify({ session_id: session }), 2000);
296
+ if (session) run(["hook-session-end"], JSON.stringify({ session_id: session, cwd: process.cwd() }), 2000);
255
297
  } catch {}
256
298
  });
257
299
  }
package/lib.mjs CHANGED
@@ -7,19 +7,22 @@ import { join } from "node:path"
7
7
  // installerExtensionPath is where `deja install pi-auto` writes its own
8
8
  // extension. pi loads that file and this package side by side, and both
9
9
  // would inject recall and register /deja, so this package stands down when
10
- // the installer's copy is present. Mirrors PiConfigDir() in the installer,
11
- // which is ~/.pi/agent with no override — pi has none either.
12
- export function installerExtensionPath(home) {
13
- return join(home || "", ".pi", "agent", "extensions", "deja.ts")
10
+ // the installer's copy is present. Mirrors PiConfigDir() in the installer:
11
+ // ~/.pi/agent, or PI_CODING_AGENT_DIR when pi was told to live elsewhere.
12
+ export function installerExtensionPath(home, env = {}) {
13
+ let dir = String(env.PI_CODING_AGENT_DIR || "")
14
+ if (dir === "~") dir = home || ""
15
+ else if (dir.startsWith("~/") || dir.startsWith("~\\")) dir = join(home || "", dir.slice(2))
16
+ return join(dir || join(home || "", ".pi", "agent"), "extensions", "deja.ts")
14
17
  }
15
18
 
16
19
  // installerExtensionPaths are every place `deja install` writes an extension
17
20
  // this package would duplicate. omp runs the same package and the installer
18
21
  // writes it under ~/.omp/agent/extensions/deja/index.js, so checking pi's path
19
22
  // alone left both running side by side (#3180).
20
- export function installerExtensionPaths(home) {
23
+ export function installerExtensionPaths(home, env = {}) {
21
24
  return [
22
- installerExtensionPath(home),
25
+ installerExtensionPath(home, env),
23
26
  join(home || "", ".omp", "agent", "extensions", "deja", "index.js"),
24
27
  ]
25
28
  }
@@ -64,3 +67,24 @@ export function sessionKey(event, ctx) {
64
67
  } catch {}
65
68
  return (event && (event.sessionId || event.session_id)) || (ctx && ctx.session && ctx.session.id) || ""
66
69
  }
70
+
71
+ // branchHoldsRecall reports whether what pi sends the model from this branch
72
+ // holds a recall block this package injected: what follows the last
73
+ // compaction, and the entries it kept from before it, firstKeptEntryId on.
74
+ // pi keeps the block as a custom_message and sends it again on a resume, and
75
+ // a fork copies it from its source.
76
+ export function branchHoldsRecall(branch) {
77
+ const entries = Array.isArray(branch) ? branch : []
78
+ let keep = ""
79
+ for (let i = entries.length - 1; i >= 0; i--) {
80
+ const e = entries[i] || {}
81
+ if (keep === "" && e.type === "compaction") {
82
+ keep = String(e.firstKeptEntryId || "")
83
+ if (!keep) return false
84
+ continue
85
+ }
86
+ if (e.type === "custom_message" && e.customType === "deja-recall") return true
87
+ if (keep && e.id === keep) return false
88
+ }
89
+ return false
90
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vshulcz/pi-deja",
3
- "version": "0.22.2",
3
+ "version": "0.23.0",
4
4
  "description": "deja-vu memory for pi: the coding sessions already on this machine — pi, Claude Code, Codex, Cursor and 38 more — recalled at session start and on every prompt, searchable with /deja. A local index, no LLM.",
5
5
  "type": "module",
6
6
  "files": [
@@ -45,7 +45,7 @@
45
45
  "node": ">=18"
46
46
  },
47
47
  "dependencies": {
48
- "@vshulcz/deja-vu": "^0.22.2"
48
+ "@vshulcz/deja-vu": "^0.23.0"
49
49
  },
50
50
  "pi": {
51
51
  "extensions": [