@tangle-network/harness-sessions 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 CHANGED
@@ -12,7 +12,7 @@ npm install @tangle-network/harness-sessions
12
12
 
13
13
  ## Where this fits
14
14
 
15
- - The sidecar's harness session contract (agent-dev-container `packages/cli-agent-registry`) imports `STORES` and `SessionFormatId` from here; it keeps no second copy of where a harness writes.
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
18
  - Its only dependencies are `@tangle-network/agent-trace-contract` and Node built-ins (`node:sqlite` for OpenCode).
@@ -14,6 +14,7 @@ import { fileRecords } from '../source.js';
14
14
  import { runFold } from './fold.js';
15
15
  import { EVIDENCE, STORES, mtimeMs, storeGlob, walkFiles } from '../stores.js';
16
16
  import { headRecord } from './head.js';
17
+ import { claudeRecordIsMachine, claudeTypedPromptText } from './claude-records.js';
17
18
  const FORMAT = 'claude-code.projects-jsonl';
18
19
  const STORE = STORES[FORMAT];
19
20
  const HARNESS = 'claude-code';
@@ -29,6 +30,12 @@ function usageOf(value) {
29
30
  cacheWrite: count(value.cache_creation_input_tokens),
30
31
  });
31
32
  }
33
+ function firstText(parts) {
34
+ for (const part of parts)
35
+ if (part.type === 'text')
36
+ return part.text;
37
+ return '';
38
+ }
32
39
  const INTERRUPTED = /^\[Request interrupted by user/u;
33
40
  export function createFold(ref, mode, stats) {
34
41
  const subagent = ref.parentNativeSessionId !== null;
@@ -37,6 +44,8 @@ export function createFold(ref, mode, stats) {
37
44
  let aborted = null;
38
45
  let errors = 0;
39
46
  let identified = false;
47
+ // A resumed agent's file writes its earlier records again, each with the uuid it had.
48
+ const seenUuids = new Set();
40
49
  const observe = (record) => {
41
50
  if (!subagent && record.isSidechain === true) {
42
51
  sidechainSkipped += 1;
@@ -45,6 +54,11 @@ export function createFold(ref, mode, stats) {
45
54
  const type = record.type;
46
55
  if (type !== 'user' && type !== 'assistant' && type !== 'system')
47
56
  return;
57
+ if (typeof record.uuid === 'string') {
58
+ if (seenUuids.has(record.uuid))
59
+ return;
60
+ seenUuids.add(record.uuid);
61
+ }
48
62
  const at = builder.seen(record.timestamp);
49
63
  if (builder.cwd === null && typeof record.cwd === 'string')
50
64
  builder.cwd = record.cwd;
@@ -60,6 +74,7 @@ export function createFold(ref, mode, stats) {
60
74
  }
61
75
  }
62
76
  const uuid = str(record.uuid) ?? `${builder.nativeSessionId}:${stats.line}`;
77
+ const machine = (text) => claudeRecordIsMachine({ text, isMeta: record.isMeta === true, isCompactSummary: record.isCompactSummary === true, entrypoint: str(record.entrypoint) ?? undefined });
63
78
  if (type === 'system') {
64
79
  if (record.subtype !== 'api_error')
65
80
  return;
@@ -95,10 +110,10 @@ export function createFold(ref, mode, stats) {
95
110
  builder.message({
96
111
  id: uuid,
97
112
  role: 'user',
98
- actor: record.isMeta === true || record.isCompactSummary === true ? 'injected' : subagent ? 'subagent-spawn' : 'human',
113
+ actor: machine(content) ? 'injected' : subagent ? 'subagent-spawn' : 'human',
99
114
  at,
100
115
  modelCallId: null,
101
- parts: [{ type: 'text', text: content }],
116
+ parts: [{ type: 'text', text: machine(content) ? content : claudeTypedPromptText(content) }],
102
117
  });
103
118
  return;
104
119
  }
@@ -129,7 +144,7 @@ export function createFold(ref, mode, stats) {
129
144
  else if (block.type === 'text' && typeof block.text === 'string') {
130
145
  if (INTERRUPTED.test(block.text))
131
146
  aborted = { at };
132
- userParts.push({ type: 'text', text: block.text });
147
+ userParts.push({ type: 'text', text: claudeTypedPromptText(block.text) });
133
148
  }
134
149
  else if (block.type === 'image' || block.type === 'document') {
135
150
  const source = isRecord(block.source) ? block.source : {};
@@ -142,7 +157,7 @@ export function createFold(ref, mode, stats) {
142
157
  builder.message({
143
158
  id: uuid,
144
159
  role: 'user',
145
- actor: record.isMeta === true ? 'injected' : subagent ? 'subagent-spawn' : 'human',
160
+ actor: machine(firstText(userParts)) ? 'injected' : subagent ? 'subagent-spawn' : 'human',
146
161
  at,
147
162
  modelCallId: null,
148
163
  parts: userParts,
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Which Claude Code `user` records a person typed and which the harness wrote. Claude Code stores
3
+ * both as `type: "user"`; the markers below are the only difference. This is the one owner of the
4
+ * rule, so every reader of a Claude Code transcript (traces, agent-eval) classifies turns alike.
5
+ */
6
+ export interface ClaudeRecordFacts {
7
+ /** The record's text (the first text part for block content). */
8
+ text: string;
9
+ isMeta?: boolean | undefined;
10
+ isCompactSummary?: boolean | undefined;
11
+ /** The record's `entrypoint`; `sdk-cli` is a programmatic caller, not a person. */
12
+ entrypoint?: string | undefined;
13
+ }
14
+ /** True when the harness, not a person, wrote this user record. */
15
+ export declare function claudeRecordIsMachine(facts: ClaudeRecordFacts): boolean;
16
+ /**
17
+ * What a person typed. A slash command is stored as a `<command-name>` wrapper; this returns the
18
+ * command and its arguments (`/effort`, `/model opus`). Text without a wrapper is unchanged.
19
+ */
20
+ export declare function claudeTypedPromptText(text: string): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Which Claude Code `user` records a person typed and which the harness wrote. Claude Code stores
3
+ * both as `type: "user"`; the markers below are the only difference. This is the one owner of the
4
+ * rule, so every reader of a Claude Code transcript (traces, agent-eval) classifies turns alike.
5
+ */
6
+ const MACHINE_TEXT_PREFIXES = [
7
+ '<local-command-caveat>',
8
+ '<local-command-stdout>',
9
+ '<local-command-stderr>',
10
+ 'Caveat: The messages below',
11
+ '<task-notification>',
12
+ '<bash-stdout>',
13
+ '<bash-stderr>',
14
+ '<user-prompt-submit-hook>',
15
+ '[Request interrupted by user',
16
+ ];
17
+ /** True when the harness, not a person, wrote this user record. */
18
+ export function claudeRecordIsMachine(facts) {
19
+ if (facts.isMeta === true || facts.isCompactSummary === true || facts.entrypoint === 'sdk-cli')
20
+ return true;
21
+ const head = facts.text.trimStart();
22
+ return MACHINE_TEXT_PREFIXES.some((prefix) => head.startsWith(prefix));
23
+ }
24
+ /**
25
+ * What a person typed. A slash command is stored as a `<command-name>` wrapper; this returns the
26
+ * command and its arguments (`/effort`, `/model opus`). Text without a wrapper is unchanged.
27
+ */
28
+ export function claudeTypedPromptText(text) {
29
+ const name = /<command-name>([\s\S]*?)<\/command-name>/u.exec(text)?.[1]?.trim();
30
+ if (!name)
31
+ return text;
32
+ const args = /<command-args>([\s\S]*?)<\/command-args>/u.exec(text)?.[1]?.trim();
33
+ return args ? `${name} ${args}` : name;
34
+ }
package/dist/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { HARNESS_SESSION_SCHEMA } from './schema.js';
13
13
  export { EVIDENCE, SESSION_FORMAT_IDS, STORES, assertStoreRoot, storeGlob } from './catalog.js';
14
14
  export { knownHarnesses, listReaders, maybeReaderFor, readerFor, readerForFormat } from './registry.js';
15
15
  export { claudeCodeReader, claudeCodeRefForFile } from './formats/claude-code.js';
16
+ export { claudeRecordIsMachine, claudeTypedPromptText, type ClaudeRecordFacts } from './formats/claude-records.js';
16
17
  export { codexReader, codexRefForFile } from './formats/codex.js';
17
18
  export { opencodeReader, opencodeSessionsInStore } from './formats/opencode.js';
18
19
  export { piReader, piRefForFile } from './formats/pi.js';
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ export { HARNESS_SESSION_SCHEMA } from './schema.js';
2
2
  export { EVIDENCE, SESSION_FORMAT_IDS, STORES, assertStoreRoot, storeGlob } from './catalog.js';
3
3
  export { knownHarnesses, listReaders, maybeReaderFor, readerFor, readerForFormat } from './registry.js';
4
4
  export { claudeCodeReader, claudeCodeRefForFile } from './formats/claude-code.js';
5
+ export { claudeRecordIsMachine, claudeTypedPromptText } from './formats/claude-records.js';
5
6
  export { codexReader, codexRefForFile } from './formats/codex.js';
6
7
  export { opencodeReader, opencodeSessionsInStore } from './formats/opencode.js';
7
8
  export { piReader, piRefForFile } from './formats/pi.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/harness-sessions",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
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,