memhouse 0.18.12 → 0.20.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.
Files changed (169) hide show
  1. package/AGENTS.md +22 -151
  2. package/CHANGELOG.md +263 -29
  3. package/LICENSE +202 -22
  4. package/LICENSES/agentlytics-MIT.txt +21 -0
  5. package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
  6. package/LICENSES/models.dev-MIT.txt +22 -0
  7. package/NOTICE +28 -0
  8. package/README.md +42 -20
  9. package/SECURITY.md +21 -8
  10. package/bin/memhouse.js +2223 -311
  11. package/docs/README.md +11 -9
  12. package/docs/guides/admin-password.md +37 -10
  13. package/docs/guides/configuration.md +13 -6
  14. package/docs/guides/daemons.md +64 -3
  15. package/docs/guides/import-claude-ai.md +204 -0
  16. package/docs/guides/install.md +11 -6
  17. package/docs/guides/invite.md +21 -7
  18. package/docs/guides/operate.md +114 -7
  19. package/docs/guides/search-and-resume.md +72 -3
  20. package/docs/guides/share.md +91 -52
  21. package/docs/guides/storage.md +44 -4
  22. package/docs/guides/troubleshooting.md +121 -14
  23. package/docs/guides/uninstall.md +4 -3
  24. package/docs/guides/update.md +16 -0
  25. package/docs/guides/upgrade-to-0.20.md +188 -0
  26. package/docs/releases/README.md +2 -1
  27. package/docs/releases/v0.18.10.md +5 -5
  28. package/docs/releases/v0.18.11.md +3 -3
  29. package/docs/releases/v0.20.0.md +627 -0
  30. package/docs/releases/v0.20.1.md +114 -0
  31. package/docs/tutorials/01-first-house.md +10 -7
  32. package/docs/tutorials/02-join-a-house.md +24 -15
  33. package/docs/tutorials/03-share-your-memory.md +40 -31
  34. package/editors/antigravity.js +1 -80
  35. package/editors/base.js +463 -9
  36. package/editors/bridge.js +119 -0
  37. package/editors/claude.js +273 -105
  38. package/editors/codex.js +278 -52
  39. package/editors/copilot.js +1 -70
  40. package/editors/cursor.js +1 -75
  41. package/editors/derive-fingerprints.json +18 -0
  42. package/editors/harness-paths.js +69 -0
  43. package/editors/index.js +1 -20
  44. package/editors/normalise/agy.js +910 -0
  45. package/editors/normalise/copilot-cli.js +321 -0
  46. package/editors/normalise/cursor-agent.js +852 -0
  47. package/editors/normalise/cursor.js +474 -0
  48. package/editors/normalise/devin.js +575 -0
  49. package/editors/normalise/gemini-cli.js +602 -0
  50. package/editors/normalise/goose.js +673 -0
  51. package/editors/normalise/opencode.js +633 -0
  52. package/editors/normalise/vscode.js +549 -0
  53. package/editors/normalise/zed.js +473 -0
  54. package/editors/opencode-v4.js +119 -0
  55. package/editors/scope.js +14 -3
  56. package/editors/sessions-log.js +349 -0
  57. package/editors/sqlite.js +28 -1
  58. package/editors/vscode.js +1 -72
  59. package/editors/windsurf.js +1 -143
  60. package/examples/01-local-one-machine/README.md +24 -19
  61. package/examples/01-local-one-machine/run.sh +1 -1
  62. package/examples/02-existing-clickhouse/README.md +30 -22
  63. package/examples/02-existing-clickhouse/run.sh +5 -4
  64. package/examples/03-team-house/README.md +44 -37
  65. package/examples/03-team-house/run.sh +35 -18
  66. package/examples/04-self-hosted-team/README.md +29 -14
  67. package/examples/04-self-hosted-team/compose.yml +5 -2
  68. package/examples/04-self-hosted-team/smoke.sh +26 -12
  69. package/examples/README.md +8 -4
  70. package/examples/fixtures/README.md +4 -1
  71. package/examples/fixtures/make-sessions.js +8 -5
  72. package/memhouse/admin-secret.js +89 -24
  73. package/memhouse/argv-secrets.js +64 -0
  74. package/memhouse/channel.js +49 -9
  75. package/memhouse/codex-plugin.js +194 -0
  76. package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
  77. package/memhouse/delivery/AGENT-INSTALL.md +22 -11
  78. package/memhouse/delivery/INVITATION.md.tpl +44 -45
  79. package/memhouse/delivery/PROMPT.md +53 -5
  80. package/memhouse/delivery/UPGRADE.md.tpl +1 -1
  81. package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
  82. package/memhouse/delivery/codex/SKILL.md +50 -0
  83. package/memhouse/delivery/kernel-install.md +17 -9
  84. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
  85. package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
  86. package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
  87. package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
  88. package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
  89. package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
  90. package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
  91. package/memhouse/deploy.js +1 -1
  92. package/memhouse/flags.js +35 -18
  93. package/memhouse/help.js +37 -0
  94. package/memhouse/host.js +82 -2
  95. package/memhouse/house/HOUSE.md +2 -2
  96. package/memhouse/house/convert-profile.js +638 -0
  97. package/memhouse/house/convert-v3.js +742 -0
  98. package/memhouse/house/doctor-v3.js +324 -0
  99. package/memhouse/house/host-alias.js +85 -0
  100. package/memhouse/house/house.js +3 -3
  101. package/memhouse/house/migrate.js +2 -2
  102. package/memhouse/house/profile-dedupe.js +183 -0
  103. package/memhouse/house/profile-edges.js +554 -0
  104. package/memhouse/house/profile-house.js +430 -0
  105. package/memhouse/house/profile-rollup.js +190 -0
  106. package/memhouse/house/q-cli.js +863 -0
  107. package/memhouse/house/queries-v3.js +1013 -0
  108. package/memhouse/house/reconcile-v3.js +192 -0
  109. package/memhouse/house/records-to-profile.js +174 -0
  110. package/memhouse/house/records-to-rows.js +169 -0
  111. package/memhouse/house/relocate.js +2 -1
  112. package/memhouse/house/schema-choice.js +121 -0
  113. package/memhouse/house/schema-profile.js +290 -0
  114. package/memhouse/house/schema-profile.sql.tpl +398 -0
  115. package/memhouse/house/schema-v3.js +267 -0
  116. package/memhouse/house/schema-v3.sql.tpl +213 -0
  117. package/memhouse/import/claude-ai.js +420 -0
  118. package/memhouse/import/import.js +372 -0
  119. package/memhouse/import/pyjson.js +206 -0
  120. package/memhouse/import/records.js +64 -0
  121. package/memhouse/import/writer-profile.js +192 -0
  122. package/memhouse/import/writer-v3.js +240 -0
  123. package/memhouse/import/zip.js +112 -0
  124. package/memhouse/invitation.js +102 -0
  125. package/memhouse/invitefile.js +5 -2
  126. package/memhouse/plugin-copies.js +176 -0
  127. package/memhouse/provision.js +7 -2
  128. package/memhouse/release-hint.js +24 -0
  129. package/memhouse/resume.js +4 -4
  130. package/memhouse/server/api-v3.js +632 -0
  131. package/memhouse/server/dashboard-auth.js +181 -0
  132. package/memhouse/server/dashboard-port.js +91 -0
  133. package/memhouse/server/queries.js +30 -0
  134. package/memhouse/server/server.js +118 -20
  135. package/memhouse/service.js +1 -1
  136. package/memhouse/share.js +687 -1
  137. package/memhouse/shipper/adapter-capture.js +756 -0
  138. package/memhouse/shipper/capture.js +2571 -0
  139. package/memhouse/shipper/dedupe-profile.js +237 -0
  140. package/memhouse/shipper/dedupe-v3.js +1027 -0
  141. package/memhouse/shipper/forks.js +233 -0
  142. package/memhouse/shipper/log-adapter.js +219 -0
  143. package/memhouse/shipper/maintenance.js +49 -0
  144. package/memhouse/shipper/normalise-run.js +148 -0
  145. package/memhouse/shipper/passlog.js +68 -5
  146. package/memhouse/shipper/profile-capture.js +903 -0
  147. package/memhouse/shipper/ship.js +91 -8
  148. package/memhouse/shipper/snapshots.js +193 -0
  149. package/memhouse/shipper/source-roots.js +111 -0
  150. package/memhouse/version-notice.js +66 -0
  151. package/package.json +14 -7
  152. package/public/THIRD_PARTY_NOTICES.txt +2 -2
  153. package/public/assets/index-CcNnhfM3.js +78 -0
  154. package/public/index.html +1 -1
  155. package/ui/package-lock.json +56 -37
  156. package/ui/package.json +1 -0
  157. package/ui/src/App.jsx +29 -14
  158. package/ui/src/components/ChatSidebar.jsx +69 -12
  159. package/ui/src/lib/api.js +76 -0
  160. package/ui/src/lib/dashboard-token.js +41 -0
  161. package/ui/src/main.jsx +22 -5
  162. package/ui/src/pages/Dashboard.jsx +22 -36
  163. package/ui/third-party-notices.js +119 -0
  164. package/ui/vite.config.js +5 -1
  165. package/docs/releases/v0.18.12.md +0 -59
  166. package/memhouse/COMPETITION.md +0 -221
  167. package/memhouse/DESIGN.md +0 -222
  168. package/memhouse/RESUME-AND-UPDATE.md +0 -182
  169. package/public/assets/index-CnWzrjF3.js +0 -78
