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,321 @@
1
+ 'use strict';
2
+ // v4 wave 1: GitHub Copilot CLI → normalised session log records (docs/design/sessions-log.md).
3
+ //
4
+ // Store: <COPILOT_HOME|~/.copilot>/session-state/<sid>/events.jsonl, plus workspace.yaml. VS Code's
5
+ // Copilot engine writes into the same store (workspace.yaml client_name: vscode-agent-host), so those
6
+ // sessions are captured here as harness copilot-cli with client 'vscode'.
7
+ // Spec: docs/design/adapter-specs/copilot-cli.md (repository), sessions-model v0.
8
+ //
9
+ // Rules from the spec:
10
+ // - order is line order; parentId is only a hint (it dangles after rewinds and side calls);
11
+ // - message ids are event ids (uuid v4, stable across resume and compaction);
12
+ // - a tool call is matched to tool.execution_complete by toolCallId; one part holds both;
13
+ // - usage: session.usage_record attaches to the assistant.message with the same apiCallId;
14
+ // basis unknown (the vendor does not define it). The record's `cost` is Copilot's own credit
15
+ // unit (0 under BYOK), so it goes to credits (0 → null), never to cost. Usage with no
16
+ // assistant.message is a `usage` record of its own (sessions-model §2.4: "Copilot leaves a
17
+ // usage_record when a reply is capped and no assistant.message follows"):
18
+ // - a usage_record whose apiCallId no assistant.message has: usage_id = its apiCallId (one longer
19
+ // than 64 characters, VS Code's opaque base64, is keyed `sha256:<32 hex>` of it), at its own
20
+ // place, with its finishReason (`length` for a capped reply) as the reason. It waits, with all
21
+ // after it, until its reply can no longer come (its agent replied again, or its turn ended), so
22
+ // a reply written just after it still takes it, and its release only appends;
23
+ // - a usage_record with no apiCallId (optional in Copilot's schema): no reply can take it, so it
24
+ // is written at once, at its own place, keyed by its own event id (`usage_record:<event id>`).
25
+ // apiCallId is needed only to match a reply;
26
+ // - a compaction's own model call, which has no usage_record: `compactionTokensUsed`, usage_id
27
+ // `compaction:<event id>#1` (§2.4's form for a compaction anchor), reason compaction, after the
28
+ // compaction's summary;
29
+ // - neither is written for an event a fork copied from its origin (below): the origin bills it;
30
+ // - a sub-agent's events carry the envelope agentId: same session, agent_id set;
31
+ // - incarnation: the session.start event id (a reused --session-id gets a new one);
32
+ // - a fork child copies its origin's events with their ids and timestamps, so every copy is older
33
+ // than the child's own session.start (the first one in the file): older than the earlier of its
34
+ // `startTime` and the event's own timestamp, the session's birth. A copied `session.info` marker
35
+ // (fork or rewind) is the origin's history: it states nothing about this session. A child's copy
36
+ // of "Forked this session into \"…\" (<id>)" names its origin's earlier fork, a sibling, never
37
+ // its own child;
38
+ // - an assistant message with a tool call still running is held back until the call completes or
39
+ // the turn ends, and everything after it waits too (order is the native event index), so its
40
+ // release only appends;
41
+ // - a turn ends with its agent's own user message, abort or turn end; a sub-agent's also with its
42
+ // subagent.completed or subagent.failed; and every agent's with the root agent's user message or
43
+ // abort (no agentId), or the session's shutdown. A sub-agent writes no turn end of its own;
44
+ // - system.message, model.*, hook.*, permission.*, binary assets' bytes and other bookkeeping are
45
+ // not conversation: they are left out of the log.
46
+
47
+ const fs = require('fs');
48
+ const path = require('path');
49
+ const crypto = require('crypto');
50
+ const harnessPaths = require('../harness-paths');
51
+
52
+ const HARNESS = 'copilot-cli';
53
+ // Bumped when the records change for an unchanged store, so the runner normalises every session again
54
+ // (normalise-run.js) and the engine re-derives its rewritten log at a higher rev. 2: usage with no
55
+ // assistant.message (a capped reply, a compaction's own tokens); a fork's copied markers ignored.
56
+ const VERSION = 2;
57
+ const SID_RE = /\(([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\)/i;
58
+
59
+ function storeRootDefault() { return harnessPaths.defaultFolder(HARNESS); }
60
+
61
+ /** Sessions in a store: one per session-state/<sid>/ that has an events.jsonl. */
62
+ function list(storeRoot = storeRootDefault()) {
63
+ const base = path.join(storeRoot, 'session-state');
64
+ // The data folder's real path: absolute, symlinks resolved, in the filesystem's own case.
65
+ const harnessPath = harnessPaths.resolveNative(storeRoot);
66
+ let names = [];
67
+ try { names = fs.readdirSync(base); } catch { return []; }
68
+ const out = [];
69
+ for (const sid of names) {
70
+ const dir = path.join(base, sid);
71
+ const events = path.join(dir, 'events.jsonl');
72
+ try { if (!fs.statSync(events).isFile()) continue; } catch { continue; }
73
+ out.push({ harness: HARNESS, harness_path: harnessPath, session_id: sid, dir, events, workspace: path.join(dir, 'workspace.yaml') });
74
+ }
75
+ return out;
76
+ }
77
+
78
+ /** Complete lines only: a partial last line is held back. */
79
+ function parseEvents(text) {
80
+ const end = text.lastIndexOf('\n');
81
+ if (end < 0) return [];
82
+ const out = [];
83
+ for (const line of text.slice(0, end).split('\n')) {
84
+ if (!line.trim()) continue;
85
+ try { out.push(JSON.parse(line)); } catch { /* malformed line: skipped, the archive keeps it */ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ /** workspace.yaml is flat `key: value`; only the top-level scalars are read. */
91
+ function parseWorkspace(text) {
92
+ const ws = {};
93
+ for (const line of String(text || '').split('\n')) {
94
+ const m = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(line);
95
+ if (!m) continue;
96
+ let v = m[2].trim();
97
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) v = v.slice(1, -1);
98
+ ws[m[1]] = v;
99
+ }
100
+ return ws;
101
+ }
102
+
103
+ const ms = (iso) => { const t = Date.parse(iso); return Number.isFinite(t) ? t : null; };
104
+ const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
105
+ function argsOf(a) {
106
+ if (a == null) return {};
107
+ if (typeof a === 'string') { try { return JSON.parse(a); } catch { return { raw: a }; } }
108
+ return a;
109
+ }
110
+ /** Copilot's usage counters (a usage_record's `usage`, or `compactionTokensUsed`) as the log's usage. */
111
+ function usageOf(u) {
112
+ return { basis: 'unknown', input: u.inputTokens ?? null, output: u.outputTokens ?? null, cache_read: u.cacheReadTokens ?? null, cache_write: u.cacheWriteTokens ?? null, reasoning: u.reasoningTokens ?? null, cost: null, credits: u.cost || null };
113
+ }
114
+ const modelOf = (u) => (u.modelDisplayName || u.model ? { model: String(u.modelDisplayName || u.model) } : {});
115
+ /**
116
+ * A usage record's key from its call's apiCallId: the id itself when it is short, else a hash of
117
+ * it (stable, 39 characters). VS Code's Copilot engine writes an opaque base64 apiCallId of
118
+ * hundreds of characters, which would otherwise fill a key column. A reply is still matched on the
119
+ * raw value.
120
+ */
121
+ const KEY_MAX = 64;
122
+ const keyOfApiCall = (api) => { const s = String(api); return s.length <= KEY_MAX ? s : `sha256:${sha256(s).slice(0, 32)}`; };
123
+ function resultText(d) {
124
+ if (!d) return null;
125
+ const r = d.result;
126
+ if (r && typeof r.content === 'string') return r.content;
127
+ if (r && Array.isArray(r.content)) return r.content.map((c) => (typeof c === 'string' ? c : c.text || '')).join('');
128
+ if (d.error) return typeof d.error === 'string' ? d.error : (d.error.message || d.error.code || 'error');
129
+ return '';
130
+ }
131
+
132
+ /**
133
+ * Normalise one session. `session` is a list() entry, or `{ session_id, events: [...], workspace: {...} }`
134
+ * already parsed (tests). Returns the log records: the session record first, then messages.
135
+ */
136
+ function normalise(session) {
137
+ const events = Array.isArray(session.events)
138
+ ? session.events
139
+ : parseEvents(fs.readFileSync(session.events, 'utf-8'));
140
+ let ws = session.workspace;
141
+ if (!ws || typeof ws === 'string') {
142
+ let txt = '';
143
+ try { txt = fs.readFileSync(ws || '', 'utf-8'); } catch { txt = ''; }
144
+ ws = parseWorkspace(txt);
145
+ }
146
+ const start = events.find((e) => e.type === 'session.start');
147
+ if (!start) return null;
148
+ // An event a fork copied from its origin: older than this session's birth, the earlier of its
149
+ // start's data.startTime and the start event's own timestamp (the fixtures write the event a
150
+ // fraction of a second after startTime, so an event of its own may fall between the two).
151
+ const born = [ms((start.data || {}).startTime), ms(start.timestamp)].filter((t) => t !== null);
152
+ const birth = born.length ? Math.min(...born) : null;
153
+ const copied = (e) => { const t = ms(e.timestamp); return birth !== null && t !== null && t < birth; };
154
+
155
+ // Index tool results, usage and assets.
156
+ const results = new Map(); // toolCallId → completion event
157
+ const usage = new Map(); // apiCallId → usage
158
+ const replied = new Set(); // apiCallIds an assistant.message has
159
+ const assets = new Map(); // assetId → { mime, bytes }
160
+ const servers = new Map(); // toolCallId → MCP server name (tool.execution_start)
161
+ for (const e of events) {
162
+ const d = e.data || {};
163
+ if (e.type === 'tool.execution_complete' && d.toolCallId) results.set(d.toolCallId, e);
164
+ else if (e.type === 'tool.execution_start' && d.toolCallId && d.mcpServerName) servers.set(d.toolCallId, d.mcpServerName);
165
+ else if (e.type === 'session.usage_record' && d.usage && d.usage.apiCallId) usage.set(d.usage.apiCallId, d.usage);
166
+ else if (e.type === 'session.binary_asset' && d.assetId) assets.set(d.assetId, d);
167
+ else if (e.type === 'assistant.message' && d.apiCallId) replied.add(d.apiCallId);
168
+ }
169
+
170
+ // For each event, whether its agent's turn ends later in the file: scanned once from the end.
171
+ // - Its own agent ends it: a user message, an abort or the turn's end of that agent, or, for a
172
+ // sub-agent, its subagent.completed / subagent.failed (they carry the sub-agent's agentId).
173
+ // - The root agent ends every agent: a shutdown, or a user message or abort with no agentId. A
174
+ // sub-agent writes no turn end of its own, so without this a sub-agent's unanswered call (or
175
+ // its running tool) would hold back the rest of the session for good.
176
+ const ENDS = new Set(['user.message', 'abort', 'session.shutdown', 'assistant.turn_end', 'subagent.completed', 'subagent.failed']);
177
+ const ROOT_ENDS = new Set(['user.message', 'abort']);
178
+ const endsAfter = new Array(events.length).fill(false);
179
+ const seenEnd = new Set();
180
+ let seenRootEnd = false;
181
+ // And whether its agent writes another assistant.message later (a reply after it).
182
+ const repliesAfter = new Array(events.length).fill(false);
183
+ const seenReply = new Set();
184
+ for (let i = events.length - 1; i >= 0; i--) {
185
+ const a = events[i].agentId || '';
186
+ endsAfter[i] = seenRootEnd || seenEnd.has(a);
187
+ repliesAfter[i] = seenReply.has(a);
188
+ if (ENDS.has(events[i].type)) seenEnd.add(a);
189
+ if (events[i].type === 'session.shutdown' || (!a && ROOT_ENDS.has(events[i].type))) seenRootEnd = true;
190
+ if (events[i].type === 'assistant.message') seenReply.add(a);
191
+ }
192
+
193
+ const links = [];
194
+ const messages = [];
195
+ for (let i = 0; i < events.length; i++) {
196
+ const e = events[i];
197
+ const d = e.data || {};
198
+ const agent = e.agentId || '';
199
+ const ts = ms(e.timestamp);
200
+ if (e.type === 'user.message') {
201
+ const parts = [];
202
+ if (d.content) parts.push({ part_id: '0', kind: 'text', text: String(d.content) });
203
+ (d.attachments || []).forEach((a, j) => {
204
+ const asset = a && a.assetId ? assets.get(a.assetId) : null;
205
+ const bytes = asset && asset.data ? Buffer.from(asset.data, 'base64') : null;
206
+ parts.push({
207
+ part_id: `att:${j}`, kind: 'file',
208
+ media_type: (asset && asset.mimeType) || a.mimeType || '',
209
+ att_sha: bytes ? sha256(bytes) : null, att_len: bytes ? bytes.length : (asset && asset.byteLength) || null,
210
+ ref: a.path || a.displayName || a.assetId || '',
211
+ });
212
+ });
213
+ // A sub-agent's prompt is written by its parent agent (data.source 'agent-<sid>…'): automated.
214
+ const auto = typeof d.source === 'string' && d.source.startsWith('agent-');
215
+ messages.push({ k: 'message', message_id: e.id, role: 'user', ts, order: i, agent_id: agent, state: 'current', status: 'complete', ...(auto ? { source: 'automated' } : {}), parts });
216
+ continue;
217
+ }
218
+ if (e.type === 'assistant.message') {
219
+ const reqs = d.toolRequests || [];
220
+ const pending = reqs.some((r) => !results.has(r.toolCallId));
221
+ if (pending && !endsAfter[i]) break; // a tool still running: held back, and all after it
222
+ const parts = [];
223
+ if (d.reasoningText) parts.push({ part_id: 'r', kind: 'reasoning', text: String(d.reasoningText) });
224
+ if (d.content) parts.push({ part_id: 't', kind: 'text', text: String(d.content) });
225
+ for (const r of reqs) {
226
+ const done = results.get(r.toolCallId);
227
+ const dd = done ? done.data || {} : null;
228
+ parts.push({
229
+ part_id: `tool:${r.toolCallId}`, kind: 'tool', call_id: r.toolCallId, tool: r.name, args: argsOf(r.arguments),
230
+ output: dd ? resultText(dd) : null, is_error: dd ? dd.success === false : false,
231
+ status: dd ? (dd.success === false ? (dd.error && dd.error.code === 'denied' ? 'denied' : 'error') : 'completed') : 'interrupted',
232
+ ...(dd && dd.shellExecution && Number.isFinite(dd.shellExecution.exitCode) ? { exit_code: dd.shellExecution.exitCode } : {}),
233
+ ...(servers.has(r.toolCallId) ? { server: servers.get(r.toolCallId) } : {}),
234
+ ts: done ? ms(done.timestamp) : ts,
235
+ });
236
+ }
237
+ const u = d.apiCallId ? usage.get(d.apiCallId) : null;
238
+ messages.push({
239
+ k: 'message', message_id: e.id, role: 'assistant', ts, order: i, agent_id: agent, state: 'current',
240
+ status: pending ? 'interrupted' : 'complete',
241
+ model: (u && u.modelDisplayName) || d.model || '', model_requested: d.model || (u && u.model) || '', effort: '',
242
+ usage: u ? usageOf(u) : null,
243
+ parts,
244
+ });
245
+ continue;
246
+ }
247
+ if (e.type === 'system.notification') {
248
+ // The text, or a type/status line built from plain strings only: never the native record
249
+ // itself, whose other fields are not conversation (and could carry anything).
250
+ const kind = d.kind && typeof d.kind === 'object' ? d.kind : {};
251
+ const word = (v) => (typeof v === 'string' && v ? v : '');
252
+ const text = typeof d.content === 'string' ? d.content
253
+ : [word(typeof d.kind === 'string' ? d.kind : kind.type), word(kind.status || d.status)].filter(Boolean).join(' ');
254
+ if (!text) continue;
255
+ messages.push({ k: 'message', message_id: e.id, role: 'system', ts, order: i, agent_id: agent, state: 'current', status: 'complete', source: 'injected', parts: [{ part_id: '0', kind: 'notice', text }] });
256
+ continue;
257
+ }
258
+ if (e.type === 'session.usage_record') {
259
+ // Usage with no assistant.message (the header's usage rule): a capped reply, or a record with
260
+ // no apiCallId (the schema makes it optional), which no reply can take.
261
+ const u = d.usage;
262
+ if (!u || typeof u !== 'object' || copied(e)) continue;
263
+ const api = u.apiCallId != null && u.apiCallId !== '' ? u.apiCallId : null;
264
+ if (api !== null && replied.has(api)) continue; // on its assistant.message
265
+ const id = api !== null ? keyOfApiCall(api) : (e.id ? `usage_record:${e.id}` : null);
266
+ if (id === null) continue; // no stable key of any kind
267
+ if (api !== null && !endsAfter[i] && !repliesAfter[i]) break; // its reply may still come: held back, and all after it
268
+ messages.push({ k: 'usage', usage_id: id, ts, ...modelOf(u), ...(agent ? { agent_id: agent } : {}), ...(typeof u.finishReason === 'string' && u.finishReason ? { reason: u.finishReason } : {}), usage: usageOf(u) });
269
+ continue;
270
+ }
271
+ if (e.type === 'session.compaction_complete') {
272
+ if (d.summaryContent) {
273
+ messages.push({ k: 'lifecycle', event: 'compaction', at: ts, ...(Number.isFinite(d.messagesRemoved) ? { count: d.messagesRemoved } : {}) });
274
+ messages.push({
275
+ k: 'message', message_id: e.id, role: 'system', ts, order: i, agent_id: agent, state: 'current', status: 'complete',
276
+ parts: [{ part_id: '0', kind: 'summary', text: String(d.summaryContent), replaces: { count: d.messagesRemoved ?? null } }],
277
+ });
278
+ }
279
+ // The compaction's own tokens (the header's usage rule), whether or not a summary was kept.
280
+ const t = d.compactionTokensUsed;
281
+ const cu = t && typeof t === 'object' && e.id && !copied(e) ? usageOf(t) : null;
282
+ if (cu && [cu.input, cu.output, cu.cache_read, cu.cache_write, cu.reasoning].some((x) => Number(x) > 0)) {
283
+ messages.push({ k: 'usage', usage_id: `compaction:${e.id}#1`, ts, ...modelOf(t), ...(agent ? { agent_id: agent } : {}), reason: 'compaction', usage: cu });
284
+ }
285
+ continue;
286
+ }
287
+ if (e.type === 'session.info' && copied(e)) continue; // the origin's marker, copied with its events
288
+ if (e.type === 'session.info' && d.infoType === 'fork') {
289
+ // Stated on both sides, naming the other session in text. The child's statement gives its
290
+ // origin (links); the origin's ("Forked this session into …") names the child (a child record).
291
+ const m = SID_RE.exec(String(d.message || ''));
292
+ if (m && m[1] !== session.session_id) {
293
+ // The child at the fork event's own place, not after the message before it: two forks in a
294
+ // row would otherwise put the second child before the first fork's lifecycle record.
295
+ if (/\binto\b/i.test(d.message)) messages.push({ k: 'child', kind: 'fork', session_id: m[1], evidence: 'explicit' });
296
+ else links.push({ kind: 'fork', origin: m[1], evidence: 'explicit', at: '' });
297
+ messages.push({ k: 'lifecycle', event: 'fork', at: ts });
298
+ }
299
+ continue;
300
+ }
301
+ if (e.type === 'session.info' && d.infoType === 'rewind') {
302
+ // A rewind rewrites events.jsonl (the rewound events vanish) and appends this marker.
303
+ messages.push({ k: 'lifecycle', event: 'rewind', at: ts });
304
+ }
305
+ }
306
+
307
+ const head = {
308
+ k: 'session',
309
+ title: ws.name || '',
310
+ cwd: ((start.data || {}).context || {}).cwd || '',
311
+ created: ms((start.data || {}).startTime) || ms(start.timestamp),
312
+ updated: null, archived: null,
313
+ version: (start.data || {}).copilotVersion || '',
314
+ incarnation: start.id,
315
+ client: ws.client_name === 'vscode-agent-host' ? 'vscode' : 'cli',
316
+ links,
317
+ };
318
+ return [head, ...messages];
319
+ }
320
+
321
+ module.exports = { HARNESS, VERSION, list, normalise, parseEvents, parseWorkspace };