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,237 @@
|
|
|
1
|
+
// Step B's legacy-to-profile reconciliation: dedupe-v3's matcher over harness-sessions-sql profile rows.
|
|
2
|
+
//
|
|
3
|
+
// One matcher for both paths. On the profile, the legacy rows are the conversion's (adapter
|
|
4
|
+
// 'legacy', convert-profile.js) and the captured rows are an adapter's (records-to-profile.js).
|
|
5
|
+
// Two pure adapters at the matcher's boundary turn them into the shapes it already reads; the
|
|
6
|
+
// matcher core does not change.
|
|
7
|
+
//
|
|
8
|
+
// converted legacy part row → legacy shape
|
|
9
|
+
// text where the conversion kept 0.18's text: `text` (text, reasoning), `output`
|
|
10
|
+
// (tool_result), extra.legacy_text (tool_call); a usage-only header row (kind
|
|
11
|
+
// 'none') is '[assistant activity]'
|
|
12
|
+
// role the conversion's role 'tool' comes from a tool_result marker in a 0.18
|
|
13
|
+
// assistant row: back to 'assistant'. A 0.18 row stored with role 'tool' kept
|
|
14
|
+
// kind 'text' and keeps its role
|
|
15
|
+
// kind back to 0.18's names: reasoning → thinking, none → usage
|
|
16
|
+
// tokens input and output from the row's message-anchored session_usage row, in any
|
|
17
|
+
// version (a tombstone keeps them, so a pass stopped after the usage went still
|
|
18
|
+
// proves; Codex's re-report proof reads them); a per_record run's usage is not split back
|
|
19
|
+
// profile part row → captured shape
|
|
20
|
+
// part_ord part_index; ord the message's rank by order_key in the session
|
|
21
|
+
// text a tool call is `tool_name args` (the package form the matcher reads); a tool
|
|
22
|
+
// result is `output`; an error part its `error_message` (where the profile keeps an
|
|
23
|
+
// API error's line); other kinds their text. Kinds and role 'tool' pass as is
|
|
24
|
+
// calls one per tool_call part with a call_id
|
|
25
|
+
// ts both sides in the house's wire form ('YYYY-MM-DD hh:mm:ss.mmm', UTC)
|
|
26
|
+
//
|
|
27
|
+
// A retired row is tombstoned as a full copy of the newest version of the profile row it came
|
|
28
|
+
// from: same key, rev + 1, is_deleted 1. A retired message's session_usage rows go with it: a
|
|
29
|
+
// message-anchored row by its message, a Claude Code run's row ('response:<run>#<n>') by the
|
|
30
|
+
// message's rank in its run (the conversion counts n in seq order). Every dead legacy message's
|
|
31
|
+
// usage is retired, not only this plan's, so a pass that stopped between the two writes is
|
|
32
|
+
// finished by the next. Pure: rows in, the plan out.
|
|
33
|
+
|
|
34
|
+
'use strict';
|
|
35
|
+
|
|
36
|
+
const { capture: base } = require('../../editors/base');
|
|
37
|
+
const dedupe = require('./dedupe-v3');
|
|
38
|
+
|
|
39
|
+
const s = (v) => (v == null ? '' : String(v));
|
|
40
|
+
/** A ts in the house's wire form; an ISO string (records-to-profile) is converted, null stays null. */
|
|
41
|
+
function wireTs(v) {
|
|
42
|
+
if (v == null || v === '') return null;
|
|
43
|
+
const t = String(v);
|
|
44
|
+
if (/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3}$/.test(t)) return t;
|
|
45
|
+
const ms = Date.parse(t);
|
|
46
|
+
return Number.isFinite(ms) ? base.chTs(ms) : t;
|
|
47
|
+
}
|
|
48
|
+
const LEGACY_KIND = { reasoning: 'thinking', none: 'usage' };
|
|
49
|
+
|
|
50
|
+
/** The 0.18 text the conversion kept for a converted legacy part row. */
|
|
51
|
+
function legacyTextOf(p) {
|
|
52
|
+
const kind = s(p.kind);
|
|
53
|
+
if (kind === 'tool_result') return s(p.output);
|
|
54
|
+
if (kind === 'none') return '[assistant activity]';
|
|
55
|
+
if (kind === 'tool_call') {
|
|
56
|
+
try { const x = JSON.parse(s(p.extra) || '{}').legacy_text; if (typeof x === 'string') return x; } catch { /* no extra */ }
|
|
57
|
+
return `[tool-call: ${s(p.tool_name)}]`;
|
|
58
|
+
}
|
|
59
|
+
return s(p.text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The schema-3 turn columns of a row, defaults where the profile has none. */
|
|
63
|
+
function turnRow(o) {
|
|
64
|
+
return {
|
|
65
|
+
harness: s(o.harness), harness_version: '', harness_path: s(o.harness_path), session_id: s(o.session_id), incarnation: s(o.incarnation),
|
|
66
|
+
agent_id: s(o.agent_id), message_id: s(o.message_id), part_id: s(o.part_id), part_ord: Number(o.part_ord) || 0, parent_id: s(o.parent_id),
|
|
67
|
+
response_id: s(o.response_id), ts: o.ts, ord: Number(o.ord) || 0, rev: s(o.rev || 0), is_deleted: Number(o.is_deleted) || 0, removed: Number(o.removed) || 0,
|
|
68
|
+
state: s(o.state), role: s(o.role), kind: s(o.kind), model: s(o.model), effort: '',
|
|
69
|
+
input_tokens: Number(o.input_tokens) || 0, output_tokens: Number(o.output_tokens) || 0, cache_read_tokens: 0, cache_write_tokens: 0, reasoning_tokens: 0,
|
|
70
|
+
text: s(o.text), text_len: [...s(o.text)].length, folder: '', project: '', host: s(o.host), origin: '', extra: s(o.extra || '{}'), adapter_ver: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function callRow(o) {
|
|
74
|
+
return {
|
|
75
|
+
harness: s(o.harness), harness_path: s(o.harness_path), session_id: s(o.session_id), incarnation: s(o.incarnation), agent_id: s(o.agent_id),
|
|
76
|
+
call_id: s(o.call_id), message_id: s(o.message_id), ts: o.ts, rev: s(o.rev || 0), is_deleted: Number(o.is_deleted) || 0, removed: Number(o.removed) || 0,
|
|
77
|
+
tool_name: s(o.tool_name), args: s(o.args), result_ts: null, is_error: false, result_len: 0, model: s(o.model), folder: '', project: '', host: s(o.host), adapter_ver: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Converted legacy part rows (and their session_usage rows) → legacy turn rows. */
|
|
82
|
+
function legacyTurnsOf(parts, usage = []) {
|
|
83
|
+
const tokens = new Map();
|
|
84
|
+
for (const u of usage) if (s(u.anchor_kind) === 'message' && !tokens.has(s(u.anchor_id))) tokens.set(s(u.anchor_id), u);
|
|
85
|
+
return parts.map((p) => {
|
|
86
|
+
const kind = s(p.kind);
|
|
87
|
+
const u = tokens.get(s(p.message_id)) || {};
|
|
88
|
+
const seq = Number(s(p.message_id).split(':').pop());
|
|
89
|
+
return turnRow({
|
|
90
|
+
...p, ts: wireTs(p.ts), ord: Number.isFinite(seq) ? seq : 0, part_ord: 0, model: p.served_model,
|
|
91
|
+
role: kind === 'tool_result' ? 'assistant' : s(p.role), kind: LEGACY_KIND[kind] || kind, text: legacyTextOf(p),
|
|
92
|
+
input_tokens: u.input, output_tokens: u.output,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/** Converted legacy calls rows ({m}_calls, adapter 'legacy') → legacy call rows. */
|
|
97
|
+
function legacyCallsOf(calls) { return calls.map((c) => callRow({ ...c, ts: wireTs(c.ts) })); }
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* An adapter's session_parts rows → captured turn rows and calls. With its session_usage rows: a usage
|
|
101
|
+
* record whose anchor (a message, or a response) holds no part 0.18 would have shown (a Codex turn that
|
|
102
|
+
* spent its tokens on opaque reasoning, or kept nothing at all) is a `usage` turn as well, as the 0.20
|
|
103
|
+
* engines wrote one: what a converted '[assistant activity]' row matches. It sits at its anchor's
|
|
104
|
+
* message, or by its time between the messages around it. One with neither (an anchor that kept no
|
|
105
|
+
* message: its usage has no time) has no place in the order: it comes back in `loose`.
|
|
106
|
+
*/
|
|
107
|
+
function capturedOf(parts, usage = []) {
|
|
108
|
+
const order = [...new Set([...parts].sort((a, b) => (s(a.order_key) < s(b.order_key) ? -1 : s(a.order_key) > s(b.order_key) ? 1 : 0)).map((p) => s(p.message_id)))];
|
|
109
|
+
const rank = new Map(order.map((m, i) => [m, i]));
|
|
110
|
+
const turns = []; const calls = [];
|
|
111
|
+
const textOf = (p, kind) => (kind === 'tool_call' ? `${s(p.tool_name)} ${s(p.args)}` : kind === 'tool_result' ? s(p.output)
|
|
112
|
+
: kind === 'error' ? (s(p.text) || s(p.error_message)) : s(p.text));
|
|
113
|
+
for (const p of parts) {
|
|
114
|
+
const kind = s(p.kind);
|
|
115
|
+
const ts = wireTs(p.ts);
|
|
116
|
+
turns.push(turnRow({ ...p, ts, ord: rank.get(s(p.message_id)), part_ord: p.part_index, model: p.served_model || p.requested_model, text: textOf(p, kind),
|
|
117
|
+
extra: JSON.stringify(p.call_id ? { call_id: s(p.call_id) } : {}) }));
|
|
118
|
+
if (kind === 'tool_call' && s(p.call_id)) calls.push(callRow({ ...p, ts, model: p.served_model || p.requested_model }));
|
|
119
|
+
}
|
|
120
|
+
const shown = (p) => { const kind = s(p.kind); return kind === 'tool_call' || kind === 'tool_result' || textOf(p, kind) !== ''; };
|
|
121
|
+
const firstTs = new Map();
|
|
122
|
+
const byMessage = new Map(); const byResponse = new Map();
|
|
123
|
+
const add = (m, k, p) => { if (!k) return; if (!m.has(k)) m.set(k, []); m.get(k).push(p); };
|
|
124
|
+
for (const p of parts) {
|
|
125
|
+
const t = wireTs(p.ts); const m = s(p.message_id);
|
|
126
|
+
if (t && (!firstTs.has(m) || t < firstTs.get(m))) firstTs.set(m, t);
|
|
127
|
+
add(byMessage, m, p); add(byResponse, s(p.response_id), p);
|
|
128
|
+
}
|
|
129
|
+
const loose = [];
|
|
130
|
+
for (const u of usage) {
|
|
131
|
+
const ak = s(u.anchor_kind); const id = s(u.anchor_id);
|
|
132
|
+
if (ak !== 'message' && ak !== 'response') continue;
|
|
133
|
+
const at = (ak === 'message' ? byMessage : byResponse).get(id) || [];
|
|
134
|
+
if (at.some(shown)) continue;
|
|
135
|
+
const ts = wireTs(u.ts) || (at.length ? wireTs(at[0].ts) : null);
|
|
136
|
+
if (!at.length && !ts) { loose.push(u); continue; }
|
|
137
|
+
let ord;
|
|
138
|
+
if (at.length) ord = rank.get(s(at[0].message_id));
|
|
139
|
+
else {
|
|
140
|
+
// Between the messages around it: after the last one that began no later.
|
|
141
|
+
let before = -1;
|
|
142
|
+
for (const m of order) if (ts && firstTs.get(m) && firstTs.get(m) <= ts) before = Math.max(before, rank.get(m));
|
|
143
|
+
ord = before + 0.5;
|
|
144
|
+
}
|
|
145
|
+
turns.push(turnRow({ ...(at[0] || parts[0] || {}), message_id: `@usage:${s(u.usage_id)}`, part_id: '', part_ord: at.reduce((a, p) => Math.max(a, (Number(p.part_index) || 0) + 1), 0),
|
|
146
|
+
ts, ord, role: 'assistant', kind: 'usage', text: '', response_id: ak === 'response' ? id : s(at[0] && at[0].response_id), model: s(u.model), extra: '{}' }));
|
|
147
|
+
}
|
|
148
|
+
return { turns, calls, loose };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A full copy of the profile row a retired legacy row came from: rev + 1, is_deleted 1. */
|
|
152
|
+
function tombstone(src) { return { ...src, rev: String(BigInt(src.rev || 0) + 1n), is_deleted: 1 }; }
|
|
153
|
+
|
|
154
|
+
/** key → the newest version's source row (rows[i] was read from src[i]). */
|
|
155
|
+
function newestSources(rows, src, keyOf) {
|
|
156
|
+
return new Map(dedupe.newestPerKey(rows.map((r, i) => ({ ...r, i })), keyOf).map((r) => [keyOf(r), src[r.i]]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const cmp = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
160
|
+
/** The live session_usage rows of these legacy messages, in the stores they were converted to. */
|
|
161
|
+
function usageOf(dead, parts, usage) {
|
|
162
|
+
const store = (r) => `${s(r.host)}\u0000${s(r.harness_path)}`;
|
|
163
|
+
const runs = new Map();
|
|
164
|
+
for (const p of parts) {
|
|
165
|
+
if (!s(p.response_id)) continue;
|
|
166
|
+
const k = `${store(p)}\u0000${s(p.response_id)}`;
|
|
167
|
+
if (!runs.has(k)) runs.set(k, new Map());
|
|
168
|
+
runs.get(k).set(s(p.message_id), s(p.order_key));
|
|
169
|
+
}
|
|
170
|
+
const ids = new Set();
|
|
171
|
+
for (const p of dead) {
|
|
172
|
+
const rid = s(p.response_id);
|
|
173
|
+
if (!rid) { ids.add(`${store(p)}\u0000message:${s(p.message_id)}#1`); continue; }
|
|
174
|
+
const run = [...runs.get(`${store(p)}\u0000${rid}`)].sort((a, b) => cmp(a[1], b[1]) || cmp(a[0], b[0])).map(([m]) => m);
|
|
175
|
+
ids.add(`${store(p)}\u0000response:${rid}#${run.indexOf(s(p.message_id)) + 1}`);
|
|
176
|
+
}
|
|
177
|
+
const key = (u) => `${store(u)}\u0000${s(u.usage_id)}`;
|
|
178
|
+
return dedupe.newestPerKey(usage, key).filter((u) => !Number(u.is_deleted) && ids.has(key(u)));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The dedupe plan on profile rows.
|
|
183
|
+
* @param {object} o
|
|
184
|
+
* @param {string} o.harness
|
|
185
|
+
* @param {string} o.session_id
|
|
186
|
+
* @param {object[]} o.legacyParts converted session_parts rows (adapter 'legacy'), live and tombstoned
|
|
187
|
+
* @param {object[]} [o.legacyUsage] their session_usage rows, live and tombstoned (tokens, and what a retired message takes with it)
|
|
188
|
+
* @param {object[]} [o.legacyCalls] converted {m}_calls rows
|
|
189
|
+
* @param {object[]} o.capturedParts an adapter's session_parts rows
|
|
190
|
+
* @param {object[]} [o.capturedUsage] its session_usage rows (usage_id, anchor_kind, anchor_id, ts, model, input, output): a turn's tokens with no part to show
|
|
191
|
+
* @param {Array} [o.repeats] Codex re-report proofs, [{ input, output }] (dedupe-v3 codexRepeats)
|
|
192
|
+
* @returns {{tombstones: {session_parts: object[], calls: object[], session_usage: object[]}, kept: {session_parts: object[], calls: object[]},
|
|
193
|
+
* report: object, pairs: Array, dead: number}} dead: the legacy ship rows dead once the tombstones land (dedupe-v3 deadShip)
|
|
194
|
+
*/
|
|
195
|
+
function planProfileDedupe(o) {
|
|
196
|
+
const parts = o.legacyParts || []; const calls = o.legacyCalls || []; const usage = o.legacyUsage || [];
|
|
197
|
+
const lt = legacyTurnsOf(parts, usage);
|
|
198
|
+
const lc = legacyCallsOf(calls);
|
|
199
|
+
const cap = capturedOf(o.capturedParts || [], o.capturedUsage || []);
|
|
200
|
+
const srcT = newestSources(lt, parts, base.turnKey);
|
|
201
|
+
const srcC = newestSources(lc, calls, base.callKey);
|
|
202
|
+
const plan = dedupe.planDedupe({
|
|
203
|
+
harness: o.harness, session_id: o.session_id, profile: o.profile, repeats: o.repeats,
|
|
204
|
+
legacyTurns: lt, legacyCalls: lc, capturedTurns: cap.turns, capturedCalls: cap.calls,
|
|
205
|
+
});
|
|
206
|
+
// A kept '[assistant activity]' row (a shipped one: imports are never touched) whose turn kept no
|
|
207
|
+
// message: its captured usage has no place in the order, so it is matched by its tokens, each record
|
|
208
|
+
// once: the same non-zero output, and the same input net of the cache on each side (0.18 kept Codex
|
|
209
|
+
// input without its cached tokens; the package counts them in).
|
|
210
|
+
const net = (u) => { const i = Number(u.input) || 0; return u.input_includes_cache === true || u.input_includes_cache === 'true' || u.input_includes_cache === 1
|
|
211
|
+
? Math.max(i - (Number(u.cache_read) || 0) - (Number(u.cache_write) || 0), 0) : i; };
|
|
212
|
+
const legacyUsageOf = new Map(usage.filter((u) => s(u.anchor_kind) === 'message').map((u) => [s(u.anchor_id), u]));
|
|
213
|
+
const loose = [...cap.loose];
|
|
214
|
+
const retiredTurns = [...plan.tombstones.turns]; const keptTurns = [];
|
|
215
|
+
for (const t of plan.kept.turns) {
|
|
216
|
+
const lu = legacyUsageOf.get(s(t.message_id));
|
|
217
|
+
const i = t.kind === 'usage' && s(t.message_id).startsWith('legacy:ship:') && lu && (Number(lu.input) || Number(lu.output))
|
|
218
|
+
? loose.findIndex((u) => Number(u.output) === Number(lu.output) && net(u) === net(lu)) : -1;
|
|
219
|
+
if (i < 0) { keptTurns.push(t); continue; }
|
|
220
|
+
loose.splice(i, 1);
|
|
221
|
+
retiredTurns.push(t);
|
|
222
|
+
plan.report.matched.turns++; plan.report.tombstoned.turns++; plan.report.kept.turns--; plan.deadShip++;
|
|
223
|
+
}
|
|
224
|
+
const retired = retiredTurns.map((t) => srcT.get(base.turnKey(t)));
|
|
225
|
+
const deadBefore = [...srcT.values()].filter((p) => Number(p.is_deleted));
|
|
226
|
+
return {
|
|
227
|
+
tombstones: {
|
|
228
|
+
session_parts: retired.map(tombstone),
|
|
229
|
+
calls: plan.tombstones.calls.map((t) => tombstone(srcC.get(base.callKey(t)))),
|
|
230
|
+
session_usage: usageOf([...deadBefore, ...retired], parts, usage).map(tombstone),
|
|
231
|
+
},
|
|
232
|
+
kept: { session_parts: keptTurns.map((t) => srcT.get(base.turnKey(t))), calls: plan.kept.calls.map((t) => srcC.get(base.callKey(t))) },
|
|
233
|
+
report: plan.report, pairs: plan.pairs, dead: plan.deadShip,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { planProfileDedupe, legacyTurnsOf, legacyCallsOf, capturedOf, legacyTextOf, wireTs };
|