@@ -0,0 +1,549 @@
1
+ 'use strict';
2
+ // v4 wave 1: VS Code, Local chat engine → normalised session log records (docs/design/sessions-log.md).
3
+ //
4
+ // Store: <user-data>/User/workspaceStorage/<ws>/chatSessions/<sid>.jsonl (and
5
+ // User/globalStorage/emptyWindowChatSessions/ for chats opened with no folder). The Copilot engine's
6
+ // sessions live in ~/.copilot/session-state and are captured by the copilot-cli normaliser, not here
7
+ // (sessions-model §1 rule 3: the vscode harness covers only chatSessions, engine local).
8
+ // Spec: docs/design/editor-shapes.md "VS Code 1.142", "The Local change log" (repository),
9
+ // sessions-model v0 and its annex row "vscode (engine local)".
10
+ //
11
+ // Rules from the spec:
12
+ // - the file is a change log: kind 0 = snapshot, 1 = set at path k, 2 = splice at k (truncate at i
13
+ // when given, then append v), 3 = delete at k. The conversation is the fold's `requests[]`;
14
+ // - a kind 2 with i is an explicit, append-only retraction (an edited request or a restored
15
+ // checkpoint; sessions-model §2.6 (a)): the fold remembers the cut requests, they stay in the log as
16
+ // messages with state 'retracted', and a lifecycle record {event: 'rewind'} follows them. A cut
17
+ // request whose id the same splice re-appends is an in-place update, not a retraction. What leaves
18
+ // the store for real (a new kind 0 snapshot) is left to vanish;
19
+ // - one speaker per message: the user message is `requestId`, the reply is `responseId`, falling back
20
+ // to `<requestId>:response`;
21
+ // - `toolInvocationSerialized` holds the call and its result (`toolCallId`). Arguments and the result
22
+ // the model saw are in `result.metadata.toolCallRounds` / `toolCallResults`, keyed by the call id
23
+ // plus a `__vscode-<n>` suffix;
24
+ // - thinking parts are empty plus an opaque id: a reasoning part with `opaque: true`;
25
+ // - the model is the one Auto resolved (`result.metadata.resolvedModel`, the `autoModeResolution`
26
+ // part), never the `copilot/auto` placeholder, which is kept as `model_requested`;
27
+ // - usage: per request `promptTokens` / `completionTokens` (not `result.metadata.outputTokens`),
28
+ // basis unknown, and `copilotCredits` as `credits`; a stored zero is null;
29
+ // - `hiddenFromTranscript` marks the request's messages `hidden`; a tool shown with presentation
30
+ // `hidden` marks its part; a terminal tool keeps `terminalCommandState.exitCode` as `exit_code`;
31
+ // - an MCP tool's `server` is its ToolDataSource label (`source: {type: 'mcp', label, serverLabel}`:
32
+ // the configured name, else the name the server reports). The toolId prefix (`mcp_<label>_<tool>`,
33
+ // sanitised and cut to a length) is not split: a label with '_' in it would be cut wrong. No
34
+ // `source` is written on messages: the Local store marks no request as injected or automated
35
+ // (hiddenFromTranscript is kept as `hidden`, not read as a source);
36
+ // - writes lag the UI by up to ~2 minutes: a reply with no result and no final state is held back;
37
+ // - title: the last `customTitle`; the file name stem is the session id; no fork link exists
38
+ // (a fork mints new ids and timestamps, in one kind 0 snapshot);
39
+ // - redaction by field path: `selectedModel.metadata.auth`, wherever a `selectedModel` sits.
40
+
41
+ const fs = require('fs');
42
+ const path = require('path');
43
+ const { fileURLToPath } = require('url');
44
+
45
+ const HARNESS = 'vscode';
46
+ const ENGINE = 'local';
47
+ const SECRET_PATHS = [['selectedModel', 'metadata', 'auth']];
48
+ const CLIENTS = { Code: 'vscode', 'Code - Insiders': 'vscode-insiders', 'Code - OSS': 'code-oss', VSCodium: 'vscodium' };
49
+ // modelState.value, as VS Code's ResponseModelState: 0 pending, 1 complete, 2 cancelled, 3 failed,
50
+ // 4 needs input. Only 1 is observed in the fixtures; the rest is UNVERIFIED.
51
+ const MS = { PENDING: 0, COMPLETE: 1, CANCELLED: 2, FAILED: 3, NEEDS_INPUT: 4 };
52
+ // isConfirmed.type, as VS Code's ToolConfirmKind: 0 denied, 1 not needed, 2 setting, 3 per tool,
53
+ // 4 user action, 5 skipped. 1 and 4 are observed; 0 and 5 are UNVERIFIED.
54
+ const CONFIRM_DENIED = 0;
55
+ const CONFIRM_SKIPPED = 5;
56
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
57
+
58
+ /** The default user-data folder of VS Code stable on this platform. */
59
+ function storeRootDefault() {
60
+ // harness-paths' data folder: Application Support, APPDATA or XDG_CONFIG_HOME, as VS Code resolves it.
61
+ return require('../harness-paths').dataFolder(HARNESS);
62
+ }
63
+
64
+ /** A workspace's folder from workspaceStorage/<ws>/workspace.json: a path for file URIs, else the URI. */
65
+ function workspaceFolder(wsDir) {
66
+ let j;
67
+ try { j = JSON.parse(fs.readFileSync(path.join(wsDir, 'workspace.json'), 'utf-8')); } catch { return ''; }
68
+ const uri = typeof j.folder === 'string' ? j.folder : (typeof j.workspace === 'string' ? j.workspace : '');
69
+ if (!uri.startsWith('file:')) return uri;
70
+ try { return fileURLToPath(uri); } catch { return decodeURIComponent(uri.replace(/^file:\/\//, '')); }
71
+ }
72
+
73
+ /**
74
+ * Sessions in one store (a VS Code user-data folder, the parent of User/). One entry per chat file;
75
+ * where one id has both a .json (pre-change-log) and a .jsonl file, the .jsonl wins. Each entry names
76
+ * its own chat file, so the runner re-reads it when that changes. The workspace.json is shared by the
77
+ * workspace's chats and not named; the cwd read from it is in the entry. Nothing in an entry moves
78
+ * while the file stays the same (the mtime only picks between two files).
79
+ */
80
+ function list(storeRoot = storeRootDefault()) {
81
+ let harnessPath;
82
+ try { harnessPath = fs.realpathSync.native(storeRoot); } catch { return []; }
83
+ const client = CLIENTS[path.basename(harnessPath)] || 'vscode';
84
+ const user = path.join(harnessPath, 'User');
85
+ const wsRoot = path.join(user, 'workspaceStorage');
86
+ const dirs = [];
87
+ let wss = [];
88
+ try { wss = fs.readdirSync(wsRoot).sort(); } catch { wss = []; }
89
+ for (const ws of wss) dirs.push({ dir: path.join(wsRoot, ws, 'chatSessions'), wsDir: path.join(wsRoot, ws), workspace: ws });
90
+ dirs.push({ dir: path.join(user, 'globalStorage', 'emptyWindowChatSessions'), wsDir: null, workspace: '' });
91
+
92
+ const bySid = new Map();
93
+ for (const d of dirs) {
94
+ let names;
95
+ try { names = fs.readdirSync(d.dir).sort(); } catch { continue; }
96
+ let cwd = null;
97
+ for (const name of names) {
98
+ const m = /^(.+)\.(jsonl|json)$/.exec(name);
99
+ if (!m) continue;
100
+ const file = path.join(d.dir, name);
101
+ let st;
102
+ try { st = fs.statSync(file); } catch { continue; }
103
+ if (!st.isFile()) continue;
104
+ if (cwd === null) cwd = d.wsDir ? workspaceFolder(d.wsDir) : '';
105
+ const e = {
106
+ harness: HARNESS, session_id: m[1], file, format: m[2], workspace: d.workspace, cwd, client, harness_path: harnessPath, mtime: st.mtimeMs,
107
+ };
108
+ const prev = bySid.get(e.session_id);
109
+ const better = !prev || (e.format === 'jsonl' && prev.format !== 'jsonl') || (e.format === prev.format && e.mtime > prev.mtime);
110
+ if (better) bySid.set(e.session_id, e);
111
+ }
112
+ }
113
+ return [...bySid.values()].map(({ mtime, ...e }) => e);
114
+ }
115
+
116
+ /** Change-log lines: complete lines only (a partial last line is held back), malformed ones skipped. */
117
+ function parseLog(text) {
118
+ const end = text.lastIndexOf('\n');
119
+ if (end < 0) return [];
120
+ const out = [];
121
+ for (const line of text.slice(0, end).split('\n')) {
122
+ if (!line.trim()) continue;
123
+ try { out.push(JSON.parse(line)); } catch { /* malformed: skipped */ }
124
+ }
125
+ return out;
126
+ }
127
+
128
+ /** Apply one change-log op to the state; returns the new state. */
129
+ function applyOp(state, o) {
130
+ if (!o || typeof o !== 'object') return state;
131
+ if (o.kind === 0) return o.v && typeof o.v === 'object' ? o.v : null;
132
+ if (!state || typeof state !== 'object') return state;
133
+ const p = Array.isArray(o.k) ? o.k : null;
134
+ if (!p || !p.length || p.some((s) => UNSAFE_KEYS.has(String(s)))) return state;
135
+ let t = state;
136
+ for (let j = 0; j < p.length - 1; j++) {
137
+ if (t[p[j]] == null || typeof t[p[j]] !== 'object') {
138
+ if (o.kind === 3) return state;
139
+ t[p[j]] = typeof p[j + 1] === 'number' ? [] : {};
140
+ }
141
+ t = t[p[j]];
142
+ }
143
+ const last = p[p.length - 1];
144
+ if (o.kind === 1) t[last] = o.v;
145
+ else if (o.kind === 2) {
146
+ if (!Array.isArray(t[last])) t[last] = [];
147
+ const a = t[last];
148
+ if (Number.isInteger(o.i) && o.i >= 0 && o.i < a.length) a.length = o.i;
149
+ if (Array.isArray(o.v)) for (const x of o.v) a.push(x);
150
+ } else if (o.kind === 3) {
151
+ if (Array.isArray(t) && Number.isInteger(last)) t.splice(last, 1);
152
+ else delete t[last];
153
+ }
154
+ return state;
155
+ }
156
+
157
+ const reqId = (r) => (r && typeof r === 'object' && r.requestId ? String(r.requestId) : '');
158
+
159
+ /**
160
+ * Fold a change log, and keep the requests' timeline beside the state: every request in the order it
161
+ * entered, and a `cut` entry where a kind 2 with i truncated `requests` (the cut requests, the time
162
+ * of the send that wrote it, the last request kept). Each kind 0 resets both.
163
+ */
164
+ function replay(ops) {
165
+ let state = null;
166
+ let timeline = [];
167
+ const swap = (from, to) => { const e = timeline.find((x) => x.req === from); if (e) e.req = to; };
168
+ for (const o of ops) {
169
+ if (!o || typeof o !== 'object') continue;
170
+ if (o.kind === 0) {
171
+ state = applyOp(state, o);
172
+ timeline = state && Array.isArray(state.requests) ? state.requests.map((req) => ({ req })) : [];
173
+ continue;
174
+ }
175
+ const p = Array.isArray(o.k) ? o.k : [];
176
+ const reqs = state && Array.isArray(state.requests) ? state.requests : null;
177
+ if (p.length === 1 && p[0] === 'requests' && o.kind === 2) {
178
+ const added = Array.isArray(o.v) ? o.v : [];
179
+ const placed = new Set();
180
+ if (reqs && Number.isInteger(o.i) && o.i >= 0 && o.i < reqs.length) {
181
+ const cut = [];
182
+ for (const old of reqs.slice(o.i)) {
183
+ const same = reqId(old) ? added.find((x) => reqId(x) === reqId(old) && !placed.has(x)) : null;
184
+ if (same) { swap(old, same); placed.add(same); } else cut.push(old);
185
+ }
186
+ const first = added.find((x) => x && typeof x === 'object');
187
+ if (cut.length) timeline.push({ cut, at: first ? num(first.timestamp) : null, through: o.i > 0 ? reqs[o.i - 1] : null });
188
+ }
189
+ state = applyOp(state, o);
190
+ for (const x of added) if (!placed.has(x)) timeline.push({ req: x });
191
+ continue;
192
+ }
193
+ if (p.length === 2 && p[0] === 'requests' && reqs && (o.kind === 1 || o.kind === 3)) {
194
+ const old = reqs[p[1]];
195
+ state = applyOp(state, o);
196
+ if (o.kind === 1) { if (old) swap(old, state.requests[p[1]]); else timeline.push({ req: state.requests[p[1]] }); }
197
+ else timeline = timeline.filter((e) => e.req !== old); // a delete: it vanishes
198
+ continue;
199
+ }
200
+ state = applyOp(state, o);
201
+ if (p[0] === 'requests' && p.length === 1) {
202
+ // The whole array replaced (never observed): what left it vanishes, what is new is appended.
203
+ const now = state && Array.isArray(state.requests) ? state.requests : [];
204
+ const nowSet = new Set(now);
205
+ timeline = timeline.filter((e) => e.cut || nowSet.has(e.req));
206
+ const known = new Set(timeline.map((e) => e.req));
207
+ for (const r of now) if (!known.has(r)) timeline.push({ req: r });
208
+ }
209
+ }
210
+ return { state, timeline };
211
+ }
212
+
213
+ /** Fold a change log into the session state. Each kind 0 resets everything before it. */
214
+ function fold(ops) {
215
+ return replay(ops).state;
216
+ }
217
+
218
+ function canonical(v) {
219
+ if (Array.isArray(v)) return `[${v.map(canonical).join(',')}]`;
220
+ if (v && typeof v === 'object') return `{${Object.keys(v).sort().map((k) => `${JSON.stringify(k)}:${canonical(v[k])}`).join(',')}}`;
221
+ return JSON.stringify(v === undefined ? null : v);
222
+ }
223
+ const isMarker = (v) => typeof v === 'string' && /^<redacted len=\d+>$/.test(v);
224
+ function marker(v) {
225
+ const n = typeof v === 'string' ? Buffer.byteLength(v, 'utf-8') : Buffer.byteLength(canonical(v), 'utf-8');
226
+ return `<redacted len=${n}>`;
227
+ }
228
+
229
+ /** Redact every declared secret path in place, at any depth (sessions-model §4). Returns the state. */
230
+ function redact(state) {
231
+ const stack = [state];
232
+ while (stack.length) {
233
+ const o = stack.pop();
234
+ if (!o || typeof o !== 'object') continue;
235
+ for (const sp of SECRET_PATHS) {
236
+ let t = o;
237
+ for (let j = 0; j < sp.length - 1 && t && typeof t === 'object'; j++) t = Object.prototype.hasOwnProperty.call(t, sp[j]) ? t[sp[j]] : null;
238
+ const leaf = sp[sp.length - 1];
239
+ if (t && typeof t === 'object' && Object.prototype.hasOwnProperty.call(t, leaf) && t[leaf] != null && !isMarker(t[leaf])) t[leaf] = marker(t[leaf]);
240
+ }
241
+ for (const v of Array.isArray(o) ? o : Object.values(o)) if (v && typeof v === 'object') stack.push(v);
242
+ }
243
+ return state;
244
+ }
245
+
246
+ const num = (v) => { const x = Number(v); return Number.isFinite(x) && x > 0 ? Math.trunc(x) : null; };
247
+ const str = (v) => (typeof v === 'string' ? v : '');
248
+ function argsOf(a) {
249
+ if (a == null) return null;
250
+ if (typeof a === 'string') { try { return JSON.parse(a); } catch { return { raw: a }; } }
251
+ return a;
252
+ }
253
+ function uriPath(u) {
254
+ if (!u) return '';
255
+ if (typeof u === 'string') return u;
256
+ if (typeof u !== 'object') return '';
257
+ return str(u.fsPath) || str(u.path) || str(u.external) || (u.uri ? uriPath(u.uri) : '');
258
+ }
259
+
260
+ /** A prompt-tsx node tree ($mid 23) as text: its text leaves in order, honouring lineBreakBefore. */
261
+ function tsxText(node) {
262
+ let out = '';
263
+ const walk = (n) => {
264
+ if (!n || typeof n !== 'object') return;
265
+ if (typeof n.text === 'string') {
266
+ if (n.lineBreakBefore && out && !out.endsWith('\n')) out += '\n';
267
+ out += n.text;
268
+ }
269
+ if (Array.isArray(n.children)) n.children.forEach(walk);
270
+ };
271
+ walk(node);
272
+ return out;
273
+ }
274
+ /** The text of one toolCallResults entry, as the model saw it. */
275
+ function toolResultText(r) {
276
+ if (r == null) return null;
277
+ if (typeof r === 'string') return r;
278
+ const content = Array.isArray(r.content) ? r.content : [];
279
+ return content.map((c) => {
280
+ if (!c || typeof c !== 'object') return typeof c === 'string' ? c : '';
281
+ if (typeof c.value === 'string') return c.value;
282
+ if (c.value && typeof c.value === 'object' && c.value.node) return tsxText(c.value.node);
283
+ return typeof c.text === 'string' ? c.text : '';
284
+ }).join('');
285
+ }
286
+
287
+ /** The model's own tool calls, by the call id VS Code shows (the round id without `__vscode-<n>`). */
288
+ function roundCalls(meta) {
289
+ const map = new Map();
290
+ const results = meta.toolCallResults && typeof meta.toolCallResults === 'object' ? meta.toolCallResults : {};
291
+ for (const rd of Array.isArray(meta.toolCallRounds) ? meta.toolCallRounds : []) {
292
+ for (const tc of (rd && Array.isArray(rd.toolCalls)) ? rd.toolCalls : []) {
293
+ const full = String((tc && tc.id) || '');
294
+ if (!full) continue;
295
+ const base = full.split('__vscode-')[0];
296
+ if (!map.has(base)) map.set(base, { name: str(tc.name), arguments: tc.arguments, ts: num(rd.timestamp), result: Object.prototype.hasOwnProperty.call(results, full) ? results[full] : null });
297
+ }
298
+ }
299
+ return map;
300
+ }
301
+
302
+ /** The served model: Auto's resolution, never the `copilot/auto` placeholder. */
303
+ function servedModel(r, resp, meta) {
304
+ if (str(meta.resolvedModel)) return meta.resolvedModel;
305
+ for (const p of resp) if (p && p.kind === 'autoModeResolution' && p.resolved && str(p.resolved.id)) return p.resolved.id;
306
+ const rounds = Array.isArray(meta.toolCallRounds) ? meta.toolCallRounds : [];
307
+ for (let i = rounds.length - 1; i >= 0; i--) if (rounds[i] && str(rounds[i].modelId)) return rounds[i].modelId;
308
+ const id = str(r.modelId);
309
+ return id && !/(^|\/)auto$/.test(id) ? id : '';
310
+ }
311
+
312
+ /**
313
+ * How the reply ended, or null while it is still being written (held back). A reply that a later
314
+ * request follows has ended even without a result.
315
+ */
316
+ function replyStatus(r, isLast) {
317
+ const ms = r.modelState && typeof r.modelState === 'object' ? r.modelState.value : undefined;
318
+ if (r.isCanceled === true || ms === MS.CANCELLED) return 'cancelled';
319
+ if (ms === MS.FAILED) return 'error';
320
+ const running = ms === MS.PENDING || ms === MS.NEEDS_INPUT || (ms == null && !r.result);
321
+ if (running && isLast) return null;
322
+ if (r.result && r.result.errorDetails) return 'error';
323
+ if (running) return r.result ? 'complete' : 'interrupted';
324
+ return 'complete';
325
+ }
326
+
327
+ function toolPart(p, partId, callId, call) {
328
+ const tsd = p.toolSpecificData && typeof p.toolSpecificData === 'object' ? p.toolSpecificData : {};
329
+ let args = call ? argsOf(call.arguments) : null;
330
+ if (args == null && tsd.kind === 'terminal' && tsd.commandLine) {
331
+ const cl = tsd.commandLine;
332
+ args = { command: typeof cl === 'string' ? cl : str(cl.original) || str(cl.toolEdited) || str(cl.forDisplay) };
333
+ }
334
+ let output = call && call.result != null ? toolResultText(call.result) : null;
335
+ if (output == null && tsd.kind === 'terminal' && tsd.terminalCommandOutput && typeof tsd.terminalCommandOutput.text === 'string') output = tsd.terminalCommandOutput.text;
336
+ if (output == null && typeof p.resultError === 'string' && p.resultError) output = p.resultError;
337
+ const confirm = p.isConfirmed && typeof p.isConfirmed === 'object' ? p.isConfirmed.type : undefined;
338
+ const denied = confirm === CONFIRM_DENIED;
339
+ const skipped = confirm === CONFIRM_SKIPPED;
340
+ const isError = denied || p.resultError === true || (typeof p.resultError === 'string' && p.resultError !== '');
341
+ const done = p.isComplete === true || output != null;
342
+ const tstate = tsd.kind === 'terminal' && tsd.terminalCommandState && typeof tsd.terminalCommandState === 'object' ? tsd.terminalCommandState : {};
343
+ const exit = typeof tstate.exitCode === 'number' && Number.isFinite(tstate.exitCode) ? tstate.exitCode : null;
344
+ const src = p.source && typeof p.source === 'object' ? p.source : {};
345
+ const server = src.type === 'mcp' ? str(src.label) || str(src.serverLabel) : '';
346
+ return {
347
+ part_id: partId, kind: 'tool', call_id: callId, tool: (call && call.name) || str(p.toolId), args: args || {},
348
+ output, is_error: isError,
349
+ status: denied ? 'denied' : skipped ? 'cancelled' : isError ? 'error' : done ? 'completed' : 'interrupted',
350
+ ...(exit !== null ? { exit_code: exit } : {}),
351
+ ...(server ? { server } : {}),
352
+ ...(p.presentation === 'hidden' || p.presentation === 'hiddenAfterComplete' ? { hidden: true } : {}),
353
+ ...(call && call.ts ? { ts: call.ts } : {}),
354
+ };
355
+ }
356
+
357
+ function inlineLabel(p) {
358
+ const ir = p.inlineReference && typeof p.inlineReference === 'object' ? p.inlineReference : {};
359
+ const name = str(p.name) || str(ir.name);
360
+ if (name) return `\`${name}\``;
361
+ const ref = uriPath(ir.uri || ir.location || ir);
362
+ return ref ? path.posix.basename(ref) : '';
363
+ }
364
+ function thinkingText(v) {
365
+ if (typeof v === 'string') return v;
366
+ if (Array.isArray(v)) return v.map((x) => (typeof x === 'string' ? x : str(x && x.value))).join('');
367
+ return '';
368
+ }
369
+
370
+ /** The reply's parts in native order; a sub-agent's own calls are split out by subAgentInvocationId. */
371
+ function replyParts(resp, aid, calls) {
372
+ const main = [];
373
+ const subs = new Map();
374
+ let run = null;
375
+ const close = () => { run = null; };
376
+ const addText = (i, s) => {
377
+ if (!s) return;
378
+ if (!run) { run = { part_id: String(i), kind: 'text', text: '' }; main.push(run); }
379
+ run.text += s;
380
+ };
381
+ for (let i = 0; i < resp.length; i++) {
382
+ const p = resp[i];
383
+ if (!p || typeof p !== 'object') continue;
384
+ const kind = p.kind;
385
+ if (kind == null || kind === 'markdownContent') { addText(i, str(p.value) || str(p.content && p.content.value)); continue; }
386
+ if (kind === 'markdownVuln') { addText(i, str(p.content && p.content.value) || str(p.value)); continue; }
387
+ if (kind === 'inlineReference') { addText(i, inlineLabel(p)); continue; }
388
+ if (kind === 'thinking') {
389
+ const text = thinkingText(p.value);
390
+ if (!text && !p.id) continue; // the reasoning-done marker, not a block
391
+ close();
392
+ main.push(text ? { part_id: String(i), kind: 'reasoning', text } : { part_id: String(i), kind: 'reasoning', opaque: true });
393
+ continue;
394
+ }
395
+ if (kind === 'toolInvocationSerialized' || kind === 'toolInvocation') {
396
+ close();
397
+ const id = str(p.toolCallId);
398
+ const part = toolPart(p, String(i), id || `@${aid}:${i}`, id ? calls.get(id) : null);
399
+ const sub = str(p.subAgentInvocationId);
400
+ if (sub) { if (!subs.has(sub)) subs.set(sub, []); subs.get(sub).push(part); } else main.push(part);
401
+ continue;
402
+ }
403
+ if (kind === 'warning') {
404
+ const text = str(p.content && p.content.value) || str(p.value);
405
+ if (text) { close(); main.push({ part_id: String(i), kind: 'notice', text }); }
406
+ continue;
407
+ }
408
+ // Bookkeeping and UI state (mcpServersStarting, autoModeResolution, undoStop, codeblockUri,
409
+ // textEditGroup, progressMessage, confirmation, …): not conversation, and it does not split text.
410
+ }
411
+ return { main: main.filter((x) => x.kind !== 'text' || x.text.trim()), subs };
412
+ }
413
+
414
+ function userParts(r) {
415
+ const parts = [];
416
+ const msg = r.message && typeof r.message === 'object' ? r.message : {};
417
+ const text = typeof msg.text === 'string' ? msg.text
418
+ : (Array.isArray(msg.parts) ? msg.parts.map((x) => str(x && x.text)).join('') : (typeof r.message === 'string' ? r.message : ''));
419
+ if (text) parts.push({ part_id: '0', kind: 'text', text });
420
+ const vars = r.variableData && Array.isArray(r.variableData.variables) ? r.variableData.variables : [];
421
+ vars.forEach((v, j) => {
422
+ if (!v || typeof v !== 'object') return;
423
+ const image = v.kind === 'image' || v.isImage === true;
424
+ const file = v.kind === 'file' || v.kind === 'directory' || v.isFile === true || v.isDirectory === true;
425
+ if (!image && !file) return;
426
+ const ref = uriPath(v.value) || uriPath(Array.isArray(v.references) && v.references[0] && v.references[0].reference) || str(v.name);
427
+ // The store keeps no image bytes (a path and an asset id only), so there is no sha to give.
428
+ parts.push({ part_id: `var:${j}`, kind: 'file', media_type: str(v.mimeType), att_sha: null, att_len: null, ref });
429
+ });
430
+ return parts;
431
+ }
432
+
433
+ /** The folded state and the requests' timeline (a snapshot alone gives a timeline with no cuts). */
434
+ function load(session) {
435
+ const snap = (state) => ({ state, timeline: state && Array.isArray(state.requests) ? state.requests.map((req) => ({ req })) : [] });
436
+ if (session.state && typeof session.state === 'object') return snap(JSON.parse(JSON.stringify(session.state)));
437
+ if (Array.isArray(session.ops)) return replay(session.ops.map((o) => (typeof o === 'string' ? JSON.parse(o) : JSON.parse(JSON.stringify(o)))));
438
+ let text = typeof session.text === 'string' ? session.text : null;
439
+ if (text == null) {
440
+ try { text = fs.readFileSync(session.file, 'utf-8'); } catch { return snap(null); }
441
+ }
442
+ const json = session.format === 'json' || (session.file && /\.json$/.test(session.file));
443
+ if (json) { try { return snap(JSON.parse(text)); } catch { return snap(null); } }
444
+ return replay(parseLog(text));
445
+ }
446
+
447
+ /** One request's messages: the user message, then (once it has ended) the reply and its sub-agents. */
448
+ function requestMessages(r, { isLast, retracted, next }) {
449
+ const uid = String(r.requestId);
450
+ const state = retracted ? 'retracted' : 'current';
451
+ const hidden = r.hiddenFromTranscript === true ? { hidden: true } : {};
452
+ const out = [{ k: 'message', message_id: uid, role: 'user', ts: num(r.timestamp), order: next(), state, status: 'complete', ...hidden, parts: userParts(r) }];
453
+
454
+ const status = replyStatus(r, isLast);
455
+ if (!status) return out; // still being written: held back until it has a result
456
+ const aid = r.responseId ? String(r.responseId) : `${uid}:response`;
457
+ const meta = (r.result && r.result.metadata && typeof r.result.metadata === 'object') ? r.result.metadata : {};
458
+ const resp = Array.isArray(r.response) ? r.response : (r.response && typeof r.response === 'object' ? Object.values(r.response) : []);
459
+ const model = servedModel(r, resp, meta);
460
+ const requested = str(r.modelId);
461
+ const { main, subs } = replyParts(resp, aid, roundCalls(meta));
462
+ const err = r.result && r.result.errorDetails;
463
+ if (err && str(err.message)) main.push({ part_id: 'error', kind: 'error', text: err.message });
464
+ const summary = typeof meta.summary === 'string' ? meta.summary : str(meta.summary && meta.summary.text);
465
+ if (summary) main.push({ part_id: 'summary', kind: 'summary', text: summary, replaces: 'unknown' });
466
+ const input = num(r.promptTokens) ?? num(meta.promptTokens);
467
+ const output = num(r.completionTokens);
468
+ const credits = typeof r.copilotCredits === 'number' && Number.isFinite(r.copilotCredits) && r.copilotCredits > 0 ? r.copilotCredits : null;
469
+ const ts = num(r.responseTimestamp) ?? num(r.timestamp);
470
+ out.push({
471
+ k: 'message', message_id: aid, role: 'assistant', ts, order: next(), parent_id: uid, state, status,
472
+ model, model_requested: requested, effort: '', ...hidden,
473
+ usage: input != null || output != null || credits != null
474
+ ? { basis: 'unknown', input, output, cache_read: null, cache_write: null, reasoning: null, cost: null, credits } : null,
475
+ parts: main,
476
+ });
477
+ // A sub-agent's calls carry subAgentInvocationId: its own speaker, anchored to the reply's id.
478
+ let s = 0;
479
+ for (const [agent, parts] of subs) {
480
+ out.push({ k: 'message', message_id: `${aid}+${++s}`, role: 'assistant', ts, order: next(), parent_id: aid, agent_id: agent, state, status, model, model_requested: requested, effort: '', ...hidden, usage: null, parts });
481
+ }
482
+ return out;
483
+ }
484
+
485
+ /**
486
+ * Normalise one session. `session` is a list() entry, or `{ session_id, ops: [...] }` / `{ session_id,
487
+ * text }` / `{ session_id, state }` already loaded (tests), with optional `cwd` and `client`. Returns the
488
+ * log records: the session record first, then the messages and lifecycle records in native order (the
489
+ * order requests entered the change log); null if there is no state.
490
+ */
491
+ function normalise(session) {
492
+ const { state, timeline } = load(session);
493
+ if (!state || typeof state !== 'object' || Array.isArray(state)) return null;
494
+ redact(state);
495
+ for (const e of timeline) if (e.cut) redact(e.cut);
496
+ const reqs = Array.isArray(state.requests) ? state.requests : [];
497
+ const current = new Set(reqs);
498
+ const lastReq = reqs[reqs.length - 1];
499
+ const retractedReqs = new Set(timeline.flatMap((e) => e.cut || []));
500
+ // Keys stay unique: a retracted copy never shadows a current message with the same id.
501
+ const currentIds = new Set();
502
+ for (const r of reqs) if (reqId(r)) { currentIds.add(reqId(r)); currentIds.add(r.responseId ? String(r.responseId) : `${reqId(r)}:response`); }
503
+
504
+ const records = [];
505
+ const idsOf = new Map(); // request → the message ids written for it
506
+ const seen = new Set();
507
+ let order = 0;
508
+ const next = () => order++;
509
+ let version = '';
510
+ for (const e of timeline) {
511
+ if (e.req) {
512
+ const r = e.req;
513
+ if (!reqId(r)) continue; // no native id: nothing to key it by
514
+ const live = current.has(r);
515
+ const retracted = !live && retractedReqs.has(r);
516
+ if (!live && !retracted) continue;
517
+ if (retracted && (currentIds.has(reqId(r)) || seen.has(reqId(r)))) continue;
518
+ // The FIRST request's extension version: an extension update must not rewrite the session line.
519
+ if (live && !version && r.agent && str(r.agent.extensionVersion)) version = r.agent.extensionVersion;
520
+ const msgs = requestMessages(r, { isLast: r === lastReq, retracted, next });
521
+ for (const m of msgs) seen.add(m.message_id);
522
+ idsOf.set(r, msgs.map((m) => m.message_id));
523
+ records.push(...msgs);
524
+ continue;
525
+ }
526
+ // A kind 2 with i: the requests it cut stay above, retracted; this records the act.
527
+ const ids = e.cut.flatMap((r) => idsOf.get(r) || []);
528
+ if (!ids.length) continue;
529
+ const kept = e.through ? idsOf.get(e.through) : null;
530
+ records.push({ k: 'lifecycle', event: 'rewind', at: e.at, message_ids: ids, through: kept && kept.length ? kept[kept.length - 1] : null });
531
+ }
532
+ const messages = records;
533
+
534
+ const head = {
535
+ k: 'session',
536
+ title: str(state.customTitle),
537
+ cwd: str(session.cwd),
538
+ created: num(state.creationDate),
539
+ updated: null, archived: null,
540
+ version,
541
+ incarnation: '',
542
+ engine: ENGINE,
543
+ client: str(session.client),
544
+ links: [],
545
+ };
546
+ return [head, ...messages];
547
+ }
548
+
549
+ module.exports = { HARNESS, ENGINE, SECRET_PATHS, list, normalise, parseLog, replay, fold, applyOp, redact, storeRootDefault };