@threadcrumbs/common 0.1.2 → 0.1.4
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 +45 -12
- package/lib/claude-transcript.mjs +108 -5
- package/lib/context-breakdown.mjs +149 -0
- package/lib/jsonl-follower.mjs +11 -0
- package/lib/opencode-store.mjs +130 -37
- package/lib/telemetry-fields.mjs +109 -2
- package/lib/tool-kinds.mjs +256 -0
- package/package.json +1 -1
- package/telemetry/HARNESS-NOTES.md +246 -0
- package/telemetry/README.md +137 -11
- package/telemetry/antigravity.mjs +29 -4
- package/telemetry/claude-code.mjs +59 -23
- package/telemetry/codex.mjs +162 -28
- package/telemetry/cursor.mjs +45 -13
- package/telemetry/hermes.mjs +54 -18
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
export const TOOL_KINDS = Object.freeze([
|
|
2
|
+
'read',
|
|
3
|
+
'search',
|
|
4
|
+
'edit',
|
|
5
|
+
'delete',
|
|
6
|
+
'move',
|
|
7
|
+
'execute',
|
|
8
|
+
'think',
|
|
9
|
+
'fetch',
|
|
10
|
+
'agent',
|
|
11
|
+
'plan',
|
|
12
|
+
'communicate',
|
|
13
|
+
'switch_mode',
|
|
14
|
+
'other',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
const NAMES = {
|
|
18
|
+
read: [
|
|
19
|
+
'read', 'notebookread', 'read_file', 'read_many_files', 'view', 'view_file', 'view_file_outline', 'view_code_item',
|
|
20
|
+
'view_image', 'todoread', 'cat',
|
|
21
|
+
],
|
|
22
|
+
search: [
|
|
23
|
+
'grep', 'glob', 'ls', 'list', 'list_dir', 'list_directory', 'search_file_content', 'search_files', 'grep_search',
|
|
24
|
+
'find_by_name', 'codesearch', 'file_search', 'codebase_search',
|
|
25
|
+
],
|
|
26
|
+
edit: [
|
|
27
|
+
'edit', 'multiedit', 'write', 'notebookedit', 'apply_patch', 'applypatch', 'patch', 'create', 'write_file', 'replace',
|
|
28
|
+
'str_replace', 'strreplace', 'write_to_file', 'replace_file_content', 'multi_replace_file_content', 'search_replace',
|
|
29
|
+
],
|
|
30
|
+
delete: ['delete', 'delete_file'],
|
|
31
|
+
move: ['move', 'move_file', 'rename', 'rename_file'],
|
|
32
|
+
execute: [
|
|
33
|
+
'bash', 'powershell', 'shell', 'exec_command', 'write_stdin', 'run_shell_command', 'run_command', 'terminal',
|
|
34
|
+
'execute_code', 'process', 'bashoutput', 'killshell', 'command_status', 'send_command_input', 'local_shell',
|
|
35
|
+
],
|
|
36
|
+
think: ['think', 'save_memory', 'memory', 'report_intent'],
|
|
37
|
+
fetch: [
|
|
38
|
+
'webfetch', 'websearch', 'web_fetch', 'web_search', 'google_web_search', 'web_extract', 'read_url_content',
|
|
39
|
+
'search_web', 'fetch',
|
|
40
|
+
],
|
|
41
|
+
agent: ['agent', 'task', 'spawn_agent', 'wait_agent', 'send_input', 'delegate_task', 'subagent'],
|
|
42
|
+
plan: ['todowrite', 'todo_write', 'update_plan', 'write_todos', 'todo', 'todo_list', 'task_boundary'],
|
|
43
|
+
communicate: ['askuserquestion', 'ask_user', 'request_user_input', 'request_permissions', 'question', 'clarify', 'notify_user'],
|
|
44
|
+
switch_mode: ['enterplanmode', 'exitplanmode', 'switch_mode'],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const KIND_OF = new Map(Object.entries(NAMES).flatMap(([kind, names]) => names.map((name) => [name, kind])))
|
|
48
|
+
|
|
49
|
+
const PREFIXES = [
|
|
50
|
+
[/^browser_/, 'fetch'],
|
|
51
|
+
[/^web_/, 'fetch'],
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
const normalize = (name) => String(name || '').trim().toLowerCase().replace(/[\s-]+/g, '_')
|
|
55
|
+
|
|
56
|
+
export function mcpServer(name) {
|
|
57
|
+
return /^mcp__(.+?)__./.exec(String(name || ''))?.[1] || null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function nameKind(name) {
|
|
61
|
+
if (mcpServer(name)) return 'other'
|
|
62
|
+
const key = normalize(name)
|
|
63
|
+
return KIND_OF.get(key) || PREFIXES.find(([re]) => re.test(key))?.[1] || 'other'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const READS = /^(cat|head|tail|less|more|bat|nl|wc|type|get-content|gc)$/
|
|
67
|
+
const SEARCHES = /^(rg|grep|egrep|fgrep|ag|ack|findstr|select-string|sls|ls|dir|tree|fd|get-childitem|gci)$/
|
|
68
|
+
const DELETES = /^(rm|rmdir|del|erase|remove-item|ri)$/
|
|
69
|
+
const MOVES = /^(mv|move|move-item|rename-item|ren)$/
|
|
70
|
+
const FILTERS = /^(head|tail|sort|uniq|wc|cut|tr|grep|rg|findstr|select-string|sls|select-object|select|measure-object|measure|sort-object|less|more|cat|nl)$/
|
|
71
|
+
const GIT_KINDS = { show: 'read', diff: 'read', log: 'read', status: 'read', blame: 'read', grep: 'search', 'ls-files': 'search', rm: 'delete', mv: 'move' }
|
|
72
|
+
const QUIET_TARGET = /^(\/dev\/null|\$null|nul)$/i
|
|
73
|
+
|
|
74
|
+
function splitCommand(command) {
|
|
75
|
+
const steps = [{ words: [], piped: false }]
|
|
76
|
+
let word = ''
|
|
77
|
+
let quote = ''
|
|
78
|
+
let unsafe = false
|
|
79
|
+
const pushWord = () => {
|
|
80
|
+
if (word) steps.at(-1).words.push(word)
|
|
81
|
+
word = ''
|
|
82
|
+
}
|
|
83
|
+
const pushStep = (join) => {
|
|
84
|
+
pushWord()
|
|
85
|
+
steps.push({ words: [], piped: join === 'pipe' })
|
|
86
|
+
}
|
|
87
|
+
for (let i = 0; i < command.length; i++) {
|
|
88
|
+
const c = command[i]
|
|
89
|
+
const next = command[i + 1]
|
|
90
|
+
if (quote === "'") {
|
|
91
|
+
if (c === "'") quote = ''
|
|
92
|
+
else word += c
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (c === '`' || (c === '$' && next === '(')) unsafe = true
|
|
96
|
+
if (quote === '"') {
|
|
97
|
+
if (c === '"') quote = ''
|
|
98
|
+
else if (c === '\\' && next) word += command[++i]
|
|
99
|
+
else word += c
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
if (c === "'" || c === '"') {
|
|
103
|
+
quote = c
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
if (c === '\\' && next && next !== '\n') {
|
|
107
|
+
word += command[++i]
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if ((c === '<' || c === '>') && next === '(') {
|
|
111
|
+
unsafe = true
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
if (c === '<') {
|
|
115
|
+
pushWord()
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
if (c === '>') {
|
|
119
|
+
if (/^\d*$/.test(word)) word = ''
|
|
120
|
+
else pushWord()
|
|
121
|
+
const rest = /^>?(&\d|\s*([^\s;&|<>]+))?/.exec(command.slice(i + 1))
|
|
122
|
+
if (!rest[1]?.startsWith('&') && !QUIET_TARGET.test(rest[2] || '')) unsafe = true
|
|
123
|
+
i += rest[0].length
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
if (c === '&' && next === '&') {
|
|
127
|
+
pushStep('and')
|
|
128
|
+
i++
|
|
129
|
+
continue
|
|
130
|
+
}
|
|
131
|
+
if (c === '|' && next === '|') {
|
|
132
|
+
pushStep('or')
|
|
133
|
+
i++
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
if (c === '|') {
|
|
137
|
+
pushStep('pipe')
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
if (c === ';' || c === '\n') {
|
|
141
|
+
pushStep('then')
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
if (c === '&') {
|
|
145
|
+
unsafe = true
|
|
146
|
+
continue
|
|
147
|
+
}
|
|
148
|
+
if (/\s/.test(c)) {
|
|
149
|
+
pushWord()
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
word += c
|
|
153
|
+
}
|
|
154
|
+
pushWord()
|
|
155
|
+
if (quote) unsafe = true
|
|
156
|
+
return { steps: steps.filter((s) => s.words.length), unsafe }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const SED_ADDRESS = String.raw`(?:\d+(?:~\d+)?|\$|\/(?:[^\/\\]|\\.)*\/[IM]*)`
|
|
160
|
+
const SED_PRINT = new RegExp(String.raw`^(?:${SED_ADDRESS}(?:,(?:${SED_ADDRESS}|[+~]\d+))?)?\s*!?\s*p$`)
|
|
161
|
+
const SED_FLAGS = /^(-[nEruzs]+|--(quiet|silent|regexp-extended|separate|unbuffered|null-data|posix|debug|sandbox))$/
|
|
162
|
+
|
|
163
|
+
function sedOnlyPrints(args) {
|
|
164
|
+
const scripts = []
|
|
165
|
+
let script = null
|
|
166
|
+
for (let i = 0; i < args.length; i++) {
|
|
167
|
+
const arg = args[i]
|
|
168
|
+
if (arg === '-e' || arg === '--expression') scripts.push(args[++i] ?? '')
|
|
169
|
+
else if (arg.startsWith('--expression=')) scripts.push(arg.slice('--expression='.length))
|
|
170
|
+
else if (SED_FLAGS.test(arg)) continue
|
|
171
|
+
else if (arg.startsWith('-') && arg !== '-') return false
|
|
172
|
+
else if (!scripts.length && script == null) script = arg
|
|
173
|
+
}
|
|
174
|
+
if (script != null) scripts.push(script)
|
|
175
|
+
const parts = scripts.flatMap((s) => s.split(';')).map((s) => s.trim()).filter(Boolean)
|
|
176
|
+
return parts.length > 0 && parts.every((part) => SED_PRINT.test(part))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function positionals(args, valued) {
|
|
180
|
+
const out = []
|
|
181
|
+
for (let i = 0; i < args.length; i++) {
|
|
182
|
+
if (valued.test(args[i])) i++
|
|
183
|
+
else if (args[i] === '-' || !args[i].startsWith('-')) out.push(args[i])
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const WRITES_OR_RUNS = {
|
|
189
|
+
find: (args) => args.some((a) => /^-(exec|execdir|ok|okdir|delete|fprint|fprint0|fprintf|fls)$/.test(a)),
|
|
190
|
+
fd: (args) => args.some((a) => /^--exec(-batch)?(=|$)/.test(a) || /^-[^-]*[xX]/.test(a)),
|
|
191
|
+
rg: (args) => args.some((a) => /^--pre(=|$)/.test(a)),
|
|
192
|
+
tree: (args) => args.some((a) => /^-o/.test(a) || a === '-R'),
|
|
193
|
+
less: (args) => args.some((a) => /^-[oO]|^--(log-file|LOG-FILE)(=|$)/.test(a)),
|
|
194
|
+
sort: (args) => args.some((a) => /^-[^-]*o/.test(a) || /^--output(=|$)/.test(a)),
|
|
195
|
+
uniq: (args) => positionals(args, /^-[fsw]$/).length > 1,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function gitArgs(args) {
|
|
199
|
+
let i = 0
|
|
200
|
+
while (i < args.length) {
|
|
201
|
+
if (args[i] === '-C') i += 2
|
|
202
|
+
else if (args[i] === '--no-pager') i++
|
|
203
|
+
else break
|
|
204
|
+
}
|
|
205
|
+
return args.slice(i)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function stepKind(words) {
|
|
209
|
+
const [first, ...args] = words
|
|
210
|
+
const verb = first.toLowerCase().replace(/\.exe$/, '')
|
|
211
|
+
if (verb === 'git') {
|
|
212
|
+
const [sub, ...rest] = gitArgs(args)
|
|
213
|
+
return rest.some((a) => a.startsWith('--output')) ? null : GIT_KINDS[String(sub || '').toLowerCase()] || null
|
|
214
|
+
}
|
|
215
|
+
if (verb === 'sed') return sedOnlyPrints(args) ? 'read' : null
|
|
216
|
+
if (WRITES_OR_RUNS[verb]?.(args)) return null
|
|
217
|
+
if (verb === 'find') return 'search'
|
|
218
|
+
if (READS.test(verb)) return 'read'
|
|
219
|
+
if (SEARCHES.test(verb)) return 'search'
|
|
220
|
+
if (DELETES.test(verb)) return 'delete'
|
|
221
|
+
if (MOVES.test(verb)) return 'move'
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isFilter(words) {
|
|
226
|
+
const [first, ...args] = words
|
|
227
|
+
const verb = first.toLowerCase()
|
|
228
|
+
return FILTERS.test(verb) && !WRITES_OR_RUNS[verb]?.(args)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function shellKind(command) {
|
|
232
|
+
const source = String(command || '').trim()
|
|
233
|
+
if (!source) return 'execute'
|
|
234
|
+
const { steps, unsafe } = splitCommand(source)
|
|
235
|
+
if (unsafe || !steps.length) return 'execute'
|
|
236
|
+
const kinds = []
|
|
237
|
+
for (const { words, piped } of steps) {
|
|
238
|
+
if (piped) {
|
|
239
|
+
if (!isFilter(words)) return 'execute'
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (/^(cd|pushd|popd|set-location|sl|chdir)$/i.test(words[0])) continue
|
|
243
|
+
const kind = stepKind(words)
|
|
244
|
+
if (!kind) return 'execute'
|
|
245
|
+
kinds.push(kind)
|
|
246
|
+
}
|
|
247
|
+
if (!kinds.length) return 'execute'
|
|
248
|
+
return kinds.every((k) => k === kinds[0]) ? kinds[0] : 'execute'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function toolKind(name, command = null) {
|
|
252
|
+
const kind = nameKind(name)
|
|
253
|
+
const server = mcpServer(name)
|
|
254
|
+
if (kind !== 'execute' || command == null) return { kind, server }
|
|
255
|
+
return { kind: shellKind(command), server }
|
|
256
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# Harness notes
|
|
2
|
+
|
|
3
|
+
What each harness saves of its reasoning and replies, what hooks it offers to other programs,
|
|
4
|
+
and how much of its context it keeps free for compacting. The readers use the first two; the
|
|
5
|
+
hooks are for apps that answer or message agents, whose code stays out of these readers.
|
|
6
|
+
|
|
7
|
+
Each section says which version was checked and when. Formats change between versions, so
|
|
8
|
+
recheck a fact before relying on it again.
|
|
9
|
+
|
|
10
|
+
## Claude Code
|
|
11
|
+
|
|
12
|
+
Checked against Claude Code 2.1.287 to 2.1.317 and its docs, 2026-11-08.
|
|
13
|
+
|
|
14
|
+
- **Thoughts:** `thinking` blocks in assistant records, timed by the record's `timestamp`.
|
|
15
|
+
Claude 5 models usually write them with no text, only a `signature` that holds the encrypted
|
|
16
|
+
reasoning: the API's `thinking.display` defaults to `omitted` for them, and `updates` also
|
|
17
|
+
leaves the reasoning empty. Text is there only when the API returned a summary. The record's
|
|
18
|
+
`thinkingDisplay` field (`summarized`, `updates`, or absent) isn't documented.
|
|
19
|
+
`redacted_thinking` blocks hold no text either.
|
|
20
|
+
- **Replies:** `text` blocks in assistant records. Each content block is its own record, and the
|
|
21
|
+
blocks of one reply share `message.id`.
|
|
22
|
+
- **What a request held:** recent versions write it into the transcript. None of it is
|
|
23
|
+
documented. The attachments below were seen from 2.1.287 and `api-request-shape` from 2.1.301:
|
|
24
|
+
- `api-request-shape`: the system prompt's text and every tool definition loaded, written
|
|
25
|
+
again when the set changes (`shapeHash`).
|
|
26
|
+
- Attachments: `instructions` (memory files, with path, type, and text), `skill_listing`
|
|
27
|
+
(skill names and the listing's text), `deferred_tools_delta` (deferred tool names, with MCP
|
|
28
|
+
tools as `mcp__<server>__<tool>`), `mcp_instructions_delta` (server names and their
|
|
29
|
+
instructions), and `agent_listing_delta` (custom agents). After a compaction they're written
|
|
30
|
+
again, marked `isInitial`.
|
|
31
|
+
- **`/context`:** the docs don't say its output reaches the transcript, and no transcript with
|
|
32
|
+
it has been checked. The 2.1.319 changelog mentions a table form that Claude reads.
|
|
33
|
+
- **Kept free for compacting:** about 33,000 tokens by default. The docs say 1M-window models
|
|
34
|
+
compact at about 967K. The `autoCompactWindow` setting, `CLAUDE_CODE_AUTO_COMPACT_WINDOW`,
|
|
35
|
+
`--autocompact`, and `/autocompact` change it.
|
|
36
|
+
- **Hooks:** 33 events, each a command or `type: "http"` with `url`, `headers`, `timeout`, and
|
|
37
|
+
`onFailure`.
|
|
38
|
+
- Answering a permission request: a `PermissionRequest` hook returns
|
|
39
|
+
`hookSpecificOutput.decision.behavior`, `allow` or `deny`. Deny rules still win.
|
|
40
|
+
- A message when a turn ends: a `Stop` hook returns `decision: "block"` with a `reason`, and
|
|
41
|
+
the agent carries on with it.
|
|
42
|
+
- Hooks fire in the CLI, the IDE extensions, and the desktop app; cloud sessions don't read
|
|
43
|
+
local settings. They're set in user, project, or local settings, and edits are picked up
|
|
44
|
+
without a restart.
|
|
45
|
+
- **Sources:** <https://code.claude.com/docs/en/hooks>,
|
|
46
|
+
<https://code.claude.com/docs/en/model-config>,
|
|
47
|
+
<https://platform.claude.com/docs/en/build-with-claude/thinking>.
|
|
48
|
+
|
|
49
|
+
## Codex
|
|
50
|
+
|
|
51
|
+
Checked against Codex 0.166 rollouts and openai/codex `main`, 2026-11-08. Rollouts come in two
|
|
52
|
+
modes, `paginated` and the older default; the records below are in both.
|
|
53
|
+
|
|
54
|
+
- **Thoughts:** `response_item` of type `reasoning`: `summary` is a list of `summary_text`
|
|
55
|
+
parts, timed by the record's `timestamp`. A summary can be empty. `encrypted_content` is the
|
|
56
|
+
full reasoning, encrypted. `content` holds raw reasoning only for models that return it.
|
|
57
|
+
Copies to skip: `event_msg` `agent_reasoning` (older mode) and `item_completed` with a
|
|
58
|
+
`Reasoning` item (paginated mode), which adds start and end times.
|
|
59
|
+
- **Replies:** `response_item` of type `message` with role `assistant`: `content` is a list of
|
|
60
|
+
`output_text` parts, and `phase` is `commentary` or `final_answer`. Copies to skip:
|
|
61
|
+
`agent_message`, `item_completed` with an `AgentMessage` item, and
|
|
62
|
+
`task_complete.last_agent_message`.
|
|
63
|
+
- **What a request held:** `session_meta.base_instructions` and the `developer` messages are the
|
|
64
|
+
system prompt; a user message starting `# AGENTS.md instructions` carries AGENTS.md;
|
|
65
|
+
`additional_tools` items list the MCP and app tools by namespace. Built-in tool definitions
|
|
66
|
+
aren't written.
|
|
67
|
+
- **Kept free for compacting:** Codex compacts at 90% of the model's window, and rollouts report
|
|
68
|
+
95% of it as `model_context_window`, so compaction comes at about 94.7% of the reported window.
|
|
69
|
+
`model_auto_compact_token_limit` can only lower it. (`codex-rs/protocol/src/openai_models.rs`)
|
|
70
|
+
- **Hooks:** on by default, set in `~/.codex/hooks.json`, `[hooks]` in `config.toml`, a trusted
|
|
71
|
+
project's `.codex/`, or plugins. A hook is a command (JSON on stdin and stdout, exit 2 blocks)
|
|
72
|
+
or an MCP tool call, and must be trusted in `/hooks`; a changed hook must be trusted again.
|
|
73
|
+
- Events: SessionStart, SessionEnd, SubagentStart, SubagentStop, UserPromptSubmit,
|
|
74
|
+
PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, Stop, Interrupt,
|
|
75
|
+
PreModelRequest, PostModelRequest.
|
|
76
|
+
- Answering a permission request: `PermissionRequest` returns
|
|
77
|
+
`hookSpecificOutput.decision.behavior`, `allow` or `deny`, with a `message`.
|
|
78
|
+
- A message when a turn ends: `Stop` returns `decision: "block"` with a `reason`, which becomes
|
|
79
|
+
the next prompt.
|
|
80
|
+
- The older `notify` setting runs a command once a turn ends, with the thread, turn, and last
|
|
81
|
+
reply as JSON; it can't answer.
|
|
82
|
+
- Whether editing `hooks.json` takes effect without a restart isn't documented.
|
|
83
|
+
- **Sources:** `codex-rs/protocol/src/models.rs`, `codex-rs/protocol/src/protocol.rs`,
|
|
84
|
+
`codex-rs/hooks/`, <https://learn.chatgpt.com/docs/hooks>.
|
|
85
|
+
|
|
86
|
+
## Gemini CLI
|
|
87
|
+
|
|
88
|
+
Not read yet. Checked against google-gemini/gemini-cli `main`, 2026-11-08.
|
|
89
|
+
|
|
90
|
+
- **Thoughts:** chats are `~/.gemini/tmp/<project>/chats/session-<time>-<id>.jsonl`. A record
|
|
91
|
+
with `type: "gemini"` has `thoughts: [{ subject, description, timestamp }]`, the API's thought
|
|
92
|
+
summaries. They're stamped when the reply finishes, so their time is about the reply's.
|
|
93
|
+
- **Replies:** the same record's `content`, timed by its `timestamp`. Other lines are `$set`,
|
|
94
|
+
`$rewindTo`, and `$patch`.
|
|
95
|
+
- **Kept free for compacting:** Gemini CLI compresses at 50% of the model's limit
|
|
96
|
+
(`model.compressionThreshold`) and keeps the newest 30%.
|
|
97
|
+
- **Hooks:** set under `hooks` in settings (project, `~/.gemini`, system, or extensions), as
|
|
98
|
+
commands only, on by default. Events: BeforeTool, AfterTool, BeforeAgent, AfterAgent,
|
|
99
|
+
BeforeModel, AfterModel, BeforeToolSelection, SessionStart, SessionEnd, Notification,
|
|
100
|
+
PreCompress.
|
|
101
|
+
- Answering a permission request: no. BeforeTool can deny or ask, but `allow` doesn't skip the
|
|
102
|
+
prompt.
|
|
103
|
+
- A message when a turn ends: AfterAgent returns `decision: "deny"` with a `reason`, sent as a
|
|
104
|
+
new prompt.
|
|
105
|
+
- **Sources:** `packages/core/src/services/chatRecordingService.ts`,
|
|
106
|
+
`packages/core/src/services/chatCompressionService.ts`, `packages/core/src/hooks/types.ts`.
|
|
107
|
+
|
|
108
|
+
## Copilot CLI
|
|
109
|
+
|
|
110
|
+
Not read yet. Checked against the github/copilot-sdk event types and GitHub's docs, 2026-11-08.
|
|
111
|
+
|
|
112
|
+
- **Thoughts:** `~/.copilot/session-state/<id>/events.jsonl`, one `{ type, data, id, parentId,
|
|
113
|
+
timestamp }` per line. `assistant.message` has `data.reasoningText` beside an encrypted copy
|
|
114
|
+
(`reasoningOpaque`, or `encryptedContent` for OpenAI models). The SDK also defines
|
|
115
|
+
`assistant.reasoning` events. Whether the text is full reasoning or a summary isn't documented.
|
|
116
|
+
- **Replies:** `assistant.message` `data.content`, empty on steps that only call tools.
|
|
117
|
+
- **Kept free for compacting:** background compaction starts at about 80% of the window and the
|
|
118
|
+
CLI waits for it at about 95%, so about 20% is kept free.
|
|
119
|
+
- **Hooks:** set in `~/.copilot/hooks/*.json`, `.github/hooks/*.json`, or `hooks` in settings,
|
|
120
|
+
as a command, `http` (HTTPS only; localhost needs `COPILOT_HOOK_ALLOW_LOCALHOST=1`), or a
|
|
121
|
+
prompt at session start. A hook that times out (30 seconds) lets things go on.
|
|
122
|
+
- Answering a permission request: `permissionRequest` returns `behavior`, `allow` or `deny`.
|
|
123
|
+
- A message when a turn ends: `agentStop` returns `decision: "block"` with a `reason`, which
|
|
124
|
+
becomes the next prompt, at most 8 times in a row.
|
|
125
|
+
- **Sources:** <https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference>,
|
|
126
|
+
`nodejs/src/types.ts` and `session-events.ts` in github/copilot-sdk.
|
|
127
|
+
|
|
128
|
+
## Cursor
|
|
129
|
+
|
|
130
|
+
Checked against Cursor's docs and staff answers on its forum, 2026-11-08. Not checked on an
|
|
131
|
+
install.
|
|
132
|
+
|
|
133
|
+
- **Thoughts:** not saved apart from the reply. Agent transcripts write the thinking and the reply
|
|
134
|
+
into one `text` block on purpose, so they can't be told apart. The `afterAgentThought` hook
|
|
135
|
+
gets the thinking, in the IDE only.
|
|
136
|
+
- **Replies:** `{ role: "assistant", message: { content: [...] } }` with `text` and `tool_use`
|
|
137
|
+
blocks. Replies and tool calls carry no time and no tool results; only prompts have a
|
|
138
|
+
`<timestamp>` tag. Newer transcripts don't write `turn_ended`.
|
|
139
|
+
- **Kept free for compacting:** not documented. The `preCompact` hook is told the context's use,
|
|
140
|
+
size, and window when it fires.
|
|
141
|
+
- **Hooks:** `hooks.json` in `~/.cursor/`, a project's `.cursor/`, or enterprise settings,
|
|
142
|
+
reloaded when saved. Each hook is a command, with JSON on stdin and stdout, and every payload
|
|
143
|
+
includes `transcript_path`.
|
|
144
|
+
- Answering a permission request: `beforeShellExecution`, `beforeMCPExecution`, and
|
|
145
|
+
`preToolUse` return `permission`, `allow`, `deny`, or `ask` (`ask` isn't enforced in
|
|
146
|
+
`preToolUse`).
|
|
147
|
+
- A message when a turn ends: `stop` and `subagentStop` return `followup_message`, which Cursor
|
|
148
|
+
sends on its own, up to `loop_limit` (5) times.
|
|
149
|
+
- Others: `sessionStart`, `sessionEnd`, `postToolUse`, `afterAgentResponse`, `preCompact`.
|
|
150
|
+
- **Sources:** <https://cursor.com/docs/agent/hooks>, Cursor forum threads 161788 (2026-05-28)
|
|
151
|
+
and 166592 (2026-07-26).
|
|
152
|
+
|
|
153
|
+
## Antigravity CLI
|
|
154
|
+
|
|
155
|
+
Checked against Antigravity's docs and open-source tools that parse its files, 2026-11-08. Not
|
|
156
|
+
checked on an install, and its own format isn't documented.
|
|
157
|
+
|
|
158
|
+
- **Thoughts:** `PLANNER_RESPONSE` steps carry an optional `thinking` string, timed by
|
|
159
|
+
`created_at`. Whether it's the full thinking or a summary isn't known.
|
|
160
|
+
- **Replies:** the same steps' `content`. `transcript.jsonl` cuts long fields and lists them in
|
|
161
|
+
`truncated_fields`; `transcript_full.jsonl` keeps them whole.
|
|
162
|
+
- **Kept free for compacting:** not documented. `CHECKPOINT` steps seem to mark compactions.
|
|
163
|
+
- **Hooks:** `.agents/hooks.json` in a project, `~/.gemini/config/hooks.json`, or a plugin's
|
|
164
|
+
`hooks.json`. Commands only, with camelCase JSON on stdin and stdout and a 30-second timeout.
|
|
165
|
+
- Answering a permission request: `PreToolUse` returns `decision`: `allow`, `deny`, `ask`, or
|
|
166
|
+
a stronger form such as `auto_approve`.
|
|
167
|
+
- A message when a turn ends: `Stop` returns `decision: "continue"` with a `reason`, added as a
|
|
168
|
+
system message.
|
|
169
|
+
- Others: `SessionStart`, `PostToolUse`, `PreInvocation`, `PostInvocation`. Payloads include
|
|
170
|
+
`transcriptPath`.
|
|
171
|
+
- **Sources:** <https://antigravity.google/docs/hooks?tab=cli>, Heeler issue 644 and
|
|
172
|
+
cli-sessionr pull request 17 for the step fields.
|
|
173
|
+
|
|
174
|
+
## Hermes
|
|
175
|
+
|
|
176
|
+
Checked against NousResearch/hermes-agent at commit `0b82ddc`, 2026-11-08.
|
|
177
|
+
|
|
178
|
+
- **Thoughts:** the `messages` table's `reasoning` column, the raw reasoning text for providers
|
|
179
|
+
that return it, or `reasoning_content`. A reply that is only reasoning has empty `content`.
|
|
180
|
+
`reasoning_details` and `codex_reasoning_items` are JSON that holds the providers' encrypted
|
|
181
|
+
state. Timed by the row's `timestamp`, in epoch seconds.
|
|
182
|
+
- **Replies:** an assistant row's `content`, with `finish_reason` and `timestamp`. A reply through
|
|
183
|
+
the Responses API can sit in `codex_message_items` instead.
|
|
184
|
+
- **Kept free for compacting:** it compacts at 50% of the window, raised to 75% for windows
|
|
185
|
+
under 512K, so most models keep 25% free (`context_compressor.py`).
|
|
186
|
+
- **Hooks:** shell hooks in a `hooks:` block in `config.yaml`, in Claude Code's JSON format,
|
|
187
|
+
registered when the CLI or gateway starts and allowed once on first use.
|
|
188
|
+
- Answering a permission request: not through hooks. `pre_approval_request` and
|
|
189
|
+
`post_approval_response` only observe. Approvals can be answered through
|
|
190
|
+
`POST /v1/runs/{id}/approval` on its API server, ACP, or a messaging gateway's `/approve`.
|
|
191
|
+
- A message when a turn ends: `pre_verify` can return `{ "action": "continue", "message" }`,
|
|
192
|
+
but only on turns that edited code, at most 3 times. `POST /api/sessions/{id}/chat` runs a
|
|
193
|
+
new turn.
|
|
194
|
+
- Others: `on_session_start`, `post_llm_call`, `post_compaction`, `subagent_stop`,
|
|
195
|
+
`on_stream_delta`.
|
|
196
|
+
- **Sources:** `hermes_state_common.py`, `context_compressor.py`, and the `hooks.md`,
|
|
197
|
+
`session-storage.md`, and `api-server.md` docs in NousResearch/hermes-agent.
|
|
198
|
+
|
|
199
|
+
## OpenCode
|
|
200
|
+
|
|
201
|
+
Checked against anomalyco/opencode (formerly sst/opencode) at tag `v1.19.5` for 1.x and `dev`
|
|
202
|
+
commit `ff4a652` for 2.x, 2026-11-08.
|
|
203
|
+
|
|
204
|
+
- **Thoughts:**
|
|
205
|
+
- 1.x: a `part` row with `{ type: "reasoning", text, metadata, time: { start, end } }`. The
|
|
206
|
+
text is written once the reasoning ends, so it's empty while the model is thinking.
|
|
207
|
+
`metadata` holds the provider's signature or encrypted copy.
|
|
208
|
+
- 2.x: a `{ type: "reasoning", text, native, time }` part, filled in when the reasoning ends.
|
|
209
|
+
`native` holds the provider's data for replaying it.
|
|
210
|
+
- **Replies:**
|
|
211
|
+
- 1.x: `part` rows with `{ type: "text", text, synthetic, ignored, time }`, written empty when
|
|
212
|
+
the text starts and filled when it ends. `synthetic` and `ignored` parts weren't written by
|
|
213
|
+
the model.
|
|
214
|
+
- 2.x: `{ type: "text", text, native }` parts with no time of their own; the assistant
|
|
215
|
+
message's time stands for them. Parts get their own `session_part` rows only when
|
|
216
|
+
`experimental.part_rows` is on, which it isn't by default; otherwise they're in the
|
|
217
|
+
message's `data.content`.
|
|
218
|
+
- **Kept free for compacting:**
|
|
219
|
+
- 1.x: `compaction.reserved` tokens, by default the smaller of 20,000 and the model's largest
|
|
220
|
+
output (`session/overflow.ts`).
|
|
221
|
+
- 2.x: 15% of the window, or 16,000 tokens if that's more. `compaction.buffer` changes it, as
|
|
222
|
+
tokens or a share such as `"20%"` (`compaction.ts`).
|
|
223
|
+
- **Hooks:** plugins, in JavaScript or TypeScript, and the HTTP server the TUI starts.
|
|
224
|
+
- 1.x plugins: `event` gets every event, `tool.execute.before` and `tool.execute.after` wrap
|
|
225
|
+
calls, and `permission.ask` is declared but no call to it was found.
|
|
226
|
+
- 2.x plugins: `ctx.permission.hook("evaluate")` can allow, ask, or deny;
|
|
227
|
+
`ctx.session.prompt` sends a message to steer or queue; config folders reload on their own.
|
|
228
|
+
- Server: `GET /event` streams events; `POST /session/:id/permissions/:permissionID` answers a
|
|
229
|
+
permission request (1.x), or `POST /api/session/:sessionID/permission/:requestID/reply`
|
|
230
|
+
(2.x); `POST /session/:id/prompt_async` sends a prompt.
|
|
231
|
+
- **Sources:** `packages/schema/src/v1/session.ts`, `session-message.ts`, `processor.ts`,
|
|
232
|
+
`compaction.ts`, `packages/plugin/src/index.ts`, <https://opencode.ai/docs/server>.
|
|
233
|
+
|
|
234
|
+
## Kilo Code
|
|
235
|
+
|
|
236
|
+
Checked against Kilo-Org/kilocode at commit `b6a49d0` (CLI 7.8.28), 2026-11-08.
|
|
237
|
+
|
|
238
|
+
- **Thoughts and replies:** the same `part` rows as OpenCode 1.x. Kilo has no 2.x tables.
|
|
239
|
+
- **Kept free for compacting:** OpenCode 1.x's rule, and `compaction.threshold_percent` can set
|
|
240
|
+
a lower share of the window (`kilocode/session/overflow.ts`).
|
|
241
|
+
- **Hooks:** plugins in `~/.config/kilo/plugin/` and a project's `.kilo/plugin/`, loaded at
|
|
242
|
+
startup, with events such as `session.idle`, `permission.asked`, and `message.part.updated`.
|
|
243
|
+
Its server answers permission requests at `POST /permission/:requestID/reply` and questions at
|
|
244
|
+
`POST /question/:requestID/reply`.
|
|
245
|
+
- **Sources:** `packages/schema/src/v1/session.ts`, `kilocode/session/overflow.ts`, and the
|
|
246
|
+
plugins page in `kilo-docs`.
|