@tangle-network/harness-sessions 0.1.5 → 0.1.7

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.
@@ -66,14 +66,59 @@ function contentParts(content) {
66
66
  }
67
67
  return parts;
68
68
  }
69
- function outputOf(output) {
69
+ // `exec_command` and `write_stdin` answer with a text header (`Chunk ID`, `Wall time`, then
70
+ // `Process exited with code N` once the process is done) before `Output:`; a still-running process
71
+ // says `Process running with session ID` instead and has no status yet.
72
+ const PROCESS_EXIT = /^Process exited with code (-?\d+)\s*$/mu;
73
+ function processFailed(text) {
74
+ const header = text.split(/^Output:/mu, 1)[0] ?? '';
75
+ const match = PROCESS_EXIT.exec(header);
76
+ return match !== null && Number(match[1]) !== 0;
77
+ }
78
+ /** A nonzero numeric `exit_code` anywhere in a parsed JSON value (code-mode tool results nest it). */
79
+ function jsonExitFailed(value, depth = 0) {
80
+ if (depth > 4)
81
+ return false;
82
+ if (Array.isArray(value))
83
+ return value.some((v) => jsonExitFailed(v, depth + 1));
84
+ if (!isRecord(value))
85
+ return false;
86
+ if (typeof value.exit_code === 'number' && value.exit_code !== 0)
87
+ return true;
88
+ return Object.values(value).some((v) => jsonExitFailed(v, depth + 1));
89
+ }
90
+ /**
91
+ * Code mode runs JavaScript in an `exec` call; its `tools.exec_command(...)` results are printed
92
+ * as JSON items carrying `exit_code`. The first item is `Script failed` when the script threw.
93
+ */
94
+ function codeModeFailed(output) {
95
+ if (!Array.isArray(output))
96
+ return false;
97
+ return output.some((item, i) => {
98
+ const text = isRecord(item) && typeof item.text === 'string' ? item.text : '';
99
+ if (i === 0 && /^Script failed\b/u.test(text))
100
+ return true;
101
+ if (!text.includes('exit_code'))
102
+ return false;
103
+ try {
104
+ return jsonExitFailed(JSON.parse(text));
105
+ }
106
+ catch {
107
+ return false;
108
+ }
109
+ });
110
+ }
111
+ function outputOf(output, tool) {
112
+ const execFailed = () => tool.name === 'exec_command' || tool.name === 'write_stdin'
113
+ ? typeof output === 'string' && processFailed(output)
114
+ : tool.name === 'exec' && tool.input.includes('exec_command') && codeModeFailed(output);
70
115
  if (typeof output === 'string')
71
- return { output, text: output, isError: false };
116
+ return { output, text: output, isError: execFailed() };
72
117
  if (isRecord(output)) {
73
118
  const content = output.content ?? output.output;
74
119
  return { output, text: textOf(content) ?? (typeof content === 'string' ? content : null), isError: output.success === false };
75
120
  }
76
- return { output: output ?? null, text: textOf(output), isError: false };
121
+ return { output: output ?? null, text: textOf(output), isError: execFailed() };
77
122
  }
78
123
  const MODEL_OUTPUT = new Set(['message', 'reasoning']);
79
124
  export function createFold(ref, mode, stats) {
@@ -97,6 +142,7 @@ export function createFold(ref, mode, stats) {
97
142
  let inherited = false;
98
143
  let inheritedRecords = 0;
99
144
  const foreignThreads = new Set();
145
+ const tools = new Map();
100
146
  const openCall = (at) => {
101
147
  if (open)
102
148
  return open;
@@ -196,6 +242,7 @@ export function createFold(ref, mode, stats) {
196
242
  const raw = kind === 'function_call' ? payload.arguments : kind === 'custom_tool_call' ? payload.input : (payload.action ?? payload.arguments ?? payload.input);
197
243
  const call = openCall(at);
198
244
  assistantMessage(at, call, [{ type: 'tool-call', toolCallId: callId }]);
245
+ tools.set(callId, { name, input: typeof raw === 'string' ? raw : '' });
199
246
  builder.toolCall({ id: callId, name, ...argumentsOf(raw), at, messageId: openMessage });
200
247
  // A web search is answered inside the response; there is no output item.
201
248
  if (kind === 'web_search_call')
@@ -203,7 +250,7 @@ export function createFold(ref, mode, stats) {
203
250
  }
204
251
  else if (kind.endsWith('_call_output')) {
205
252
  const callId = str(payload.call_id) ?? '';
206
- const result = outputOf(payload.output);
253
+ const result = outputOf(payload.output, tools.get(callId) ?? { name: '', input: '' });
207
254
  const id = `${callId}:output`;
208
255
  builder.message({ id, role: 'tool', actor: 'tool-result', at, modelCallId: null, parts: [{ type: 'tool-result', toolCallId: callId }] });
209
256
  builder.toolResult(callId, { ...result, at, messageId: id });
package/dist/input.d.ts CHANGED
@@ -11,6 +11,12 @@ export interface SessionInputInit {
11
11
  parentNativeSessionId?: string | null;
12
12
  /** Names the source in integrity records (a path, a blob ref). */
13
13
  label?: string;
14
+ /**
15
+ * The file line `input` starts on, when it continues a longer file (records appended since a
16
+ * previous read). A record without an id of its own is named by its line, so the same record
17
+ * gets the same id whichever way the file is read. Default 1.
18
+ */
19
+ firstLine?: number;
14
20
  }
15
21
  /** Read a JSONL-format session (Claude Code, Codex, Pi, Kimi) from text or parsed records, synchronously. */
16
22
  export declare function readSessionInput(harness: string, input: SessionInput, init?: SessionInputInit, options?: Pick<ReadOptions, 'corruption'>): HarnessSession;
package/dist/input.js CHANGED
@@ -31,7 +31,10 @@ function foldInput(harness, input, init, mode, options) {
31
31
  parentNativeSessionId: init.parentNativeSessionId ?? null,
32
32
  };
33
33
  const fold = create(ref, mode, stats);
34
- const records = 'text' in input ? textRecordsSync(input.text, stats, { strict: options.corruption === 'strict' }) : arrayRecordsSync(input.records, stats);
34
+ const firstLine = init.firstLine ?? 1;
35
+ const records = 'text' in input
36
+ ? textRecordsSync(input.text, stats, { strict: options.corruption === 'strict', firstLine })
37
+ : arrayRecordsSync(input.records, stats, firstLine);
35
38
  for (const record of records)
36
39
  fold.observe(record);
37
40
  return fold.finish();
package/dist/source.d.ts CHANGED
@@ -40,12 +40,16 @@ export declare function fileRecords(path: string, options?: {
40
40
  }): RecordSource;
41
41
  /** Stats for bytes already in memory. */
42
42
  export declare function memoryStats(label: string, text?: string): SourceStats;
43
- /** The records of JSONL text already in memory (a retained copy, a blob), read synchronously. */
43
+ /**
44
+ * The records of JSONL text already in memory (a retained copy, a blob), read synchronously.
45
+ * `firstLine` numbers the text's first line, for text that continues a longer file.
46
+ */
44
47
  export declare function textRecordsSync(text: string, stats: SourceStats, options?: {
45
48
  strict?: boolean;
49
+ firstLine?: number;
46
50
  }): Generator<Record<string, unknown>>;
47
- /** Records a caller already parsed (one per line, in order). Non-objects count as unparsed. */
48
- export declare function arrayRecordsSync(values: readonly unknown[], stats: SourceStats): Generator<Record<string, unknown>>;
51
+ /** Records a caller already parsed (one per line, in order, the first on line `firstLine`). Non-objects count as unparsed. */
52
+ export declare function arrayRecordsSync(values: readonly unknown[], stats: SourceStats, firstLine?: number): Generator<Record<string, unknown>>;
49
53
  /**
50
54
  * Run `use` against a SQLite store opened read-only in place ({@link openSqlite}). A store SQLite
51
55
  * cannot open in place is read from a private copy instead: a write-ahead log with no `-shm` in a
package/dist/source.js CHANGED
@@ -116,10 +116,15 @@ export function memoryStats(label, text) {
116
116
  const bytes = Buffer.from(text, 'utf8');
117
117
  return { path: label, sha256: `sha256:${createHash('sha256').update(bytes).digest('hex')}`, bytes: bytes.length, unparsed: 0, tornTail: false, line: 0 };
118
118
  }
119
- /** The records of JSONL text already in memory (a retained copy, a blob), read synchronously. */
119
+ /**
120
+ * The records of JSONL text already in memory (a retained copy, a blob), read synchronously.
121
+ * `firstLine` numbers the text's first line, for text that continues a longer file.
122
+ */
120
123
  export function* textRecordsSync(text, stats, options = {}) {
121
124
  const lines = text.split('\n');
122
- for (const [index, raw] of lines.entries()) {
125
+ const first = options.firstLine ?? 1;
126
+ for (const [position, raw] of lines.entries()) {
127
+ const index = first - 1 + position;
123
128
  const line = raw.replace(/\r$/u, '');
124
129
  if (line.trim().length === 0)
125
130
  continue;
@@ -128,7 +133,7 @@ export function* textRecordsSync(text, stats, options = {}) {
128
133
  value = JSON.parse(line);
129
134
  }
130
135
  catch {
131
- if (index === lines.length - 1) {
136
+ if (position === lines.length - 1) {
132
137
  stats.tornTail = true;
133
138
  continue;
134
139
  }
@@ -147,9 +152,10 @@ export function* textRecordsSync(text, stats, options = {}) {
147
152
  yield value;
148
153
  }
149
154
  }
150
- /** Records a caller already parsed (one per line, in order). Non-objects count as unparsed. */
151
- export function* arrayRecordsSync(values, stats) {
152
- for (const [index, value] of values.entries()) {
155
+ /** Records a caller already parsed (one per line, in order, the first on line `firstLine`). Non-objects count as unparsed. */
156
+ export function* arrayRecordsSync(values, stats, firstLine = 1) {
157
+ for (const [position, value] of values.entries()) {
158
+ const index = firstLine - 1 + position;
153
159
  if (value === null || typeof value !== 'object' || Array.isArray(value)) {
154
160
  noteUnparsed(stats, index + 1);
155
161
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/harness-sessions",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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,