@manyoars/oars 0.4.1 → 0.5.2

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.
Files changed (188) hide show
  1. package/README.md +79 -29
  2. package/dist/bin.js +3 -1
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +207 -21
  5. package/dist/club.d.ts +19 -0
  6. package/dist/club.js +132 -0
  7. package/dist/gate/hook-fill-main.js +1 -1
  8. package/dist/gate/hook-fill.d.ts +1 -1
  9. package/dist/gate/hook-fill.js +1 -1
  10. package/dist/gate/hook-main.js +7 -5
  11. package/dist/gate/hook.d.ts +12 -1
  12. package/dist/gate/hook.js +38 -7
  13. package/dist/hook-bin.js +23 -4
  14. package/dist/main.js +5 -0
  15. package/dist/node-floor.d.ts +10 -0
  16. package/dist/node-floor.js +23 -0
  17. package/dist/oars-hook.d.ts +2 -0
  18. package/dist/oars-hook.js +7 -0
  19. package/dist/sessions/allow.d.ts +1 -0
  20. package/dist/sessions/allow.js +1 -1
  21. package/dist/sessions/call-shape.d.ts +111 -0
  22. package/dist/sessions/call-shape.js +464 -0
  23. package/dist/sessions/candidates.d.ts +65 -0
  24. package/dist/sessions/candidates.js +236 -0
  25. package/dist/sessions/card-command.d.ts +24 -0
  26. package/dist/sessions/card-command.js +71 -0
  27. package/dist/sessions/card-font.d.ts +6 -0
  28. package/dist/sessions/card-font.js +36 -0
  29. package/dist/sessions/card-line.d.ts +30 -0
  30. package/dist/sessions/card-line.js +105 -0
  31. package/dist/sessions/card-render.d.ts +15 -0
  32. package/dist/sessions/card-render.js +146 -0
  33. package/dist/sessions/card.d.ts +59 -0
  34. package/dist/sessions/card.js +107 -0
  35. package/dist/sessions/cell-increments.d.ts +14 -3
  36. package/dist/sessions/cell-increments.js +79 -12
  37. package/dist/sessions/cells.d.ts +42 -4
  38. package/dist/sessions/cells.js +72 -18
  39. package/dist/sessions/codex-hook-bin.d.ts +1 -0
  40. package/dist/sessions/codex-hook-bin.js +24 -0
  41. package/dist/sessions/codex-hook-install.d.ts +21 -0
  42. package/dist/sessions/codex-hook-install.js +47 -0
  43. package/dist/sessions/codex-hook-view.d.ts +20 -0
  44. package/dist/sessions/codex-hook-view.js +20 -0
  45. package/dist/sessions/codex-hook.d.ts +77 -0
  46. package/dist/sessions/codex-hook.js +255 -0
  47. package/dist/sessions/codex.d.ts +9 -0
  48. package/dist/sessions/codex.js +104 -18
  49. package/dist/sessions/command.d.ts +17 -2
  50. package/dist/sessions/command.js +145 -33
  51. package/dist/sessions/copilot.d.ts +3 -0
  52. package/dist/sessions/copilot.js +12 -4
  53. package/dist/sessions/derive.js +2 -1
  54. package/dist/sessions/empty-state.d.ts +17 -0
  55. package/dist/sessions/empty-state.js +38 -0
  56. package/dist/sessions/exclusion.js +48 -3
  57. package/dist/sessions/extract.d.ts +16 -3
  58. package/dist/sessions/extract.js +281 -24
  59. package/dist/sessions/gemini.d.ts +2 -0
  60. package/dist/sessions/gemini.js +29 -2
  61. package/dist/sessions/harness-files.d.ts +27 -0
  62. package/dist/sessions/harness-files.js +109 -0
  63. package/dist/sessions/head-only.d.ts +7 -0
  64. package/dist/sessions/head-only.js +21 -0
  65. package/dist/sessions/hook-calls.js +4 -3
  66. package/dist/sessions/hooks.d.ts +1 -1
  67. package/dist/sessions/hooks.js +6 -2
  68. package/dist/sessions/identity.d.ts +6 -0
  69. package/dist/sessions/injected.d.ts +13 -0
  70. package/dist/sessions/injected.js +260 -0
  71. package/dist/sessions/install.d.ts +31 -20
  72. package/dist/sessions/install.js +80 -82
  73. package/dist/sessions/load-status.d.ts +41 -0
  74. package/dist/sessions/load-status.js +156 -0
  75. package/dist/sessions/machine-child-deps.d.ts +5 -0
  76. package/dist/sessions/machine-child-deps.js +83 -0
  77. package/dist/sessions/machine-child.d.ts +43 -0
  78. package/dist/sessions/machine-child.js +120 -0
  79. package/dist/sessions/machine-credential.d.ts +10 -0
  80. package/dist/sessions/machine-credential.js +20 -0
  81. package/dist/sessions/machine-enroll.d.ts +47 -0
  82. package/dist/sessions/machine-enroll.js +230 -0
  83. package/dist/sessions/machine-hold.d.ts +49 -0
  84. package/dist/sessions/machine-hold.js +88 -0
  85. package/dist/sessions/machine-ids.d.ts +6 -0
  86. package/dist/sessions/machine-ids.js +9 -0
  87. package/dist/sessions/machine-notice.d.ts +21 -0
  88. package/dist/sessions/machine-notice.js +89 -0
  89. package/dist/sessions/machine-pull.d.ts +40 -0
  90. package/dist/sessions/machine-pull.js +220 -0
  91. package/dist/sessions/machine-repo.d.ts +7 -0
  92. package/dist/sessions/machine-repo.js +30 -0
  93. package/dist/sessions/machine-scan-cache.d.ts +12 -0
  94. package/dist/sessions/machine-scan-cache.js +47 -0
  95. package/dist/sessions/machine-send.d.ts +12 -0
  96. package/dist/sessions/machine-send.js +156 -0
  97. package/dist/sessions/machine-stop.d.ts +22 -0
  98. package/dist/sessions/machine-stop.js +53 -0
  99. package/dist/sessions/machine.d.ts +41 -0
  100. package/dist/sessions/machine.js +89 -0
  101. package/dist/sessions/opencode.d.ts +2 -0
  102. package/dist/sessions/opencode.js +6 -0
  103. package/dist/sessions/otel.d.ts +21 -0
  104. package/dist/sessions/otel.js +109 -38
  105. package/dist/sessions/owner-only.d.ts +7 -0
  106. package/dist/sessions/owner-only.js +34 -0
  107. package/dist/sessions/pi.d.ts +13 -1
  108. package/dist/sessions/pi.js +27 -13
  109. package/dist/sessions/pool-books.d.ts +1 -1
  110. package/dist/sessions/pool-books.js +1 -1
  111. package/dist/sessions/pool-choices.d.ts +4 -2
  112. package/dist/sessions/pool-choices.js +4 -2
  113. package/dist/sessions/pool-command.d.ts +46 -6
  114. package/dist/sessions/pool-command.js +613 -136
  115. package/dist/sessions/pool-delta.d.ts +7 -0
  116. package/dist/sessions/pool-delta.js +28 -5
  117. package/dist/sessions/pool-due.d.ts +43 -0
  118. package/dist/sessions/pool-due.js +113 -0
  119. package/dist/sessions/pool-nightly.d.ts +9 -1
  120. package/dist/sessions/pool-nightly.js +65 -21
  121. package/dist/sessions/pool-outbox.d.ts +51 -5
  122. package/dist/sessions/pool-outbox.js +217 -24
  123. package/dist/sessions/pool-print.d.ts +1 -1
  124. package/dist/sessions/pool-raw.js +4 -3
  125. package/dist/sessions/pool-replica.d.ts +4 -4
  126. package/dist/sessions/pool-replica.js +26 -7
  127. package/dist/sessions/pool-send-lock.js +1 -1
  128. package/dist/sessions/pool-sent.d.ts +2 -0
  129. package/dist/sessions/pool-sent.js +21 -4
  130. package/dist/sessions/pool-wire.d.ts +176 -15
  131. package/dist/sessions/pool-wire.js +297 -33
  132. package/dist/sessions/pool.js +4 -2
  133. package/dist/sessions/public-vocabulary.d.ts +5 -0
  134. package/dist/sessions/public-vocabulary.js +16 -0
  135. package/dist/sessions/read-call-shape.js +12 -6
  136. package/dist/sessions/read-fit.d.ts +5 -0
  137. package/dist/sessions/read-fit.js +23 -2
  138. package/dist/sessions/read-interaction.js +1 -1
  139. package/dist/sessions/read-lead-lag.js +1 -1
  140. package/dist/sessions/read-readable.js +1 -1
  141. package/dist/sessions/read.d.ts +1 -1
  142. package/dist/sessions/recall-consequence.d.ts +2 -2
  143. package/dist/sessions/recall-consequence.js +14 -9
  144. package/dist/sessions/recall-object.d.ts +9 -2
  145. package/dist/sessions/receipt.d.ts +8 -0
  146. package/dist/sessions/receipt.js +78 -0
  147. package/dist/sessions/recovery-calls.d.ts +3 -2
  148. package/dist/sessions/recovery-calls.js +9 -3
  149. package/dist/sessions/redact.d.ts +11 -2
  150. package/dist/sessions/redact.js +201 -23
  151. package/dist/sessions/repo-root.d.ts +7 -0
  152. package/dist/sessions/repo-root.js +17 -13
  153. package/dist/sessions/reversion.d.ts +8 -0
  154. package/dist/sessions/reversion.js +114 -0
  155. package/dist/sessions/schema.d.ts +78 -4
  156. package/dist/sessions/schema.js +31 -1
  157. package/dist/sessions/session-shape.d.ts +2 -1
  158. package/dist/sessions/session-shape.js +15 -0
  159. package/dist/sessions/sources.d.ts +9 -0
  160. package/dist/sessions/sources.js +16 -5
  161. package/dist/sessions/steps.js +3 -2
  162. package/dist/sessions/store.d.ts +41 -2
  163. package/dist/sessions/store.js +68 -16
  164. package/dist/sessions/try-demo.d.ts +8 -0
  165. package/dist/sessions/try-demo.js +90 -0
  166. package/dist/sessions/try.d.ts +13 -0
  167. package/dist/sessions/try.js +50 -0
  168. package/dist/sessions/vscode-copilot.d.ts +4 -0
  169. package/dist/sessions/vscode-copilot.js +25 -18
  170. package/dist/telemetry/command.js +12 -6
  171. package/dist/telemetry/consent.d.ts +9 -3
  172. package/dist/telemetry/consent.js +25 -9
  173. package/dist/telemetry/fields.d.ts +11 -0
  174. package/dist/telemetry/fields.js +56 -0
  175. package/dist/telemetry/measures.d.ts +13 -0
  176. package/dist/telemetry/measures.js +30 -0
  177. package/dist/telemetry/observe.d.ts +15 -2
  178. package/dist/telemetry/observe.js +34 -7
  179. package/dist/telemetry/region.d.ts +6 -0
  180. package/dist/telemetry/region.js +28 -0
  181. package/dist/telemetry/report.js +1 -1
  182. package/dist/telemetry/schema.d.ts +3 -0
  183. package/dist/telemetry/schema.js +11 -1
  184. package/dist/telemetry/send.d.ts +5 -2
  185. package/dist/telemetry/send.js +15 -7
  186. package/dist/telemetry/words.d.ts +6 -0
  187. package/dist/telemetry/words.js +6 -0
  188. package/package.json +24 -5
