@phenx-inc/ctlsurf 0.17.2 → 0.17.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 +25 -2
- package/out/headless/index.mjs +1033 -128
- package/out/headless/index.mjs.map +4 -4
- package/out/main/index.js +929 -17
- package/out/preload/index.js +15 -0
- package/out/renderer/assets/{cssMode-BtQxI2Eb.js → cssMode-BJfoToLX.js} +3 -3
- package/out/renderer/assets/{freemarker2-DRW5dmY5.js → freemarker2-T0eMtP1Z.js} +1 -1
- package/out/renderer/assets/{handlebars-CFLF3cP7.js → handlebars-Dbbib8fi.js} +1 -1
- package/out/renderer/assets/{html-DtnBOkwp.js → html-BRHT_oxc.js} +1 -1
- package/out/renderer/assets/{htmlMode-DMVysDCg.js → htmlMode-BRlZTxME.js} +3 -3
- package/out/renderer/assets/{index-CEjwKjtd.css → index-CBWAeUYW.css} +91 -0
- package/out/renderer/assets/{index-Dm70hk5s.js → index-RCAeuJTr.js} +476 -411
- package/out/renderer/assets/{javascript-ChlyA1sf.js → javascript-BbOZOxq9.js} +2 -2
- package/out/renderer/assets/{jsonMode-D1_oHzeN.js → jsonMode-CUvAeYcT.js} +3 -3
- package/out/renderer/assets/{liquid-D4ftdy1G.js → liquid-DRIACo_s.js} +1 -1
- package/out/renderer/assets/{lspLanguageFeatures-Ckqu5ubG.js → lspLanguageFeatures-Ds-3nH9p.js} +1 -1
- package/out/renderer/assets/{mdx-B9wsVYSC.js → mdx-D76jlpLO.js} +1 -1
- package/out/renderer/assets/{python-e23tD6IS.js → python-CMuRPKJi.js} +1 -1
- package/out/renderer/assets/{razor-CrDyXj3d.js → razor-DRoH5mal.js} +1 -1
- package/out/renderer/assets/{tsMode-BwGC5o4O.js → tsMode-dPJzgFD7.js} +1 -1
- package/out/renderer/assets/{typescript-Do-98d_O.js → typescript-D5USKnLj.js} +1 -1
- package/out/renderer/assets/{xml-Dpdu5R3m.js → xml-qYg1wt2P.js} +1 -1
- package/out/renderer/assets/{yaml-B0Zt7LCJ.js → yaml-Uc-CvnvL.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/package.json +4 -2
- package/scripts/check-work-map-gpu.cjs +62 -0
- package/scripts/check-work-map-gpu.d.cts +13 -0
- package/scripts/postinstall.cjs +15 -0
- package/scripts/prepare-work-map.py +37 -0
- package/scripts/test-work-map-capability.cjs +45 -0
- package/scripts/test-work-map-ui.cjs +130 -0
- package/scripts/test-work-map.cjs +352 -0
- package/scripts/work-map-clef.py +81 -0
- package/src/main/index.ts +35 -1
- package/src/main/observer/observer.ts +2 -0
- package/src/main/orchestrator.ts +39 -4
- package/src/main/transcripts.ts +35 -16
- package/src/main/workMap/clef.ts +156 -0
- package/src/main/workMap/normalize.ts +172 -0
- package/src/main/workMap/source.ts +28 -0
- package/src/main/workMap/tree.ts +199 -0
- package/src/main/workMap/workMap.ts +244 -0
- package/src/preload/index.ts +17 -0
- package/src/renderer/App.tsx +15 -16
- package/src/renderer/components/ObserverPanel.tsx +3 -0
- package/src/renderer/components/SettingsDialog.tsx +0 -104
- package/src/renderer/components/WorkMapPanel.tsx +235 -0
- package/src/renderer/components/WorkMapToggle.tsx +50 -0
- package/src/renderer/lib/workMapHighlights.ts +41 -0
- package/src/renderer/styles.css +91 -0
- package/src/shared/workMap.d.ts +70 -0
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
// Exercise native transcript ingestion, evidence linkage, bounds, and the model process lifecycle.
|
|
2
|
+
const assert = require('node:assert/strict')
|
|
3
|
+
const fs = require('node:fs/promises')
|
|
4
|
+
const os = require('node:os')
|
|
5
|
+
const path = require('node:path')
|
|
6
|
+
const { build } = require('esbuild')
|
|
7
|
+
const { once } = require('node:events')
|
|
8
|
+
const { createReadStream } = require('node:fs')
|
|
9
|
+
const { createInterface } = require('node:readline')
|
|
10
|
+
const pause = ms => new Promise(resolve => setTimeout(resolve, ms))
|
|
11
|
+
|
|
12
|
+
async function until(check, timeout = 7000) {
|
|
13
|
+
const end = Date.now() + timeout
|
|
14
|
+
while (Date.now() < end) { if (check()) return; await pause(40) }
|
|
15
|
+
assert.fail('Timed out waiting for work map state')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'ctlsurf-map-'))
|
|
20
|
+
const repo = path.resolve(__dirname, '..')
|
|
21
|
+
let map
|
|
22
|
+
let worker
|
|
23
|
+
let tailer
|
|
24
|
+
try {
|
|
25
|
+
await build({ stdin: { contents: `export { WorkMapTree, MAP_NODE_LIMIT } from './src/main/workMap/tree';
|
|
26
|
+
export { normalizeMapEvents, mapThinkingStatus } from './src/main/workMap/normalize';
|
|
27
|
+
export { ClefWorker, validateDecisions } from './src/main/workMap/clef';
|
|
28
|
+
export { WorkMap } from './src/main/workMap/workMap';
|
|
29
|
+
export { TranscriptTailer } from './src/main/transcripts';
|
|
30
|
+
export { readMapSource } from './src/main/workMap/source';
|
|
31
|
+
export { thinkingHighlights } from './src/renderer/lib/workMapHighlights';`, resolveDir: repo },
|
|
32
|
+
bundle: true, platform: 'node', format: 'cjs', outfile: path.join(root, 'map.cjs') })
|
|
33
|
+
const { WorkMapTree, MAP_NODE_LIMIT, normalizeMapEvents, mapThinkingStatus, ClefWorker, validateDecisions, WorkMap, TranscriptTailer, readMapSource, thinkingHighlights } = require(path.join(root, 'map.cjs'))
|
|
34
|
+
const unavailableMap = new WorkMap(root, () => {})
|
|
35
|
+
unavailableMap.configure({ enabled: true, engine: 'clef-flash' })
|
|
36
|
+
unavailableMap.attach('headless-tab', 'codex', 'Codex', '/headless')
|
|
37
|
+
assert.equal(unavailableMap.snapshot('headless-tab').settings.enabled, false)
|
|
38
|
+
assert.equal(unavailableMap.snapshot('headless-tab').capability.state, 'unavailable')
|
|
39
|
+
assert.equal(unavailableMap.tabs.get('headless-tab').tailer, null, 'saved settings cannot activate collection in terminal mode')
|
|
40
|
+
assert.equal(unavailableMap.clef.child, null)
|
|
41
|
+
unavailableMap.shutdown()
|
|
42
|
+
const timestamp = new Date().toISOString()
|
|
43
|
+
const normalize = raw => normalizeMapEvents({ timestamp, ...raw }, 'session.jsonl', 'claude')
|
|
44
|
+
const tree = new WorkMapTree()
|
|
45
|
+
for (const e of normalize({ type: 'user', uuid: 'u', message: { content: 'Fix terminal scrolling' } })) tree.ingest(e)
|
|
46
|
+
for (const e of normalize({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'test-1', name: 'Bash', input: { command: 'npm test' } }] } })) tree.ingest(e)
|
|
47
|
+
for (const e of normalize({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'test-1', is_error: true, content: 'All tests passed (misleading output)' }] } })) tree.ingest(e)
|
|
48
|
+
const step = tree.nodes.find(n => n.kind === 'step')
|
|
49
|
+
assert.equal(step.status, 'failed')
|
|
50
|
+
assert.equal(step.evidence.length, 2)
|
|
51
|
+
const job = tree.request([step.id])
|
|
52
|
+
const previousBranch = step.parentId
|
|
53
|
+
const answers = Object.fromEntries(Object.entries(job.request.questions).map(([key, q]) => {
|
|
54
|
+
const choice = key.startsWith('branch') ? 'new_topic' : 'editing'
|
|
55
|
+
return [key, { choice, probabilities: Object.fromEntries(Object.keys(q.criteria).map(k => [k, +(k === choice)])) }]
|
|
56
|
+
}))
|
|
57
|
+
validateDecisions({ answers }, job.request)
|
|
58
|
+
tree.apply(job, answers)
|
|
59
|
+
assert.equal(step.status, 'failed', 'model suggestions cannot overwrite runtime failures')
|
|
60
|
+
assert.equal(step.placement, 'clef-flash')
|
|
61
|
+
assert.notEqual(step.parentId, previousBranch, 'a new-topic decision must create an actual fork')
|
|
62
|
+
assert.throws(() => validateDecisions({ answers: { ...answers, invented: {} } }, job.request))
|
|
63
|
+
assert.throws(() => validateDecisions({ answers: { ...answers, branch_0: { choice: 'invented', probabilities: {} } } }, job.request))
|
|
64
|
+
assert.deepEqual(normalizeMapEvents({ type: 'response_item', payload: { type: 'reasoning', encrypted_content: 'private' } }, 'x', 'codex'), [])
|
|
65
|
+
assert.deepEqual(normalizeMapEvents({ type: 'response_item', payload: { type: 'message', role: 'developer', content: [{ type: 'input_text', text: 'injected' }] } }, 'x', 'codex'), [])
|
|
66
|
+
const modern = normalizeMapEvents({ timestamp, type: 'event_msg', payload: { type: 'item_completed', item: {
|
|
67
|
+
type: 'CommandExecution', id: 'command-1', command: ['npm', 'test'], status: 'completed', exit_code: 0, aggregated_output: 'The word FAILED is just fixture text',
|
|
68
|
+
} } }, 'codex.jsonl', 'codex')
|
|
69
|
+
assert.equal(modern.length, 2)
|
|
70
|
+
assert.equal(modern[1].status, 'succeeded')
|
|
71
|
+
const unknown = normalizeMapEvents({ timestamp, type: 'response_item', payload: { type: 'function_call_output', call_id: 'x', output: 'Looks successful' } }, 'codex.jsonl', 'codex')
|
|
72
|
+
assert.equal(unknown[0].status, 'unknown')
|
|
73
|
+
const cli = normalizeMapEvents({ timestamp, type: 'response_item', payload: { type: 'function_call_output', call_id: 'x', output: 'Process exited with code 2' } }, 'codex.jsonl', 'codex')
|
|
74
|
+
assert.equal(cli[0].status, 'failed')
|
|
75
|
+
|
|
76
|
+
const thinkingText = 'The likely cause is a stale viewport flag. I will preserve the reader position instead of forcing the viewport down. Next I will check the scroll regression.'
|
|
77
|
+
const claudeThought = { timestamp, type: 'assistant', uuid: 'thought-message', message: { content: [{ type: 'thinking', thinking: thinkingText, signature: 'SECRET_SIGNATURE' }] } }
|
|
78
|
+
const codexThought = { timestamp, type: 'response_item', payload: { type: 'reasoning', id: 'thought-1', summary: [{ type: 'summary_text', text: thinkingText }], encrypted_content: 'SECRET_ENCRYPTED' } }
|
|
79
|
+
const completedThought = { timestamp, type: 'event_msg', payload: { type: 'item_completed', item: { type: 'Reasoning', id: 'thought-1', summary_text: [thinkingText], raw_content: ['SECRET_RAW'] } } }
|
|
80
|
+
const jsonRpcThought = { timestamp, method: 'item/completed', params: { item: { type: 'reasoning', id: 'thought-1', summary: [thinkingText], content: ['SECRET_RAW'] } } }
|
|
81
|
+
const legacyThought = { timestamp, type: 'event_msg', payload: { type: 'agent_reasoning', text: thinkingText } }
|
|
82
|
+
const intentTrees = []
|
|
83
|
+
for (const [agent, record] of [['claude', claudeThought], ['codex', codexThought], ['codex', completedThought], ['codex', jsonRpcThought], ['codex', legacyThought]]) {
|
|
84
|
+
const events = normalizeMapEvents(record, 'thinking.jsonl', agent)
|
|
85
|
+
assert.equal(events.length, 1)
|
|
86
|
+
assert.equal(events[0].kind, 'thinking')
|
|
87
|
+
assert.equal(events[0].text, thinkingText)
|
|
88
|
+
assert(!JSON.stringify(events).includes('SECRET_'), 'signatures, raw content, and encrypted reasoning never reach the map')
|
|
89
|
+
assert.equal(mapThinkingStatus(record, agent), 'available')
|
|
90
|
+
const intentTree = new WorkMapTree()
|
|
91
|
+
const thoughtId = intentTree.ingest(events[0])
|
|
92
|
+
const thoughtJob = intentTree.request([thoughtId])
|
|
93
|
+
const statement = thoughtJob.statements[thoughtId].findIndex(s => s.includes('preserve'))
|
|
94
|
+
assert(statement >= 0)
|
|
95
|
+
const selections = Object.fromEntries(Object.entries(thoughtJob.request.questions).map(([key, q]) => {
|
|
96
|
+
const choice = key.startsWith('statement') ? `s${statement}` : key.startsWith('intent') ? 'hypothesis' : Object.keys(q.criteria)[0]
|
|
97
|
+
return [key, { choice, probabilities: Object.fromEntries(Object.keys(q.criteria).map(k => [k, +(k === choice)])) }]
|
|
98
|
+
}))
|
|
99
|
+
validateDecisions({ answers: selections }, thoughtJob.request)
|
|
100
|
+
intentTree.apply(thoughtJob, selections)
|
|
101
|
+
const thoughtNode = intentTree.nodes.find(n => n.id === thoughtId)
|
|
102
|
+
assert.equal(thoughtNode.activity, 'thinking')
|
|
103
|
+
assert.equal(thoughtNode.intent, 'decision', 'a model label cannot contradict explicit wording in the selected excerpt')
|
|
104
|
+
assert.match(thoughtNode.title, /preserve/)
|
|
105
|
+
assert.equal(thoughtNode.focus.text, thoughtJob.statements[thoughtId][statement])
|
|
106
|
+
assert.equal(thoughtNode.status, 'unknown', 'thinking must not count as a verified outcome')
|
|
107
|
+
const actionId = intentTree.ingest({ id: 'following-action', kind: 'tool', source: 'thinking.jsonl', ts: timestamp, tool: 'Read', text: 'src/terminal.ts', status: 'running' })
|
|
108
|
+
assert.equal(intentTree.nodes.find(n => n.id === actionId).contextId, thoughtId)
|
|
109
|
+
const actionState = JSON.parse(intentTree.request([actionId]).request.state)
|
|
110
|
+
assert.equal(actionState.events[0].preceding_intent, thinkingText)
|
|
111
|
+
intentTrees.push(intentTree)
|
|
112
|
+
}
|
|
113
|
+
const duplicateTree = new WorkMapTree()
|
|
114
|
+
for (const record of [codexThought, completedThought, jsonRpcThought, legacyThought]) {
|
|
115
|
+
for (const e of normalizeMapEvents(record, 'thinking.jsonl', 'codex')) duplicateTree.ingest(e)
|
|
116
|
+
}
|
|
117
|
+
assert.equal(duplicateTree.nodes.filter(n => n.activity === 'thinking').length, 1)
|
|
118
|
+
const redacted = { type: 'assistant', message: { content: [{ type: 'redacted_thinking', data: 'SECRET_REDACTED' }] } }
|
|
119
|
+
assert.deepEqual(normalizeMapEvents(redacted, 'x', 'claude'), [])
|
|
120
|
+
assert.equal(mapThinkingStatus(redacted, 'claude'), 'unavailable')
|
|
121
|
+
const omitted = { type: 'response_item', payload: { type: 'reasoning', summary: [], encrypted_content: 'SECRET_ENCRYPTED' } }
|
|
122
|
+
assert.deepEqual(normalizeMapEvents(omitted, 'x', 'codex'), [])
|
|
123
|
+
assert.equal(mapThinkingStatus(omitted, 'codex'), 'unavailable')
|
|
124
|
+
assert.deepEqual(normalizeMapEvents({ type: 'event_msg', payload: { type: 'agent_reasoning_raw_content', text: 'SECRET_RAW' } }, 'x', 'codex'), [])
|
|
125
|
+
console.log('PASS Claude thinking, Codex summary formats, decision excerpts, action context, deduplication and omitted thinking')
|
|
126
|
+
|
|
127
|
+
const longThinking = 'Background detail. '.repeat(150) + 'I will choose curses instead of installing pygame. Because it avoids third-party dependencies. ' + 'More routine detail. '.repeat(150)
|
|
128
|
+
const fullBlock = normalizeMapEvents({ ...claudeThought, message: { content: [{ type: 'thinking', thinking: longThinking }] } }, 'middle.jsonl', 'claude')[0]
|
|
129
|
+
assert(!fullBlock.text.includes('choose curses'))
|
|
130
|
+
assert(fullBlock.segments.some(s => s.includes('choose curses')), 'important middle segments survive display truncation')
|
|
131
|
+
const snakeText = 'Building it as a single snake.py with only the standard library, so nothing gets installed into the virtual environment.'
|
|
132
|
+
const snakeUpdate = normalizeMapEvents({ timestamp, type: 'assistant', uuid: 'snake-choice', message: { content: [{ type: 'text', text: snakeText }] } }, 'snake.jsonl', 'claude')[0]
|
|
133
|
+
const snakeTree = new WorkMapTree()
|
|
134
|
+
const snakeId = snakeTree.ingest(snakeUpdate)
|
|
135
|
+
const snakeJob = snakeTree.request([snakeId])
|
|
136
|
+
const snakeStatement = snakeJob.statements[snakeId].findIndex(s => s.includes('Building'))
|
|
137
|
+
const snakeReason = snakeJob.reasons[snakeId].findIndex(s => s.startsWith('so nothing'))
|
|
138
|
+
assert(snakeStatement >= 0 && snakeReason >= 0)
|
|
139
|
+
const snakeAnswers = Object.fromEntries(Object.entries(snakeJob.request.questions).map(([key, q]) => {
|
|
140
|
+
const choice = key.startsWith('statement') ? `s${snakeStatement}` : key.startsWith('reason') ? `r${snakeReason}` : key.startsWith('intent') ? 'decision' : Object.keys(q.criteria)[0]
|
|
141
|
+
return [key, { choice, probabilities: Object.fromEntries(Object.keys(q.criteria).map(k => [k, +(k === choice)])) }]
|
|
142
|
+
}))
|
|
143
|
+
validateDecisions({ answers: snakeAnswers }, snakeJob.request)
|
|
144
|
+
snakeTree.apply(snakeJob, snakeAnswers)
|
|
145
|
+
const snakeNode = snakeTree.nodes.find(n => n.id === snakeId)
|
|
146
|
+
assert.equal(snakeNode.intent, 'decision')
|
|
147
|
+
assert.equal(snakeNode.evidence[0].kind, 'message', 'a stated update is not mislabeled as private thinking')
|
|
148
|
+
assert.match(snakeNode.focus.text, /single snake.py/)
|
|
149
|
+
assert.match(snakeNode.focus.reason, /nothing gets installed/)
|
|
150
|
+
const snakeAction = snakeTree.ingest({ id: 'write-snake', kind: 'tool', tool: 'Write', text: 'snake.py', source: 'snake.jsonl', ts: timestamp })
|
|
151
|
+
assert.equal(snakeTree.nodes.find(n => n.id === snakeAction).contextId, snakeId)
|
|
152
|
+
const sourceText = 'Before the choice.\n**I will use `curses`** for rendering,\n because it avoids dependencies.\nAfter the choice.'
|
|
153
|
+
const ranges = thinkingHighlights(sourceText, { text: 'I will use curses', reason: 'because it avoids dependencies.' })
|
|
154
|
+
assert.equal(ranges.length, 2)
|
|
155
|
+
assert.equal(sourceText.slice(ranges[0].start, ranges[0].end), '**I will use `curses`**')
|
|
156
|
+
assert.equal(sourceText.slice(ranges[1].start, ranges[1].end), 'because it avoids dependencies.')
|
|
157
|
+
assert.deepEqual(thinkingHighlights(sourceText, { text: 'An invented choice' }), [])
|
|
158
|
+
const sourceFile = path.join(root, 'full-thinking.jsonl')
|
|
159
|
+
const sourceRecord = { ...claudeThought, message: { content: [{ type: 'thinking', thinking: longThinking, signature: 'SECRET_SIGNATURE' }] } }
|
|
160
|
+
await fs.writeFile(sourceFile, JSON.stringify(sourceRecord) + '\n')
|
|
161
|
+
const sourceEvent = normalizeMapEvents(sourceRecord, sourceFile, 'claude')[0]
|
|
162
|
+
assert.equal(sourceEvent.fullText, undefined, 'full thinking stays out of live snapshots')
|
|
163
|
+
const fullSource = await readMapSource(sourceFile, 'claude', sourceEvent.id)
|
|
164
|
+
assert.equal(fullSource.text, longThinking.trim())
|
|
165
|
+
assert(!fullSource.text.includes('SECRET_SIGNATURE'))
|
|
166
|
+
assert.equal(await readMapSource(sourceFile, 'claude', 'missing-event'), null)
|
|
167
|
+
console.log('PASS full-block segment selection and snake-game choice with its stated reason')
|
|
168
|
+
|
|
169
|
+
for (const agent of ['claude', 'codex']) {
|
|
170
|
+
const index = process.argv.indexOf(`--replay-${agent}`)
|
|
171
|
+
if (index < 0) continue
|
|
172
|
+
const file = process.argv[index + 1]
|
|
173
|
+
assert(file && !file.startsWith('--'), 'Replay requires a transcript file path')
|
|
174
|
+
const replayTree = new WorkMapTree()
|
|
175
|
+
let records = 0, readableThoughts = 0, omittedThoughts = 0, decisions = 0
|
|
176
|
+
for await (const line of createInterface({ input: createReadStream(file), crlfDelay: Infinity })) {
|
|
177
|
+
let raw
|
|
178
|
+
try { raw = JSON.parse(line) } catch { continue }
|
|
179
|
+
records++
|
|
180
|
+
if (mapThinkingStatus(raw, agent) === 'unavailable') omittedThoughts++
|
|
181
|
+
for (const e of normalizeMapEvents(raw, file, agent)) {
|
|
182
|
+
const id = replayTree.ingest(e)
|
|
183
|
+
if (id && e.kind === 'thinking') readableThoughts++
|
|
184
|
+
if (id && replayTree.nodes.find(n => n.id === id)?.intent !== undefined && replayTree.nodes.find(n => n.id === id)?.intent !== 'context') decisions++
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
console.log('LOCAL REPLAY', JSON.stringify({ agent, records, readableThoughts, omittedThoughts, statedChoicesOrHypotheses: decisions }))
|
|
188
|
+
}
|
|
189
|
+
for (let i = 0; i < 1000; i++) {
|
|
190
|
+
tree.ingest({ id: `prompt-${i}`, kind: 'prompt', ts: timestamp, text: `Goal ${i}`, source: 'x' })
|
|
191
|
+
tree.ingest({ id: `tool-${i}`, kind: 'tool', ts: timestamp, text: `src/work-${i}.ts`, tool: 'Read', toolId: `tool-${i}`, source: 'x', status: 'running' })
|
|
192
|
+
}
|
|
193
|
+
assert(tree.nodes.length <= MAP_NODE_LIMIT)
|
|
194
|
+
const ids = new Set(tree.nodes.map(n => n.id))
|
|
195
|
+
assert(tree.nodes.every(n => !n.parentId || ids.has(n.parentId)), 'pruning leaves no orphan branches')
|
|
196
|
+
assert(tree.dropped > 0)
|
|
197
|
+
tree.end()
|
|
198
|
+
assert(!tree.nodes.some(n => n.status === 'running'))
|
|
199
|
+
console.log('PASS event normalization, linked outcomes, validated decisions, bounded trees')
|
|
200
|
+
|
|
201
|
+
const sessions = path.join(root, 'sessions')
|
|
202
|
+
const d = new Date()
|
|
203
|
+
const dateDir = path.join(sessions, String(d.getFullYear()), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0'))
|
|
204
|
+
await fs.mkdir(dateDir, { recursive: true })
|
|
205
|
+
const lines = events => events.map(e => JSON.stringify({ timestamp: new Date().toISOString(), ...e })).join('\n') + '\n'
|
|
206
|
+
const native = path.join(dateDir, 'rollout-main.jsonl')
|
|
207
|
+
const nativeEvents = [
|
|
208
|
+
{ type: 'session_meta', payload: { cwd: '/project', thread_source: 'user', timestamp } },
|
|
209
|
+
{ type: 'response_item', payload: { type: 'message', role: 'user', content: [{ type: 'input_text', text: 'Fix terminal scrolling' }] } },
|
|
210
|
+
{ ...codexThought, timestamp: undefined },
|
|
211
|
+
{ ...completedThought, timestamp: undefined },
|
|
212
|
+
{ type: 'response_item', payload: { type: 'function_call', call_id: 'read-1', name: 'read_file', arguments: JSON.stringify({ file_path: 'src/terminal.ts' }) } },
|
|
213
|
+
{ type: 'response_item', payload: { type: 'function_call_output', call_id: 'read-1', output: 'Process exited with code 0' } },
|
|
214
|
+
]
|
|
215
|
+
await fs.writeFile(native, lines(nativeEvents))
|
|
216
|
+
await fs.writeFile(path.join(dateDir, 'rollout-foreign.jsonl'), lines([
|
|
217
|
+
{ type: 'session_meta', payload: { cwd: '/other' } },
|
|
218
|
+
{ type: 'response_item', payload: { type: 'function_call', call_id: 'foreign', name: 'read_file', arguments: '{}' } },
|
|
219
|
+
]))
|
|
220
|
+
const raw = []
|
|
221
|
+
tailer = new TranscriptTailer({ agentId: 'codex', cwd: '/project', codexSessionsDir: sessions, sink() {}, rawSink: e => raw.push(e) })
|
|
222
|
+
tailer.start()
|
|
223
|
+
await until(() => raw.length >= nativeEvents.length)
|
|
224
|
+
assert(!raw.some(e => e.payload?.call_id === 'foreign' || e.payload?.cwd === '/other'))
|
|
225
|
+
// A very long line without a newline cannot grow the tailer's buffer indefinitely.
|
|
226
|
+
await fs.appendFile(native, 'x'.repeat(2 * 1024 * 1024) + '\n' + lines([{ type: 'event_msg', payload: { type: 'agent_message', message: 'After the oversized line' } }]))
|
|
227
|
+
await until(() => raw.some(e => e.payload?.message === 'After the oversized line'), 9000)
|
|
228
|
+
assert([...tailer.files.values()].every(f => f.remainder.length <= 1024 * 1024))
|
|
229
|
+
tailer.stop(); tailer = null
|
|
230
|
+
console.log('PASS project isolation and bounded transcript reading')
|
|
231
|
+
|
|
232
|
+
if (process.platform !== 'darwin' || process.arch !== 'arm64') {
|
|
233
|
+
console.log('SKIP local MLX process checks: Apple Silicon required')
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// A tiny protocol fixture exercises cancellation and process cleanup without loading weights.
|
|
238
|
+
await fs.mkdir(path.join(root, 'scripts'))
|
|
239
|
+
const fake = path.join(root, 'scripts/work-map-clef.py')
|
|
240
|
+
await fs.writeFile(fake, `import json,sys,time\nprint(json.dumps({'ready':True}),flush=True)\nfor line in sys.stdin:\n r=json.loads(line); answers={}\n for k,q in r['request']['questions'].items():\n choice=next(iter(q['criteria'])); answers[k]={'choice':choice,'probabilities':{o:int(o==choice) for o in q['criteria']}}\n time.sleep(.1)\n print(json.dumps({'id':r['id'],'answers':answers}),flush=True)\n`)
|
|
241
|
+
await fs.mkdir(path.join(root, 'model'))
|
|
242
|
+
await fs.writeFile(path.join(root, 'model/clef_mlx.py'), '')
|
|
243
|
+
const settings = { enabled: true, engine: 'clef-flash', pythonPath: '/usr/bin/python3', modelPath: path.join(root, 'model') }
|
|
244
|
+
worker = new ClefWorker(fake, () => {})
|
|
245
|
+
const req = { model: 'clef-flash', state: 'Example', questions: { topic: { type: 'choice', instructions: 'Pick', criteria: { a: 'A', b: 'B' } } } }
|
|
246
|
+
await worker.predict(settings, req)
|
|
247
|
+
const child = worker.child
|
|
248
|
+
const closed = once(child, 'close')
|
|
249
|
+
worker.stop()
|
|
250
|
+
await closed
|
|
251
|
+
assert.equal(worker.state, 'idle')
|
|
252
|
+
assert.equal(worker.child, null)
|
|
253
|
+
await assert.rejects(worker.predict({ ...settings, pythonPath: '/missing/python' }, req))
|
|
254
|
+
assert.equal(worker.state, 'error')
|
|
255
|
+
assert.match(worker.error, /runtime not found/)
|
|
256
|
+
worker.stop()
|
|
257
|
+
const cancelled = worker.predict(settings, req)
|
|
258
|
+
worker.stop()
|
|
259
|
+
await assert.rejects(cancelled)
|
|
260
|
+
assert.equal(worker.state, 'idle', 'off must not become an error when an old request settles')
|
|
261
|
+
const inFlight = worker.predict(settings, req)
|
|
262
|
+
await until(() => worker.state === 'thinking')
|
|
263
|
+
const runningChild = worker.child
|
|
264
|
+
const runningClosed = once(runningChild, 'close')
|
|
265
|
+
worker.stop()
|
|
266
|
+
await assert.rejects(inFlight)
|
|
267
|
+
await runningClosed
|
|
268
|
+
assert.equal(worker.state, 'idle', 'in-flight cancellation must release the process without a stale error')
|
|
269
|
+
worker.stop(); worker = null
|
|
270
|
+
|
|
271
|
+
await fs.writeFile(native, lines(nativeEvents))
|
|
272
|
+
await fs.writeFile(path.join(dateDir, 'rollout-foreign.jsonl'), lines([
|
|
273
|
+
{ type: 'session_meta', payload: { cwd: '/other' } },
|
|
274
|
+
{ type: 'response_item', payload: { type: 'function_call', call_id: 'foreign', name: 'read_file', arguments: '{}' } },
|
|
275
|
+
]))
|
|
276
|
+
await fs.writeFile(path.join(dateDir, 'rollout-guardian.jsonl'), lines([
|
|
277
|
+
{ type: 'session_meta', payload: { cwd: '/project', thread_source: 'guardian' } },
|
|
278
|
+
{ type: 'response_item', payload: { type: 'function_call', call_id: 'guardian', name: 'read_file', arguments: '{}' } },
|
|
279
|
+
]))
|
|
280
|
+
await fs.writeFile(path.join(dateDir, 'rollout-omitted.jsonl'), lines([
|
|
281
|
+
{ type: 'session_meta', payload: { cwd: '/omitted-project' } }, omitted,
|
|
282
|
+
]))
|
|
283
|
+
const claudeProjectsDir = path.join(root, 'claude')
|
|
284
|
+
await fs.mkdir(path.join(claudeProjectsDir, '-claude-project'), { recursive: true })
|
|
285
|
+
await fs.writeFile(path.join(claudeProjectsDir, '-claude-project/session.jsonl'), lines([
|
|
286
|
+
{ ...claudeThought, timestamp: new Date().toISOString(), cwd: '/claude-project' },
|
|
287
|
+
]))
|
|
288
|
+
map = new WorkMap(root, () => {}, { codexSessionsDir: sessions, claudeProjectsDir }, { state: 'available', reason: null, gpuName: 'Test GPU' })
|
|
289
|
+
map.configure(settings)
|
|
290
|
+
map.attach('tab-a', 'codex', 'Codex', '/project')
|
|
291
|
+
map.attach('tab-b', 'codex', 'Codex', '/other')
|
|
292
|
+
map.attach('tab-c', 'claude', 'Claude Code', '/claude-project')
|
|
293
|
+
map.attach('tab-omitted', 'codex', 'Codex', '/omitted-project')
|
|
294
|
+
await until(() => map.snapshot('tab-a').nodes.some(n => n.placement === 'clef-flash'), 12000)
|
|
295
|
+
assert(!map.snapshot('tab-a').nodes.some(n => n.evidence.some(e => e.toolId === 'foreign')))
|
|
296
|
+
assert(!map.snapshot('tab-a').nodes.some(n => n.evidence.some(e => e.toolId === 'guardian')))
|
|
297
|
+
assert(map.snapshot('tab-b').nodes.some(n => n.evidence.some(e => e.toolId === 'foreign')))
|
|
298
|
+
assert(map.snapshot('tab-a').nodes.some(n => n.status === 'succeeded'))
|
|
299
|
+
await until(() => map.snapshot('tab-c').nodes.some(n => n.placement === 'clef-flash'), 12000)
|
|
300
|
+
assert.equal(map.snapshot('tab-a').thinkingStatus, 'available')
|
|
301
|
+
assert.equal(map.snapshot('tab-c').thinkingStatus, 'available')
|
|
302
|
+
assert.equal(map.snapshot('tab-omitted').thinkingStatus, 'unavailable')
|
|
303
|
+
assert.equal(map.snapshot('tab-omitted').nodes.length, 0)
|
|
304
|
+
const mappedThought = map.snapshot('tab-c').nodes.find(n => n.evidence[0]?.kind === 'thinking').evidence[0]
|
|
305
|
+
assert.equal((await map.getSource('tab-c', mappedThought.id)).text, thinkingText)
|
|
306
|
+
assert.equal(await map.getSource('tab-b', mappedThought.id), null, 'a different tab cannot fetch this source record')
|
|
307
|
+
map.configure({ enabled: false })
|
|
308
|
+
const count = map.snapshot('tab-a').nodes.length
|
|
309
|
+
await fs.appendFile(native, lines([{ type: 'response_item', payload: { type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'This arrives while off' }] } }]))
|
|
310
|
+
await pause(1200)
|
|
311
|
+
assert.equal(map.snapshot('tab-a').nodes.length, count)
|
|
312
|
+
assert.equal(map.snapshot('tab-a').runtime, 'off')
|
|
313
|
+
assert.equal(map.clef.child, null)
|
|
314
|
+
map.clear('tab-a')
|
|
315
|
+
assert.equal(map.snapshot('tab-a').nodes.length, 0)
|
|
316
|
+
map.attach('tab-a', 'shell', 'Shell', '/project')
|
|
317
|
+
assert.equal(map.snapshot('tab-a').supported, false)
|
|
318
|
+
map.shutdown(); map = null
|
|
319
|
+
console.log('PASS Clef protocol, cancellation, error recovery, tab isolation, off and clear')
|
|
320
|
+
if (process.argv.includes('--real-clef')) {
|
|
321
|
+
const cache = path.join(repo, 'experiments/live-map/.cache')
|
|
322
|
+
worker = new ClefWorker(path.join(repo, 'scripts/work-map-clef.py'), () => {})
|
|
323
|
+
const intentTree = intentTrees[0]
|
|
324
|
+
const intentNode = intentTree.nodes.find(n => n.activity === 'thinking')
|
|
325
|
+
const intentJob = intentTree.request([intentNode.id])
|
|
326
|
+
const answer = await worker.predict({ enabled: true, engine: 'clef-flash',
|
|
327
|
+
pythonPath: path.join(cache, 'envs/clef-flash/bin/python'), modelPath: path.join(cache, 'models/clef-flash'),
|
|
328
|
+
}, intentJob.request)
|
|
329
|
+
intentTree.apply(intentJob, answer)
|
|
330
|
+
const realSnakeJob = snakeTree.request([snakeId])
|
|
331
|
+
const realSnakeAnswer = await worker.predict({ enabled: true, engine: 'clef-flash',
|
|
332
|
+
pythonPath: path.join(cache, 'envs/clef-flash/bin/python'), modelPath: path.join(cache, 'models/clef-flash'),
|
|
333
|
+
}, realSnakeJob.request)
|
|
334
|
+
snakeTree.apply(realSnakeJob, realSnakeAnswer)
|
|
335
|
+
assert(snakeUpdate.segments.includes(snakeNode.focus.text))
|
|
336
|
+
if (snakeNode.focus.reason) assert(snakeUpdate.segments.includes(snakeNode.focus.reason))
|
|
337
|
+
const process = worker.child
|
|
338
|
+
const closed = once(process, 'close')
|
|
339
|
+
worker.stop()
|
|
340
|
+
await closed
|
|
341
|
+
worker = null
|
|
342
|
+
assert(answer.branch_0.choice)
|
|
343
|
+
assert(intentJob.statements[intentNode.id].some(s => s.startsWith(intentNode.title.replace(/…$/, ''))))
|
|
344
|
+
console.log('PASS real offline Clef-Flash choice/reason extraction and unload:', JSON.stringify({ selectedExcerpt: answer.statement_0.choice, displayedIntent: intentNode.intent, snakeIntent: snakeNode.intent, snakeHasReason: !!snakeNode.focus.reason }))
|
|
345
|
+
}
|
|
346
|
+
} finally {
|
|
347
|
+
tailer?.stop(); map?.shutdown(); worker?.stop()
|
|
348
|
+
await fs.rm(root, { recursive: true, force: true })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
main().catch(err => { console.error(err); process.exitCode = 1 })
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Offline JSONL worker for the live work map. One request at a time; no HTTP listener."""
|
|
2
|
+
import contextlib
|
|
3
|
+
import copy
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
import resource
|
|
10
|
+
import threading
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def emit(value):
|
|
14
|
+
print(json.dumps(value), flush=True)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def guard_memory():
|
|
18
|
+
# Inspect our own RSS; no ps subprocess or OS process-inspection permission needed.
|
|
19
|
+
while True:
|
|
20
|
+
peak_rss = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
|
|
21
|
+
rss_bytes = peak_rss if sys.platform == 'darwin' else peak_rss * 1024
|
|
22
|
+
if rss_bytes > 16 * 1024 ** 3:
|
|
23
|
+
emit({'error': 'Clef exceeded the 16 GiB process memory budget and was unloaded.'})
|
|
24
|
+
os._exit(1)
|
|
25
|
+
time.sleep(1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def budget_request(request, tokenizer, encode_record):
|
|
29
|
+
"""Keep the typed schema and at least 256 state tokens within the fixed context budget."""
|
|
30
|
+
for length in (320, 240, 160, 100, 64, 40):
|
|
31
|
+
bounded = copy.deepcopy(request)
|
|
32
|
+
for question in bounded['questions'].values():
|
|
33
|
+
question['criteria'] = {key: text[:length] for key, text in question['criteria'].items()}
|
|
34
|
+
try:
|
|
35
|
+
encode_record(tokenizer, bounded, max_length=1792, truncate=True)
|
|
36
|
+
return bounded
|
|
37
|
+
except ValueError as exc:
|
|
38
|
+
if 'schema requires' not in str(exc):
|
|
39
|
+
raise
|
|
40
|
+
raise ValueError('Work-map schema cannot fit in its token budget')
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def main():
|
|
44
|
+
model_path = Path(sys.argv[1]).resolve(strict=True)
|
|
45
|
+
threading.Thread(target=guard_memory, daemon=True).start()
|
|
46
|
+
# Set before importing any Hub/tokenizer code. Never download during inference.
|
|
47
|
+
os.environ.update(HF_HUB_OFFLINE="1", TRANSFORMERS_OFFLINE="1", HF_HUB_DISABLE_TELEMETRY="1",
|
|
48
|
+
TOKENIZERS_PARALLELISM="false", DO_NOT_TRACK="1",
|
|
49
|
+
HF_HOME=str(model_path.parent / '.runtime-cache'),
|
|
50
|
+
XDG_CACHE_HOME=str(model_path.parent / '.runtime-cache'))
|
|
51
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
52
|
+
import mlx.core as mx
|
|
53
|
+
mx.set_memory_limit(14 * 1024 ** 3)
|
|
54
|
+
mx.set_cache_limit(256 * 1024 ** 2)
|
|
55
|
+
sys.path.insert(0, str(model_path))
|
|
56
|
+
import clef_mlx
|
|
57
|
+
model = clef_mlx.load(model_path)
|
|
58
|
+
emit({"ready": True})
|
|
59
|
+
for line in sys.stdin:
|
|
60
|
+
req = {}
|
|
61
|
+
try:
|
|
62
|
+
if len(line) > 32_000:
|
|
63
|
+
raise ValueError("Work-map request exceeded its input budget")
|
|
64
|
+
req = json.loads(line)
|
|
65
|
+
start = time.monotonic()
|
|
66
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
67
|
+
request = budget_request(req['request'], model.tokenizer, clef_mlx.encode_record)
|
|
68
|
+
response = model.systemone(request, max_length=2048, truncate=True)
|
|
69
|
+
mx.clear_cache()
|
|
70
|
+
emit({"id": req["id"], "answers": response["answers"],
|
|
71
|
+
"latency_ms": round((time.monotonic() - start) * 1000)})
|
|
72
|
+
except Exception as exc:
|
|
73
|
+
emit({"id": req.get("id"), "error": f"{type(exc).__name__}: {exc}"[:600]})
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
try:
|
|
78
|
+
main()
|
|
79
|
+
except Exception as exc:
|
|
80
|
+
emit({"error": f"{type(exc).__name__}: {exc}"[:600]})
|
|
81
|
+
sys.exit(1)
|
package/src/main/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ function log(...args: unknown[]): void {
|
|
|
25
25
|
import { AgentConfig, getDefaultAgent, getAvailableAgents } from './agents'
|
|
26
26
|
import { getSettingsDir } from './settingsDir'
|
|
27
27
|
import { Orchestrator } from './orchestrator'
|
|
28
|
+
import type { MapSettings } from '../shared/workMap'
|
|
29
|
+
import { detectCapability, usableElectronGpu } from '../../scripts/check-work-map-gpu.cjs'
|
|
28
30
|
import { AttentionController } from './agentState'
|
|
29
31
|
import { listSshHosts, getRecentHosts, addRecentHost, probeRemote, buildRemoteAgent, type RemoteAction } from './ssh'
|
|
30
32
|
import { ResourceMonitor } from './monitor'
|
|
@@ -230,6 +232,8 @@ const orchestrator = new Orchestrator(
|
|
|
230
232
|
onAgentMessage: (text) => mainWindow?.webContents.send('agent:message', text),
|
|
231
233
|
onObserverUpdate: (item) => mainWindow?.webContents.send('observer:update', item),
|
|
232
234
|
onObserverActivity: (reviewing) => mainWindow?.webContents.send('observer:activity', reviewing),
|
|
235
|
+
onWorkMapUpdate: (snapshot) => mainWindow?.webContents.send('workmap:update', snapshot),
|
|
236
|
+
onWorkMapCapability: (capability) => mainWindow?.webContents.send('workmap:capability', capability),
|
|
233
237
|
onAgentStateChange: (tabId, state, anyAwaiting) => {
|
|
234
238
|
log(`[agent-state] tab=${tabId} state=${state} anyAwaiting=${anyAwaiting}`)
|
|
235
239
|
mainWindow?.webContents.send('agent:state', tabId, state, anyAwaiting)
|
|
@@ -242,7 +246,8 @@ const orchestrator = new Orchestrator(
|
|
|
242
246
|
// the local launch cwd for unbound/local tabs.
|
|
243
247
|
updateProjectBadge(orchestrator.getActiveTabProjectPath() || orchestrator.cwd)
|
|
244
248
|
},
|
|
245
|
-
}
|
|
249
|
+
},
|
|
250
|
+
{ appRoot: app.isPackaged ? process.resourcesPath : app.getAppPath(), desktop: true },
|
|
246
251
|
)
|
|
247
252
|
|
|
248
253
|
// ─── Window ───────────────────────────────────────
|
|
@@ -728,6 +733,33 @@ ipcMain.handle('speak:set', (_event, enabled: boolean) => {
|
|
|
728
733
|
return { enabled: orchestrator.speakRepliesEnabled }
|
|
729
734
|
})
|
|
730
735
|
|
|
736
|
+
// ─── Work map IPC ─────────────────────────────────
|
|
737
|
+
|
|
738
|
+
ipcMain.handle('workmap:get', (_event, tabId: string) => orchestrator.workMap.snapshot(String(tabId)))
|
|
739
|
+
ipcMain.handle('workmap:capability', () => orchestrator.workMap.getCapability())
|
|
740
|
+
ipcMain.handle('workmap:source', (_event, tabId: string, eventId: string) => orchestrator.workMap.getSource(String(tabId), String(eventId)))
|
|
741
|
+
ipcMain.handle('workmap:set', (_event, tabId: string, data: unknown) => {
|
|
742
|
+
if (!data || typeof data !== 'object') throw new Error('Invalid work map settings')
|
|
743
|
+
const input = data as Record<string, unknown>
|
|
744
|
+
const clean: MapSettings = {
|
|
745
|
+
...orchestrator.workMap.snapshot(String(tabId)).settings,
|
|
746
|
+
}
|
|
747
|
+
if (typeof input.enabled === 'boolean') clean.enabled = input.enabled
|
|
748
|
+
if (input.engine === 'rules' || input.engine === 'clef-flash') clean.engine = input.engine
|
|
749
|
+
for (const key of ['pythonPath', 'modelPath'] as const) {
|
|
750
|
+
if (typeof input[key] === 'string') {
|
|
751
|
+
if (input[key].length > 4096 || !path.isAbsolute(input[key])) throw new Error('Model paths must be absolute local paths')
|
|
752
|
+
clean[key] = input[key]
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
orchestrator.configureWorkMap(clean)
|
|
756
|
+
return orchestrator.workMap.snapshot(String(tabId))
|
|
757
|
+
})
|
|
758
|
+
ipcMain.handle('workmap:clear', (_event, tabId: string) => {
|
|
759
|
+
orchestrator.workMap.clear(String(tabId))
|
|
760
|
+
return orchestrator.workMap.snapshot(String(tabId))
|
|
761
|
+
})
|
|
762
|
+
|
|
731
763
|
// ─── Observer IPC ─────────────────────────────────
|
|
732
764
|
|
|
733
765
|
ipcMain.handle('observer:get', () => orchestrator.getObserverSettingsSummary())
|
|
@@ -824,6 +856,8 @@ app.whenReady().then(() => {
|
|
|
824
856
|
|
|
825
857
|
orchestrator.loadSettings()
|
|
826
858
|
createWindow()
|
|
859
|
+
void detectCapability({ desktop: true, verifyGpu: async () => usableElectronGpu(await app.getGPUInfo('complete')) })
|
|
860
|
+
.then(capability => { if (!shutdownStarted) orchestrator.setWorkMapCapability(capability) })
|
|
827
861
|
|
|
828
862
|
// Kick off an initial npm update check shortly after launch, then poll periodically.
|
|
829
863
|
setTimeout(() => { void checkForUpdate() }, 3000)
|
|
@@ -26,6 +26,8 @@ import { createModelCaller } from './anthropic'
|
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* Observer — supervision layer over the session ledger (spec v0.1, Phase 1).
|
|
29
|
+
* ON HOLD: retained engine; the app's orchestrator disables runtime and the
|
|
30
|
+
* renderer does not expose the feature. Standalone test harnesses still work.
|
|
29
31
|
*
|
|
30
32
|
* Consumes the agent's native transcript (via its own TranscriptTailer) and
|
|
31
33
|
* pty-derived activity signals, normalizes them into a local append-only
|