@volter/supercode-interchange 0.1.1 → 0.1.3
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/README.md +88 -0
- package/hermes-store.mjs +5 -2
- package/message.mjs +85 -0
- package/native-json.mjs +94 -7
- package/package.json +16 -3
- package/session-claude-append.mjs +107 -0
- package/session-claude-content.mjs +209 -0
- package/session-claude-files.mjs +78 -0
- package/session-claude-graph.mjs +285 -0
- package/session-claude-index.mjs +173 -0
- package/session-claude.mjs +73 -0
- package/session-codec-utils.mjs +115 -0
- package/session-codecs.d.ts +64 -0
- package/session-codecs.mjs +19 -0
- package/session-codex-context.mjs +68 -0
- package/session-codex-display-items.mjs +89 -0
- package/session-codex-display.mjs +81 -0
- package/session-codex-items.mjs +117 -0
- package/session-codex-user-history.mjs +62 -0
- package/session-codex.mjs +135 -0
- package/session-display.mjs +135 -0
- package/session-document.mjs +19 -0
- package/session-file.mjs +27 -0
- package/session-follower.mjs +181 -0
- package/session-gemini.mjs +93 -0
- package/session-goose.mjs +139 -0
- package/session-grok.mjs +72 -0
- package/session-hermes.mjs +116 -0
- package/session-lines.mjs +20 -0
- package/session-model.d.ts +114 -0
- package/session-model.mjs +70 -0
- package/session-native-residue.mjs +109 -0
- package/session-native.mjs +62 -0
- package/session-opencode-content.mjs +115 -0
- package/session-opencode-store.mjs +101 -0
- package/session-opencode.mjs +115 -0
- package/session-path.mjs +129 -0
- package/session-pi-content.mjs +129 -0
- package/session-pi.mjs +86 -0
- package/session-read.d.ts +12 -0
- package/session-read.mjs +104 -0
- package/session-source.mjs +30 -0
- package/session-time.mjs +46 -0
- package/session-views.mjs +171 -0
package/README.md
CHANGED
|
@@ -7,6 +7,94 @@ The port branch routes discovery and index windows through this library and the
|
|
|
7
7
|
cover selected small homes; publication, all-format and large-home qualification remain open.
|
|
8
8
|
Full session translation codecs remain at their later boundary.
|
|
9
9
|
|
|
10
|
+
The `./session-model` subpath supplies the step-5 read service's typed message
|
|
11
|
+
and retained-session representation, native raw-line framing, normalized views,
|
|
12
|
+
projected message windows, inline-media elision and recorded profile history.
|
|
13
|
+
Source metadata is separate from provider-facing message serialization. Bounding
|
|
14
|
+
a display view keeps its raw records and imported count; a view is not an export
|
|
15
|
+
or a continuation artifact. The port branch's public load/follow handlers now
|
|
16
|
+
use these readers through caller-owned worker lanes; shared Rust models remain for
|
|
17
|
+
the native consumers named in the architecture plan. No runtime qualification
|
|
18
|
+
is claimed for these new helpers.
|
|
19
|
+
|
|
20
|
+
The read-codec slice has named document and store readers in
|
|
21
|
+
`./session-codecs`. They preserve raw text, native
|
|
22
|
+
message extensions, tool pairing and source metadata; the Goose store read
|
|
23
|
+
uses one read-only DB/WAL snapshot and refuses a changed source. They do not
|
|
24
|
+
own a native parsing process. The `./session-read` service selects a structural
|
|
25
|
+
reader and never falls back to Rust. These readers
|
|
26
|
+
have source and syntax checks only, with no runtime
|
|
27
|
+
qualification claimed.
|
|
28
|
+
Hermes selects only native active rows for replay while retaining inactive rows
|
|
29
|
+
in its reconstructed raw container. Its lineage, trigger, surface and profile
|
|
30
|
+
interpretation is shared with the metadata reader; recorded lifecycle and
|
|
31
|
+
message timestamps retain their separate native rounding rules. No Hermes
|
|
32
|
+
write tier is introduced.
|
|
33
|
+
Grok separates user/system envelopes, assistant tools and screenshot results
|
|
34
|
+
from raw reasoning/backend records. Its portable residue is verified using the
|
|
35
|
+
existing BLAKE3 digest contract before restoration; missing-record summaries
|
|
36
|
+
name the lost residue. Path/summary overlays belong to the read service slice.
|
|
37
|
+
Claude selects its active graph branch and compacted history before decoding
|
|
38
|
+
messages, retaining exact reasoning blocks, tool outcomes, media and creating
|
|
39
|
+
record provenance. Semantic views name disconnected or degraded graph segments;
|
|
40
|
+
strict reads refuse a reconstruction they cannot establish. Its file reader
|
|
41
|
+
attaches native subagent transcripts and verifies the main and child file
|
|
42
|
+
identities. Its scoped index retains graph references and ordering/count
|
|
43
|
+
skeletons while materializing only requested assistant groups and summary
|
|
44
|
+
messages. The file handle closes before the result leaves the scope. The
|
|
45
|
+
public read/follow handoff has source and syntax checks only, with no runtime
|
|
46
|
+
qualification claimed yet.
|
|
47
|
+
Codex has separate model-context and bounded display-history readers. The
|
|
48
|
+
model reader retains compaction replacement, turn grouping, reasoning and
|
|
49
|
+
native typed-input pairing; the display reader projects durable TurnItems or
|
|
50
|
+
legacy typed events with human-boundary anchors. Context and hook classification
|
|
51
|
+
is shared between both. Hook XML is parsed by the pinned `saxes` dependency;
|
|
52
|
+
source checks do not establish XML or display runtime parity. These additions
|
|
53
|
+
are used by the public load/follow path on this port branch.
|
|
54
|
+
Pi's active parent chain retains compaction flags, native signatures and
|
|
55
|
+
message clocks, custom messages and source-native entry ids. Its OpenClaw
|
|
56
|
+
dialect follows recorded leaf redirects, excludes side appends from default
|
|
57
|
+
leaf selection and keeps vendor references inert. Unknown Pi image shapes
|
|
58
|
+
remain raw-only, following the existing reader's refusal to synthesize image
|
|
59
|
+
data it cannot establish. These are read-library additions, with no new
|
|
60
|
+
OpenClaw write tier or runtime qualification.
|
|
61
|
+
OpenCode's envelope and export-document readers share creation/id ordering,
|
|
62
|
+
compaction boundaries, portable message extensions and task-child provenance.
|
|
63
|
+
Completed tool parts split into calls/results, retaining masked original output
|
|
64
|
+
and attachment content. Its SQLite reader reconstructs native columns, reads
|
|
65
|
+
the external revert sidecar and verifies both identities; malformed JSON columns
|
|
66
|
+
remain diagnostic. Legacy JSON-tree directories retain their native
|
|
67
|
+
unsupported-source refusal. No runtime qualification is claimed for this
|
|
68
|
+
checkpoint.
|
|
69
|
+
The path layer selects formats structurally, pins native store selectors and
|
|
70
|
+
reads compressed transcripts through Node's zstd support. Native sidecars
|
|
71
|
+
validate framing and retain malformed appended turns as raw parse loss.
|
|
72
|
+
Grok summaries and OpenClaw path profiles keep their source meanings. Display
|
|
73
|
+
reads retain the native byte-window, human-anchor and fixed-byte-anchor rules;
|
|
74
|
+
those bounded views remain unsuitable as continuation artifacts. Public
|
|
75
|
+
load/follow uses `./session-read` on this branch. Its TypeScript follower pins
|
|
76
|
+
the store selector, compares filesystem identities and reports snapshot,
|
|
77
|
+
append, rewrite and recoverable-error events. Cold follower opening performs
|
|
78
|
+
one selection read followed by a fingerprint-bracketed pinned read. Changed
|
|
79
|
+
full-history Claude files can reuse a conservative append projection after
|
|
80
|
+
comparing every old prefix byte. Partial, tool, branch, assistant-chunk and
|
|
81
|
+
companion changes retain the canonical full loader. This optimization has
|
|
82
|
+
source/syntax checks only, with no runtime qualification yet.
|
|
83
|
+
|
|
84
|
+
The npm stdio router and the machine's harness streams use caller-owned load
|
|
85
|
+
and follow worker lanes from `@volter/supercode-teams/session-read-client`.
|
|
86
|
+
Large parsing jobs do not run on their routing thread. Each stream owns its
|
|
87
|
+
subscriptions; response admission precedes activation, and close terminates
|
|
88
|
+
its workers. The machine rechecks operator authority before content leaves
|
|
89
|
+
an asynchronous read. Runtime-state events use authenticated loopback
|
|
90
|
+
`frontend.v2.describe` with an `observe` narrowing header, not transcript
|
|
91
|
+
activity. This read-only probe does not retire runtime receipts; the retained
|
|
92
|
+
registry owns that behavior until its later replacement. No Windows caller
|
|
93
|
+
authority implementation or runtime qualification is implied by this slice.
|
|
94
|
+
The SDK checks the read capability before moved requests; an older service
|
|
95
|
+
refuses explicitly instead of supplying a native read. Its catalog and other
|
|
96
|
+
retained operations remain independently available on the same client.
|
|
97
|
+
|
|
10
98
|
`resolveHarnessHomes` follows native HOME-first defaults and preserves explicitly empty harness-specific
|
|
11
99
|
home values. `projectMetadata` applies workspace scope, native lineage, repository-family, profile, time and
|
|
12
100
|
metadata-search filters with stable newest-first ordering. It clones raw rows before descendant roll-up.
|
package/hermes-store.mjs
CHANGED
|
@@ -37,8 +37,8 @@ function nouns(row, relationship, config, extended) {
|
|
|
37
37
|
const source = nonempty(row.source) ?? '';
|
|
38
38
|
let trigger = relationship === 'delegate' ? 'parent' : sourceTrigger(source);
|
|
39
39
|
let recurrence = null;
|
|
40
|
-
const cron = /^cron_(.+)_([0-9]{8})_([0-9]{6})
|
|
41
|
-
if (cron) { trigger = 'cron'; recurrence = { job_id: cron[1], kind: 'cron' }; }
|
|
40
|
+
const cron = /^cron_(.+)_([0-9]{8})_([0-9]{6})$/s.exec(row.id);
|
|
41
|
+
if (cron?.[0].length === row.id.length) { trigger = 'cron'; recurrence = { job_id: cron[1], kind: 'cron' }; }
|
|
42
42
|
recurrence ??= mirrorRecurrence(config);
|
|
43
43
|
let surface = {}, profile = null;
|
|
44
44
|
if (extended) {
|
|
@@ -66,6 +66,9 @@ function nouns(row, relationship, config, extended) {
|
|
|
66
66
|
...(state != null ? { cross_surface: { state, ...(platform != null ? { platform } : {}), ...(error != null ? { error } : {}) } } : {}) };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// Transcript and metadata readers share these native row interpretations.
|
|
70
|
+
export { lineage as hermesLineageKind, nouns as hermesSessionNouns };
|
|
71
|
+
|
|
69
72
|
export async function readHermesStore(path, { metrics = {} } = {}) {
|
|
70
73
|
return readStore(path, (db) => {
|
|
71
74
|
statement(db, 'SELECT count(*) FROM sessions').get();
|
package/message.mjs
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Provider-neutral messages. Source provenance lives beside the provider wire
|
|
2
|
+
// fields; serialization to a model never sends it as another instruction.
|
|
3
|
+
import { cloneNativeJson, parseNativeJson, stringifyNativeJsonMember } from './native-json.mjs';
|
|
4
|
+
import { nativeTrim } from './text.mjs';
|
|
5
|
+
|
|
6
|
+
export const ROLES = Object.freeze(['system', 'user', 'assistant', 'tool']);
|
|
7
|
+
export const TOOL_ERROR_METADATA_KEY = 'sc.tool_error';
|
|
8
|
+
export const TOOL_OUTCOME_UNKNOWN_METADATA_KEY = 'sc.tool_outcome_unknown';
|
|
9
|
+
|
|
10
|
+
const object = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
const invalid = (field) => { throw new TypeError(`invalid canonical message: ${field}`); };
|
|
12
|
+
const optionalString = (value, key) => {
|
|
13
|
+
if (value[key] == null) return null;
|
|
14
|
+
if (typeof value[key] !== 'string') invalid(`${key} must be a string`);
|
|
15
|
+
return value[key];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Internal representation: null and an explicitly empty array are distinct. */
|
|
19
|
+
export function createMessage(role, content = null) {
|
|
20
|
+
if (!ROLES.includes(role)) invalid('role must be system, user, assistant or tool');
|
|
21
|
+
if (content !== null && typeof content !== 'string') invalid('text content must be a string');
|
|
22
|
+
return { role, content, content_parts: null, tool_calls: null, tool_call_id: null, name: null, metadata: Object.create(null) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The provider-shaped decoder deliberately ignores metadata, as ChatMessage's
|
|
26
|
+
* Deserialize implementation does. Native-turn readers restore it separately. */
|
|
27
|
+
export function deserializeMessage(value) {
|
|
28
|
+
if (!object(value)) invalid('expected an object');
|
|
29
|
+
const message = createMessage(value.role);
|
|
30
|
+
if (typeof value.content === 'string') message.content = value.content;
|
|
31
|
+
else if (Array.isArray(value.content)) message.content_parts = cloneNativeJson(value.content);
|
|
32
|
+
else if (value.content != null) message.content = stringifyNativeJsonMember(value, 'content');
|
|
33
|
+
message.tool_call_id = optionalString(value, 'tool_call_id');
|
|
34
|
+
message.name = optionalString(value, 'name');
|
|
35
|
+
if (value.tool_calls != null) {
|
|
36
|
+
if (!Array.isArray(value.tool_calls)) invalid('tool_calls must be an array');
|
|
37
|
+
message.tool_calls = value.tool_calls.map((call) => {
|
|
38
|
+
if (!object(call) || typeof call.id !== 'string') invalid('tool call id must be a string');
|
|
39
|
+
const kind = Object.hasOwn(call, 'type') ? call.type : 'function';
|
|
40
|
+
if (typeof kind !== 'string') invalid('tool call type must be a string');
|
|
41
|
+
if (!object(call.function) || typeof call.function.name !== 'string' || typeof call.function.arguments !== 'string') {
|
|
42
|
+
invalid('tool call function needs string name and arguments');
|
|
43
|
+
}
|
|
44
|
+
return { id: call.id, type: kind, function: { name: call.function.name, arguments: call.function.arguments } };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return message;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Model-facing serialization, omitting absent fields and source metadata. */
|
|
51
|
+
export function messageWire(message) {
|
|
52
|
+
const value = { role: message.role };
|
|
53
|
+
if (message.content_parts != null) value.content = cloneNativeJson(message.content_parts);
|
|
54
|
+
else if (message.content != null) value.content = message.content;
|
|
55
|
+
if (message.tool_calls != null) value.tool_calls = cloneNativeJson(message.tool_calls);
|
|
56
|
+
if (message.tool_call_id != null) value.tool_call_id = message.tool_call_id;
|
|
57
|
+
if (message.name != null) value.name = message.name;
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Session-read serialization adds the complete source provenance map. */
|
|
62
|
+
export function messageJson(message) {
|
|
63
|
+
return { ...messageWire(message), metadata: cloneNativeJson(message.metadata) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function parsedArguments(call) {
|
|
67
|
+
const text = nativeTrim(call.function.arguments);
|
|
68
|
+
return text === '' ? {} : parseNativeJson(text);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function markToolError(message) {
|
|
72
|
+
delete message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY];
|
|
73
|
+
message.metadata[TOOL_ERROR_METADATA_KEY] = 'true';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function markToolOutcomeUnknown(message) {
|
|
77
|
+
delete message.metadata[TOOL_ERROR_METADATA_KEY];
|
|
78
|
+
message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY] = 'true';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function isToolError(message) { return message.metadata[TOOL_ERROR_METADATA_KEY] === 'true'; }
|
|
82
|
+
export function toolOutcome(message) {
|
|
83
|
+
if (isToolError(message)) return 'known_error';
|
|
84
|
+
return message.metadata[TOOL_OUTCOME_UNKNOWN_METADATA_KEY] === 'true' ? 'unknown' : 'known_success';
|
|
85
|
+
}
|
package/native-json.mjs
CHANGED
|
@@ -123,7 +123,25 @@ export function parseNativeJson(text) {
|
|
|
123
123
|
return result.value;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
export function nativeObjectKeys(value) {
|
|
126
|
+
export function nativeObjectKeys(value) {
|
|
127
|
+
const keys = facts.get(value)?.keys;
|
|
128
|
+
if (!keys) return Object.keys(value);
|
|
129
|
+
const known = new Set(keys);
|
|
130
|
+
return [...keys.filter((key) => Object.hasOwn(value, key)), ...Object.keys(value).filter((key) => !known.has(key))];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Transcript projections clone JSON before bounding strings or eliding media.
|
|
134
|
+
// Keep the source's numeric tokens and insertion order with those values.
|
|
135
|
+
export function cloneNativeJson(value) {
|
|
136
|
+
if (value == null || typeof value !== 'object') return value;
|
|
137
|
+
const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value) === null ? null : Object.prototype);
|
|
138
|
+
for (const key of nativeObjectKeys(value)) {
|
|
139
|
+
Object.defineProperty(result, key, { value: cloneNativeJson(value[key]), enumerable: true, writable: true, configurable: true });
|
|
140
|
+
}
|
|
141
|
+
const meta = facts.get(value);
|
|
142
|
+
if (meta) facts.set(result, { keys: [...meta.keys], numbers: new Map(meta.numbers) });
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
127
145
|
|
|
128
146
|
export function nativeInteger(value, key, { signed = false } = {}) {
|
|
129
147
|
if (value == null || typeof value !== 'object') return null;
|
|
@@ -132,6 +150,30 @@ export function nativeInteger(value, key, { signed = false } = {}) {
|
|
|
132
150
|
const whole = fact.integer;
|
|
133
151
|
return whole >= (signed ? intMin : 0n) && whole <= (signed ? intMax : uintMax) ? whole : null;
|
|
134
152
|
}
|
|
153
|
+
export function nativeNumberKind(value, key) {
|
|
154
|
+
const fact = facts.get(value)?.numbers.get(String(key));
|
|
155
|
+
return fact && Object.is(value[key], fact.value) ? fact.kind : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Native SQLite INTEGER columns enter JSON without a lossy Number round
|
|
159
|
+
* trip. This helper records their exact i64/u64 value for the serializer. */
|
|
160
|
+
export function setNativeInteger(value, key, integer) {
|
|
161
|
+
if (typeof integer !== 'bigint' || integer < intMin || integer > uintMax) throw new RangeError('not a native JSON integer');
|
|
162
|
+
let meta = facts.get(value);
|
|
163
|
+
if (!meta) { meta = { keys: Object.keys(value), numbers: new Map() }; facts.set(value, meta); }
|
|
164
|
+
if (!Object.hasOwn(value, key)) meta.keys.push(String(key));
|
|
165
|
+
Object.defineProperty(value, key, { value: Number(integer), enumerable: true, writable: true, configurable: true });
|
|
166
|
+
meta.numbers.set(String(key), { value: Number(integer), kind: 'integer', integer });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function setNativeFloat(value, key, number) {
|
|
170
|
+
let meta = facts.get(value);
|
|
171
|
+
if (!meta) { meta = { keys: Object.keys(value), numbers: new Map() }; facts.set(value, meta); }
|
|
172
|
+
if (!Object.hasOwn(value, key)) meta.keys.push(String(key));
|
|
173
|
+
value[key] = Number.isFinite(number) ? number : null;
|
|
174
|
+
if (value[key] == null) meta.numbers.delete(String(key));
|
|
175
|
+
else meta.numbers.set(String(key), { value: number, kind: 'float' });
|
|
176
|
+
}
|
|
135
177
|
|
|
136
178
|
function formatFloat(number) {
|
|
137
179
|
if (number === 0) return Object.is(number, -0) ? '-0.0' : '0.0';
|
|
@@ -149,12 +191,57 @@ function formatFloat(number) {
|
|
|
149
191
|
}
|
|
150
192
|
|
|
151
193
|
export function stringifyNativeJson(value) {
|
|
194
|
+
if (typeof value === 'bigint') return String(value);
|
|
152
195
|
if (value == null || typeof value !== 'object') return JSON.stringify(value);
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
196
|
+
const child = (key) => stringifyNativeJsonMember(value, key);
|
|
197
|
+
if (Array.isArray(value)) return `[${Array.from({ length: value.length }, (_, at) => child(String(at)) ?? 'null').join(',')}]`;
|
|
198
|
+
return `{${nativeObjectKeys(value).flatMap((key) => {
|
|
199
|
+
const encoded = child(key); return encoded === undefined ? [] : [`${JSON.stringify(key)}:${encoded}`];
|
|
200
|
+
}).join(',')}}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** serde_json Value equality ignores object order but distinguishes integers
|
|
204
|
+
* from floats. Keep those facts when comparing transcript content; ordinary
|
|
205
|
+
* deep JS equality cannot distinguish an exact u64 from its rounded Number. */
|
|
206
|
+
export function nativeJsonEqual(left, right) {
|
|
207
|
+
const number = (parent, key) => {
|
|
208
|
+
const value = parent[key], fact = facts.get(parent)?.numbers.get(String(key));
|
|
209
|
+
if (fact && Object.is(value, fact.value)) return fact;
|
|
210
|
+
return typeof value === 'number' ? { kind: Number.isInteger(value) && !Object.is(value, -0) ? 'integer' : 'float',
|
|
211
|
+
value, integer: Number.isInteger(value) ? BigInt(value) : null } : null;
|
|
212
|
+
};
|
|
213
|
+
const equal = (a, b) => {
|
|
214
|
+
if (a == null || b == null || typeof a !== 'object' || typeof b !== 'object') return a === b;
|
|
215
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
216
|
+
const keys = Object.keys(a);
|
|
217
|
+
if (keys.length !== Object.keys(b).length) return false;
|
|
218
|
+
for (const key of keys) {
|
|
219
|
+
if (!Object.hasOwn(b, key)) return false;
|
|
220
|
+
const first = number(a, key), second = number(b, key);
|
|
221
|
+
if (first || second) {
|
|
222
|
+
if (!first || !second || first.kind !== second.kind
|
|
223
|
+
|| (first.kind === 'integer' ? first.integer !== second.integer : first.value !== second.value)) return false;
|
|
224
|
+
} else if (!equal(a[key], b[key])) return false;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
157
227
|
};
|
|
158
|
-
|
|
159
|
-
|
|
228
|
+
return equal(left, right);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function stringifyNativeJsonMember(value, key) {
|
|
232
|
+
const fact = facts.get(value)?.numbers.get(String(key));
|
|
233
|
+
return fact && Object.is(value[key], fact.value) ? fact.kind === 'integer' ? String(fact.integer) : formatFloat(fact.value) : stringifyNativeJson(value[key]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function stringifyNativeJsonPretty(value, depth = 0) {
|
|
237
|
+
if (value == null || typeof value !== 'object') return stringifyNativeJson(value);
|
|
238
|
+
const keys = nativeObjectKeys(value), array = Array.isArray(value);
|
|
239
|
+
if (!keys.length) return array ? '[]' : '{}';
|
|
240
|
+
const padding = ' '.repeat(depth + 1);
|
|
241
|
+
const lines = keys.map((key) => {
|
|
242
|
+
const child = value[key] != null && typeof value[key] === 'object'
|
|
243
|
+
? stringifyNativeJsonPretty(value[key], depth + 1) : stringifyNativeJsonMember(value, key);
|
|
244
|
+
return padding + (array ? child : `${JSON.stringify(key)}: ${child}`);
|
|
245
|
+
});
|
|
246
|
+
return `${array ? '[' : '{'}\n${lines.join(',\n')}\n${' '.repeat(depth)}${array ? ']' : '}'}`;
|
|
160
247
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/supercode-interchange",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Read-only harness metadata and
|
|
5
|
+
"description": "Read-only harness metadata, transcript codecs and session projections for Supercode",
|
|
6
6
|
"license": "MIT OR Apache-2.0",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
9
|
"types": "./index.d.ts",
|
|
10
10
|
"import": "./index.mjs"
|
|
11
|
+
},
|
|
12
|
+
"./session-model": {
|
|
13
|
+
"types": "./session-model.d.ts",
|
|
14
|
+
"import": "./session-model.mjs"
|
|
15
|
+
},
|
|
16
|
+
"./session-codecs": {
|
|
17
|
+
"types": "./session-codecs.d.ts",
|
|
18
|
+
"import": "./session-codecs.mjs"
|
|
19
|
+
},
|
|
20
|
+
"./session-read": {
|
|
21
|
+
"types": "./session-read.d.ts",
|
|
22
|
+
"import": "./session-read.mjs"
|
|
11
23
|
}
|
|
12
24
|
},
|
|
13
25
|
"files": [
|
|
@@ -21,6 +33,7 @@
|
|
|
21
33
|
"node": ">=22.15.0"
|
|
22
34
|
},
|
|
23
35
|
"dependencies": {
|
|
24
|
-
"@noble/hashes": "2.4.0"
|
|
36
|
+
"@noble/hashes": "2.4.0",
|
|
37
|
+
"saxes": "6.0.0"
|
|
25
38
|
}
|
|
26
39
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Conservative native append optimization. Every retained prefix byte is
|
|
2
|
+
// compared before reuse. Complex graph/tool/chunk changes use the full codec.
|
|
3
|
+
import { withSessionFile } from './file-io.mjs';
|
|
4
|
+
import { decodeSessionUtf8 } from './session-file.mjs';
|
|
5
|
+
import { parseNativeJson, cloneNativeJson, nativeJsonEqual } from './native-json.mjs';
|
|
6
|
+
import { nativeTrim } from './text.mjs';
|
|
7
|
+
import { field, stringField } from './session-codec-utils.mjs';
|
|
8
|
+
import { ClaudeReplayIndex } from './session-claude-graph.mjs';
|
|
9
|
+
import { captureClaudeMeta } from './session-claude.mjs';
|
|
10
|
+
import { pushClaudeUser, captureClaudeProvenance, mergeClaudeAssistantChunk, flushClaudeAssistant } from './session-claude-content.mjs';
|
|
11
|
+
|
|
12
|
+
const messageId = (value) => stringField(field(value, 'message'), 'id');
|
|
13
|
+
function simpleMessage(value) {
|
|
14
|
+
if (value != null && typeof value === 'object' && Object.keys(value).some((key) => key.startsWith('_supercode_'))) return false;
|
|
15
|
+
if (!['user', 'assistant'].includes(stringField(value, 'type'))) return false;
|
|
16
|
+
const content = field(field(value, 'message'), 'content');
|
|
17
|
+
return typeof content === 'string' || Array.isArray(content)
|
|
18
|
+
&& content.every((block) => ['text', 'image', 'thinking', 'redacted_thinking', 'fallback'].includes(stringField(block, 'type')));
|
|
19
|
+
}
|
|
20
|
+
function* rawChunks(raw) {
|
|
21
|
+
for (const line of raw) {
|
|
22
|
+
for (let at = 0; at < line.length;) {
|
|
23
|
+
let end = Math.min(line.length, at + 32 * 1024);
|
|
24
|
+
const last = line.charCodeAt(end - 1);
|
|
25
|
+
if (end < line.length && last >= 0xd800 && last <= 0xdbff) end--;
|
|
26
|
+
yield Buffer.from(line.slice(at, end)); at = end;
|
|
27
|
+
}
|
|
28
|
+
yield Buffer.from('\n');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async function prefixMatches(reader, raw) {
|
|
32
|
+
let position = 0, available = Buffer.alloc(0), used = 0;
|
|
33
|
+
for (const expected of rawChunks(raw)) {
|
|
34
|
+
for (let at = 0; at < expected.length;) {
|
|
35
|
+
if (used === available.length) { available = await reader.read(position, 64 * 1024); used = 0; }
|
|
36
|
+
const length = Math.min(available.length - used, expected.length - at);
|
|
37
|
+
if (!length || !expected.subarray(at, at + length).equals(available.subarray(used, used + length))) return false;
|
|
38
|
+
used += length; at += length; position += length;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class ClaudeAppendState {
|
|
45
|
+
static fromSession(session, fidelity) {
|
|
46
|
+
if (session.meta.source !== 'claude_code' || !session.raw_is_verbatim || !session.raw_trailing_newline
|
|
47
|
+
|| session.parse_error_lines !== 0 || session.subagents.length) return null;
|
|
48
|
+
const graph = new ClaudeReplayIndex(); let bytes = 0;
|
|
49
|
+
session.raw.forEach((line, index) => {
|
|
50
|
+
bytes += Buffer.byteLength(line) + 1;
|
|
51
|
+
if (nativeTrim(line) !== '') graph.observe(index, parseNativeJson(line));
|
|
52
|
+
});
|
|
53
|
+
const selection = graph.clone().selectLines(fidelity);
|
|
54
|
+
if (!nativeJsonEqual(selection.residue, session.load_residue)) return null;
|
|
55
|
+
let lastAssistant = null;
|
|
56
|
+
if (selection.lines.length) {
|
|
57
|
+
const last = parseNativeJson(session.raw[selection.lines.at(-1)]);
|
|
58
|
+
if (stringField(last, 'type') === 'assistant') lastAssistant = messageId(last);
|
|
59
|
+
}
|
|
60
|
+
return new ClaudeAppendState(graph, selection.lines, bytes, lastAssistant);
|
|
61
|
+
}
|
|
62
|
+
constructor(graph, replay, bytes, lastAssistant) { this.graph = graph; this.replay = replay; this.bytes = bytes; this.lastAssistant = lastAssistant; }
|
|
63
|
+
async append(path, current, fidelity, { metrics } = {}) {
|
|
64
|
+
return (await withSessionFile(path, async (reader) => {
|
|
65
|
+
if (reader.size <= this.bytes || reader.size - this.bytes > 8 * 1024 * 1024) return null;
|
|
66
|
+
if (!await prefixMatches(reader, current.raw)) return null;
|
|
67
|
+
const appended = decodeSessionUtf8(await reader.read(this.bytes, reader.size - this.bytes));
|
|
68
|
+
if (!appended.endsWith('\n')) return null;
|
|
69
|
+
const lines = appended.slice(0, -1).split('\n'), values = [], graph = this.graph.clone(), meta = cloneNativeJson(current.meta);
|
|
70
|
+
for (let at = 0; at < lines.length; at++) {
|
|
71
|
+
const line = lines[at]; if (nativeTrim(line) === '') { values.push(null); continue; }
|
|
72
|
+
let value; try { value = parseNativeJson(line); } catch { return null; }
|
|
73
|
+
if (!simpleMessage(value)) return null;
|
|
74
|
+
graph.observe(current.raw.length + at, value); captureClaudeMeta(value, meta, line); values.push(value);
|
|
75
|
+
}
|
|
76
|
+
const selection = graph.clone().selectLines(fidelity);
|
|
77
|
+
if (selection.lines.length < this.replay.length || this.replay.some((index, at) => selection.lines[at] !== index)) return null;
|
|
78
|
+
const added = selection.lines.slice(this.replay.length);
|
|
79
|
+
if (added.some((index) => index < current.raw.length)) return null;
|
|
80
|
+
if (added.length && this.lastAssistant != null && messageId(values[added[0] - current.raw.length]) === this.lastAssistant) return null;
|
|
81
|
+
const messages = []; let pending = null;
|
|
82
|
+
for (const index of added) {
|
|
83
|
+
const value = values[index - current.raw.length];
|
|
84
|
+
if (stringField(value, 'type') === 'assistant') {
|
|
85
|
+
if (field(value, 'isApiErrorMessage') === true) { flushClaudeAssistant(pending, messages); pending = null; continue; }
|
|
86
|
+
if (pending != null) {
|
|
87
|
+
if (messageId(pending) != null && messageId(pending) === messageId(value)) { mergeClaudeAssistantChunk(pending, value); continue; }
|
|
88
|
+
flushClaudeAssistant(pending, messages);
|
|
89
|
+
}
|
|
90
|
+
pending = cloneNativeJson(value);
|
|
91
|
+
} else {
|
|
92
|
+
flushClaudeAssistant(pending, messages); pending = null;
|
|
93
|
+
const before = messages.length; pushClaudeUser(value, messages); captureClaudeProvenance(value, messages, before);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
flushClaudeAssistant(pending, messages);
|
|
97
|
+
const last = added.length ? values[added.at(-1) - current.raw.length] : null;
|
|
98
|
+
const lastAssistant = added.length ? stringField(last, 'type') === 'assistant' ? messageId(last) : null : this.lastAssistant;
|
|
99
|
+
const session = cloneNativeJson(current), count = (current.imported_message_count ?? current.messages.length) + messages.length;
|
|
100
|
+
session.meta = meta;
|
|
101
|
+
for (const message of messages) session.messages.push(message);
|
|
102
|
+
for (const line of lines) session.raw.push(line);
|
|
103
|
+
session.imported_message_count = count; session.load_residue = selection.residue;
|
|
104
|
+
return { session, state: new ClaudeAppendState(graph, selection.lines, reader.size, lastAssistant) };
|
|
105
|
+
}, metrics)).result;
|
|
106
|
+
}
|
|
107
|
+
}
|