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.
- package/AGENTS.md +22 -151
- package/CHANGELOG.md +263 -29
- package/LICENSE +202 -22
- package/LICENSES/agentlytics-MIT.txt +21 -0
- package/LICENSES/memhouse-MIT-until-0.18.11.txt +22 -0
- package/LICENSES/models.dev-MIT.txt +22 -0
- package/NOTICE +28 -0
- package/README.md +42 -20
- package/SECURITY.md +21 -8
- package/bin/memhouse.js +2223 -311
- package/docs/README.md +11 -9
- package/docs/guides/admin-password.md +37 -10
- package/docs/guides/configuration.md +13 -6
- package/docs/guides/daemons.md +64 -3
- package/docs/guides/import-claude-ai.md +204 -0
- package/docs/guides/install.md +11 -6
- package/docs/guides/invite.md +21 -7
- package/docs/guides/operate.md +114 -7
- package/docs/guides/search-and-resume.md +72 -3
- package/docs/guides/share.md +91 -52
- package/docs/guides/storage.md +44 -4
- package/docs/guides/troubleshooting.md +121 -14
- package/docs/guides/uninstall.md +4 -3
- package/docs/guides/update.md +16 -0
- package/docs/guides/upgrade-to-0.20.md +188 -0
- package/docs/releases/README.md +2 -1
- package/docs/releases/v0.18.10.md +5 -5
- package/docs/releases/v0.18.11.md +3 -3
- package/docs/releases/v0.20.0.md +627 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/docs/tutorials/01-first-house.md +10 -7
- package/docs/tutorials/02-join-a-house.md +24 -15
- package/docs/tutorials/03-share-your-memory.md +40 -31
- package/editors/antigravity.js +1 -80
- package/editors/base.js +463 -9
- package/editors/bridge.js +119 -0
- package/editors/claude.js +273 -105
- package/editors/codex.js +278 -52
- package/editors/copilot.js +1 -70
- package/editors/cursor.js +1 -75
- package/editors/derive-fingerprints.json +18 -0
- package/editors/harness-paths.js +69 -0
- package/editors/index.js +1 -20
- package/editors/normalise/agy.js +910 -0
- package/editors/normalise/copilot-cli.js +321 -0
- package/editors/normalise/cursor-agent.js +852 -0
- package/editors/normalise/cursor.js +474 -0
- package/editors/normalise/devin.js +575 -0
- package/editors/normalise/gemini-cli.js +602 -0
- package/editors/normalise/goose.js +673 -0
- package/editors/normalise/opencode.js +633 -0
- package/editors/normalise/vscode.js +549 -0
- package/editors/normalise/zed.js +473 -0
- package/editors/opencode-v4.js +119 -0
- package/editors/scope.js +14 -3
- package/editors/sessions-log.js +349 -0
- package/editors/sqlite.js +28 -1
- package/editors/vscode.js +1 -72
- package/editors/windsurf.js +1 -143
- package/examples/01-local-one-machine/README.md +24 -19
- package/examples/01-local-one-machine/run.sh +1 -1
- package/examples/02-existing-clickhouse/README.md +30 -22
- package/examples/02-existing-clickhouse/run.sh +5 -4
- package/examples/03-team-house/README.md +44 -37
- package/examples/03-team-house/run.sh +35 -18
- package/examples/04-self-hosted-team/README.md +29 -14
- package/examples/04-self-hosted-team/compose.yml +5 -2
- package/examples/04-self-hosted-team/smoke.sh +26 -12
- package/examples/README.md +8 -4
- package/examples/fixtures/README.md +4 -1
- package/examples/fixtures/make-sessions.js +8 -5
- package/memhouse/admin-secret.js +89 -24
- package/memhouse/argv-secrets.js +64 -0
- package/memhouse/channel.js +49 -9
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -1
- package/memhouse/delivery/AGENT-INSTALL.md +22 -11
- package/memhouse/delivery/INVITATION.md.tpl +44 -45
- package/memhouse/delivery/PROMPT.md +53 -5
- package/memhouse/delivery/UPGRADE.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/kernel-install.md +17 -9
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +168 -41
- package/memhouse/delivery/plugin/skills/access/SKILL.md +13 -5
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +14 -6
- package/memhouse/delivery/plugin/skills/house/SKILL.md +25 -8
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +66 -8
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +66 -7
- package/memhouse/deploy.js +1 -1
- package/memhouse/flags.js +35 -18
- package/memhouse/help.js +37 -0
- package/memhouse/host.js +82 -2
- package/memhouse/house/HOUSE.md +2 -2
- package/memhouse/house/convert-profile.js +638 -0
- package/memhouse/house/convert-v3.js +742 -0
- package/memhouse/house/doctor-v3.js +324 -0
- package/memhouse/house/host-alias.js +85 -0
- package/memhouse/house/house.js +3 -3
- package/memhouse/house/migrate.js +2 -2
- package/memhouse/house/profile-dedupe.js +183 -0
- package/memhouse/house/profile-edges.js +554 -0
- package/memhouse/house/profile-house.js +430 -0
- package/memhouse/house/profile-rollup.js +190 -0
- package/memhouse/house/q-cli.js +863 -0
- package/memhouse/house/queries-v3.js +1013 -0
- package/memhouse/house/reconcile-v3.js +192 -0
- package/memhouse/house/records-to-profile.js +174 -0
- package/memhouse/house/records-to-rows.js +169 -0
- package/memhouse/house/relocate.js +2 -1
- package/memhouse/house/schema-choice.js +121 -0
- package/memhouse/house/schema-profile.js +290 -0
- package/memhouse/house/schema-profile.sql.tpl +398 -0
- package/memhouse/house/schema-v3.js +267 -0
- package/memhouse/house/schema-v3.sql.tpl +213 -0
- package/memhouse/import/claude-ai.js +420 -0
- package/memhouse/import/import.js +372 -0
- package/memhouse/import/pyjson.js +206 -0
- package/memhouse/import/records.js +64 -0
- package/memhouse/import/writer-profile.js +192 -0
- package/memhouse/import/writer-v3.js +240 -0
- package/memhouse/import/zip.js +112 -0
- package/memhouse/invitation.js +102 -0
- package/memhouse/invitefile.js +5 -2
- package/memhouse/plugin-copies.js +176 -0
- package/memhouse/provision.js +7 -2
- package/memhouse/release-hint.js +24 -0
- package/memhouse/resume.js +4 -4
- package/memhouse/server/api-v3.js +632 -0
- package/memhouse/server/dashboard-auth.js +181 -0
- package/memhouse/server/dashboard-port.js +91 -0
- package/memhouse/server/queries.js +30 -0
- package/memhouse/server/server.js +118 -20
- package/memhouse/service.js +1 -1
- package/memhouse/share.js +687 -1
- package/memhouse/shipper/adapter-capture.js +756 -0
- package/memhouse/shipper/capture.js +2571 -0
- package/memhouse/shipper/dedupe-profile.js +237 -0
- package/memhouse/shipper/dedupe-v3.js +1027 -0
- package/memhouse/shipper/forks.js +233 -0
- package/memhouse/shipper/log-adapter.js +219 -0
- package/memhouse/shipper/maintenance.js +49 -0
- package/memhouse/shipper/normalise-run.js +148 -0
- package/memhouse/shipper/passlog.js +68 -5
- package/memhouse/shipper/profile-capture.js +903 -0
- package/memhouse/shipper/ship.js +91 -8
- package/memhouse/shipper/snapshots.js +193 -0
- package/memhouse/shipper/source-roots.js +111 -0
- package/memhouse/version-notice.js +66 -0
- package/package.json +14 -7
- package/public/THIRD_PARTY_NOTICES.txt +2 -2
- package/public/assets/index-CcNnhfM3.js +78 -0
- package/public/index.html +1 -1
- package/ui/package-lock.json +56 -37
- package/ui/package.json +1 -0
- package/ui/src/App.jsx +29 -14
- package/ui/src/components/ChatSidebar.jsx +69 -12
- package/ui/src/lib/api.js +76 -0
- package/ui/src/lib/dashboard-token.js +41 -0
- package/ui/src/main.jsx +22 -5
- package/ui/src/pages/Dashboard.jsx +22 -36
- package/ui/third-party-notices.js +119 -0
- package/ui/vite.config.js +5 -1
- package/docs/releases/v0.18.12.md +0 -59
- package/memhouse/COMPETITION.md +0 -221
- package/memhouse/DESIGN.md +0 -222
- package/memhouse/RESUME-AND-UPDATE.md +0 -182
- 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 };
|