@tangle-network/harness-sessions 0.1.7 → 0.2.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/README.md +13 -3
- package/dist/builder.d.ts +0 -1
- package/dist/builder.js +1 -1
- package/dist/capture.js +2 -1
- package/dist/formats/claude-code.js +2 -1
- package/dist/formats/codex.js +2 -1
- package/dist/formats/factory.js +2 -1
- package/dist/formats/kimi.js +22 -9
- package/dist/formats/opencode.js +2 -1
- package/dist/formats/pi.js +2 -1
- package/dist/projections/turns.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ npm install @tangle-network/harness-sessions
|
|
|
15
15
|
- The sidecar's harness session contract (agent-container `packages/cli-agent-registry`) imports `STORES` and `SessionFormatId` from here; it keeps no second copy of where a harness writes.
|
|
16
16
|
- The capture path reads captures through `listCapture` / `readCapture`.
|
|
17
17
|
- Discovery's trace gate, agent-eval, agent-record and the blog read sessions only through this package.
|
|
18
|
-
- Its
|
|
18
|
+
- Its dependencies are `@tangle-network/agent-core`, `@tangle-network/agent-trace-contract` and Node built-ins (`node:sqlite` for OpenCode).
|
|
19
19
|
|
|
20
20
|
## Formats
|
|
21
21
|
|
|
@@ -25,6 +25,8 @@ npm install @tangle-network/harness-sessions
|
|
|
25
25
|
| `codex` | `codex.rollout-jsonl` | `rollout-*-{id}.jsonl` under `sessions/` and `archived_sessions/` of every Codex home (below) | turn context (the configured model) | per response (`last_token_usage`); session total from `total_token_usage` |
|
|
26
26
|
| `opencode` | `opencode.sqlite` | `.local/share/opencode/opencode.db` (shared by every session) | session record (`providerID/modelID`) | per step (`step-finish`) |
|
|
27
27
|
| `pi` | `pi.session-jsonl` | `.pi/agent/sessions/**/*_{id}.jsonl` | session record (`provider/model`) | per assistant message |
|
|
28
|
+
| `kimi-code` | `kimi.session-dir` | `.kimi/sessions/*/{id}/wire.jsonl`, child wires at `*/{id}/subagents/*/wire.jsonl` | none in the wire | per step, excluding wrapped child events |
|
|
29
|
+
| `factory-droids` | `factory.session-jsonl` | `.factory/sessions/*/{id}.jsonl` plus settings | configured model in settings (`turn-context`) | session total in settings |
|
|
28
30
|
|
|
29
31
|
A HOME can hold several Codex homes, and `codexReader.locate` reads all of them (`codexHomes(home)` lists them):
|
|
30
32
|
|
|
@@ -35,7 +37,15 @@ A HOME can hold several Codex homes, and `codexReader.locate` reads all of them
|
|
|
35
37
|
|
|
36
38
|
Homes that resolve to one directory count once. A session copied into several homes (`acct move`) is one ref, its largest copy. A lookup by id takes the rollout whose name ends in the id; when no name does, it reads the `session_meta` of each rollout modified since the id's UUIDv7 time.
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
`listReaders()` and the exported catalog own the complete installed format inventory.
|
|
41
|
+
|
|
42
|
+
## Child execution evidence
|
|
43
|
+
|
|
44
|
+
Kimi records a child `agent_id` and `parent_tool_call_id` on each wrapped `SubagentEvent`, and writes the child's own wire under the parent's `subagents/<agent_id>/` directory. `kimiReader.locate(home, { nativeSessionId: parentId })` returns the parent and these child refs. A child-ID lookup selects that child. The reader keeps wrapped child events out of the parent's messages and usage; read each returned child ref for its own recorded work. `kimiRefForFile` preserves the same parent binding when a child wire is read directly. A missing child wire remains missing execution evidence, even when its ID appears in the parent.
|
|
45
|
+
|
|
46
|
+
Pi's session-header `parentSession` is fork/clone/handoff lineage; it does not prove a delegated subagent. The native reader does not turn it or conversation-entry `parentId` into subagent execution. Extension workflow execution remains a separate recorded source. Factory Droid also needs an explicit producer-recorded parent/child binding before this reader can expose delegated children. Neither reader guesses relationships from a tool name, working directory, timing, or current project.
|
|
47
|
+
|
|
48
|
+
A bounded local capture inspection on 2026-10-25 found explicit Kimi child events and separate child wires. Its Pi and Factory Droid samples contained no explicit subagent binding: the latest 20 files per harness held 7,724 and 245 records respectively; an additional size-stratified selection read seven Pi files (8,229 records) and 20 Factory files (252 records). These are sample limits, not a claim that every producer version lacks the metadata. The remaining Pi/Factory child coverage is unknown. Closing it requires a retained parent/child capture with an explicit source binding, followed by a reader-to-OTLP integration proof; the graph-corpus integrating owner owns that follow-up.
|
|
39
49
|
|
|
40
50
|
## Use
|
|
41
51
|
|
|
@@ -77,5 +87,5 @@ harness-sessions read <file|home|capture> [--harness h] [--session id] [--summar
|
|
|
77
87
|
|
|
78
88
|
## Tests
|
|
79
89
|
|
|
80
|
-
The fixtures are sessions that the real harness CLIs wrote while talking to the trace-proof scripted model; the tests assert that script.
|
|
90
|
+
The baseline fixtures are sessions that the real harness CLIs wrote while talking to the trace-proof scripted model; the tests assert that script. The Kimi child fixture retains complete existing parent and child wire records with all content removed, IDs pseudonymized, and source hashes, record counts, redaction version and token counters recorded in `test/fixtures/kimi/provenance.json`. Its integration test exercises discovery, full/summary reads, in-memory input and contract-span projection, preserving parent and child usage separately.
|
|
81
91
|
The proof against the readers this package replaces is a comparison over stored sessions, recorded with each release.
|
package/dist/builder.d.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { SourceStats } from './source.js';
|
|
|
3
3
|
export type BuildMode = 'full' | 'summary';
|
|
4
4
|
/** An ISO-8601 instant from an ISO string, epoch milliseconds or epoch seconds; null otherwise. */
|
|
5
5
|
export declare function isoTime(value: unknown): string | null;
|
|
6
|
-
export declare const isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
7
6
|
export declare const str: (value: unknown) => string | null;
|
|
8
7
|
export declare function count(value: unknown): number | null;
|
|
9
8
|
/** Parse tool arguments recorded as text; keep the text when it is not JSON. */
|
package/dist/builder.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
import { HARNESS_SESSION_SCHEMA } from './schema.js';
|
|
2
3
|
/** An ISO-8601 instant from an ISO string, epoch milliseconds or epoch seconds; null otherwise. */
|
|
3
4
|
export function isoTime(value) {
|
|
@@ -11,7 +12,6 @@ export function isoTime(value) {
|
|
|
11
12
|
}
|
|
12
13
|
return null;
|
|
13
14
|
}
|
|
14
|
-
export const isRecord = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
15
15
|
export const str = (value) => (typeof value === 'string' ? value : null);
|
|
16
16
|
export function count(value) {
|
|
17
17
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : null;
|
package/dist/capture.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Reading the native sessions in a capture.
|
|
3
4
|
*
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { readdir, readFile } from 'node:fs/promises';
|
|
13
14
|
import { join } from 'node:path';
|
|
14
|
-
import {
|
|
15
|
+
import { str } from './builder.js';
|
|
15
16
|
import { opencodeSessionsInStore } from './formats/opencode.js';
|
|
16
17
|
import { maybeReaderFor, readerFor, readerForFormat } from './registry.js';
|
|
17
18
|
import { isMissing } from './source.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Claude Code: `~/.claude/projects/<cwd slug>/<session id>.jsonl`, one JSON record per line, and
|
|
3
4
|
* each subagent's own transcript at `<session id>/subagents/agent-<agent id>.jsonl`.
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
* the API answered with.
|
|
10
11
|
*/
|
|
11
12
|
import { basename, join } from 'node:path';
|
|
12
|
-
import { SessionBuilder, count,
|
|
13
|
+
import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
|
|
13
14
|
import { fileRecords } from '../source.js';
|
|
14
15
|
import { runFold } from './fold.js';
|
|
15
16
|
import { EVIDENCE, LOCATE_READS, STORES, mapLimit, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
|
package/dist/formats/codex.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Codex: `~/.codex/sessions/YYYY/MM/DD/rollout-<timestamp>-<thread id>.jsonl`, one
|
|
3
4
|
* `{timestamp, type, payload}` record per line.
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
* (`total_token_usage`); the cumulative counter is the session total.
|
|
11
12
|
*/
|
|
12
13
|
import { basename } from 'node:path';
|
|
13
|
-
import { SessionBuilder, argumentsOf, count,
|
|
14
|
+
import { SessionBuilder, argumentsOf, count, sessionError, str, textOf, usageOrNull, } from '../builder.js';
|
|
14
15
|
import { fileRecords, isMissing } from '../source.js';
|
|
15
16
|
import { runFold } from './fold.js';
|
|
16
17
|
import { CODEX_HOME_RULE, EVIDENCE, LOCATE_READS, STORES, codexHomes, mapLimit, storeGlob, walkFiles, withinPath } from '../stores.js';
|
package/dist/formats/factory.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Factory Droid: `~/.factory/sessions/<encoded cwd>/<session id>.jsonl` plus a
|
|
3
4
|
* `<session id>.settings.json` beside it.
|
|
@@ -10,7 +11,7 @@
|
|
|
10
11
|
*/
|
|
11
12
|
import { readFile } from 'node:fs/promises';
|
|
12
13
|
import { basename, join } from 'node:path';
|
|
13
|
-
import { SessionBuilder, count,
|
|
14
|
+
import { SessionBuilder, count, str, textOf, usageOrNull } from '../builder.js';
|
|
14
15
|
import { fileRecords, isMissing } from '../source.js';
|
|
15
16
|
import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
|
|
16
17
|
import { runFold } from './fold.js';
|
package/dist/formats/kimi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Kimi Code: one directory per session, `~/.kimi/sessions/<workdir hash>/<session id>/`, holding
|
|
3
4
|
* `wire.jsonl` (the event stream this reader folds) and `context.jsonl` (the model context).
|
|
@@ -11,7 +12,7 @@
|
|
|
11
12
|
* subagent's events arrive wrapped in `SubagentEvent`. The wire records no model.
|
|
12
13
|
*/
|
|
13
14
|
import { basename, dirname, join } from 'node:path';
|
|
14
|
-
import { SessionBuilder, count,
|
|
15
|
+
import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
|
|
15
16
|
import { fileRecords } from '../source.js';
|
|
16
17
|
import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles } from '../stores.js';
|
|
17
18
|
import { runFold } from './fold.js';
|
|
@@ -20,6 +21,13 @@ const STORE = STORES[FORMAT];
|
|
|
20
21
|
const HARNESS = 'kimi-code';
|
|
21
22
|
/** The event stream this reader folds; the store also holds context.jsonl and state.json. */
|
|
22
23
|
const WIRE = '*/{id}/wire.jsonl';
|
|
24
|
+
const CHILD_WIRE = '*/*/subagents/{id}/wire.jsonl';
|
|
25
|
+
/** Kimi's producer places each agent's wire under its owning session's subagents directory. */
|
|
26
|
+
function sessionIdentity(path) {
|
|
27
|
+
const directory = dirname(path);
|
|
28
|
+
const container = dirname(directory);
|
|
29
|
+
return { id: basename(directory), parent: basename(container) === 'subagents' ? basename(dirname(container)) : null };
|
|
30
|
+
}
|
|
23
31
|
function usageOf(value) {
|
|
24
32
|
if (!isRecord(value))
|
|
25
33
|
return null;
|
|
@@ -141,7 +149,7 @@ export function createFold(ref, mode, stats) {
|
|
|
141
149
|
subagentEvents += 1;
|
|
142
150
|
const agent = str(payload.agent_id);
|
|
143
151
|
if (agent)
|
|
144
|
-
builder.child(agent, str(payload.parent_tool_call_id));
|
|
152
|
+
builder.child(agent, str(payload.parent_tool_call_id) ?? str(payload.task_tool_call_id));
|
|
145
153
|
break;
|
|
146
154
|
}
|
|
147
155
|
default:
|
|
@@ -154,7 +162,7 @@ export function createFold(ref, mode, stats) {
|
|
|
154
162
|
if (compactions > 0)
|
|
155
163
|
builder.gap(`${compactions} compactions replaced earlier context`);
|
|
156
164
|
if (subagentEvents > 0)
|
|
157
|
-
builder.gap(`${subagentEvents} subagent events
|
|
165
|
+
builder.gap(`${subagentEvents} wrapped subagent events were excluded from this session's own messages and usage; separate child wires must be read for child execution`);
|
|
158
166
|
builder.inferEnding(aborted ? { at: aborted.at, error: null } : undefined);
|
|
159
167
|
return builder;
|
|
160
168
|
};
|
|
@@ -165,10 +173,15 @@ export async function fold(ref, mode, options, source = fileRecords(ref.path, {
|
|
|
165
173
|
}
|
|
166
174
|
async function locate(home, opts) {
|
|
167
175
|
const root = join(home, STORE.root);
|
|
168
|
-
const pattern = storeGlob(WIRE
|
|
176
|
+
const pattern = storeGlob(WIRE);
|
|
177
|
+
const children = storeGlob(CHILD_WIRE);
|
|
169
178
|
const refs = [];
|
|
170
179
|
for (const rel of await walkFiles(root)) {
|
|
171
|
-
if (!pattern.test(rel))
|
|
180
|
+
if (!pattern.test(rel) && !children.test(rel))
|
|
181
|
+
continue;
|
|
182
|
+
const { id, parent } = sessionIdentity(rel);
|
|
183
|
+
// A parent lookup includes its recorded child wires; a child lookup selects that agent.
|
|
184
|
+
if (opts.nativeSessionId && id !== opts.nativeSessionId && parent !== opts.nativeSessionId)
|
|
172
185
|
continue;
|
|
173
186
|
const path = join(root, rel);
|
|
174
187
|
const mtime = await mtimeMs(path);
|
|
@@ -177,10 +190,9 @@ async function locate(home, opts) {
|
|
|
177
190
|
// Kimi keys the session directory by a hash of its working directory and records no cwd.
|
|
178
191
|
if (opts.cwd)
|
|
179
192
|
continue;
|
|
180
|
-
const id = basename(dirname(path));
|
|
181
193
|
const context = join(dirname(path), 'context.jsonl');
|
|
182
194
|
const files = (await mtimeMs(context)) > 0 ? [path, context] : [path];
|
|
183
|
-
refs.push({ harness: HARNESS, format: FORMAT, nativeSessionId: id, path, files, home, cwd: null, mtimeMs: mtime, parentNativeSessionId:
|
|
195
|
+
refs.push({ harness: HARNESS, format: FORMAT, nativeSessionId: id, path, files, home, cwd: null, mtimeMs: mtime, parentNativeSessionId: parent });
|
|
184
196
|
}
|
|
185
197
|
return refs.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
186
198
|
}
|
|
@@ -189,7 +201,7 @@ export const kimiReader = {
|
|
|
189
201
|
aliases: ['kimi', 'kimi-cli'],
|
|
190
202
|
formats: [FORMAT],
|
|
191
203
|
stores: [STORE],
|
|
192
|
-
globs: { session: WIRE },
|
|
204
|
+
globs: { session: WIRE, children: CHILD_WIRE },
|
|
193
205
|
evidence: EVIDENCE[FORMAT],
|
|
194
206
|
locate: (home, opts = {}) => locate(home, opts),
|
|
195
207
|
async read(ref, opts = {}) {
|
|
@@ -201,5 +213,6 @@ export const kimiReader = {
|
|
|
201
213
|
};
|
|
202
214
|
/** A session read from its `wire.jsonl` outside a HOME layout. */
|
|
203
215
|
export function kimiRefForFile(path) {
|
|
204
|
-
|
|
216
|
+
const { id, parent } = sessionIdentity(path);
|
|
217
|
+
return { harness: HARNESS, format: FORMAT, nativeSessionId: id, path, files: [path], home: null, cwd: null, mtimeMs: 0, parentNativeSessionId: parent };
|
|
205
218
|
}
|
package/dist/formats/opencode.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* OpenCode 1.x: one SQLite store for every session in the HOME,
|
|
3
4
|
* `~/.local/share/opencode/opencode.db` (with `-wal` and `-shm` siblings while it is open).
|
|
@@ -12,7 +13,7 @@
|
|
|
12
13
|
*/
|
|
13
14
|
import { createHash } from 'node:crypto';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
|
-
import { SessionBuilder, count,
|
|
16
|
+
import { SessionBuilder, count, isoTime, sessionError, str, usageOrNull } from '../builder.js';
|
|
16
17
|
import { isMissing, withSqlite } from '../source.js';
|
|
17
18
|
import { EVIDENCE, STORES, mtimeMs, withinPath } from '../stores.js';
|
|
18
19
|
const FORMAT = 'opencode.sqlite';
|
package/dist/formats/pi.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '@tangle-network/agent-core';
|
|
1
2
|
/**
|
|
2
3
|
* Pi: `~/.pi/agent/sessions/<encoded cwd>/<timestamp>_<session id>.jsonl`, one entry per line.
|
|
3
4
|
*
|
|
@@ -9,7 +10,7 @@
|
|
|
9
10
|
* `toolCallId`. Entries form a tree through `parentId`; branches share one file.
|
|
10
11
|
*/
|
|
11
12
|
import { basename, join } from 'node:path';
|
|
12
|
-
import { SessionBuilder, count,
|
|
13
|
+
import { SessionBuilder, count, sessionError, str, textOf, usageOrNull } from '../builder.js';
|
|
13
14
|
import { fileRecords } from '../source.js';
|
|
14
15
|
import { runFold } from './fold.js';
|
|
15
16
|
import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles, withinPath } from '../stores.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/harness-sessions",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "One reader for every coding-agent harness's native session (Claude Code, Codex, OpenCode, Pi): store layouts, a normalized session with messages, tool calls, served models, usage and ending errors, and projections to chat messages, OTLP spans and turns",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"LICENSE"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"@tangle-network/agent-core": "^0.13.0",
|
|
43
44
|
"@tangle-network/agent-trace-contract": "^2.0.0"
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|