@@ -1,30 +1,37 @@
1
+ import { randomBytes } from 'node:crypto';
1
2
  import { homedir, userInfo } from 'node:os';
2
3
  import { basename } from 'node:path';
4
+ import { injectedRule, learnInjected } from './injected.js';
3
5
  // Redaction before anything is written to the session store. Deterministic and conservative: it would rather over-mask a
4
6
  // long random-looking token than let a secret through. Pattern rules first, then emails, the organization domains the
5
7
  // operator names, and last an entropy rule for long mixed-case tokens no pattern knew.
6
8
  export const PATTERNS = [
7
9
  [/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?(?:-----END [A-Z ]*PRIVATE KEY-----|$)/g, '<secret>'],
8
10
  [/\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, '<secret>'],
9
- [/\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, '<secret>'],
10
- [/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g, '<secret>'],
11
+ // A synthetic fixture quoted in a session is masked like a real token (a secret scanner flags the prefix, not the value), so the minimums are short and the
12
+ // left guard is a non-alphanumeric: `FAKE_ghp_...` still starts a token where `\b` would miss it.
13
+ [/(?<![A-Za-z0-9])gh[pousr]_[A-Za-z0-9]{8,}(?![A-Za-z0-9])/g, '<secret>'],
14
+ [/(?<![A-Za-z0-9])github_pat_[A-Za-z0-9_]{8,}(?![A-Za-z0-9])/g, '<secret>'],
11
15
  [/\bsk-(?:ant-)?[A-Za-z0-9_-]{16,}\b/g, '<secret>'],
12
- [/\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{10,}\b/g, '<secret>'],
13
- [/\bwhsec_[A-Za-z0-9]{10,}\b/g, '<secret>'],
14
- [/\bxox[abprs]-[A-Za-z0-9-]{10,}\b/g, '<secret>'],
16
+ [/(?<![A-Za-z0-9])(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{6,}(?![A-Za-z0-9])/g, '<secret>'],
17
+ [/(?<![A-Za-z0-9])whsec_[A-Za-z0-9]{6,}(?![A-Za-z0-9])/g, '<secret>'],
18
+ [/(?<![A-Za-z0-9])xox[abprs]-[A-Za-z0-9-]{6,}(?![A-Za-z0-9])/g, '<secret>'],
15
19
  [/\bAIza[0-9A-Za-z_-]{30,}\b/g, '<secret>'],
16
20
  [/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g, '<secret>'],
17
21
  [/\b(Bearer|Basic)\s+[A-Za-z0-9._~+/=-]{12,}/gi, '$1 <secret>'],
18
22
  // user:password@host inside a URL
19
23
  [/(\b[a-z][a-z0-9+.-]*:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1<secret>@'],
20
- // NAME=value and "name": "value" where the name says it is a secret
21
- [/\b([A-Za-z0-9_-]*(?:password|passwd|secret|token|api[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]*)(["']?\s*[=:]\s*)(["']?)[^\s"'&,;]{4,}\3/gi, '$1$2$3<secret>$3'],
24
+ // NAME=value and "name": "value" where the name says it is a secret. A vendor name before a colon (`1password: ...` in prose) is no key. A plural
25
+ // `tokens` name (`tokens`, `max_tokens`, `inputTokens`) is a count and keeps a bare number, as the JSON walk does; any other `token` name masks a number too.
26
+ [/\b(?!1password(?![A-Za-z0-9_-])["']?\s*:\s)([A-Za-z0-9_-]*(?:password|passwd|secret|api[_-]?key|private[_-]?key|credential)[A-Za-z0-9_-]*)(["']?\s*[=:]\s*)(["']?)[^\s"'&,;]{4,}\3/gi, '$1$2$3<secret>$3'],
27
+ [/\b((?![A-Za-z0-9_-]*tokens(?![A-Za-z0-9_-]))[A-Za-z0-9_-]*token[A-Za-z0-9_-]*)(["']?\s*[=:]\s*)(["']?)[^\s"'&,;]{4,}\3/gi, '$1$2$3<secret>$3'],
28
+ [/\b([A-Za-z0-9_-]*tokens)(["']?\s*[=:]\s*)(["']?)(?!\d+(?![^\s"'&,;]))[^\s"'&,;]{4,}\3/gi, '$1$2$3<secret>$3'],
22
29
  ];
23
30
  const EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
24
31
  // A pinned alias, `foo@npm:bar@1.0.0` or `jsr:@scope/bar@1.0.0`, ends in `bar@1.0.0`, which EMAIL reads as an address. Digits and dots after
25
32
  // the `@` are a version, never a domain, so that one shape right after `npm:` or `jsr:` (and an optional scope) is kept. An all-numeric four-part tail is an
26
33
  // IP address, a real host, so it stays masked (as in `extract.ts` `VERSIONED`); any other address stays masked too.
27
- const ALIAS_PREFIX = /(?<![A-Za-z0-9])(?:npm|jsr):(?:@[A-Za-z0-9._~-]+\/)?$/;
34
+ const ALIAS_PREFIX = /(?<![A-Za-z0-9])(?:npm|jsr):(?:@[A-Za-z0-9._~-]+\/)?$|(?<![A-Za-z0-9._%+-])@[A-Za-z0-9._~-]+\/$/;
28
35
  const DOTTED_VERSION = /@(?!\d+\.\d+\.\d+\.\d+$)\d+(?:\.\d+)+$/;
29
36
  const TOKEN = /[A-Za-z0-9+_=-]{24,}/g;
30
37
  const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -69,7 +76,108 @@ const PROMPT = /(?:<user>|\b[A-Za-z0-9._-]+)@[A-Za-z0-9-]+(?=:[~/.])/g;
69
76
  // Full, compressed, and loopback forms. A clock time (`10:20:30`) has no `::`, so it is never one.
70
77
  const IPV6 = /(?<![0-9A-Za-z:])(?:(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}|(?:[0-9a-f]{1,4}:){1,7}:(?:[0-9a-f]{1,4}(?::[0-9a-f]{1,4}){0,6})?|::[0-9a-f]{1,4}(?::[0-9a-f]{1,4}){0,6})(?![0-9A-Za-z:])/gi;
71
78
  const MIN_NAME = 3;
72
- const PLACEHOLDER = /<(?:secret|email|org-domain|host|user|env)>/g;
79
+ const PLACEHOLDER = /<(?:secret|email|org-domain|host|user|env|owner)>/g;
80
+ // A `.env` line whose name is lower case (`mapbox=q8z7...`): only a whole line of that shape, so `--flag=value` and `?page=2` are left alone.
81
+ const LOWER_ENVIRONMENT = /^([ \t]*(?:export[ \t]+)?[a-z][a-z0-9_]{2,}=)(?!<)(?:(["'])[^"'\n]*\2|[^\s"']+)[ \t]*$/gm;
82
+ // Shapes `makeRedactor` does not know (t955, census S3). They run only on a raw copy, and before the shared rules, because the entropy rule would
83
+ // split a key at its `/` first. A bare 40-character secret key needs a `/` or `+` and a random look, so a git sha (hex) and the front of a
84
+ // base64 image (longer than 40) stay. A password flag is masked only when the value looks like one: four characters with a digit or a symbol, and not a
85
+ // port, so `-p 8080` and `-p3000:3000` stay and `-pthread` is not read as a password.
86
+ const AWS_SECRET_KEY = /(?<![A-Za-z0-9/+=_-])[A-Za-z0-9/+]{40}(?![A-Za-z0-9/+=_-])/g;
87
+ const PORT = /^[\d.:]+(?:\/[a-z]+)?$/i;
88
+ const passwordLike = (value) => value.length >= 4 && /[\d!@#$%^&*]/.test(value) && !PORT.test(value);
89
+ const rawRules = (text) => text
90
+ .replace(/-----BEGIN PGP PRIVATE KEY BLOCK-----[\s\S]*?(?:-----END PGP PRIVATE KEY BLOCK-----|$)/g, '<secret>')
91
+ .replace(/PuTTY-User-Key-File-\d+:[\s\S]*?Private-Lines:[\s\S]*?(?:Private-MAC:[ \t]*\w*|$)/g, '<secret>')
92
+ .replace(/\bdop_v1_[a-f0-9]{64}\b/g, '<secret>')
93
+ .replace(AWS_SECRET_KEY, (key) => (/[/+]/.test(key) && looksRandom(key) ? '<secret>' : key))
94
+ .replace(/((?<!\S)(?:-u\s*|--user\s+)["']?(?!\d+:)[^\s:"']+:)[^\s"']{4,}/g, '$1<secret>')
95
+ .replace(/(?<!\S)-p(["'])[^"'\n]{4,}\1/g, '-p$1<secret>$1')
96
+ .replace(/(?<!\S)-p([^\s"'-][^\s"']{3,})/g, (flag, value) => (passwordLike(value) ? '-p<secret>' : flag))
97
+ .replace(/(\bsshpass\s+-p\s*)(?:"[^"]*"|'[^']*'|[^\s"']+)/g, '$1<secret>')
98
+ .replace(/((?<!\S)-W\s+)(?:(["'])[^"']+\2|([^\s"'/:-][^\s"'/:]{3,}))/g, (flag, lead, quote, bare) => quote !== undefined || passwordLike(bare ?? '') ? `${lead}<secret>` : flag)
99
+ .replace(/(--(?:password|passwd|pwd)\s+)(?:(["'])[^"']*\2|[^\s"'-][^\s"']*)/g, '$1<secret>');
100
+ // A raw copy is JSONL, so a tool's output sits inside a JSON string where a newline is `\n` and a quote is `\"`: a rule written for plain text
101
+ // misses `\nAKIA...` (no word boundary), cannot cross `\"`, and can eat the `n` of `\n` (t955, census S1, S2, S22). So a line that parses is
102
+ // walked and every string in it redacted decoded (a string that itself holds JSON, in turn); the line is written again only if a value changed,
103
+ // and any other text keeps the plain-text path with its bytes as they were.
104
+ const SECRET_NAME = /password|passwd|secret|token|api[_-]?key|private[_-]?key|credential/i;
105
+ // A count of tokens is a number under a `token` name, so a number is masked only under the names that are always a secret.
106
+ const SECRET_NUMBER_NAME = /password|passwd|secret|api[_-]?key|private[_-]?key|credential/i;
107
+ const CONTAINER = /^\s*[{[]/;
108
+ const isContainer = (value) => typeof value === 'object' && value !== null;
109
+ const parseContainer = (text) => {
110
+ if (!CONTAINER.test(text))
111
+ return undefined;
112
+ try {
113
+ const parsed = JSON.parse(text);
114
+ return isContainer(parsed) ? parsed : undefined;
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ };
120
+ const redactDecoded = (body, redactText, injected, enter) => {
121
+ // A name reaches the strings of an array under it (`"tokens": ["..."]`); an object's own keys name their values.
122
+ const walk = (value, name) => {
123
+ if (typeof value === 'string')
124
+ return SECRET_NAME.test(name) && value.length >= 4 ? '<secret>' : redactBlock(value);
125
+ if (typeof value === 'number')
126
+ return SECRET_NUMBER_NAME.test(name) && String(value).length >= 4 ? '<secret>' : value;
127
+ if (Array.isArray(value)) {
128
+ const items = value.map((item) => walk(item, name));
129
+ return items.some((item, at) => item !== value[at]) ? items : value;
130
+ }
131
+ if (typeof value === 'object' && value !== null) {
132
+ const before = Object.entries(value);
133
+ const after = before.map(([key, item]) => [redactBlock(key), walk(item, key)]);
134
+ return after.some(([key, item], at) => key !== before[at]?.[0] || item !== before[at][1]) ? Object.fromEntries(after) : value;
135
+ }
136
+ return value;
137
+ };
138
+ // The text as it was when nothing in it changed; otherwise the value written again, inside the whitespace (a `\r`, a newline) it had around it.
139
+ // A transcript line is a top-level object: the harness's own blocks in it are marked first (their hash is of the text as written), then the rest is walked.
140
+ const rewrite = (text, parsed, top) => {
141
+ if (top && !Array.isArray(parsed))
142
+ enter(parsed);
143
+ const marked = top && !Array.isArray(parsed) ? injected(parsed) : parsed;
144
+ const next = walk(marked, '');
145
+ return next === parsed ? text : `${text.slice(0, text.length - text.trimStart().length)}${JSON.stringify(next)}${text.slice(text.trimEnd().length)}`;
146
+ };
147
+ // A top-level plain-text run follows no tool result: a host a fetch named is kept in that result's line only (a string inside the line is not top level).
148
+ const redactPlain = (run, top) => {
149
+ if (top)
150
+ enter();
151
+ return redactText(run.join('\n'));
152
+ };
153
+ // `text` is a whole document, or lines of which some are JSON: the JSON ones are walked, the runs between them take the plain-text path.
154
+ const redactBlock = (text, top = false) => {
155
+ const whole = parseContainer(text);
156
+ // A top-level array line is no tool-result line: it keeps no fetched host.
157
+ if (whole !== undefined) {
158
+ if (top && Array.isArray(whole))
159
+ enter();
160
+ return rewrite(text, whole, top);
161
+ }
162
+ const out = [];
163
+ let run = [];
164
+ for (const line of text.split('\n')) {
165
+ const parsed = parseContainer(line);
166
+ if (parsed === undefined) {
167
+ run.push(line);
168
+ continue;
169
+ }
170
+ if (run.length > 0)
171
+ out.push(redactPlain(run, top));
172
+ run = [];
173
+ out.push(rewrite(line, parsed, top));
174
+ }
175
+ if (run.length > 0)
176
+ out.push(redactPlain(run, top));
177
+ return out.join('\n');
178
+ };
179
+ return redactBlock(body, true);
180
+ };
73
181
  /** How many values of a redacted text were masked: each placeholder is one. */
74
182
  export const placeholders = (text) => text.match(PLACEHOLDER)?.length ?? 0;
75
183
  /** The account name; `userInfo()` throws when the uid has no passwd entry (a container run with `--user <uid>`), so the environment stands in. */
@@ -92,22 +200,92 @@ const nameRule = (names) => {
92
200
  const forms = [...new Set(usable.flatMap((name) => [name, name.replace(/[^A-Za-z0-9]/g, '-')]))].sort((a, b) => b.length - a.length);
93
201
  return forms.length === 0 ? undefined : new RegExp(`(?<![A-Za-z0-9<])(?:${forms.map(escapeRegExp).join('|')})(?![A-Za-z0-9>])`, 'gi');
94
202
  };
203
+ const TOOL_ID = /(?<![A-Za-z0-9])(?:srv)?tool(?:u|use)_(?:bdrk_|vrtx_)?[A-Za-z0-9_-]{8,}/g;
204
+ const FETCH_TOOLS = new Set(['WebFetch', 'WebSearch']);
205
+ const HOST_OF_URL = /^[a-z][a-z0-9+.-]*:\/\/([A-Za-z0-9.-]+)/i;
206
+ const URLS = /\b[a-z][a-z0-9+.-]*:\/\/[^\s/@"'\\]*@?[A-Za-z0-9.-]+/gi;
207
+ // Loopback, an IP literal, a bare name, and the internal suffixes of INTERNAL_NAME are never a public host.
208
+ const INTERNAL_SUFFIX = /\.(?:internal|local|lan|corp|intranet|home|localdomain)$/;
209
+ const publicHost = (host) => host.includes('.') && !/^[\d.]+$/.test(host) && !INTERNAL_SUFFIX.test(host) && host !== 'localhost';
210
+ const hostsIn = (value, found, listed = false) => {
211
+ if (typeof value === 'string') {
212
+ for (const url of value.match(URLS) ?? [])
213
+ if (!url.includes('@'))
214
+ found.add((HOST_OF_URL.exec(url)?.[1] ?? '').toLowerCase());
215
+ // `allowed_domains` lists bare domains.
216
+ if (listed)
217
+ found.add(value.toLowerCase().replace(/^[a-z][a-z0-9+.-]*:\/\//, '').replace(/[/:].*$/, ''));
218
+ }
219
+ else if (Array.isArray(value))
220
+ for (const item of value)
221
+ hostsIn(item, found, listed);
222
+ else if (typeof value === 'object' && value !== null)
223
+ for (const [key, item] of Object.entries(value))
224
+ hostsIn(item, found, key === 'allowed_domains');
225
+ };
226
+ /** The blocks of a transcript line's message content, when it has any. */
227
+ const blocksOf = (line) => {
228
+ const message = line['message'];
229
+ const content = typeof message === 'object' && message !== null && !Array.isArray(message) ? message['content'] : undefined;
230
+ return Array.isArray(content) ? content.filter((block) => typeof block === 'object' && block !== null && !Array.isArray(block)) : [];
231
+ };
232
+ /** The public hosts each WebFetch or WebSearch call of a transcript named, by the call's tool use id. */
233
+ const fetchedBy = (lines) => {
234
+ const byId = new Map();
235
+ for (const line of lines) {
236
+ for (const block of blocksOf(line)) {
237
+ const { type, id, name, input } = block;
238
+ if (type !== 'tool_use' || typeof id !== 'string' || typeof name !== 'string' || !FETCH_TOOLS.has(name) || input === undefined)
239
+ continue;
240
+ const found = new Set();
241
+ hostsIn(input, found);
242
+ byId.set(id, new Set([...found].filter(publicHost)));
243
+ }
244
+ }
245
+ return byId;
246
+ };
247
+ // A handle is a whole path segment (`/acme/app`, `\acme\app`, `git@host:acme/app.git`) or a hyphen-bounded word of an encoded project folder (`-repos-acme-app`).
248
+ const handleRule = (handles) => {
249
+ const usable = [...new Set(handles.filter((handle) => handle.length >= MIN_NAME))].sort((a, b) => b.length - a.length);
250
+ return usable.length === 0 ? undefined : new RegExp(String.raw `(?<=[\\/:-])(?:${usable.map(escapeRegExp).join('|')})(?=[\\/.-]|["'\s]|$)`, 'gi');
251
+ };
95
252
  /** A whole transcript body redacted for upload, and how many values were masked (placeholders added, a count and nothing else). `names` are the local usernames to mask wherever they stand as a whole word. */
96
- export const redactTranscript = (body, domains = [], names = []) => {
253
+ export const redactTranscript = (body, domains = [], names = [], injectedKey = randomBytes(16).toString('hex'), pack = {}) => {
97
254
  const named = nameRule(names);
98
- // The names go first: a hyphenated or spaced name must be masked whole before the path rules take its first word.
99
- const known = makeRedactor(domains)(body);
100
- const redacted = (named === undefined ? known : known.replace(named, '<user>'))
101
- .replace(URL_HOST, '$1<host>')
102
- .replace(INTERNAL_NAME, '<host>')
103
- .replace(IPV4, '<host>')
104
- .replace(IPV6, '<host>')
105
- .replace(UNIX_HOME, '$1<user>')
106
- .replace(WINDOWS_HOME, '$1<user>')
107
- .replace(ENCODED_HOME, '$1<user>')
108
- .replace(PROMPT, '<user>@<host>')
109
- .replace(ENVIRONMENT, '$1<env>');
255
+ const owner = handleRule(pack.handles ?? []);
256
+ const known = makeRedactor(domains);
257
+ // The hosts the line being redacted may keep: those its tool results' calls fetched.
258
+ let kept = new Set();
259
+ const keepHost = (match, scheme) => (!match.includes('@') && kept.has((HOST_OF_URL.exec(match)?.[1] ?? '').toLowerCase()) ? match : `${scheme}<host>`);
260
+ const redactText = (original) => {
261
+ const text = pack.toolId === undefined ? original : original.replace(TOOL_ID, pack.toolId);
262
+ // The names go first: a hyphenated or spaced name must be masked whole before the path rules take its first word.
263
+ const masked = known(rawRules(text));
264
+ const mine = named === undefined ? masked : masked.replace(named, '<user>');
265
+ return (owner === undefined ? mine : mine.replace(owner, '<owner>'))
266
+ .replace(URL_HOST, keepHost)
267
+ .replace(INTERNAL_NAME, '<host>')
268
+ .replace(IPV4, '<host>')
269
+ .replace(IPV6, '<host>')
270
+ .replace(UNIX_HOME, '$1<user>')
271
+ .replace(WINDOWS_HOME, '$1<user>')
272
+ .replace(ENCODED_HOME, '$1<user>')
273
+ .replace(PROMPT, '<user>@<host>')
274
+ .replace(ENVIRONMENT, '$1<env>')
275
+ .replace(LOWER_ENVIRONMENT, '$1<env>');
276
+ };
277
+ // A request blob repeats messages other lines hold, so what those lines carry is collected before any line is marked.
278
+ const repeated = new Set();
279
+ const whole = parseContainer(body);
280
+ for (const parsed of whole !== undefined ? [whole] : body.split('\n').map(parseContainer))
281
+ if (parsed !== undefined && !Array.isArray(parsed))
282
+ learnInjected(parsed, repeated);
283
+ const calls = pack.fetchedHosts === true ? fetchedBy([...(whole !== undefined ? [whole] : body.split('\n').map(parseContainer))].filter((parsed) => parsed !== undefined && !Array.isArray(parsed))) : new Map();
284
+ const enter = (line) => {
285
+ kept = new Set(line === undefined ? [] : blocksOf(line).flatMap((block) => (block['type'] === 'tool_result' && typeof block['tool_use_id'] === 'string' ? [...(calls.get(block['tool_use_id']) ?? [])] : [])));
286
+ };
287
+ const redacted = redactDecoded(body, redactText, injectedRule(injectedKey, repeated), enter);
110
288
  return { text: redacted, masked: Math.max(0, placeholders(redacted) - placeholders(body)) };
111
289
  };
112
290
  /** The version of the rules above, stated to the pool with every raw part; a change to any rule of `redactTranscript` raises it. */
113
- export const REDACTION_VERSION = '1';
291
+ export const REDACTION_VERSION = '4';
@@ -2,6 +2,13 @@
2
2
  export declare const makeRepoRoot: (exists?: (path: string) => boolean) => ((cwd: string) => string | null);
3
3
  /** `https://User:pw@GitHub.com/org/repo.git`, `git@github.com:org/repo.git`, and `ssh://git@github.com/org/repo/` all read `github.com/org/repo`. */
4
4
  export declare const normalizeRemote: (url: string) => string;
5
+ /** The `url` of `[remote "origin"]` in a git `config`, or null. */
6
+ export declare const originUrl: (config: string) => string | null;
7
+ /**
8
+ * The git `config` text of a repository root, or null: `<root>/.git/config`, or for a linked worktree or submodule (`.git` is a file)
9
+ * the common directory's, reached through the file's `gitdir:` line and that directory's `commondir` file.
10
+ */
11
+ export declare const configOf: (root: string, read?: (path: string) => string | null) => string | null;
5
12
  /**
6
13
  * A resolver from a repository root to its identity string: `origin:<normalized remote>` when the repository's `config` names an
7
14
  * `origin`, else `path:<root>`. The `config` is `<root>/.git/config`, or for a linked worktree (`.git` is a file) the common
@@ -46,7 +46,7 @@ export const normalizeRemote = (url) => {
46
46
  return slash < 0 ? bare.toLowerCase() : `${bare.slice(0, slash).toLowerCase()}${bare.slice(slash)}`;
47
47
  };
48
48
  /** The `url` of `[remote "origin"]` in a git `config`, or null. */
49
- const originUrl = (config) => {
49
+ export const originUrl = (config) => {
50
50
  let inOrigin = false;
51
51
  for (const line of config.split(/\r?\n/)) {
52
52
  const section = /^\s*\[(.*)\]\s*$/.exec(line);
@@ -71,6 +71,21 @@ const readOrNull = (path) => {
71
71
  return null;
72
72
  }
73
73
  };
74
+ /**
75
+ * The git `config` text of a repository root, or null: `<root>/.git/config`, or for a linked worktree or submodule (`.git` is a file)
76
+ * the common directory's, reached through the file's `gitdir:` line and that directory's `commondir` file.
77
+ */
78
+ export const configOf = (root, read = readOrNull) => {
79
+ const direct = read(`${root}/.git/config`);
80
+ if (direct !== null)
81
+ return direct;
82
+ const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(read(`${root}/.git`) ?? '')?.[1];
83
+ if (pointer === undefined)
84
+ return null;
85
+ const gitdir = resolveFrom(root, pointer);
86
+ const common = read(`${gitdir}/commondir`)?.trim();
87
+ return read(`${common === undefined || common === '' ? gitdir : resolveFrom(gitdir, common)}/config`);
88
+ };
74
89
  /**
75
90
  * A resolver from a repository root to its identity string: `origin:<normalized remote>` when the repository's `config` names an
76
91
  * `origin`, else `path:<root>`. The `config` is `<root>/.git/config`, or for a linked worktree (`.git` is a file) the common
@@ -78,22 +93,11 @@ const readOrNull = (path) => {
78
93
  */
79
94
  export const makeRepoIdentity = (read = readOrNull) => {
80
95
  const cache = new Map();
81
- const configOf = (root) => {
82
- const direct = read(`${root}/.git/config`);
83
- if (direct !== null)
84
- return direct;
85
- const pointer = /^gitdir:\s*(.+?)\s*$/m.exec(read(`${root}/.git`) ?? '')?.[1];
86
- if (pointer === undefined)
87
- return null;
88
- const gitdir = resolveFrom(root, pointer);
89
- const common = read(`${gitdir}/commondir`)?.trim();
90
- return read(`${common === undefined || common === '' ? gitdir : resolveFrom(gitdir, common)}/config`);
91
- };
92
96
  return (root) => {
93
97
  const known = cache.get(root);
94
98
  if (known !== undefined)
95
99
  return known;
96
- const url = originUrl(configOf(root) ?? '');
100
+ const url = originUrl(configOf(root, read) ?? '');
97
101
  const identity = url === null ? `path:${root.toLowerCase()}` : `origin:${normalizeRemote(url)}`;
98
102
  cache.set(root, identity);
99
103
  return identity;
@@ -0,0 +1,8 @@
1
+ import type { Store } from './store.js';
2
+ /** Where the nearest later reversion ran: in the prompt that did the work, in a later prompt of that session, or in a later session of the same repository. */
3
+ export type Reversion = 'same-prompt' | 'later-prompt' | 'later-session';
4
+ /**
5
+ * Reads every main-line call once: the clean shell calls' commits, revert words (read segments only, since a segment whose result the call's status does not report (`read` false) could have failed behind another command), and
6
+ * restored paths; and every edit that did not fail, with its digests. A subagent's call is not on the main line, so it is not read. Returns the nearest reversion of a prompt's work, or null for none found.
7
+ */
8
+ export declare const reversionsOf: (store: Store) => ((session: string, index: number | null) => Reversion | null);
@@ -0,0 +1,114 @@
1
+ // A later reversion of the work a prompt did, read from the calls alone. Three ties: a clean `git revert <word>` whose word is a prefix of a commit sha the capture kept (or the other way round); an edit whose old and new text
2
+ // are the swap of an earlier edit's on the same path, or a Write that sets the text an earlier Write of that path set after a different Write changed it (both from the digests of the exact text); and a clean `git restore` or
3
+ // `git checkout --` that names a path an earlier edit changed. The result is descriptive: it says the work was undone later, never that it was wrong or that undoing it was the reason. The original outcome stays as it was and this sits beside it.
4
+ import { EDITS, SHELL } from './extract.js';
5
+ const NEAREST_FIRST = ['same-prompt', 'later-prompt', 'later-session'];
6
+ /** The shortest abbreviation git prints, and a full sha. Anything else a revert names (`HEAD`, a branch, a range, a short guess) is not a sha, so it ties to nothing. */
7
+ const SHA = /^[0-9a-f]{7,40}$/;
8
+ /**
9
+ * The sha-shaped words of a segment that is `git revert <words>`. A bare `git revert` is `--abort`, `--continue`, or `--quit` with the flag dropped, and names nothing. A `--no-commit` revert reads the same as a committed one,
10
+ * because the flag is not kept.
11
+ */
12
+ const revertWords = (run) => (run.startsWith('git revert ') ? run.slice('git revert '.length).split(' ').map((word) => word.toLowerCase()).filter((word) => SHA.test(word)) : []);
13
+ const tied = (commit, word) => commit.startsWith(word) || word.startsWith(commit);
14
+ /** Adds to the list a key holds, making it once, so a hot path costs one push per call and not a copy of the list. */
15
+ const append = (lists, key, items) => {
16
+ const held = lists.get(key);
17
+ if (held === undefined)
18
+ lists.set(key, [...items]);
19
+ else
20
+ held.push(...items);
21
+ };
22
+ /** Where `later` ran against `earlier`, or null when it is not later. Across sessions the calls are ordered by their time, so a call with none cannot be placed, and both sessions must sit in one known repository. */
23
+ const whereOf = (earlier, later, repos) => {
24
+ if (earlier.session === later.session) {
25
+ if (later.n < earlier.n || (later.n === earlier.n && later.k <= earlier.k))
26
+ return null;
27
+ return later.prompt === earlier.prompt ? 'same-prompt' : 'later-prompt';
28
+ }
29
+ const repo = repos.get(earlier.session) ?? null;
30
+ const sameRepo = repo !== null && repo === (repos.get(later.session) ?? null);
31
+ return sameRepo && earlier.ts !== null && later.ts !== null && later.ts > earlier.ts ? 'later-session' : null;
32
+ };
33
+ /**
34
+ * Whether a commit kept the edit before the restore ran: a `git restore` returns the file to the last commit, so an edit committed since is what it returns to, not what it undoes. A commit in the restore's own call counts, since
35
+ * the order inside a call is not kept. Across sessions a commit of the edit's session after it counts, and so does a commit of the restore's session before the restore unless both stamps are known and the commit is stamped before the
36
+ * edit; with a stamp missing the position alone decides.
37
+ */
38
+ const keptBy = (committed, edit, restore) => {
39
+ const before = committed.get(edit.session) ?? [];
40
+ if (edit.session === restore.session)
41
+ return before.some((commit) => commit.n > edit.n && commit.n <= restore.n);
42
+ return before.some((commit) => commit.n > edit.n) || (committed.get(restore.session) ?? []).some((commit) => commit.n <= restore.n && !(commit.ts !== null && edit.ts !== null && commit.ts < edit.ts));
43
+ };
44
+ /**
45
+ * Reads every main-line call once: the clean shell calls' commits, revert words (read segments only, since a segment whose result the call's status does not report (`read` false) could have failed behind another command), and
46
+ * restored paths; and every edit that did not fail, with its digests. A subagent's call is not on the main line, so it is not read. Returns the nearest reversion of a prompt's work, or null for none found.
47
+ */
48
+ export const reversionsOf = (store) => {
49
+ const repos = new Map(store.sessions.map((session) => [session.session, session.repo]));
50
+ const commits = [];
51
+ const reverts = [];
52
+ const committed = new Map();
53
+ const edited = new Map();
54
+ const written = new Map();
55
+ const restored = [];
56
+ for (const tool of store.tools) {
57
+ if (tool.side || tool.prompt === null)
58
+ continue;
59
+ const at = { session: tool.session, n: tool.n, k: 0, ts: tool.ts, prompt: tool.prompt };
60
+ if (EDITS.has(tool.tool) && tool.status !== 'error' && tool.target !== null) {
61
+ append(edited, tool.target, [at]);
62
+ append(written, tool.target, (tool.digests ?? []).map((held, k) => ({ ...at, k, old: held.old ?? null, now: held.new })));
63
+ }
64
+ if (tool.status !== 'ok' || !SHELL.has(tool.tool))
65
+ continue;
66
+ for (const sha of tool.commits ?? [])
67
+ if (sha !== null && SHA.test(sha))
68
+ commits.push({ ...at, sha });
69
+ if ((tool.commits ?? []).length > 0)
70
+ append(committed, tool.session, [{ n: tool.n, ts: tool.ts }]);
71
+ for (const target of tool.restores ?? [])
72
+ restored.push({ ...at, target });
73
+ for (const segment of tool.segments ?? [])
74
+ if (segment.read)
75
+ for (const word of revertWords(segment.run))
76
+ reverts.push({ ...at, sha: word });
77
+ }
78
+ const found = new Map();
79
+ const note = (earlier, later) => {
80
+ const where = whereOf(earlier, later, repos);
81
+ if (where === null)
82
+ return;
83
+ const key = `${earlier.session}#${String(earlier.prompt)}`;
84
+ const held = found.get(key);
85
+ if (held === undefined || NEAREST_FIRST.indexOf(where) < NEAREST_FIRST.indexOf(held))
86
+ found.set(key, where);
87
+ };
88
+ for (const commit of commits)
89
+ for (const revert of reverts)
90
+ if (tied(commit.sha, revert.sha))
91
+ note(commit, revert);
92
+ for (const pairs of written.values()) {
93
+ for (const later of pairs) {
94
+ for (const earlier of pairs) {
95
+ // An edit that changed nothing (old equals new) is its own swap, so it undoes nothing.
96
+ if (earlier.old !== null && earlier.old !== earlier.now && later.old === earlier.now && later.now === earlier.old)
97
+ note(earlier, later);
98
+ if (earlier.old !== null || later.old !== null || earlier.now !== later.now)
99
+ continue;
100
+ // A Write that sets the text an earlier Write set undoes the different Writes between them, not the earlier one.
101
+ for (const between of pairs) {
102
+ if (between.old === null && between.now !== later.now && whereOf(earlier, between, repos) !== null && whereOf(between, later, repos) !== null)
103
+ note(between, later);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ for (const restore of restored) {
109
+ for (const edit of edited.get(restore.target) ?? [])
110
+ if (!keptBy(committed, edit, restore))
111
+ note(edit, restore);
112
+ }
113
+ return (session, index) => found.get(`${session}#${String(index)}`) ?? null;
114
+ };
@@ -1,9 +1,9 @@
1
1
  import type { CanonicalAction } from './actions.js';
2
- import type { ArgClass, TargetClass } from './call-shape.js';
2
+ import type { ArgClass, ArgFeature, ArgName, ArgValue, ChangeKind, DiffShape, TargetClass } from './call-shape.js';
3
3
  import type { RepoSize } from './public-vocabulary.js';
4
4
  import type { SessionFunnel, SessionSetup, SessionTrust } from './session-shape.js';
5
5
  /** Stamped `cv` on every captured record. Bump it when the captured field list changes or a stored value of an existing captured record or skeleton changes: a bump re-reads every transcript. */
6
- export declare const CAPTURE_VERSION = "25";
6
+ export declare const CAPTURE_VERSION = "35";
7
7
  /** Capture versions the reader derives from. A version-3 record is upgraded on read; a newer one is read for the fields this code knows. */
8
8
  export declare const OLDEST_READABLE_CAPTURE = 3;
9
9
  /** Stamped `v` on every derived record. A bump re-derives from the log; no transcript is read. */
@@ -39,6 +39,36 @@ export interface Usage {
39
39
  output: number;
40
40
  thinking: number;
41
41
  }
42
+ /**
43
+ * One model message's token counts (class L: numbers and ids only, never content). Claude Code writes a message over several content-block lines that each repeat the same `usage`, so
44
+ * a message is counted once, keyed by its `message.id` (else the line's `requestId`); a Codex response is keyed by the response id the reader gives it. The counts belong to the
45
+ * message and are never split across its tool calls: `calls` lists the tool-use ids the message made, and a later read can allocate. A category the source did not report is absent,
46
+ * never a zero. Summing these over a session record gives the deduplicated total for the file, `side` entries included.
47
+ */
48
+ export interface MessageUsage {
49
+ /** The message id, else the request id; null when the line holds neither (each such line is its own message). */
50
+ id: string | null;
51
+ /** The tool-use ids of the calls this message made, in order; empty when it made none. */
52
+ calls: string[];
53
+ /** True for a message on a sidechain line of a main transcript, which the session's `usage` total leaves out. */
54
+ side?: true;
55
+ input?: number;
56
+ cacheWrite?: number;
57
+ cacheRead?: number;
58
+ output?: number;
59
+ /** From `output_tokens_details.thinking_tokens` (Codex: the reasoning output tokens); absent when the source did not report it. */
60
+ thinking?: number;
61
+ /** Codex only: the plan share used as the response's event reported it. Absent when the event held no rate-limit block, never a zero. */
62
+ plan?: {
63
+ primary?: PlanWindow;
64
+ secondary?: PlanWindow;
65
+ };
66
+ }
67
+ /** One plan window a Codex event reports (`rate_limits.primary` or `.secondary`): the percent used and the window's length in minutes when given. */
68
+ export interface PlanWindow {
69
+ usedPercent: number;
70
+ windowMinutes?: number;
71
+ }
42
72
  /** The five usage categories, in the order a record lists the ones it observed. */
43
73
  export declare const USAGE_FIELDS: readonly ["input", "cacheWrite", "cacheRead", "output", "thinking"];
44
74
  export type UsageField = (typeof USAGE_FIELDS)[number];
@@ -115,7 +145,7 @@ export interface CaptureEnv {
115
145
  /**
116
146
  * One per transcript, captured: the `parse_sessions.py` record, minus anything that is raw content. Classes (spec Section 1.2):
117
147
  * `L` local-only: `project`, `files`. `A` account store: `session`, `parent`, `spawnedBy`, `spawnedIn`, `branches`, `urls`, `searches`, `tldrs`,
118
- * `confidences`, `promptFingerprint`, `repo`, `fileHashes`, `tools`, `setup`, `funnel`, `trust`, `at`, `tsFirst`, `tsLast`. `P` pooled: `kind`, `cv`, `rev`, `sid`,
148
+ * `confidences`, `promptFingerprint`, `repo`, `fileHashes`, `tools`, `setup`, `funnel`, `trust`, `loadedInstructions`, `at`, `tsFirst`, `tsLast`. `P` pooled: `kind`, `cv`, `rev`, `sid`,
119
149
  * the counts, `versions`, `models`, `env`, `costUsd`, `usage` (`at`, `tsFirst`, and `tsLast` at UTC day). `agentType`, `frame`, and `frameId` moved to `A` at EXTRACTOR 11:
120
150
  * a custom agent's name is private, and the joined frame is one value that links rows.
121
151
  */
@@ -126,6 +156,11 @@ export interface SessionObservation {
126
156
  * the Claude Code path's records and CAPTURE_VERSION are unchanged and an older oars reads a Cursor session as a session with no harness version.
127
157
  */
128
158
  harness?: Harness;
159
+ /**
160
+ * Set on a session a later read of its source folded into another (an OpenTelemetry trace deposited before its `gen_ai.conversation.id` arrived): the id of the session that
161
+ * holds its turns now. The record is a later rev of the early session with no turns, so the log stays append-only; a reader skips a session that carries it. Class P.
162
+ */
163
+ supersededBy?: string;
129
164
  /** CAPTURE_VERSION (a version-2 line has `v` instead). Class P. */
130
165
  cv: string;
131
166
  /** The first 8 hex characters of SHA-256 of the salt this record's hashes used; null on a record captured before version 4. Class P. */
@@ -193,12 +228,28 @@ export interface SessionObservation {
193
228
  usageObserved?: UsageField[];
194
229
  /** How many responses reported any usage (class P); compare with `assistantTurns` or `turns` for the share a sum covers. Same absence rule as `usageObserved`. */
195
230
  usageResponses?: number;
231
+ /** One entry per distinct model message that reported usage, in file order (CAPTURE 28); see `MessageUsage`. Absent when the file reports no usage, and on a record captured before version 28. */
232
+ messageUsage?: MessageUsage[];
196
233
  /** What the transcript shows of the setup the session ran with (class A); absent when it shows none, and on a record captured before version 12. */
197
234
  setup?: SessionSetup;
198
235
  /** The tool names at each funnel stage the transcript shows (class A); same absence rules. */
199
236
  funnel?: SessionFunnel;
200
237
  /** How often a human or a policy stopped the session and how long it ran alone (class A); same absence rules. */
201
238
  trust?: SessionTrust;
239
+ /**
240
+ * The instruction files the transcript shows loaded, as counts and levels, never a path (class A; `load-status.ts` joins it with the session-start snapshot). Absent when the
241
+ * transcript holds no such record. Added at CAPTURE 29.
242
+ */
243
+ loadedInstructions?: LoadedInstructions;
244
+ }
245
+ /** What a transcript shows loaded. `levels` is folders above the working folder, one entry per project file. */
246
+ export interface LoadedInstructions {
247
+ user: number;
248
+ levels: number[];
249
+ /** Files loaded from the working folder, by kind; absent on a record captured before version 30. */
250
+ local?: number;
251
+ dotClaude?: number;
252
+ rules?: number;
202
253
  }
203
254
  /**
204
255
  * What the last build or test runner after a prompt's last edit returned: `passed` and `failed` when the call's status reads that runner,
@@ -395,10 +446,28 @@ export interface ToolObservation {
395
446
  targetBase: TargetBase | null;
396
447
  /** The input's key names, sorted. Class A (EXTRACTOR 11). */
397
448
  keys: string[];
449
+ /** How many key names the wire did not send (off a public tool's pinned list, or kept `private` by the gate). Never captured: set only on a record the wire projection made (`projectedStore`) or read back from a skeleton, so a local read and the pool count one `dropped` cell. */
450
+ keysDropped?: number;
398
451
  /** The class of the call's path target (`targetClassOf`): a member of `TARGET_CLASSES`, `none` for a call with no path target. Absent on a record captured before CAPTURE 22. Class P: a closed list, never the path or its base name. */
399
452
  targetClass?: TargetClass;
400
453
  /** The classes of the call's arguments (`argClassOf`), a sorted set of `ARG_CLASSES` members, `['none']` for a call with no argument value. Absent on a record captured before CAPTURE 22. Class P: never a value. */
401
454
  argClass?: ArgClass[];
455
+ /** Each argument's features (`argFeaturesOf`), sorted by key: closed-list facts about its value (size, a path's depth, extension class, and folder kind, a pattern's features), never the value. Absent on a record captured before CAPTURE 27. Class A, as `keys` is: a private tool's key names are private. */
456
+ argFeatures?: ArgFeature[];
457
+ /** Each argument's closed value (`argValuesOf`): a boolean, or a string on its tool's list in `CLOSED_ARG_VALUES`; a value off the list is not stored. Absent on a record captured before CAPTURE 27, and when the call held none. Class A, as `argFeatures`. */
458
+ argValues?: ArgValue[];
459
+ /** The name tokens of the call's values (`argNamesOf`), sorted by kind and name: a file's name, a folder's name below the root, a variable's name, a flag value that is a name, a custom agent's name; never a value. Absent when it holds none and on a record captured before CAPTURE 33. Class A. */
460
+ names?: ArgName[];
461
+ /** The old and new text of an Edit (one pair), a MultiEdit (one pair per edit, in order), or a Write (a new digest only), as a digest of the exact text (`editDigestsOf`); a line ending is a difference. Absent on any other call and on a record captured before CAPTURE 35. Class A, as `target`: the digest is plain, so the same text matches across sessions and accounts. */
462
+ digests?: EditDigest[];
463
+ /** The files a `git restore` or `git checkout --` in a command whose status the call reports names, hashed as `target` is, so a restore reads against an earlier edit of the same path. Absent when the call restores none and on a record captured before CAPTURE 35. Class A. */
464
+ restores?: string[];
465
+ /** One template per command of a shell call (`templatesOf`): its program, subcommand, and flags, as written and masked by the redactor, and every literal argument as a slot of `TEMPLATE_SLOTS`; never an argument. Absent when the call holds no command and on a record captured before CAPTURE 34. Class A. */
466
+ templates?: string[];
467
+ /** What an edit did (`diffShapeOf`), a sorted set of `DIFF_SHAPES` members: `file-written` for a write, else the shapes of the old and new text; never the text. Absent on a call that is no edit, an edit that changes nothing, and a record captured before CAPTURE 34. Class A. */
468
+ diffShape?: DiffShape[];
469
+ /** What changed from the previous call of the same tool in the same prompt (`changeKindOf`), a sorted set of `CHANGE_KINDS` members. Absent on the first such call and on a record captured before CAPTURE 27. Class P. */
470
+ changeKind?: ChangeKind[];
402
471
  /** The class of a failed call (`errorCodeOf`): a member of `ERROR_CODES`, never result text. Absent on a call that did not fail and on a record captured before CAPTURE 14. Class P. */
403
472
  errorCode?: string;
404
473
  /** From the call's line to its first result's line; null when either timestamp is missing or no result is in the file. */
@@ -426,6 +495,11 @@ export interface ToolObservation {
426
495
  /** One entry per commit a clean shell call made, in order: the sha its result names, or null when it names none (CAPTURE 6). Absent when it made none. */
427
496
  commits?: Array<string | null>;
428
497
  }
498
+ /** The digest of one edit's text: the first 32 hex digits of the SHA-256 of the exact UTF-8 text. A Write has `new` only, never the digest of an empty string for `old`. */
499
+ export interface EditDigest {
500
+ old?: string;
501
+ new: string;
502
+ }
429
503
  /** One command of a shell call. */
430
504
  export interface ShellSegment {
431
505
  /** The program's basename and its plain words: no flags, paths, variables, or quoted text; redacted. */
@@ -640,7 +714,7 @@ export interface RecallCall {
640
714
  /** One hook invocation, appended to `hook-calls.jsonl` by the hook itself; `measure` counts these per session. Class A: `session`, `at`, `error`; P: `event`, `hit`, `ms`, `startMs`. */
641
715
  export interface HookCall {
642
716
  at: string;
643
- event: 'session-start' | 'prompt' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly';
717
+ event: 'session-start' | 'prompt' | 'pre-tool' | 'post-tool' | 'stop' | 'nightly' | 'codex-record';
644
718
  /** The Claude Code session the hook ran in; null for the nightly run. */
645
719
  session: string | null;
646
720
  /** Whether the hook had something to say (an injected block, a recall hit, or a session deposited). */