mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Detach retained CI and terminal tails from oversized strings (#20960)
* fix(memory): detach retained CI and terminal tails from oversized strings * fix(terminal): detach retained error and reattach string slices * fix(terminal): release oversized recent-output backing strings * fix(terminal): release backing strings held by PTY detectors * fix(memory): own bounded Claude background task labels * fix: detach retained terminal mode scan tails * fix: own retained plugin worker output strings * fix: own incomplete OSC 133 carry strings --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
# Retained Claude background-task text
|
||||
|
||||
The actual Claude task tracker retained oversized input strings through its
|
||||
512-character description/name slices. Its live tasks, settled tasks, and
|
||||
recently removed tasks can each retain those slices. The fix uses the existing
|
||||
`ownRetainedString` at the shared text boundary; normalization, UTF-16 clipping,
|
||||
task identity, publication, and lifecycle behavior stay the same.
|
||||
|
||||
This extends [ML-018 / #20960](https://github.com/stablyai/orca/pull/20960).
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/claude-task-retention/reproduce.cjs
|
||||
```
|
||||
|
||||
The script bundles the actual tracker and its retention classes. Its baseline
|
||||
removes only the new copy call in memory. It exercises flat strings, concatenated
|
||||
strings, and JSON-parsed SDK-style frames; each input has a distinct task owner.
|
||||
It measures after GC, then clears the tracker and yields before measuring cleanup.
|
||||
[Results and bundle hashes](./results.json) preserve the complete run.
|
||||
|
||||
| JSON-parsed case | Input per task | Tasks | Visible text | Heap before | Heap after |
|
||||
| ------------------------- | ---------------: | ----: | ----------------: | ----------: | ---------: |
|
||||
| Live | 64 Ki characters | 32 | 16,384 characters | 2,125,672 | 43,536 |
|
||||
| Settled | 64 Ki characters | 32 | 16,384 characters | 2,127,072 | 44,296 |
|
||||
| Removed, awaiting outcome | 64 Ki characters | 32 | 0 characters | 2,108,136 | 25,360 |
|
||||
| Live | 4 Mi characters | 8 | 4,096 characters | 33,562,624 | 11,048 |
|
||||
| Settled | 4 Mi characters | 8 | 4,096 characters | 33,563,960 | 11,656 |
|
||||
| Removed, awaiting outcome | 4 Mi characters | 8 | 0 characters | 33,558,584 | 7,008 |
|
||||
|
||||
Captured with Node v26.6.0 on macOS. Cleanup returned near the initial heap for
|
||||
every case. Six regression tests retain the actual tracker through these three
|
||||
lifetimes for both descriptions and names. Text behavior tests preserve whitespace
|
||||
normalization, fallback names, and a clipped surrogate pair.
|
||||
|
||||
## Reachability and limits
|
||||
|
||||
`claude-stream-json-connection.ts` forwards SDK messages to the structured adapter,
|
||||
whose `emit` calls `backgroundTasks.observe`. Installed SDK 0.3.251 uses Node
|
||||
`readline` to assemble stdout records, parses each record with `JSON.parse`, then
|
||||
yields it. The inspected path imposes no record or description length limit;
|
||||
native read-chunk size does not cap an assembled JSON field. Descriptions are
|
||||
declared as plain strings in `SDKTaskStartedMessage`.
|
||||
|
||||
The description slice and this SDK version also exist in `v1.4.198`; that tag
|
||||
keeps the reader inline in `claude-background-task-tracker.ts`. The separate
|
||||
settled/recently-removed retention and name-reader paths describe current code.
|
||||
|
||||
The current maps are count-bounded: at most 256 live, 256 settled, and 256 recently
|
||||
removed entries per tracker. Settled context clears when no visible work remains;
|
||||
recently removed context awaits an outcome, eviction, or explicit clearing.
|
||||
Session end/close clears the tracker. Copy work is at most 512 UTF-16 code units
|
||||
per retained field, and it does not reduce temporary parsing allocation.
|
||||
|
||||
These are synthetic oversized task fields, not evidence that an affected user
|
||||
received such fields. The path concerns structured Claude sessions, not ordinary
|
||||
terminal output or stderr. Neither #19831 nor #19768 establishes this trigger.
|
||||
|
||||
The separate digest-bounded subagent ID was also checked at actual consumers.
|
||||
The mobile response sanitizer can temporarily retain the original until JSON
|
||||
serialization flattens its concatenated ID. Worker transcript bounding already
|
||||
serializes for its byte budget and released that parent in the probe. No durable
|
||||
ID-owner leak was established, so that helper is unchanged.
|
||||
@@ -0,0 +1,197 @@
|
||||
const fs = require('node:fs')
|
||||
const { build } = require('esbuild')
|
||||
const assert = require('node:assert/strict')
|
||||
const path = require('node:path')
|
||||
const { tmpdir } = require('node:os')
|
||||
const { createHash } = require('node:crypto')
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const bundles = {}
|
||||
|
||||
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
|
||||
assert.equal(typeof global.gc, 'function')
|
||||
|
||||
async function loadTracker(fixed) {
|
||||
const result = await build({
|
||||
entryPoints: [path.join(root, 'src/main/claude/claude-background-task-tracker.ts')],
|
||||
bundle: true,
|
||||
write: false,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
target: 'node22',
|
||||
plugins: fixed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'baseline-without-task-text-copy',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /claude-background-task-frames\.ts$/ }, (args) => {
|
||||
const source = fs.readFileSync(args.path, 'utf8')
|
||||
const boundary = 'ownRetainedString(trimmed.slice(0, MAX_TASK_TEXT_LENGTH))'
|
||||
assert.ok(
|
||||
source.includes(boundary),
|
||||
'The copy boundary changed; update the baseline transform'
|
||||
)
|
||||
return {
|
||||
loader: 'ts',
|
||||
contents: source.replace(boundary, 'trimmed.slice(0, MAX_TASK_TEXT_LENGTH)')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
bundles[fixed ? 'after' : 'before'] = createHash('sha256')
|
||||
.update(result.outputFiles[0].text)
|
||||
.digest('hex')
|
||||
const scratch = fs.mkdtempSync(path.join(tmpdir(), 'orca-claude-task-proof-'))
|
||||
let moduleId
|
||||
try {
|
||||
const bundlePath = path.join(scratch, 'tracker.cjs')
|
||||
fs.writeFileSync(bundlePath, result.outputFiles[0].text)
|
||||
moduleId = require.resolve(bundlePath)
|
||||
return require(moduleId).ClaudeBackgroundTaskTracker
|
||||
} finally {
|
||||
if (moduleId) {
|
||||
delete require.cache[moduleId]
|
||||
}
|
||||
fs.rmSync(scratch, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
function collect() {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
global.gc()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
const settle = () => new Promise((resolve) => setImmediate(resolve))
|
||||
|
||||
function frame(index, size, ingress, field) {
|
||||
const value = String.fromCharCode(65 + (index % 26)).repeat(size)
|
||||
const message = {
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: `task-${index}`,
|
||||
task_type: 'local_bash',
|
||||
is_backgrounded: true,
|
||||
[field]: value
|
||||
}
|
||||
if (ingress === 'json') {
|
||||
return JSON.parse(JSON.stringify(message))
|
||||
}
|
||||
if (ingress === 'flat') {
|
||||
value.charCodeAt(value.length - 1)
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
function populate(Tracker, { count, size, ingress, retention, field }) {
|
||||
const owner = new Tracker()
|
||||
const keeper = {
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: 'keeper',
|
||||
task_type: 'local_bash',
|
||||
is_backgrounded: true
|
||||
}
|
||||
if (retention !== 'live') {
|
||||
owner.observe(keeper)
|
||||
}
|
||||
for (let index = 0; index < count; index++) {
|
||||
owner.observe(frame(index, size, ingress, field))
|
||||
if (retention === 'settled') {
|
||||
owner.observe({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_id: `task-${index}`,
|
||||
status: 'completed'
|
||||
})
|
||||
}
|
||||
}
|
||||
if (retention === 'removed') {
|
||||
owner.observe({ type: 'system', subtype: 'background_tasks_changed', tasks: [keeper] })
|
||||
}
|
||||
return owner
|
||||
}
|
||||
|
||||
function logicalChars(owner) {
|
||||
const state = owner.state
|
||||
return [...(state?.tasks ?? []), ...(state?.settledTasks ?? [])].reduce(
|
||||
(sum, task) => sum + (task.description?.length ?? 0) + (task.name?.length ?? 0),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const Before = await loadTracker(false)
|
||||
const Fixed = await loadTracker(true)
|
||||
for (const Tracker of [Before, Fixed]) {
|
||||
const warm = populate(Tracker, {
|
||||
count: 1,
|
||||
size: 1024,
|
||||
ingress: 'json',
|
||||
retention: 'live',
|
||||
field: 'description'
|
||||
})
|
||||
warm.clear()
|
||||
}
|
||||
const results = []
|
||||
for (const [count, size] of [
|
||||
[32, 64 * 1024],
|
||||
[8, 4 * 1024 * 1024]
|
||||
]) {
|
||||
for (const ingress of ['flat', 'cons', 'json']) {
|
||||
for (const retention of ['live', 'settled', 'removed']) {
|
||||
for (const [phase, Tracker] of [
|
||||
['before', Before],
|
||||
['after', Fixed]
|
||||
]) {
|
||||
await settle()
|
||||
const baseline = collect()
|
||||
global.auditTaskOwner = populate(Tracker, {
|
||||
count,
|
||||
size,
|
||||
ingress,
|
||||
retention,
|
||||
field: 'description'
|
||||
})
|
||||
await settle()
|
||||
const retainedHeapBytes = collect() - baseline
|
||||
const visibleTextChars = logicalChars(global.auditTaskOwner)
|
||||
global.auditTaskOwner.clear()
|
||||
global.auditTaskOwner = null
|
||||
await settle()
|
||||
const afterClearHeapBytes = collect() - baseline
|
||||
if (phase === 'after') {
|
||||
assert.ok(retainedHeapBytes < 1024 * 1024, 'A bounded task retained its parent frame')
|
||||
} else {
|
||||
assert.ok(
|
||||
retainedHeapBytes > count * size * 0.75,
|
||||
'Baseline no longer reproduces retention'
|
||||
)
|
||||
}
|
||||
assert.ok(afterClearHeapBytes < 1024 * 1024, 'Tracker cleanup retained the fixture')
|
||||
results.push({
|
||||
count,
|
||||
size,
|
||||
ingress,
|
||||
retention,
|
||||
phase,
|
||||
visibleTextChars,
|
||||
retainedHeapBytes,
|
||||
afterClearHeapBytes
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, bundles, results }, null, 2)
|
||||
)
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
@@ -0,0 +1,370 @@
|
||||
{
|
||||
"node": "v26.6.0",
|
||||
"platform": "darwin",
|
||||
"bundles": {
|
||||
"before": "95425d0894ed107d85a671238f0229e6db3e229c0fa94286d1bea1a52db7112f",
|
||||
"after": "e1e5f58ddba3aeea88922be927509754b766247b81b6dcf7675b49a25c3a8d29"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2159976,
|
||||
"afterClearHeapBytes": 32392
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 97680,
|
||||
"afterClearHeapBytes": 51600
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2140416,
|
||||
"afterClearHeapBytes": 15320
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 55816,
|
||||
"afterClearHeapBytes": 12976
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 2112656,
|
||||
"afterClearHeapBytes": 5200
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "flat",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 36568,
|
||||
"afterClearHeapBytes": 11832
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2125456,
|
||||
"afterClearHeapBytes": -304
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 42736,
|
||||
"afterClearHeapBytes": -304
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2127096,
|
||||
"afterClearHeapBytes": 464
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 43896,
|
||||
"afterClearHeapBytes": 368
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 2123696,
|
||||
"afterClearHeapBytes": 16216
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "cons",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 39672,
|
||||
"afterClearHeapBytes": 15064
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2125672,
|
||||
"afterClearHeapBytes": -32
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 43536,
|
||||
"afterClearHeapBytes": 544
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 2127072,
|
||||
"afterClearHeapBytes": 1424
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 16384,
|
||||
"retainedHeapBytes": 44296,
|
||||
"afterClearHeapBytes": 1248
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 2108136,
|
||||
"afterClearHeapBytes": 1072
|
||||
},
|
||||
{
|
||||
"count": 32,
|
||||
"size": 65536,
|
||||
"ingress": "json",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 25360,
|
||||
"afterClearHeapBytes": 8832
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33562624,
|
||||
"afterClearHeapBytes": -320
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 11048,
|
||||
"afterClearHeapBytes": -320
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33563960,
|
||||
"afterClearHeapBytes": 912
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 12384,
|
||||
"afterClearHeapBytes": 464
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 33559744,
|
||||
"afterClearHeapBytes": 1112
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "flat",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 7512,
|
||||
"afterClearHeapBytes": 552
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33562624,
|
||||
"afterClearHeapBytes": -384
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 11048,
|
||||
"afterClearHeapBytes": -384
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33563960,
|
||||
"afterClearHeapBytes": 784
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 12384,
|
||||
"afterClearHeapBytes": 416
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 32432128,
|
||||
"afterClearHeapBytes": -1126504
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "cons",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 7008,
|
||||
"afterClearHeapBytes": 48
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "live",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33562624,
|
||||
"afterClearHeapBytes": -384
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "live",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 11048,
|
||||
"afterClearHeapBytes": -384
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "settled",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 33563960,
|
||||
"afterClearHeapBytes": 432
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "settled",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 4096,
|
||||
"retainedHeapBytes": 11656,
|
||||
"afterClearHeapBytes": -392
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "removed",
|
||||
"phase": "before",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 33558584,
|
||||
"afterClearHeapBytes": -48
|
||||
},
|
||||
{
|
||||
"count": 8,
|
||||
"size": 4194304,
|
||||
"ingress": "json",
|
||||
"retention": "removed",
|
||||
"phase": "after",
|
||||
"visibleTextChars": 0,
|
||||
"retainedHeapBytes": 7008,
|
||||
"afterClearHeapBytes": 48
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
# Retained OSC 133 incomplete carry
|
||||
|
||||
The shared command-lifecycle scanner keeps an incomplete OSC 133 suffix of at
|
||||
most 4,096 UTF-16 code units. A V8 sliced string can keep the entire preceding
|
||||
PTY input alive through that small suffix. The correction copies only the final
|
||||
incomplete carry through existing `ownRetainedString`; short prefixes, content,
|
||||
parsing, callbacks, authority and reset behavior are preserved.
|
||||
|
||||
This adds the fifteenth retained-text boundary to
|
||||
[#20960](https://github.com/stablyai/orca/pull/20960), following the
|
||||
[kitty/mouse tails](../terminal-mode-tail-retention/README.md) and other
|
||||
[retained text slices](../retained-text-slices/README.md). It introduces no wire
|
||||
change and applies equally to local and SSH/remote terminal bytes reaching the
|
||||
shared scanner.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/osc133-carry-retention/reproduce.cjs
|
||||
```
|
||||
|
||||
Run the same script with the installed Electron executable, setting
|
||||
`ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, with the same Node flags.
|
||||
No application window, native PTY, or network is created. The runner has a
|
||||
60-second deadline and accepts an optional output-report path as its first
|
||||
argument; otherwise it writes [Node](./node-results.json) or
|
||||
[Electron](./electron-results.json) results here.
|
||||
|
||||
The portable loader validates all 28 bundled source modules and seven additional
|
||||
caller/fixture files. Non-evaluated provenance callers accept only the recorded
|
||||
audited or named-main bytes, and reports identify which was present; evaluated
|
||||
modules each require one exact fixed hash. It reverses only the new import/copy call in memory through
|
||||
a zero-context [patch](./fix.patch), then validates the baseline hash. All
|
||||
evaluated-source and artifact hashes are recorded. It needs no Git history,
|
||||
ignored notes, or absolute developer paths. Source/patch reads normalize CRLF;
|
||||
an in-memory CRLF control checks equivalent before/after strings.
|
||||
|
||||
The scanner baseline exactly matches named main
|
||||
`291b4ddd6f1c1af480169885e0fda7f9c78ff053` and `v1.4.198`
|
||||
(`e0826956fcfc532f5a1e55b5e081f2e57e553c43`). The copier did not exist in
|
||||
`v1.4.198`; current helpers and callers are used for both sides of this
|
||||
experiment. [Source provenance](./source-versions.json) records each named
|
||||
identity/absence separately. This is not a replay of a complete historical app.
|
||||
|
||||
## Result and controls
|
||||
|
||||
Both Node 26.6 and Electron 43.7 / Node 24.21 pass **117 cases**. Each runtime
|
||||
compares the baseline, fixed Buffer copier and fixed Bufferless copier through
|
||||
the actual scanner, shared title tracker, and daemon background transient-fact
|
||||
relay. Thirty-two 64 Ki-character inputs retain roughly 2 MiB before the copy;
|
||||
eight 1 Mi-character inputs retain roughly 8 MiB. Fixed deltas are below the
|
||||
asserted 1 MiB tolerance, including owner overhead. Completion and reset/exit
|
||||
release the old parents. Exact GC-sensitive measurements are in the reports;
|
||||
they are heap deltas, not RSS or exact allocation attribution.
|
||||
|
||||
The ordinary sequence bytes come from the fish 4.7.1 capture documented in
|
||||
`src/shared/terminal-mode-2031-final-state.test.ts`: `A;click_events=1` and
|
||||
`C;cmdline_url=npx`. That capture contains complete OSC sequences. **The large
|
||||
plain-output prefix and cut before the terminator are synthetic.** This does
|
||||
not claim the original capture had those sizes or boundaries.
|
||||
|
||||
Controls preserve BEL/ST completion, split prefixes, C/D callback values,
|
||||
background disable/re-enable, reset, and every split of a Unicode/NUL/lone-
|
||||
surrogate fixture. The Bufferless copier is selected while Buffer is absent,
|
||||
then Buffer is restored before measurement; this exercises the renderer's
|
||||
actual fallback without launching a renderer. Short ordinary `D;0` prefixes,
|
||||
complete sequences and plain input are negative retention controls. Oversized
|
||||
unterminated input is separately labelled malformed-protocol stress. V8's
|
||||
independent last successful RegExp input is reset before both measurements.
|
||||
|
||||
Eight permanent tests cover both copier paths, long captured-fish suffixes,
|
||||
completion, reset and short `D;0`. With the in-memory baseline overlay, exactly
|
||||
four long-suffix regressions fail at 33,550,680–33,565,360 retained bytes against
|
||||
a 2 MiB allowance; the other 41 tests in the four-suite run pass. The fixed run
|
||||
passes all 45. Wider proof/quality validation is recorded in
|
||||
[validation.json](./validation.json).
|
||||
|
||||
## Owners and ordinary input bounds
|
||||
|
||||
Main creates a per-PTY tracker with `onCommandFinished` in
|
||||
`orca-runtime-get-unpersisted-tracked-title-for-pty.ts`; scanner enablement still
|
||||
respects transient-fact consumer/authority state. Ordinary daemon output frames
|
||||
delivered to main are sliced to 64 Ki characters in
|
||||
`daemon-stream-data-batcher.ts`, and ordinary relay output to 16 Ki characters
|
||||
in `src/relay/pty-handler.ts`. The 64 Ki cases therefore demonstrate retention
|
||||
without requiring a multi-megabyte main input; the 1 Mi cases amplify the
|
||||
mechanism. Replay and transformed output have their own existing limits.
|
||||
|
||||
The daemon's `BackgroundTransientFactRelay` owns one tracker per background
|
||||
session. `daemon-terminal-admission.ts` feeds it before output batching, so the
|
||||
batcher's later slicing is not an input cap on this daemon scanner. Native data
|
||||
passes through `pty-subprocess/subprocess-handle.ts`, the session's shell
|
||||
readiness/startup/recovery path, and its stream client. The inspected local
|
||||
intake does not impose an independent string-length limit; platform/native
|
||||
library chunk sizes were not measured here.
|
||||
|
||||
Completion/replacement of the incomplete escape, scanner reset, session exit,
|
||||
background retirement, tracker disposal or owner release drops the old parent.
|
||||
This is at most the last incomplete-parent cost per live scanner, not a list of
|
||||
every historical chunk. Multiple readers may share the same input backing
|
||||
storage; do not add their isolated measurements as independent process totals.
|
||||
|
||||
This is a reproduced code-level retention mechanism. It does not establish a
|
||||
native output pause, normal-session frequency/duration, the trigger in
|
||||
#19831/#19768, a reported sustained growth rate, or a multi-gigabyte incident's
|
||||
cause. The change does not reduce original input allocation.
|
||||
@@ -0,0 +1,23 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import baseConfig from '../../../config/vitest.config.ts'
|
||||
|
||||
const { loadSources, versions } = createRequire(import.meta.url)('./sources.cjs')
|
||||
const { baseline } = loadSources()
|
||||
const target = resolve(versions.sourcePath)
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: 'osc133-before-owned-carry',
|
||||
enforce: 'pre',
|
||||
transform(_source, id) {
|
||||
return resolve(id.split('?')[0]) === target ? { code: baseline, map: null } : undefined
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
--- a/src/shared/terminal-osc133-command-finished.ts
|
||||
+++ b/src/shared/terminal-osc133-command-finished.ts
|
||||
@@ -9,0 +10,2 @@
|
||||
+
|
||||
+import { ownRetainedString } from './own-retained-string'
|
||||
@@ -93,0 +96 @@
|
||||
+ carry = ownRetainedString(carry)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,142 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { load, loadSources, readText, sha, versions: sourceVersions } = require('./sources.cjs')
|
||||
const { inputs, heap, makeOwner, behavior } = require('./scenario.cjs')
|
||||
|
||||
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
|
||||
assert.equal(typeof global.gc, 'function', 'Run with --expose-gc')
|
||||
|
||||
async function main() {
|
||||
const reports = []
|
||||
const versions = []
|
||||
for (const variant of ['baseline', 'candidate-buffer', 'candidate-fallback']) {
|
||||
const fixed = variant !== 'baseline'
|
||||
const loaded = await load(fixed)
|
||||
loaded.api.resetOwnRetainedStringCopier()
|
||||
const originalBuffer = globalThis.Buffer
|
||||
try {
|
||||
if (variant === 'candidate-fallback') {
|
||||
globalThis.Buffer = undefined
|
||||
}
|
||||
assert.equal(
|
||||
loaded.api.ownRetainedString('prefix-\ud800a\udfff\u0000漢-suffix'),
|
||||
'prefix-\ud800a\udfff\u0000漢-suffix'
|
||||
)
|
||||
} finally {
|
||||
globalThis.Buffer = originalBuffer
|
||||
}
|
||||
const controls = behavior(loaded.api)
|
||||
versions.push({
|
||||
variant,
|
||||
fixed,
|
||||
sourceSha256: loaded.sourceSha256,
|
||||
bundleSha256: loaded.bundleSha256,
|
||||
evaluatedSources: loaded.evaluatedSources,
|
||||
callerSourceHashes: loaded.callerSourceHashes,
|
||||
controls
|
||||
})
|
||||
for (const ownerKind of ['scanner', 'title-tracker', 'background-relay']) {
|
||||
for (const input of inputs) {
|
||||
for (const [chars, count] of [
|
||||
[64 * 1024, 32],
|
||||
[1024 * 1024, 8]
|
||||
]) {
|
||||
const before = await heap()
|
||||
const owners = Array.from({ length: count }, (_, index) =>
|
||||
makeOwner(loaded.api, ownerKind, input, chars, index)
|
||||
)
|
||||
const retainedDelta = (await heap()) - before
|
||||
const expectParent = !fixed && input.retained
|
||||
assert.ok(
|
||||
expectParent ? retainedDelta > chars * count * 0.75 : retainedDelta < 1024 * 1024,
|
||||
JSON.stringify({ fixed, ownerKind, input: input.name, retainedDelta })
|
||||
)
|
||||
for (const owner of owners) {
|
||||
owner.complete()
|
||||
}
|
||||
const completedDelta = (await heap()) - before
|
||||
assert.ok(
|
||||
completedDelta < 1024 * 1024,
|
||||
JSON.stringify({ fixed, ownerKind, input: input.name, completedDelta })
|
||||
)
|
||||
for (const owner of owners) {
|
||||
owner.release()
|
||||
}
|
||||
reports.push({
|
||||
variant,
|
||||
fixed,
|
||||
ownerKind,
|
||||
input: input.name,
|
||||
inputCodeUnits: chars,
|
||||
owners: count,
|
||||
inputSuffixCodeUnits: input.suffix.length,
|
||||
retainedDelta,
|
||||
completedDelta
|
||||
})
|
||||
}
|
||||
}
|
||||
const input = inputs[0]
|
||||
const before = await heap()
|
||||
const owners = Array.from({ length: 8 }, (_, index) =>
|
||||
makeOwner(loaded.api, ownerKind, input, 1024 * 1024, index)
|
||||
)
|
||||
for (const owner of owners) {
|
||||
owner.release()
|
||||
}
|
||||
const resetDelta = (await heap()) - before
|
||||
assert.ok(resetDelta < 1024 * 1024, JSON.stringify({ fixed, ownerKind, resetDelta }))
|
||||
reports.push({ variant, fixed, ownerKind, input: 'reset-without-completion', resetDelta })
|
||||
}
|
||||
}
|
||||
assert.deepEqual(versions[0].controls, versions[1].controls)
|
||||
assert.deepEqual(versions[0].controls, versions[2].controls)
|
||||
let crlfReads = 0
|
||||
const crlfSources = loadSources((file) => {
|
||||
crlfReads += 1
|
||||
return readText(file).replaceAll('\n', '\r\n')
|
||||
})
|
||||
assert.deepEqual(crlfSources, loadSources())
|
||||
assert.equal(crlfReads, 2)
|
||||
const artifacts = [
|
||||
'sources.cjs',
|
||||
'scenario.cjs',
|
||||
'reproduce.cjs',
|
||||
'source-versions.json',
|
||||
'fix.patch',
|
||||
'before.config.mjs'
|
||||
]
|
||||
const artifactHashes = Object.fromEntries(
|
||||
artifacts.map((file) => [file, sha(readText(path.join(__dirname, file)))])
|
||||
)
|
||||
const result = {
|
||||
scope:
|
||||
'Actual-source bounded diagnostic; captured fish sequence syntax with synthetic large-prefix placement and chunk split. No affected-host, RSS, native PTY, or whole-release claim.',
|
||||
node: process.version,
|
||||
electron: process.versions.electron ?? null,
|
||||
v8: process.versions.v8,
|
||||
sourcePath: sourceVersions.sourcePath,
|
||||
crlfReads,
|
||||
artifactHashes,
|
||||
versions,
|
||||
reports
|
||||
}
|
||||
const resultPath = process.argv[2]
|
||||
? path.resolve(process.argv[2])
|
||||
: path.join(
|
||||
__dirname,
|
||||
process.versions.electron ? 'electron-results.json' : 'node-results.json'
|
||||
)
|
||||
fs.writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`)
|
||||
console.log(
|
||||
JSON.stringify({ resultPath, cases: reports.length, variants: versions.map((x) => x.variant) })
|
||||
)
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
setTimeout(() => {
|
||||
console.error('fixture deadline')
|
||||
process.exit(2)
|
||||
}, 60000).unref()
|
||||
@@ -0,0 +1,99 @@
|
||||
const assert = require('node:assert/strict')
|
||||
|
||||
const inputs = [
|
||||
{ name: 'captured-fish-prompt-partial', suffix: '\x1b]133;A;click_events=1', retained: true },
|
||||
{ name: 'captured-fish-command-partial', suffix: '\x1b]133;C;cmdline_url=npx', retained: true },
|
||||
{ name: 'short-standard-finished-partial', suffix: '\x1b]133;D;0', retained: false },
|
||||
{
|
||||
name: 'captured-fish-command-complete',
|
||||
suffix: '\x1b]133;C;cmdline_url=npx\x07',
|
||||
retained: false
|
||||
},
|
||||
{ name: 'no-escape', suffix: 'ordinary output', retained: false },
|
||||
{ name: 'oversized-incomplete-protocol', suffix: `\x1b]133;${'x'.repeat(5000)}`, retained: true }
|
||||
]
|
||||
|
||||
async function heap() {
|
||||
;/(?:)/.test('')
|
||||
for (let round = 0; round < 4; round++) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
global.gc()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function makeOwner(api, ownerKind, input, chars, index) {
|
||||
const prefix = `${index}:`
|
||||
const data = prefix + 'x'.repeat(chars - prefix.length - input.suffix.length) + input.suffix
|
||||
if (ownerKind === 'scanner') {
|
||||
const scanner = api.createOsc133CommandFinishedScanner(() => {})
|
||||
scanner.scan(data)
|
||||
return { complete: () => scanner.scan('\x07'), release: () => scanner.reset() }
|
||||
}
|
||||
if (ownerKind === 'title-tracker') {
|
||||
const tracker = api.createTerminalTitleTracker({ onCommandFinished: () => {} })
|
||||
tracker.handleChunk(data, { titleScanData: '' })
|
||||
return {
|
||||
complete: () => tracker.handleChunk('\x07', { titleScanData: '' }),
|
||||
release: () => tracker.dispose()
|
||||
}
|
||||
}
|
||||
const relay = new api.BackgroundTransientFactRelay(() => {})
|
||||
relay.setSessionBackground('fixture-session', true)
|
||||
relay.onSessionData('fixture-session', data)
|
||||
return {
|
||||
complete: () => relay.onSessionData('fixture-session', '\x07'),
|
||||
release: () => relay.onSessionExit('fixture-session')
|
||||
}
|
||||
}
|
||||
|
||||
function behavior(api) {
|
||||
const emitted = []
|
||||
const scanner = api.createOsc133CommandFinishedScanner(
|
||||
(code) => emitted.push(['finished', code]),
|
||||
() => emitted.push(['started'])
|
||||
)
|
||||
for (const chunk of [
|
||||
'\x1b]133;A;click_events=1',
|
||||
'\x07',
|
||||
'\x1b]133;C;cmdline_url=npx',
|
||||
'\x07',
|
||||
'\x1b]133;D;13',
|
||||
'7\x1b',
|
||||
'\\',
|
||||
'\x1b]133;D;0\x07',
|
||||
'\x1b]133;D;not-a-number\x07'
|
||||
]) {
|
||||
scanner.scan(chunk)
|
||||
}
|
||||
scanner.scan('\x1b]133;D;1234567890')
|
||||
scanner.reset()
|
||||
scanner.scan('\x07')
|
||||
assert.deepEqual(emitted, [['started'], ['finished', 137], ['finished', 0], ['finished', null]])
|
||||
const facts = []
|
||||
const relay = new api.BackgroundTransientFactRelay((id, fact) => facts.push([id, fact]))
|
||||
relay.setSessionBackground('s', true)
|
||||
relay.onSessionData('s', '\x1b]133;D;137')
|
||||
relay.onSessionData('s', '\x07')
|
||||
relay.onSessionData('s', '\x1b]133;D;22')
|
||||
relay.setSessionBackground('s', false)
|
||||
relay.setSessionBackground('s', true)
|
||||
relay.onSessionData('s', '\x07')
|
||||
relay.dispose()
|
||||
assert.deepEqual(facts[0], ['s', { kind: 'command-finished', exitCode: 137 }])
|
||||
assert.equal(facts.filter(([, fact]) => fact.kind === 'command-finished').length, 1)
|
||||
const utf16 = '\x1b]133;D;1234567890;\ud800a\udfff\u0000漢'
|
||||
assert.equal(api.ownRetainedString(utf16), utf16)
|
||||
const splitResults = []
|
||||
for (let cut = 1; cut < utf16.length; cut++) {
|
||||
const values = []
|
||||
const split = api.createOsc133CommandFinishedScanner((code) => values.push(code))
|
||||
split.scan(utf16.slice(0, cut))
|
||||
split.scan(`${utf16.slice(cut)}\x1b\\`)
|
||||
assert.deepEqual(values, [1234567890])
|
||||
splitResults.push(values)
|
||||
}
|
||||
return { emitted, facts, splitResults }
|
||||
}
|
||||
|
||||
module.exports = { inputs, heap, makeOwner, behavior }
|
||||
@@ -0,0 +1,646 @@
|
||||
{
|
||||
"sourcePath": "src/shared/terminal-osc133-command-finished.ts",
|
||||
"baselineSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd",
|
||||
"fixedSha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0",
|
||||
"sourceHashLineEndings": "canonical LF",
|
||||
"dependencies": {
|
||||
"src/shared/terminal-osc133-command-finished.ts": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0",
|
||||
"src/shared/agent-name-token-match.ts": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8",
|
||||
"src/shared/agent-title-decoration.ts": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41",
|
||||
"src/shared/pi-state-title-marker.ts": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8",
|
||||
"src/shared/pi-compatible-synthetic-title.ts": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f",
|
||||
"src/shared/terminal-title-classification-memo.ts": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda",
|
||||
"src/shared/terminal-title-wrapper-segments.ts": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78",
|
||||
"src/shared/agent-title-core.ts": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d",
|
||||
"src/shared/opencode-terminal-title.ts": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec",
|
||||
"src/shared/agent-title-identity.ts": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453",
|
||||
"src/shared/synthetic-agent-title.ts": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922",
|
||||
"src/shared/tui-agent-display-names.ts": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296",
|
||||
"src/shared/agent-title-evidence.ts": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea",
|
||||
"src/shared/pane-agent-evidence-sources.ts": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f",
|
||||
"src/shared/pane-agent-identity-adapter.ts": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9",
|
||||
"src/shared/terminal-title-agent-type.ts": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864",
|
||||
"src/shared/agent-title-status.ts": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb",
|
||||
"src/shared/osc-title-extraction.ts": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a",
|
||||
"src/shared/shell-process-detection.ts": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f",
|
||||
"src/shared/agent-detection.ts": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651",
|
||||
"src/shared/terminal-bell-detector.ts": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05",
|
||||
"src/shared/terminal-color-scheme-protocol.ts": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700",
|
||||
"src/shared/github/links.ts": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7",
|
||||
"src/shared/terminal-github-pr-link-detector.ts": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e",
|
||||
"src/shared/terminal-output-side-effects.ts": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49",
|
||||
"src/main/daemon/daemon-background-transient-facts.ts": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2"
|
||||
},
|
||||
"callerHashes": [
|
||||
{
|
||||
"path": "src/main/daemon/daemon-stream-data-batcher.ts",
|
||||
"sha256": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe",
|
||||
"acceptedSha256": [
|
||||
"56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe",
|
||||
"958a01e69c8e24f6eeebe44d0ba0e4e3fe50cb27e7c2302ee8582827633ee26f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/daemon-terminal-admission.ts",
|
||||
"sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251",
|
||||
"acceptedSha256": ["14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251"]
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/pty-subprocess/subprocess-handle.ts",
|
||||
"sha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a",
|
||||
"acceptedSha256": [
|
||||
"e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a",
|
||||
"12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/session.ts",
|
||||
"sha256": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338",
|
||||
"acceptedSha256": [
|
||||
"ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338",
|
||||
"fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4"
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts",
|
||||
"sha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea",
|
||||
"acceptedSha256": ["3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea"]
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts",
|
||||
"sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33",
|
||||
"acceptedSha256": ["107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33"]
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-mode-2031-final-state.test.ts",
|
||||
"sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10",
|
||||
"acceptedSha256": ["42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10"]
|
||||
}
|
||||
],
|
||||
"namedSourceProvenance": [
|
||||
{
|
||||
"path": "src/main/daemon/daemon-background-transient-facts.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "6be5c76683a0beb46a205d2f3d343f4e5570ff61b330b73213c72b62a7e7c5b2"
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/daemon-stream-data-batcher.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "958a01e69c8e24f6eeebe44d0ba0e4e3fe50cb27e7c2302ee8582827633ee26f",
|
||||
"matchesAuditedBefore": false
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "1c90541151cb3b4a2c77b731db1d2da9cf69f19ad3cd6e673555c7f652edc9ba",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "56d4e6b8b461d45791ce1d4cc2185109554ecc57cc3163e68ad43c3001e91fbe"
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/daemon-terminal-admission.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "14d1d8bbf4b662c98db123719741daffc3d0fe911d3a6a08d3516be666068251"
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/pty-subprocess/subprocess-handle.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "12ec9e8b5e5f7175310f9522c3f827b19b871c3df5b09e8b1b2c4fe1fc92ba4e",
|
||||
"matchesAuditedBefore": false
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "4623b60a0e362bb3cf218787573966aa056ee5fd1bdefc39fb5293446e4af70b",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "e28a2043f9c68c365109aab0550f97c49ee76fbbadc14c81dd7eb7b9aae6fc7a"
|
||||
},
|
||||
{
|
||||
"path": "src/main/daemon/session.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4",
|
||||
"matchesAuditedBefore": false
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "fcf79c354e29bf73549b3fdd4d905d431c210699391f45916bd27dbe858f86b4",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "ddd71285f6e72bcee012e7a9955170be4fb59c8e41e70971bc629c13504a4338"
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/orca-runtime-apply-tracked-pty-title.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "afee7baf9568d05298c7a3b7b25057130f6ef21555f2e9079f6fa0bcef8f0084",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "3c2ba07ff330875c5a9a07e44c0ffbcb1ebee1ae4af1952502ec733d6c7f7bea"
|
||||
},
|
||||
{
|
||||
"path": "src/main/runtime/orca-runtime-get-unpersisted-tracked-title-for-pty.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "107666bcc7e4bc744db51e9a023d659425947b134b9f420a0f11fb9ff0a7ba33"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-detection.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "b1d519786b07515e6518f8ab9e8bf826fb6879ac4d8866fb54f59918d3098651"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-name-token-match.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "27062a92d08e38aa140496d026e3e355cf5ad19f6ccdc18f1c06f4bbb9fef6f8"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-title-core.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "9827730762f0118274d98defd86ff8989238bb5fd4e0ef8012003ea00468301d"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-title-decoration.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "85cbebb6ca78a7d0a93f247f85da1efbce5584d5ade9154acb4a2b168c782b41"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-title-evidence.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "792a16e01191e3659e487352b21e6df8898db6cd2ab57c1363f3ec3ea1fd49fa",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "5ea226cea3867f5421cd45ac54d37f57fc2d1364cb0eafa4a5ff71ddda5fc7ea"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-title-identity.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "c7876bbf40e0e14676f9829e9b9800baa527fe9f5d63ea7f721e293405ea18f9",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "613c6284eb31aaa381db438c47d2ccf2c726ad150ae62ef269a2cfb762a03453"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/agent-title-status.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "8df0706f4074d06909d1264e22203f431a09118e056e934b96758d963a69f1bd",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "570a4710ebd0a7626dbdccff795efd087997853ac7c0fdb82c015976760b70bb"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/github/links.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "3a7bdfabed6fa37272df8c60c6933bf1bb3b783d7ca4adddf9c8d4c579295da7"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/opencode-terminal-title.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "2af51932ca77ae4d4b6edcc1d25ab181b3fdf392056f75c9defb6a14a93cc5ec"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/osc-title-extraction.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "862f2e9f5d05e3b588143e18752dbffe9b7635174abc31893ba0da4f04c9230a"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/own-retained-string.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": null,
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/owned-utf16-suffix.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/pane-agent-evidence-sources.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "26673ce2168f819d3c6c41a0e05544457f0c65ac4d70eb4b5e791bb05b239a8f"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/pane-agent-identity-adapter.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "0d27d248d6cd86eaca4d8f046cbfeae0ee1934fc67bd2cb0bb9d7e8e630cdfb9"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/pi-compatible-synthetic-title.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "2c43b6aa0b26f328bc7d51bfc8b4f7a8937156f91b430ccedc945827808e188d",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "99b401985234b242229f9f7eb244b041ffe98f0e463cf90a74c1c248943b820f"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/pi-state-title-marker.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "5cccf1bb0e00d362e9a824996a6755cf101c71a895413e6862c7a3d68e8828af",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "c398ce62a0f0dc72b23d106742833c1d60807f4dee92c78adb428a0b0031dce8"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/shell-process-detection.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "d80a58eaba385d17ef3af37a8b7c516c86539c494aa38afaef1418b1a2f4f79f"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/synthetic-agent-title.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "8944067df16920a6ed068a251d6cf4c270263db68e9b489708d0ae467ae9326c",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "40658e43e59cba687ab2304989d23ae14ed7205c5cae02f6fc7c9634bd0da922"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-bell-detector.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "fffa4e0717222ea3059b4aab06a3403b6c972398d5fa9e4acf3316159d18dc05"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-color-scheme-protocol.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "b8d1ee7693ae5053a8e3c9c099b8ab81032aed1f9a984de6eaad59e8c10c1700"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-github-pr-link-detector.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "7c9509c50cfe733b5f9b29bcbd5c684825b3c18b2fac82275bd9047e7026100e"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-mode-2031-final-state.test.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "42f3ff8839969f36a98f05c36d93abb7da291a9c3d2923a185a4933f4f245e10"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-osc133-command-finished.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "1790083b5200d11e68257556dffb49d2e8700841dccc6ccbbaa2c30b11b1efcd"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-output-side-effects.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "c1666a54339ece63e4180ab7e3eceec244f5bdd686c6dc3a0eed6e9ab93abcd4",
|
||||
"matchesAuditedBefore": false
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "f609970e2ea7e53fc9e1ab0e0caca038b910dee3c092a9297377689871845f49"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-title-agent-type.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "d7734bdfbd4d3151ae5bef3545f670c5ea931f1b17758cbbab8476b83b448864"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-title-classification-memo.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "e587a8cbfd3cf6d5fdf95400020a9346f87518e8b9d8476039d6a88a7b99beda"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-title-wrapper-segments.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "f2eb6fc485e519263f3e9394a9f3dc7004aa70373e3e36d42eba3ec509e2cb78"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/tui-agent-display-names.ts",
|
||||
"namedRefs": {
|
||||
"main291b": {
|
||||
"commit": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296",
|
||||
"matchesAuditedBefore": true
|
||||
},
|
||||
"v1.4.198": {
|
||||
"commit": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296",
|
||||
"matchesAuditedBefore": true
|
||||
}
|
||||
},
|
||||
"auditedBeforeSha256": "1a4b102bb7e8c13a48344c6da22f531e1ebf77329bd868f2580b7729684c7296"
|
||||
}
|
||||
],
|
||||
"historicalScope": "Scanner baseline is identical at main291b and v1.4.198. Current caller/helper dependencies are evaluated; own-retained-string is absent in v1.4.198. This is not a complete historical-release replay.",
|
||||
"callerScope": "Non-evaluated supporting caller provenance accepts only recorded audited-before or named main291b bytes; each runtime report records the actual selected hash. Evaluated bundle dependencies require the single fixed hash."
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const { createHash } = require('node:crypto')
|
||||
const { readFileSync } = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const Module = require('node:module')
|
||||
const { build } = require('esbuild')
|
||||
const { applyPatch, parsePatch, reversePatch } = require('diff')
|
||||
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const canonical = (value) => value.replaceAll('\r\n', '\n')
|
||||
const sha = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const readText = (file) => canonical(readFileSync(file, 'utf8'))
|
||||
const versions = JSON.parse(readText(path.join(__dirname, 'source-versions.json')))
|
||||
|
||||
function loadSources(read = readText) {
|
||||
const fixed = canonical(read(path.join(root, versions.sourcePath)))
|
||||
assert.equal(sha(fixed), versions.fixedSha256, 'Fixed scanner drift')
|
||||
const patches = parsePatch(canonical(read(path.join(__dirname, 'fix.patch'))))
|
||||
assert.equal(patches.length, 1)
|
||||
assert.equal(patches[0].newFileName, `b/${versions.sourcePath}`)
|
||||
const baseline = applyPatch(fixed, reversePatch(patches[0]))
|
||||
assert.notEqual(baseline, false)
|
||||
assert.equal(sha(baseline), versions.baselineSha256, 'Baseline scanner drift')
|
||||
return { baseline, fixed }
|
||||
}
|
||||
|
||||
async function load(fixed) {
|
||||
const sources = loadSources()
|
||||
const callerSourceHashes = {}
|
||||
for (const caller of versions.callerHashes) {
|
||||
const actual = sha(readText(path.join(root, caller.path)))
|
||||
assert.ok(caller.acceptedSha256.includes(actual), `Caller drift: ${caller.path}`)
|
||||
callerSourceHashes[caller.path] = actual
|
||||
}
|
||||
const evaluatedSources = {}
|
||||
const built = await build({
|
||||
stdin: {
|
||||
contents: [
|
||||
"export { createOsc133CommandFinishedScanner } from './src/shared/terminal-osc133-command-finished'",
|
||||
"export { BackgroundTransientFactRelay } from './src/main/daemon/daemon-background-transient-facts'",
|
||||
"export { createTerminalTitleTracker } from './src/shared/terminal-output-side-effects'",
|
||||
"export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'"
|
||||
].join('\n'),
|
||||
resolveDir: root,
|
||||
loader: 'ts'
|
||||
},
|
||||
absWorkingDir: root,
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'hash-fenced-osc133-carry',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /\.ts$/ }, ({ path: filename }) => {
|
||||
const relative = path.relative(root, filename).split(path.sep).join('/')
|
||||
const expected = versions.dependencies[relative]
|
||||
assert.ok(expected, `Unreviewed dependency: ${relative}`)
|
||||
let contents = readText(filename)
|
||||
assert.equal(sha(contents), expected, `Dependency drift: ${relative}`)
|
||||
if (relative === versions.sourcePath) {
|
||||
contents = fixed ? sources.fixed : sources.baseline
|
||||
}
|
||||
evaluatedSources[relative] = sha(contents)
|
||||
return { contents, loader: 'ts' }
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.deepEqual(Object.keys(evaluatedSources).sort(), Object.keys(versions.dependencies).sort())
|
||||
const filename = path.join(__dirname, fixed ? 'fixed-bundle.cjs' : 'baseline-bundle.cjs')
|
||||
const loaded = new Module(filename, module)
|
||||
loaded.filename = filename
|
||||
loaded.paths = Module._nodeModulePaths(root)
|
||||
loaded._compile(built.outputFiles[0].text, filename)
|
||||
return {
|
||||
api: loaded.exports,
|
||||
sourceSha256: sha(fixed ? sources.fixed : sources.baseline),
|
||||
bundleSha256: sha(built.outputFiles[0].text),
|
||||
evaluatedSources,
|
||||
callerSourceHashes
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { load, loadSources, readText, root, sha, versions }
|
||||
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"backgroundLaunch": "All tests and proofs used ORCA_BACKGROUND_LAUNCH=1; Electron only ran with ELECTRON_RUN_AS_NODE=1. No app or native PTY.",
|
||||
"fixedTests": {
|
||||
"passed": 45,
|
||||
"failed": 0,
|
||||
"files": 4,
|
||||
"newTests": 8,
|
||||
"config": "config/vitest.config.ts"
|
||||
},
|
||||
"baselineOverlay": {
|
||||
"passed": 41,
|
||||
"failed": 4,
|
||||
"config": "docs/audits/osc133-carry-retention/before.config.mjs",
|
||||
"intendedFailures": [
|
||||
{
|
||||
"test": "OSC 133 carry with Bufferless copying=false owns a retained fish suffix \"\\u001b]133;A;click_events=1\"",
|
||||
"assertion": "AssertionError: expected 33560392 to be less than 2097152",
|
||||
"retainedBytes": 33560392
|
||||
},
|
||||
{
|
||||
"test": "OSC 133 carry with Bufferless copying=false owns a retained fish suffix \"\\u001b]133;C;cmdline_url=npx\"",
|
||||
"assertion": "AssertionError: expected 33565360 to be less than 2097152",
|
||||
"retainedBytes": 33565360
|
||||
},
|
||||
{
|
||||
"test": "OSC 133 carry with Bufferless copying=true owns a retained fish suffix \"\\u001b]133;A;click_events=1\"",
|
||||
"assertion": "AssertionError: expected 33557160 to be less than 2097152",
|
||||
"retainedBytes": 33557160
|
||||
},
|
||||
{
|
||||
"test": "OSC 133 carry with Bufferless copying=true owns a retained fish suffix \"\\u001b]133;C;cmdline_url=npx\"",
|
||||
"assertion": "AssertionError: expected 33550680 to be less than 2097152",
|
||||
"retainedBytes": 33550680
|
||||
}
|
||||
]
|
||||
},
|
||||
"portableProofs": {
|
||||
"nodeCases": 117,
|
||||
"electronCases": 117,
|
||||
"crlfSourceAndPatchReads": 2,
|
||||
"evaluatedModules": 28,
|
||||
"nonEvaluatedCallerFiles": 7,
|
||||
"variants": ["baseline", "fixed Buffer copier", "fixed Bufferless copier"]
|
||||
},
|
||||
"typechecks": {
|
||||
"node": "Passed full Node project; parent root rerun after resolving its concurrent viewport-test typing.",
|
||||
"web": "Passed full Web project in parent root shared desktop run.",
|
||||
"cli": "Passed full CLI project in parent root shared desktop run."
|
||||
},
|
||||
"fullPublicationQuality": {
|
||||
"paths": [
|
||||
"src/shared/terminal-osc133-command-finished.ts",
|
||||
"src/shared/terminal-osc133-carry-retention.test.ts",
|
||||
"docs/audits/osc133-carry-retention/sources.cjs",
|
||||
"docs/audits/osc133-carry-retention/scenario.cjs",
|
||||
"docs/audits/osc133-carry-retention/reproduce.cjs",
|
||||
"docs/audits/osc133-carry-retention/before.config.mjs"
|
||||
],
|
||||
"scans": [
|
||||
"default rules and unused suppression",
|
||||
"casting",
|
||||
"type-aware",
|
||||
"React Doctor",
|
||||
"design system"
|
||||
],
|
||||
"result": "All five full-file scans passed with --deny-warnings, including CJS/MJS artifact files."
|
||||
},
|
||||
"changedQuality": {
|
||||
"base": "4a09b1d108cfd8b57ffcc5d727b3a3bd71ae71fb",
|
||||
"result": "Passed all five scans plus SAFETY rationale gate across six concurrent changed files; artifacts separately covered by explicit full-file scans."
|
||||
},
|
||||
"productHashes": [
|
||||
{
|
||||
"path": "src/shared/terminal-osc133-command-finished.ts",
|
||||
"sha256": "92bc1aa5eb975672c3b8761cce0da37d7ff6263c8f38c69d0715f73bd8e075d0"
|
||||
},
|
||||
{
|
||||
"path": "src/shared/terminal-osc133-carry-retention.test.ts",
|
||||
"sha256": "f123ddacc2d395f5896dc91acba13fbd9447f9152d36b8164a11d83271d9279d"
|
||||
}
|
||||
],
|
||||
"limits": "Synthetic parent size/boundary with captured fish sequence syntax. Heap deltas are not RSS. No historical whole-app or incident attribution. Baseline overlay retains current dependency implementations."
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
# Plugin worker output retention
|
||||
|
||||
The worker output parser capped a line at 8,192 code units, but retained slices could keep a much larger decoded input chunk alive. This artifact reproduces two ownership paths using the actual parser and actual `PluginLogBuffer`:
|
||||
|
||||
1. An unfinished line stays in the stream listener's buffer.
|
||||
2. A completed short or truncated line stays in the service's 200-entry log ring.
|
||||
|
||||
The fix uses the existing `ownRetainedString` copier for incomplete segments retained across callbacks and for the bounded string passed to the log sink. Line contents, truncation, callback invocation, ring capacity, and worker lifecycle are unchanged. Strings shorter than 13 code units keep the helper's existing fast path.
|
||||
|
||||
## Production reachability and lifetime
|
||||
|
||||
- `src/main/plugins/plugin-host-process.ts` installs the parser on child stdout and stderr at lines 101–102, with UTF-8 decoding in the parser. The production sink passes through `plugin-worker-manager.ts:148` and `plugin-service.ts:94` to `plugin-log-buffer.ts:14`, which stores the original string without copying it.
|
||||
- The parser retains at most one incomplete line per stream. The default five active workers allow ten live stdout/stderr buffers. Worker slots are acquired before startup. Idle workers are reaped after five minutes, checked every minute; stream end clears parser buffering.
|
||||
- The log ring belongs to the long-lived `PluginService`, not the worker. Worker exit and stream end preserve its last 200 entries per plugin. Ring eviction releases the entries. Several lines can share one parent; the backing allocation must be counted once.
|
||||
- This is a main-process plugin path. The plugin-system setting gates activation (`src/main/startup/main-process-plugins.ts:59–62`). It is not a terminal daemon or renderer retention path. The plugin `orca.log` IPC message is a separate producer.
|
||||
- `PluginService.getLogs` and its IPC handler expose the existing ring. Reading or serializing a concatenated string can flatten it and shorten its parent retention, but does not remove the service's ring entries.
|
||||
|
||||
## Reproduce
|
||||
|
||||
From the repository root with the project's dependencies installed:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/plugin-worker-output-retention/reproduce.cjs
|
||||
```
|
||||
|
||||
For Electron, run the installed Electron executable with `ELECTRON_RUN_AS_NODE=1`, `ORCA_BACKGROUND_LAUNCH=1`, and the same arguments. This uses Node mode without opening an app window. For example, on macOS:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 ELECTRON_RUN_AS_NODE=1 node_modules/electron/dist/Electron.app/Contents/MacOS/Electron --expose-gc --max-old-space-size=192 docs/audits/plugin-worker-output-retention/reproduce.cjs
|
||||
```
|
||||
|
||||
The runner writes `node-results.json` or `electron-results.json` beside itself. Pass `--output <path>` to preserve the captured reports. It uses inert PassThrough streams, no OS child process or network, a 192 MiB heap limit, and a 30-second deadline.
|
||||
|
||||
`sources.cjs` reverses `fix.patch` in memory and checks exact baseline/fixed parser hashes. It also checks eight dependency/caller hashes and records actual evaluated source and bundle hashes. No source files are overwritten. A synthetic CRLF read control checks all ten source/patch reads.
|
||||
|
||||
`source-versions.json` records identical parser, sink, caller, and helper hashes at main checkpoint `291b4ddd6f1c1af480169885e0fda7f9c78ff053`, main `f78483ec29891ab11f49bb25e6cd628837b1242e`, and the #20960 topic `np-oom-scan-retained-text-slices` at `0d2efbc0d3b4f902b9bc02f5451c6ff4291405cf`. The parser, sink and caller modules also match v1.4.198 (`e0826956fcfc532f5a1e55b5e081f2e57e553c43`), which lacks the newer `own-retained-string.ts` wrapper. The baseline bundle uses only the unchanged parser and ring; fixed variants use the recorded publication helper. These are source controls with current build dependencies, not a historical app binary.
|
||||
|
||||
## Controls and results
|
||||
|
||||
Both Node 26.6 and Electron 43.7 / Node 24.21 pass 24 cases: baseline, diagnostic tail-only copy, fixed Buffer copy, and fixed code-unit-copy fallback, each with two input sizes and three ownership cases. The fallback is a shared-helper compatibility control; production main normally has Buffer.
|
||||
|
||||
| Retained owner | Baseline heap delta | Fixed heap delta |
|
||||
| ----------------------------------------------- | ------------------: | ---------------: |
|
||||
| Ten unfinished tails, 64 KiB input each | 0.72–0.73 MB | 9–24 KB |
|
||||
| Eight unfinished tails, 4 MiB input each | 33.56–33.57 MB | 6–11 KB |
|
||||
| 200 short log rows from 205 × 64 KiB inputs | 13.14–13.16 MB | 27–45 KB |
|
||||
| 200 truncated log rows from 205 × 64 KiB inputs | 13.14–13.15 MB | 3.29–3.31 MB |
|
||||
| Eight short log rows, 4 MiB input each | 33.56 MB | about 1 KB |
|
||||
| Eight truncated log rows, 4 MiB input each | 33.56 MB | 128–132 KB |
|
||||
|
||||
Heap deltas include GC noise. Truncated strings legitimately retain 8,192 code units, including the non-ASCII truncation suffix. Tail-only copying fixes unfinished buffers but leaves both log-ring paths. Stream end clears no-op-sink tails while the actual ring remains live; replacing all 200 entries releases the original parents.
|
||||
|
||||
64 KiB is an ordinary-scale stdio input control. The 4 MiB input is amplified stress, not a claim about normal OS pipe reads. PassThrough delivers the selected chunk intact; real child-pipe chunk sizes depend on runtime and OS. Retention is bounded by owner count, ring capacity and backing input size; this is not an unbounded line queue.
|
||||
|
||||
Behavior comparisons cover blank and split lines, null/empty streams, CRLF, end flushing, discard/resume after overflow, log level, exact ring content, NUL, lone surrogates, emoji, and the code-unit limit. Value comparisons run separately from heap controls because comparing concatenated strings can flatten them and change retention.
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config config/vitest.config.ts src/main/plugins/plugin-worker-output-retention.test.ts src/main/plugins/plugin-worker-output-buffer.test.ts src/main/plugins/plugin-host-process.test.ts src/shared/own-retained-string.test.ts
|
||||
ORCA_BACKGROUND_LAUNCH=1 pnpm exec vitest run --config docs/audits/plugin-worker-output-retention/before.config.mjs src/main/plugins/plugin-worker-output-retention.test.ts src/main/plugins/plugin-worker-output-buffer.test.ts
|
||||
ORCA_BACKGROUND_LAUNCH=1 pnpm tc:node
|
||||
```
|
||||
|
||||
The fixed source passes 20 tests. The baseline overlay intentionally fails all three new heap regressions: approximately 33.6 MB for unfinished tails and 13.2 MB for each ring case, against 2 MiB and 5 MiB ceilings; its original behavior test passes. Node typecheck passes. All five changed-quality scan configurations pass over all five product/test/artifact code files with `--no-ignore --deny-warnings`, including the ordinary and type-aware lint rules.
|
||||
|
||||
This proves a reachable code mechanism and its repair. It does not establish affected-host plugin use, output cadence, aggregate app RSS, or attribution to #19831 or another incident.
|
||||
@@ -0,0 +1,23 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { defineConfig, mergeConfig } from 'vitest/config'
|
||||
import baseConfig from '../../../config/vitest.config.ts'
|
||||
|
||||
const { loadSources } = createRequire(import.meta.url)('./sources.cjs')
|
||||
const { before } = loadSources()
|
||||
const sourcePath = resolve('src/main/plugins/plugin-worker-output-buffer.ts')
|
||||
|
||||
export default mergeConfig(
|
||||
baseConfig,
|
||||
defineConfig({
|
||||
plugins: [
|
||||
{
|
||||
name: 'plugin-output-before-fix',
|
||||
enforce: 'pre',
|
||||
transform(_code, id) {
|
||||
return resolve(id.split('?')[0]) === sourcePath ? { code: before, map: null } : undefined
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,18 @@
|
||||
diff --git a/src/main/plugins/plugin-worker-output-buffer.ts b/src/main/plugins/plugin-worker-output-buffer.ts
|
||||
index 836330c879..6a0cb2a078 100644
|
||||
--- a/src/main/plugins/plugin-worker-output-buffer.ts
|
||||
+++ b/src/main/plugins/plugin-worker-output-buffer.ts
|
||||
@@ -1,0 +2 @@ import type { Readable } from 'node:stream'
|
||||
+import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
@@ -24,3 +25,5 @@ export function pipePluginWorkerOutput(
|
||||
- truncated
|
||||
- ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`
|
||||
- : line
|
||||
+ ownRetainedString(
|
||||
+ truncated
|
||||
+ ? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`
|
||||
+ : line
|
||||
+ )
|
||||
@@ -52 +55 @@ export function pipePluginWorkerOutput(
|
||||
- buffered += segment
|
||||
+ buffered += newline === -1 ? ownRetainedString(segment) : segment
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,249 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { PassThrough } = require('node:stream')
|
||||
const { once, EventEmitter } = require('node:events')
|
||||
const { load, loadSources, sha, read } = require('./sources.cjs')
|
||||
|
||||
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
|
||||
assert.equal(typeof global.gc, 'function')
|
||||
const suffix = 'retained-output-tail'
|
||||
|
||||
async function heap() {
|
||||
;/reset/.test('reset')
|
||||
for (let i = 0; i < 4; i++) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
global.gc()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function emitChunk(stream, chars, index, complete, truncated = false) {
|
||||
if (truncated) {
|
||||
stream.write(`${index.toString().padStart(4, '0')}${'x'.repeat(chars - 5)}\n`)
|
||||
return
|
||||
}
|
||||
const label = `${index.toString().padStart(4, '0')}:${suffix}`
|
||||
const final = `\n${label}${complete ? '\n' : ''}`
|
||||
const text = `${' '.repeat(chars - final.length)}${final}`
|
||||
stream.write(text)
|
||||
}
|
||||
|
||||
async function tail(api, variant, chars, count) {
|
||||
const streams = []
|
||||
const start = await heap()
|
||||
for (let i = 0; i < count; i++) {
|
||||
const stream = new PassThrough()
|
||||
api.pipePluginWorkerOutput(stream, 'info', () => {})
|
||||
emitChunk(stream, chars, i, false)
|
||||
streams.push(stream)
|
||||
}
|
||||
const heldDelta = (await heap()) - start
|
||||
const retains = variant === 'before'
|
||||
assert.ok(
|
||||
retains ? heldDelta > chars * count * 0.75 : heldDelta < 768 * 1024,
|
||||
JSON.stringify({ variant, kind: 'tail', chars, count, heldDelta })
|
||||
)
|
||||
for (const stream of streams) {
|
||||
const ended = once(stream, 'end')
|
||||
stream.end()
|
||||
await ended
|
||||
}
|
||||
const endedDelta = (await heap()) - start
|
||||
assert.ok(endedDelta < 768 * 1024, JSON.stringify({ variant, endedDelta }))
|
||||
return { kind: 'tail', variant, chars, count, heldDelta, endedDelta }
|
||||
}
|
||||
|
||||
function verifyRing(log, count, truncated) {
|
||||
assert.equal(log.get('plugin').length, Math.min(count, 200))
|
||||
for (const [index, row] of log.get('plugin').entries()) {
|
||||
const inputIndex = index + Math.max(0, count - 200)
|
||||
assert.equal(row.level, 'info')
|
||||
assert.equal(
|
||||
row.line,
|
||||
truncated
|
||||
? `${inputIndex.toString().padStart(4, '0')}${'x'.repeat(8192 - 4 - '… [truncated]'.length)}… [truncated]`
|
||||
: `${inputIndex.toString().padStart(4, '0')}:${suffix}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
async function ring(api, variant, chars, count, truncated = false) {
|
||||
const log = new api.PluginLogBuffer()
|
||||
const stream = new PassThrough()
|
||||
api.pipePluginWorkerOutput(stream, 'info', (level, line) => log.append('plugin', level, line))
|
||||
const start = await heap()
|
||||
for (let i = 0; i < count; i++) {
|
||||
emitChunk(stream, chars, i, true, truncated)
|
||||
}
|
||||
assert.equal(log.get('plugin').length, Math.min(count, 200))
|
||||
const heldDelta = (await heap()) - start
|
||||
const retains = variant === 'before' || variant === 'tail-only'
|
||||
const expectedParents = Math.min(count, 200)
|
||||
const fixedBudget = expectedParents * (truncated ? 20 * 1024 : 0) + 768 * 1024
|
||||
assert.ok(
|
||||
retains ? heldDelta > chars * expectedParents * 0.75 : heldDelta < fixedBudget,
|
||||
JSON.stringify({ variant, kind: 'ring', chars, count, truncated, heldDelta })
|
||||
)
|
||||
const ended = once(stream, 'end')
|
||||
stream.end()
|
||||
await ended
|
||||
const endedDelta = (await heap()) - start
|
||||
assert.ok(retains ? endedDelta > chars * expectedParents * 0.75 : endedDelta < fixedBudget)
|
||||
for (let i = 0; i < 200; i++) {
|
||||
log.append('plugin', 'info', 'replacement')
|
||||
}
|
||||
const evictedDelta = (await heap()) - start
|
||||
assert.ok(evictedDelta < 768 * 1024, JSON.stringify({ variant, evictedDelta }))
|
||||
assert.equal(log.get('plugin').length, 200)
|
||||
return {
|
||||
kind: 'ring',
|
||||
variant,
|
||||
chars,
|
||||
count,
|
||||
truncated,
|
||||
expectedParents,
|
||||
heldDelta,
|
||||
endedDelta,
|
||||
evictedDelta
|
||||
}
|
||||
}
|
||||
|
||||
async function behavior(api) {
|
||||
const lines = []
|
||||
const stream = new PassThrough()
|
||||
api.pipePluginWorkerOutput(stream, 'error', (level, line) => lines.push([level, line]))
|
||||
for (const chunk of [' \nhello', ' world\n', 'x'.repeat(8193), 'discarded', '\nok\n', suffix]) {
|
||||
stream.write(chunk)
|
||||
}
|
||||
const ended = once(stream, 'end')
|
||||
stream.end()
|
||||
await ended
|
||||
assert.equal(lines.length, 4)
|
||||
assert.deepEqual(lines[0], ['error', 'hello world'])
|
||||
assert.equal(lines[1][1].length, 8192)
|
||||
assert.ok(lines[1][1].endsWith('… [truncated]'))
|
||||
assert.deepEqual(lines[2], ['error', 'ok'])
|
||||
assert.deepEqual(lines[3], ['error', suffix])
|
||||
const unicode = []
|
||||
const direct = new EventEmitter()
|
||||
direct.setEncoding = (encoding) => assert.equal(encoding, 'utf8')
|
||||
api.pipePluginWorkerOutput(null, 'info', () => assert.fail('Null stream emitted'))
|
||||
api.pipePluginWorkerOutput(direct, 'info', (level, line) => unicode.push([level, line]))
|
||||
for (const chunk of [
|
||||
'',
|
||||
' \r\n',
|
||||
'short\n',
|
||||
'twelve chars\n',
|
||||
'\ud800a\udfff\u0000\u6f22\n',
|
||||
'😀'.repeat(4096),
|
||||
'\n',
|
||||
`${'a'.repeat(8191)}\ud800`,
|
||||
'\udfff\n',
|
||||
'q'.repeat(8193),
|
||||
'still discarding',
|
||||
'\nnext\r\n',
|
||||
'unterminated 😀'
|
||||
]) {
|
||||
direct.emit('data', chunk)
|
||||
}
|
||||
direct.emit('end')
|
||||
assert.equal(unicode.length, 8)
|
||||
assert.deepEqual(unicode[2], ['info', '\ud800a\udfff\u0000\u6f22'])
|
||||
assert.deepEqual(unicode[3], ['info', '😀'.repeat(4096)])
|
||||
assert.equal(unicode[4][1].length, 8192)
|
||||
assert.ok(unicode[4][1].endsWith('… [truncated]'))
|
||||
assert.deepEqual(unicode[6], ['info', 'next\r'])
|
||||
assert.deepEqual(unicode[7], ['info', 'unterminated 😀'])
|
||||
// Keep value comparisons outside heap controls: they can flatten cons strings.
|
||||
for (const truncated of [false, true]) {
|
||||
const log = new api.PluginLogBuffer()
|
||||
const ringStream = new PassThrough()
|
||||
api.pipePluginWorkerOutput(ringStream, 'info', (level, line) =>
|
||||
log.append('plugin', level, line)
|
||||
)
|
||||
for (let i = 0; i < 3; i++) {
|
||||
emitChunk(ringStream, 16 * 1024, i, true, truncated)
|
||||
}
|
||||
verifyRing(log, 3, truncated)
|
||||
const ringEnded = once(ringStream, 'end')
|
||||
ringStream.end()
|
||||
await ringEnded
|
||||
}
|
||||
return { lines, unicode }
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reports = [],
|
||||
bundles = {},
|
||||
behaviors = {}
|
||||
for (const variant of ['before', 'tail-only', 'fixed-buffer', 'fixed-fallback']) {
|
||||
const api = await load(variant)
|
||||
bundles[variant] = api.provenance
|
||||
if (variant !== 'before') {
|
||||
api.resetOwnRetainedStringCopier()
|
||||
const originalBuffer = globalThis.Buffer
|
||||
try {
|
||||
if (variant === 'fixed-fallback') {
|
||||
globalThis.Buffer = undefined
|
||||
}
|
||||
assert.equal(api.ownRetainedString(suffix), suffix)
|
||||
} finally {
|
||||
globalThis.Buffer = originalBuffer
|
||||
}
|
||||
}
|
||||
behaviors[variant] = await behavior(api)
|
||||
reports.push(await tail(api, variant, 64 * 1024, 10))
|
||||
reports.push(await tail(api, variant, 4 * 1024 * 1024, 8))
|
||||
reports.push(await ring(api, variant, 64 * 1024, 205))
|
||||
reports.push(await ring(api, variant, 4 * 1024 * 1024, 8))
|
||||
reports.push(await ring(api, variant, 64 * 1024, 205, true))
|
||||
reports.push(await ring(api, variant, 4 * 1024 * 1024, 8, true))
|
||||
}
|
||||
assert.deepEqual(behaviors.before, behaviors['tail-only'])
|
||||
assert.deepEqual(behaviors.before, behaviors['fixed-buffer'])
|
||||
assert.deepEqual(behaviors.before, behaviors['fixed-fallback'])
|
||||
const normalSources = loadSources()
|
||||
let crlfReads = 0
|
||||
const crlfSources = loadSources((file) => {
|
||||
crlfReads += 1
|
||||
return read(file).replaceAll('\n', '\r\n')
|
||||
})
|
||||
assert.deepEqual(crlfSources, normalSources)
|
||||
const args = process.argv.slice(2)
|
||||
assert.ok(args.length === 0 || (args.length === 2 && args[0] === '--output'))
|
||||
const output =
|
||||
args.length === 2
|
||||
? path.resolve(args[1])
|
||||
: path.join(__dirname, `${process.versions.electron ? 'electron' : 'node'}-results.json`)
|
||||
const artifactHashes = Object.fromEntries(
|
||||
['reproduce.cjs', 'sources.cjs', 'source-versions.json', 'fix.patch'].map((file) => [
|
||||
file,
|
||||
sha(read(path.join(__dirname, file)))
|
||||
])
|
||||
)
|
||||
fs.writeFileSync(
|
||||
output,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
runtime: process.versions,
|
||||
artifactHashes,
|
||||
crlfLoaderControl: { reads: crlfReads, identical: true },
|
||||
bundles,
|
||||
behaviors,
|
||||
reports
|
||||
},
|
||||
null,
|
||||
2
|
||||
)}\n`
|
||||
)
|
||||
console.log(JSON.stringify({ output, passed: reports.length }))
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
setTimeout(() => {
|
||||
console.error('deadline')
|
||||
process.exit(2)
|
||||
}, 30000).unref()
|
||||
@@ -0,0 +1,89 @@
|
||||
{
|
||||
"publicationTopic": "np-oom-scan-retained-text-slices",
|
||||
"baselineSha256": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"fixedSha256": "4c7fd9e66d1b2aa948dab72196c3e6fd9df7c646dcf0a15279ee5520bb0a2a7b",
|
||||
"tailOnlySha256": "5d68af2f66e1466b5a642fa9bb301b8f04ccd2c2741dd5731faac7a2f30c5be5",
|
||||
"dependencies": {
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
},
|
||||
"namedRevisions": {
|
||||
"HEAD": {
|
||||
"revision": "2e83de3154c4ee1bbeea816734b892c34500a5cc",
|
||||
"sources": {
|
||||
"src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"revision": "f78483ec29891ab11f49bb25e6cd628837b1242e",
|
||||
"sources": {
|
||||
"src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
}
|
||||
},
|
||||
"np-oom-scan-retained-text-slices": {
|
||||
"revision": "0d2efbc0d3b4f902b9bc02f5451c6ff4291405cf",
|
||||
"sources": {
|
||||
"src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
}
|
||||
},
|
||||
"291b4ddd6f1c1af480169885e0fda7f9c78ff053": {
|
||||
"revision": "291b4ddd6f1c1af480169885e0fda7f9c78ff053",
|
||||
"sources": {
|
||||
"src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
}
|
||||
},
|
||||
"e0826956fcfc532f5a1e55b5e081f2e57e553c43": {
|
||||
"revision": "e0826956fcfc532f5a1e55b5e081f2e57e553c43",
|
||||
"sources": {
|
||||
"src/main/plugins/plugin-worker-output-buffer.ts": "969daeddb4f4fde9d53e80b008438aa2942526af940ae9eb4f08cd9440a0f914",
|
||||
"src/main/plugins/plugin-log-buffer.ts": "c378984b0435b957265b8071f5d2da8a3cbd659a8d2bea67b363b49e573ffdd8",
|
||||
"src/main/plugins/plugin-host-process.ts": "825b938a5fe510944c26e9b503afeb3a0da13b570558a111adc2f9edc21cce6b",
|
||||
"src/main/plugins/plugin-worker-manager.ts": "b95bfac2b674b6ecf6da6ba0d08b98596e67f05bf0dff02e8133049b4d5a1e70",
|
||||
"src/main/plugins/plugin-worker-startup.ts": "4b1e3ffb800e387a7a6f677ceef27a4d72f1b0aca467b54994f0b4518137fa2e",
|
||||
"src/main/plugins/plugin-service.ts": "057bcb8af5067112f8aec498ad7824b005dfe9ed7880b3e0745ed54fcfb75420",
|
||||
"src/shared/plugins/plugin-host-protocol.ts": "6f6ce9cfda1d30879695c7c8979b1d0ae93600a3d4417a6cd3c832235d3ae689",
|
||||
"src/shared/own-retained-string.ts": null,
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
}
|
||||
}
|
||||
},
|
||||
"historicalScope": "v1.4.198 has the identical parser, ring and caller modules but lacks own-retained-string.ts. The baseline bundle imports only the unchanged parser/ring. Fixed and tail-only variants use the recorded publication helper; this is not a historical application binary."
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const Module = require('node:module')
|
||||
const { createHash } = require('node:crypto')
|
||||
const { build } = require('esbuild')
|
||||
const { applyPatch, parsePatch, reversePatch } = require('diff')
|
||||
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const sourcePath = 'src/main/plugins/plugin-worker-output-buffer.ts'
|
||||
const canonicalLf = (value) => value.replaceAll('\r\n', '\n')
|
||||
const read = (file) => canonicalLf(fs.readFileSync(file, 'utf8'))
|
||||
const sha = (value) => createHash('sha256').update(value).digest('hex')
|
||||
|
||||
function loadSources(readText = read) {
|
||||
const versionsText = read(path.join(__dirname, 'source-versions.json'))
|
||||
const versions = JSON.parse(versionsText)
|
||||
const patches = parsePatch(canonicalLf(readText(path.join(__dirname, 'fix.patch'))))
|
||||
assert.equal(patches.length, 1)
|
||||
assert.equal(patches[0].newFileName, `b/${sourcePath}`)
|
||||
const current = canonicalLf(readText(path.join(root, sourcePath)))
|
||||
const before = applyPatch(current, reversePatch(patches[0]))
|
||||
assert.notEqual(before, false, 'The parser no longer matches the reviewed patch')
|
||||
assert.equal(sha(before), versions.baselineSha256)
|
||||
assert.equal(sha(current), versions.fixedSha256)
|
||||
const checkedSources = { [sourcePath]: sha(current) }
|
||||
for (const [relative, expected] of Object.entries(versions.dependencies)) {
|
||||
const actual = sha(canonicalLf(readText(path.join(root, relative))))
|
||||
assert.equal(actual, expected, `Reviewed dependency changed: ${relative}`)
|
||||
checkedSources[relative] = actual
|
||||
}
|
||||
return { before, current, checkedSources, versions, versionsSha256: sha(versionsText) }
|
||||
}
|
||||
|
||||
async function load(variant) {
|
||||
const checked = loadSources()
|
||||
let source = variant === 'before' ? checked.before : checked.current
|
||||
if (variant === 'tail-only') {
|
||||
source = `import { ownRetainedString } from '../../shared/own-retained-string'\n${checked.before}`
|
||||
assert.equal(source.split(' buffered += segment').length, 2)
|
||||
source = source.replace(
|
||||
' buffered += segment',
|
||||
' buffered += newline === -1 ? ownRetainedString(segment) : segment'
|
||||
)
|
||||
assert.equal(sha(source), checked.versions.tailOnlySha256)
|
||||
}
|
||||
const entries = [
|
||||
"export { pipePluginWorkerOutput } from './src/main/plugins/plugin-worker-output-buffer'",
|
||||
"export { PluginLogBuffer } from './src/main/plugins/plugin-log-buffer'"
|
||||
]
|
||||
if (variant !== 'before') {
|
||||
entries.push(
|
||||
"export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'"
|
||||
)
|
||||
}
|
||||
const evaluatedSources = {}
|
||||
const built = await build({
|
||||
stdin: { contents: entries.join('\n'), resolveDir: root },
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
bundle: true,
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'hash-fenced-plugin-output',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /\.ts$/ }, ({ path: file }) => {
|
||||
const relative = path.relative(root, file).split(path.sep).join('/')
|
||||
assert.ok(Object.hasOwn(checked.checkedSources, relative), relative)
|
||||
const contents = relative === sourcePath ? source : read(file)
|
||||
evaluatedSources[relative] = sha(contents)
|
||||
return { contents, loader: 'ts' }
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const filename = path.join(__dirname, `${variant}-bundle.cjs`)
|
||||
const loaded = new Module(filename, module)
|
||||
loaded.filename = filename
|
||||
loaded.paths = Module._nodeModulePaths(root)
|
||||
loaded._compile(built.outputFiles[0].text, filename)
|
||||
return {
|
||||
...loaded.exports,
|
||||
provenance: {
|
||||
checkedSources: checked.checkedSources,
|
||||
evaluatedSources,
|
||||
sourceVersionsSha256: checked.versionsSha256,
|
||||
bundleSha256: sha(built.outputFiles[0].text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { load, loadSources, sha, read }
|
||||
@@ -0,0 +1,59 @@
|
||||
# Retained PTY detector input
|
||||
|
||||
The advertised-URL watcher keeps a 4,096-character carry for each bound PTY and
|
||||
16,384 characters for each of at most 32 unbound PTYs. The Command Code status
|
||||
detector keeps 300 characters before its agent-specific prefilter, including for
|
||||
ordinary shell, Claude, and Codex output. Each could keep the whole original
|
||||
input alive through a V8 sliced string.
|
||||
|
||||
The fix uses the existing `ownRetainedString` copier when dropping oversized
|
||||
input. It preserves URL reconstruction, status detection, UTF-16 code units,
|
||||
binding/unbinding, cache limits, and remote/local authority. These are three
|
||||
additional boundaries in [#20960](https://github.com/stablyai/orca/pull/20960).
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/pty-detector-retention/reproduce.mjs
|
||||
```
|
||||
|
||||
The script bundles the actual detector and watcher. The baseline removes only
|
||||
the three copy calls in memory. Each input has its own live owner; URL cases
|
||||
use separate watcher instances to isolate each carry. Heap is measured after
|
||||
GC, before completing the partial URLs and verifying cleanup. Results include
|
||||
owner overhead, not just text. [Bundle hashes and measurements](./results.json).
|
||||
|
||||
| Case | Input per owner | Owners | Heap before | Heap after |
|
||||
| ------------------- | ---------------: | -----: | ----------: | ---------: |
|
||||
| Status detector | 64 Ki characters | 32 | 2,112,688 | 41,400 |
|
||||
| Bound URL carry | 64 Ki characters | 32 | 2,173,848 | 204,112 |
|
||||
| URL pending binding | 64 Ki characters | 32 | 2,148,824 | 575,512 |
|
||||
| Status detector | 4 Mi characters | 8 | 33,557,144 | 6,504 |
|
||||
| Bound URL carry | 4 Mi characters | 8 | 33,569,168 | 47,112 |
|
||||
| URL pending binding | 4 Mi characters | 8 | 33,568,936 | 144,504 |
|
||||
|
||||
Captured with Node v26.6.0 on macOS. Three GC regression tests failed before the
|
||||
fix, retaining about 32 MiB each, and pass afterward. The five-suite run passes
|
||||
164 tests including existing URL/status behavior and copier Unicode/fallback
|
||||
tests.
|
||||
|
||||
## Scope and limits
|
||||
|
||||
Main feeds both observers before renderer batching. Default daemon bulk output
|
||||
frames are at most 64 Ki UTF-16 characters; main's later 16 Ki-character batching
|
||||
does not bound these readers. The 4 Mi-character cases demonstrate the retaining
|
||||
mechanism under larger inputs, not normal daemon frame size. Both implementations
|
||||
also exist in `v1.4.198`.
|
||||
|
||||
Transformed frames bypass ordinary chunk slicing but still face the daemon's
|
||||
16 MiB encoded-line limit. Native fallback output has no application chunk cap;
|
||||
this audit does not establish multi-MiB native reads. Ordinary relay chunks are
|
||||
16 Ki characters. The actual main feed is `orca-runtime-on-pty-data.ts` and the
|
||||
ordinary daemon bound is in `daemon-stream-data-batcher.ts`.
|
||||
|
||||
These are per-owner last-input costs, not unbounded growth for a fixed set of
|
||||
PTYs and fixed-size frames. Owners consuming the same input can share the same
|
||||
backing string, so the three measurements must not be added as independent
|
||||
process costs. Unbind removes URL buffers and pending entries. This improves
|
||||
memory proportional to active owners; it does not establish the cause or growth
|
||||
rate of #19831 or #19768. Copy work is bounded by the small retained tails.
|
||||
@@ -0,0 +1,144 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1' || typeof global.gc !== 'function') {
|
||||
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1 node --expose-gc')
|
||||
}
|
||||
const root = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const replacements = {
|
||||
'command-code-output-status.ts': [
|
||||
'ownRetainedString(data.slice(-RECENT_TEXT_LIMIT))',
|
||||
'data.slice(-RECENT_TEXT_LIMIT)'
|
||||
],
|
||||
'advertised-url-parsing.ts': [
|
||||
'ownRetainedString(chunk.slice(-PER_PTY_BUFFER_LIMIT))',
|
||||
'chunk.slice(-PER_PTY_BUFFER_LIMIT)'
|
||||
],
|
||||
'advertised-url-watcher.ts': [
|
||||
'ownRetainedString(combined.slice(-PENDING_PRE_BIND_LIMIT))',
|
||||
'combined.slice(-PENDING_PRE_BIND_LIMIT)'
|
||||
]
|
||||
}
|
||||
const results = []
|
||||
const bundles = {}
|
||||
|
||||
function heapAfterGc() {
|
||||
global.gc()
|
||||
global.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function measure(makeOwner, validate, inputChars, count) {
|
||||
const before = heapAfterGc()
|
||||
const owners = Array.from({ length: count }, (_, index) => {
|
||||
const prefix = `${index}:`
|
||||
const suffix = `\nhttp://localhost:${4100 + index}`
|
||||
return makeOwner(
|
||||
`${prefix}${'x'.repeat(inputChars - prefix.length - suffix.length)}${suffix}`,
|
||||
index
|
||||
)
|
||||
})
|
||||
const heapDelta = heapAfterGc() - before
|
||||
owners.forEach(validate)
|
||||
return { inputChars, count, heapDelta }
|
||||
}
|
||||
|
||||
for (const fixed of [false, true]) {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: `
|
||||
export { createCommandCodeOutputStatusDetector } from './src/shared/command-code-output-status'
|
||||
export { AdvertisedUrlWatcher } from './src/main/ports/advertised-url-watcher'
|
||||
`,
|
||||
resolveDir: root,
|
||||
loader: 'ts'
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: fixed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'baseline-without-detector-tail-copy',
|
||||
setup(builder) {
|
||||
builder.onLoad(
|
||||
{
|
||||
filter:
|
||||
/(?:command-code-output-status|advertised-url-parsing|advertised-url-watcher)\.ts$/
|
||||
},
|
||||
async ({ path }) => {
|
||||
const source = await readFile(path, 'utf8')
|
||||
const replacement = Object.entries(replacements).find(([name]) =>
|
||||
path.endsWith(name)
|
||||
)?.[1]
|
||||
if (!replacement || !source.includes(replacement[0])) {
|
||||
throw new Error('The copy boundary changed; update the baseline transform')
|
||||
}
|
||||
return { contents: source.replaceAll(...replacement), loader: 'ts' }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundle = result.outputFiles[0].text
|
||||
bundles[fixed ? 'after' : 'before'] = createHash('sha256').update(bundle).digest('hex')
|
||||
const { createCommandCodeOutputStatusDetector, AdvertisedUrlWatcher } = await import(
|
||||
`data:text/javascript;base64,${Buffer.from(bundle).toString('base64')}`
|
||||
)
|
||||
for (const [inputChars, count] of [
|
||||
[64 * 1024, 32],
|
||||
[4 * 1024 * 1024, 8]
|
||||
]) {
|
||||
results.push({
|
||||
kind: 'command-code-detector',
|
||||
fixed,
|
||||
...measure(
|
||||
(data) => {
|
||||
const detector = createCommandCodeOutputStatusDetector({ onWorking: () => {} })
|
||||
detector.observe(data)
|
||||
return detector
|
||||
},
|
||||
(detector) => assert.equal(detector.observe('\nordinary output\n'), false),
|
||||
inputChars,
|
||||
count
|
||||
)
|
||||
})
|
||||
for (const bound of [true, false]) {
|
||||
results.push({
|
||||
kind: bound ? 'url-bound-pty' : 'url-before-binding',
|
||||
fixed,
|
||||
...measure(
|
||||
(data) => {
|
||||
const watcher = new AdvertisedUrlWatcher()
|
||||
if (bound) {
|
||||
watcher.bindPty('pty', 'workspace')
|
||||
}
|
||||
watcher.ingest('pty', data)
|
||||
return watcher
|
||||
},
|
||||
(watcher, index) => {
|
||||
watcher.bindPty('pty', 'workspace')
|
||||
watcher.ingest('pty', '/\n')
|
||||
assert.equal(
|
||||
watcher.lookup('workspace', 4100 + index)?.origin,
|
||||
`http://localhost:${4100 + index}`
|
||||
)
|
||||
watcher.unbindPty('pty')
|
||||
assert.equal(watcher.lookup('workspace', 4100 + index), undefined)
|
||||
},
|
||||
inputChars,
|
||||
count
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify({ node: process.version, platform: process.platform, bundles, results }, null, 2)
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"node": "v26.6.0",
|
||||
"platform": "darwin",
|
||||
"bundles": {
|
||||
"before": "139646a3dc9316a104f472c86e674be062da8fbdf5a35563624557d44d27f471",
|
||||
"after": "a2efe55354954f78e187fd65a247b51d97017776892979b631725ff686065168"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"kind": "command-code-detector",
|
||||
"fixed": false,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 2112688
|
||||
},
|
||||
{
|
||||
"kind": "url-bound-pty",
|
||||
"fixed": false,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 2173848
|
||||
},
|
||||
{
|
||||
"kind": "url-before-binding",
|
||||
"fixed": false,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 2148824
|
||||
},
|
||||
{
|
||||
"kind": "command-code-detector",
|
||||
"fixed": false,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 33557144
|
||||
},
|
||||
{
|
||||
"kind": "url-bound-pty",
|
||||
"fixed": false,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 33569168
|
||||
},
|
||||
{
|
||||
"kind": "url-before-binding",
|
||||
"fixed": false,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 33568936
|
||||
},
|
||||
{
|
||||
"kind": "command-code-detector",
|
||||
"fixed": true,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 41400
|
||||
},
|
||||
{
|
||||
"kind": "url-bound-pty",
|
||||
"fixed": true,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 204112
|
||||
},
|
||||
{
|
||||
"kind": "url-before-binding",
|
||||
"fixed": true,
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"heapDelta": 575512
|
||||
},
|
||||
{
|
||||
"kind": "command-code-detector",
|
||||
"fixed": true,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 6504
|
||||
},
|
||||
{
|
||||
"kind": "url-bound-pty",
|
||||
"fixed": true,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 47112
|
||||
},
|
||||
{
|
||||
"kind": "url-before-binding",
|
||||
"fixed": true,
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"heapDelta": 144504
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# Retained CI and terminal text tails
|
||||
|
||||
Capped V8 string slices can keep their entire original input alive. The affected
|
||||
CI excerpt cache accepts 128 entries of 16 KiB text, from downloads up to 64 MiB.
|
||||
GitLab's raw-trace clamp reaches the same shared excerpt function. Terminal
|
||||
session/eager/shutdown buffers, deferred reattach queues, recent-output buffers,
|
||||
and error surfaces also retained oversized parents despite their logical caps.
|
||||
|
||||
The fix reuses the existing shared `ownRetainedString` copier for CI, persisted
|
||||
session tails, and main/relay recent output. Renderer queues and errors reuse
|
||||
their existing `flattenRetainedSlice` helper. Content, Unicode, earlier-error
|
||||
selection, cache counts, and transport payloads stay identical. Ordinary
|
||||
untruncated terminal chunks keep their existing path. Main/relay recent output
|
||||
preserves chunk boundaries for path-candidate backfill.
|
||||
|
||||
Local persisted scrollback is already pruned; the session-buffer fix primarily
|
||||
covers remote or not-yet-classified owners. Queue and error fixes cover local
|
||||
and remote output. The main/relay recent-output buffer has a configurable cap,
|
||||
64 Ki characters by default.
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc docs/audits/retained-text-slices/reproduce.mjs
|
||||
```
|
||||
|
||||
The script bundles actual production functions. Its baseline removes only the
|
||||
six new copy boundaries in memory; production files are not changed. Each case
|
||||
retains eight distinct inputs: 2 Mi characters per CI log and 4 Mi characters
|
||||
per terminal input. It measures heap after GC and clears V8's independent legacy
|
||||
RegExp input reference. Bundle hashes and measurements are in
|
||||
[results.json](./results.json).
|
||||
|
||||
| Case | Returned bytes, all eight | Retained heap before | After |
|
||||
| -------------------------------- | ------------------------: | -------------------: | --------: |
|
||||
| GitHub long line | 131,072 | 16,787,512 | 145,224 |
|
||||
| GitHub earlier Unicode error | 131,064 | 33,577,840 | 112,744 |
|
||||
| GitLab long line | 131,072 | 16,793,640 | 147,312 |
|
||||
| Persisted terminal buffers | 4,194,304 | 33,555,624 | 4,195,032 |
|
||||
| Eager/pre-handler/shutdown tails | 4,194,304 | 33,556,040 | 4,202,608 |
|
||||
| Main/relay recent output | 524,288 | 33,558,752 | 527,384 |
|
||||
| Terminal error surfaces | 32,000 | 33,555,864 | 33,336 |
|
||||
| Deferred reattach tails | 4,194,304 | 33,565,384 | 4,198,352 |
|
||||
|
||||
Captured on macOS with Node v26.6.0. Heap samples include allocator/GC variation;
|
||||
the large separation is the relevant result. Regression tests also retain the
|
||||
actual error state and shutdown/reattach/recent-output queue objects.
|
||||
|
||||
Validation passed: 41 tests across six CI/provider/helper suites; 69 tests across
|
||||
six terminal storage/ownership/UTF-8 suites; 28 tests across three error/reattach
|
||||
suites; and a final 63 tests across seven recent-output/CI/terminal/copier suites.
|
||||
These are per-run counts and overlap. Full typecheck and changed-code quality pass.
|
||||
|
||||
All six cap/slice paths exist in `v1.4.198`. Neither #19831 nor #19768 establishes
|
||||
the CI-log viewing or oversized terminal inputs required for incident attribution.
|
||||
Copying costs scale with retained caps: 16 KiB per CI excerpt, 4,000 characters
|
||||
per error, 512 KiB for the largest byte-capped buffer, and 512 Ki characters for
|
||||
deferred reattach. The change does not reduce temporary original-input allocation.
|
||||
|
||||
The follow-up [PTY detector reproduction](../pty-detector-retention/README.md)
|
||||
adds three boundaries in the same PR: advertised-URL carries, output waiting for
|
||||
workspace binding, and Command Code status carries used by ordinary PTYs too.
|
||||
Thirty-two production-sized 64 Ki-character inputs retain about 2.1 MB in each
|
||||
isolated baseline. Owned carries reduce that to about 41 KB, 204 KB, or 575 KB,
|
||||
including the different owner objects. These per-owner costs are not an
|
||||
unbounded growth curve, and readers of the same input can share its parent.
|
||||
The follow-up adds 164 passing tests across five detector/URL/copier suites.
|
||||
|
||||
The [Claude task metadata reproduction](../claude-task-retention/README.md) adds
|
||||
the shared 512-character description/name boundary. JSON-parsed task frames
|
||||
retained their parents in the actual live, settled, and recently removed tracker
|
||||
entries. Eight 4 Mi-character inputs retained about 32 MiB before the copy and
|
||||
7–12 KB afterward; 32 smaller 64 Ki-character inputs retained about 2.1 MB before
|
||||
and 25–45 KB afterward. These synthetic fields establish a retaining mechanism,
|
||||
not the trigger of a reported incident.
|
||||
@@ -0,0 +1,161 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { build } from 'esbuild'
|
||||
|
||||
if (process.env.ORCA_BACKGROUND_LAUNCH !== '1' || typeof global.gc !== 'function') {
|
||||
throw new Error('Run with ORCA_BACKGROUND_LAUNCH=1 node --expose-gc')
|
||||
}
|
||||
const root = fileURLToPath(new URL('../../../', import.meta.url))
|
||||
const replacements = {
|
||||
'recent-pty-output-buffer.ts': [
|
||||
'this.chunks = [data.length > this.limit ? ownRetainedString(data.slice(-this.limit)) : data]',
|
||||
'this.chunks = [data.slice(-this.limit)]'
|
||||
],
|
||||
'check-job-log-tail-slice.ts': [
|
||||
'return ownRetainedString(buildCheckLogTail(logText))',
|
||||
'return buildCheckLogTail(logText)'
|
||||
],
|
||||
'workspace-session-terminal-buffers.ts': [
|
||||
'return ownRetainedString(\n clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text\n )',
|
||||
'return clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text'
|
||||
],
|
||||
'pty-eager-buffer-clamp.ts': [
|
||||
'data: tail.text.length < data.length ? flattenRetainedSlice(tail.text) : tail.text',
|
||||
'data: tail.text'
|
||||
],
|
||||
'terminal-error-accumulation.ts': ['return flattenRetainedSlice(bounded)', 'return bounded'],
|
||||
'deferred-reattach-live-data-queue.ts': [
|
||||
'flattenRetainedSlice(chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS))',
|
||||
'chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS)'
|
||||
]
|
||||
}
|
||||
const results = []
|
||||
const bundles = {}
|
||||
const parentChars = 2 * 1024 * 1024
|
||||
const count = 8
|
||||
|
||||
function measure(excerpt, makeLog) {
|
||||
global.gc()
|
||||
const before = process.memoryUsage().heapUsed
|
||||
const retained = Array.from({ length: count }, (_, index) => excerpt(makeLog(index)))
|
||||
// Clear V8's independent legacy RegExp input reference before measuring our retained values.
|
||||
void /probe/.test('probe')
|
||||
global.gc()
|
||||
global.gc()
|
||||
const heapDelta = process.memoryUsage().heapUsed - before
|
||||
return {
|
||||
entries: retained.length,
|
||||
logicalChars: retained.reduce((total, text) => total + text.length, 0),
|
||||
logicalBytes: retained.reduce((total, text) => total + Buffer.byteLength(text), 0),
|
||||
heapDelta
|
||||
}
|
||||
}
|
||||
|
||||
for (const fixed of [false, true]) {
|
||||
const result = await build({
|
||||
stdin: {
|
||||
contents: `
|
||||
export { RecentPtyOutputBuffer } from './src/main/runtime/recent-pty-output-buffer'
|
||||
export { sliceCheckLogTail } from './src/shared/check-job-log-tail-slice'
|
||||
export { gitLabJobTraceToLogExcerpt } from './src/shared/gitlab-job-log-excerpt'
|
||||
export { capTerminalScrollbackSessionBuffer } from './src/shared/workspace-session-terminal-buffers'
|
||||
export { clampUtf8Tail } from './src/renderer/src/components/terminal-pane/pty-eager-buffer-clamp'
|
||||
export { boundTerminalErrorSurface } from './src/renderer/src/components/terminal-pane/terminal-error-accumulation'
|
||||
export { DeferredReattachLiveDataQueue } from './src/renderer/src/components/terminal-pane/deferred-reattach-live-data-queue'
|
||||
`,
|
||||
resolveDir: root,
|
||||
loader: 'ts'
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
write: false,
|
||||
plugins: fixed
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'baseline-without-retained-tail-copy',
|
||||
setup(builder) {
|
||||
builder.onLoad(
|
||||
{
|
||||
filter:
|
||||
/(?:recent-pty-output-buffer|check-job-log-tail-slice|workspace-session-terminal-buffers|pty-eager-buffer-clamp|terminal-error-accumulation|deferred-reattach-live-data-queue)\.ts$/
|
||||
},
|
||||
async ({ path }) => {
|
||||
const source = await readFile(path, 'utf8')
|
||||
const replacement = Object.entries(replacements).find(([name]) =>
|
||||
path.endsWith(name)
|
||||
)?.[1]
|
||||
if (!replacement || !source.includes(replacement[0])) {
|
||||
throw new Error('The copy boundary changed; update the baseline transform')
|
||||
}
|
||||
return {
|
||||
contents: source.replaceAll(...replacement),
|
||||
loader: 'ts'
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
const bundle = result.outputFiles[0].text
|
||||
bundles[fixed ? 'after' : 'before'] = createHash('sha256').update(bundle).digest('hex')
|
||||
const {
|
||||
sliceCheckLogTail,
|
||||
gitLabJobTraceToLogExcerpt,
|
||||
capTerminalScrollbackSessionBuffer,
|
||||
clampUtf8Tail,
|
||||
boundTerminalErrorSurface,
|
||||
DeferredReattachLiveDataQueue,
|
||||
RecentPtyOutputBuffer
|
||||
} = await import(`data:text/javascript;base64,${Buffer.from(bundle).toString('base64')}`)
|
||||
for (const [kind, makeLog, excerpt] of [
|
||||
['github-long-line', (i) => `${i}:${'x'.repeat(parentChars)}`, sliceCheckLogTail],
|
||||
[
|
||||
'github-earlier-error',
|
||||
(i) => `error: ${i}:${'界'.repeat(parentChars)}\n${'recent\n'.repeat(100)}`,
|
||||
sliceCheckLogTail
|
||||
],
|
||||
['gitlab-long-line', (i) => `${i}:${'x'.repeat(parentChars)}`, gitLabJobTraceToLogExcerpt],
|
||||
[
|
||||
'terminal-session-buffer',
|
||||
(i) => `${i}:${'x'.repeat(parentChars * 2)}`,
|
||||
capTerminalScrollbackSessionBuffer
|
||||
],
|
||||
[
|
||||
'terminal-eager-buffer',
|
||||
(i) => `${i}:${'x'.repeat(parentChars * 2)}`,
|
||||
(text) => clampUtf8Tail(text, 512 * 1024).data
|
||||
],
|
||||
[
|
||||
'terminal-recent-output',
|
||||
(i) => `${i}:${'x'.repeat(parentChars * 2)}`,
|
||||
(data) => {
|
||||
const buffer = new RecentPtyOutputBuffer()
|
||||
buffer.append(data)
|
||||
return buffer.read()
|
||||
}
|
||||
],
|
||||
['terminal-error', (i) => `${'x'.repeat(parentChars * 2)}:${i}`, boundTerminalErrorSurface],
|
||||
[
|
||||
'terminal-deferred-reattach',
|
||||
(i) => `${i}:${'x'.repeat(parentChars * 2)}`,
|
||||
(data) => {
|
||||
const queue = new DeferredReattachLiveDataQueue()
|
||||
queue.enqueue({ data, ptyId: 'p', streamGeneration: 1 })
|
||||
return queue.takeAll()[0].data
|
||||
}
|
||||
]
|
||||
]) {
|
||||
results.push({ kind, fixed, ...measure(excerpt, makeLog) })
|
||||
}
|
||||
}
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{ node: process.version, platform: process.platform, parentChars, count, bundles, results },
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
{
|
||||
"node": "v26.6.0",
|
||||
"platform": "darwin",
|
||||
"parentChars": 2097152,
|
||||
"count": 8,
|
||||
"bundles": {
|
||||
"before": "b1f295283888921d8d473649185b5146ed8379bc8344b2bf04a0a4fcf334b5ec",
|
||||
"after": "74444cedf1564c7092a46058f331ad2acc55b222b09340d5a19ca5a76de611fb"
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"kind": "github-long-line",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 131072,
|
||||
"logicalBytes": 131072,
|
||||
"heapDelta": 16787512
|
||||
},
|
||||
{
|
||||
"kind": "github-earlier-error",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 43816,
|
||||
"logicalBytes": 131064,
|
||||
"heapDelta": 33577840
|
||||
},
|
||||
{
|
||||
"kind": "gitlab-long-line",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 131072,
|
||||
"logicalBytes": 131072,
|
||||
"heapDelta": 16793640
|
||||
},
|
||||
{
|
||||
"kind": "terminal-session-buffer",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 33555624
|
||||
},
|
||||
{
|
||||
"kind": "terminal-eager-buffer",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 33556040
|
||||
},
|
||||
{
|
||||
"kind": "terminal-recent-output",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 524288,
|
||||
"logicalBytes": 524288,
|
||||
"heapDelta": 33558752
|
||||
},
|
||||
{
|
||||
"kind": "terminal-error",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 32000,
|
||||
"logicalBytes": 32000,
|
||||
"heapDelta": 33555864
|
||||
},
|
||||
{
|
||||
"kind": "terminal-deferred-reattach",
|
||||
"fixed": false,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 33565384
|
||||
},
|
||||
{
|
||||
"kind": "github-long-line",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 131072,
|
||||
"logicalBytes": 131072,
|
||||
"heapDelta": 145224
|
||||
},
|
||||
{
|
||||
"kind": "github-earlier-error",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 43816,
|
||||
"logicalBytes": 131064,
|
||||
"heapDelta": 112744
|
||||
},
|
||||
{
|
||||
"kind": "gitlab-long-line",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 131072,
|
||||
"logicalBytes": 131072,
|
||||
"heapDelta": 147312
|
||||
},
|
||||
{
|
||||
"kind": "terminal-session-buffer",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 4195032
|
||||
},
|
||||
{
|
||||
"kind": "terminal-eager-buffer",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 4202608
|
||||
},
|
||||
{
|
||||
"kind": "terminal-recent-output",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 524288,
|
||||
"logicalBytes": 524288,
|
||||
"heapDelta": 527384
|
||||
},
|
||||
{
|
||||
"kind": "terminal-error",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 32000,
|
||||
"logicalBytes": 32000,
|
||||
"heapDelta": 33336
|
||||
},
|
||||
{
|
||||
"kind": "terminal-deferred-reattach",
|
||||
"fixed": true,
|
||||
"entries": 8,
|
||||
"logicalChars": 4194304,
|
||||
"logicalBytes": 4194304,
|
||||
"heapDelta": 4198352
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
# Retained terminal mode scan tails
|
||||
|
||||
The kitty keyboard tracker and daemon mouse-mode mirror retain an incomplete
|
||||
escape-sequence tail of at most 4,096 UTF-16 code units. A V8 sliced string can
|
||||
keep the entire consumed input alive through that small tail. An ordinary
|
||||
split grouped mode sequence, `ESC[?1049;2004;1000;`, is enough: its 18-character
|
||||
tail retains each input backing string while its parser stays idle.
|
||||
|
||||
The correction copies only accepted incomplete tails through the existing
|
||||
`ownRetainedString` helper. Empty/rejected tails and short ESC/CSI prefixes keep
|
||||
their existing behavior; the helper leaves strings shorter than 13 code units
|
||||
alone. Parser state, live/replay semantics, stack caps, mode flags, and wire
|
||||
content are unchanged. These are additional boundaries in
|
||||
[#20960](https://github.com/stablyai/orca/pull/20960), alongside the
|
||||
[PTY detector carries](../pty-detector-retention/README.md).
|
||||
|
||||
## Reproduce
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 node --expose-gc --max-old-space-size=192 docs/audits/terminal-mode-tail-retention/reproduce.cjs
|
||||
```
|
||||
|
||||
Run the same script with the installed Electron executable, setting
|
||||
`ELECTRON_RUN_AS_NODE=1` and `ORCA_BACKGROUND_LAUNCH=1`, and passing the same
|
||||
Node flags. This launches no app window or native PTY. Each run has a 30-second
|
||||
deadline and writes either [Node results](./node-results.json) or
|
||||
[Electron results](./electron-results.json).
|
||||
|
||||
The loader reads the actual five source modules, verifies their fixed hashes,
|
||||
reverses only the three copy calls/imports in memory for the baseline, and
|
||||
verifies the resulting baseline hashes. All evaluated module and bundle hashes
|
||||
are recorded. It needs no Git history, absolute development paths, or ignored
|
||||
notes. CRLF source text is normalized before hashing. The parsers and flag
|
||||
parser match `v1.4.198`; all five modules match the pre-extension topic
|
||||
`8d599520e44654a5c28e9930e3070c00d6499931`, except for these copy calls. This
|
||||
tests current dependencies and the selected source modules, not a historical
|
||||
application binary. See [source versions](./source-versions.json).
|
||||
|
||||
Each runtime checks 42 bounded heap cases: baseline, fixed Buffer copier, and
|
||||
fixed Bufferless copier; kitty live/replay, mouse live; 32 distinct 64-Ki-character
|
||||
inputs and eight 4-Mi-character inputs; short, complete, oversized, and C1-CSI
|
||||
tail controls. Completion must reconstruct the correct modes and release the
|
||||
large backing strings. Additional controls preserve replay push idempotence,
|
||||
the 16-frame live stack cap, alternate-screen state, snapshot unknownness,
|
||||
mouse encodings, and RIS with a trailing partial sequence.
|
||||
|
||||
Both runtimes pass all 42 cases. Representative live-path heap deltas in bytes:
|
||||
|
||||
| Runtime | Parser | Input × owners | Baseline | Buffer copy | Bufferless copy |
|
||||
| --------------------- | ------ | -------------- | ---------: | ----------: | --------------: |
|
||||
| Node 26 | Kitty | 64 Ki × 32 | 2,120,952 | 18,376 | 9,480 |
|
||||
| Node 26 | Mouse | 64 Ki × 32 | 2,111,984 | 15,112 | 9,336 |
|
||||
| Node 26 | Kitty | 4 Mi × 8 | 33,557,944 | 3,448 | 3,448 |
|
||||
| Node 26 | Mouse | 4 Mi × 8 | 33,557,072 | 1,312 | 1,312 |
|
||||
| Electron 43 / Node 24 | Kitty | 64 Ki × 32 | 2,111,864 | 12,244 | 5,192 |
|
||||
| Electron 43 / Node 24 | Mouse | 64 Ki × 32 | 2,103,444 | 14,432 | 8,004 |
|
||||
| Electron 43 / Node 24 | Kitty | 4 Mi × 8 | 33,556,316 | 1,884 | 2,604 |
|
||||
| Electron 43 / Node 24 | Mouse | 4 Mi × 8 | 33,556,172 | 776 | 752 |
|
||||
|
||||
Heap readings include owner overhead and follow forced GC. The harness clears
|
||||
V8's last successful regexp input identically in baseline and fixed cases to
|
||||
isolate per-owner storage. That independent process-wide regexp reference can
|
||||
keep a most-recent input alive until another successful match; this change does
|
||||
not eliminate it. The Bufferless selection is memoized while Buffer is absent,
|
||||
then Buffer is restored before measuring; it exercises the actual renderer
|
||||
fallback without running a browser renderer.
|
||||
|
||||
Two permanent kitty heap regressions fail before the correction at 33,560,832
|
||||
and 33,575,040 retained bytes against a 2-MiB limit. They also verify that the
|
||||
retained prefix completes correctly and that replay/pop/snapshot state remains
|
||||
valid. Existing parser and copier tests provide the wider protocol controls.
|
||||
The two mouse regressions likewise fail before the correction at 33,559,240 and
|
||||
33,573,200 bytes, and pass afterward with both CSI encodings. The five-suite
|
||||
run passes 98 tests, including actual headless-emulator mode snapshots; Node
|
||||
and renderer TypeScript checks pass.
|
||||
|
||||
## Callers and lifetime
|
||||
|
||||
- Kitty renderer panes create or reuse one tracker per pane in
|
||||
`connect-pane-pty.ts:160`. `write-pty-output-to-xterm.ts:23` feeds application
|
||||
output; `apply-reattach-payload.ts` and `hidden-output-seq-and-skip.ts` feed
|
||||
replay. Fresh spawn and exit reset it, and
|
||||
`terminal-pane-pane-closed.ts:69` deletes the map entry. Dashboard previews
|
||||
own another tracker per effect (`AgentTerminalPreview.tsx:116`); cleanup
|
||||
removes its listeners and disposes its terminal.
|
||||
- Main's `orca-runtime-capture-provider-terminal-buffer.ts:23` registers
|
||||
temporary live scanners during provider snapshot acquisition and removes
|
||||
them in `finally`. It creates a persistent tracker only after observing an
|
||||
alternate-screen transition (`:48–57`). `orca-runtime-on-pty-data.ts:30`
|
||||
feeds those trackers before later output processing. Exit, floating PTY
|
||||
liveness cleanup, and provider generation reset delete the persistent entry.
|
||||
- The daemon does not directly instantiate the kitty tracker, despite its
|
||||
old class comment: its kitty flags come from xterm. No mobile bundle imports
|
||||
this class. Mobile can exercise main-side snapshot acquisition; SSH output
|
||||
can reach main and renderer trackers through the existing provider routes.
|
||||
- Mouse mirrors are owned by `HeadlessEmulator` (`headless-emulator.ts:59`).
|
||||
Async writes scan after xterm parses the data (`:190`); synchronous live and
|
||||
cold-restore writes scan at `:224`. Both daemon sessions and main's headless
|
||||
projections use this emulator. It therefore also covers local/remote host
|
||||
emulators serving mobile clients. Emulator disposal stops future writes;
|
||||
eventual owner release removes the mirror. Completing/replacing its tail
|
||||
also releases the old backing string. No ownership or shutdown rule changes.
|
||||
|
||||
## Scope and limits
|
||||
|
||||
This is a per-owner last-input cost. It does not grow indefinitely with a fixed
|
||||
set of parsers and bounded input chunks, and further output often completes or
|
||||
replaces the tail. Multiple readers of the same input can share its backing
|
||||
storage; do not sum their measurements as independent process memory.
|
||||
|
||||
Ordinary daemon bulk frames delivered to main are at most 64 Ki characters
|
||||
(`daemon-stream-data-batcher.ts:35`), and ordinary relay output slices are
|
||||
16 Ki characters (`relay/pty-handler.ts:343`). Mouse scanning inside the daemon
|
||||
happens before outgoing stream framing. The 64-Ki cases demonstrate the issue
|
||||
at a normal main-input bound; the 4-Mi cases amplify the mechanism, not a claim
|
||||
that ordinary native reads or daemon frames have that size. Replay inputs and
|
||||
transformed streams follow their own existing limits. No network, application
|
||||
renderer, operating-system PTY, or incident heap was used in this proof.
|
||||
|
||||
This reduces retained output in local and SSH paths without changing published
|
||||
terminal content. It neither establishes the trigger in #19831/#19768 nor
|
||||
explains a reported sustained growth rate or multi-gigabyte incident by itself.
|
||||
@@ -0,0 +1,467 @@
|
||||
{
|
||||
"node": "v24.21.0",
|
||||
"electron": "43.7.0",
|
||||
"v8": "15.0.245.31-electron.0",
|
||||
"platform": "darwin",
|
||||
"runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291",
|
||||
"loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789",
|
||||
"pendingTailCodeUnits": 18,
|
||||
"clearedRegexStatics": true,
|
||||
"bundles": {
|
||||
"baseline": {
|
||||
"bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
},
|
||||
"fixed-buffer": {
|
||||
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
},
|
||||
"fixed-fallback": {
|
||||
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
}
|
||||
},
|
||||
"reports": [
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2111864,
|
||||
"afterCompletionDelta": 29832
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33556316,
|
||||
"afterCompletionDelta": 9244
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2102416,
|
||||
"afterCompletionDelta": 4916
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33556304,
|
||||
"afterCompletionDelta": 1968
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 1756,
|
||||
"afterCompletionDelta": 1712
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1652,
|
||||
"afterCompletionDelta": 2880
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1652,
|
||||
"afterCompletionDelta": 3024
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 33556316,
|
||||
"afterCompletionDelta": 1692
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2103444,
|
||||
"afterCompletionDelta": 5584
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33556172,
|
||||
"afterCompletionDelta": 1496
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 672,
|
||||
"afterCompletionDelta": 644
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 520,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 520,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 33555224,
|
||||
"afterCompletionDelta": 560
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 12244,
|
||||
"afterCompletionDelta": 11632
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 1884,
|
||||
"afterCompletionDelta": 1640
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 5148,
|
||||
"afterCompletionDelta": 4176
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 1884,
|
||||
"afterCompletionDelta": 1640
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 1756,
|
||||
"afterCompletionDelta": 1688
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1652,
|
||||
"afterCompletionDelta": 2808
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1628,
|
||||
"afterCompletionDelta": 1640
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 1884,
|
||||
"afterCompletionDelta": 1652
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 14432,
|
||||
"afterCompletionDelta": 13692
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 776,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 672,
|
||||
"afterCompletionDelta": 628
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 520,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 520,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 792,
|
||||
"afterCompletionDelta": 560
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 5192,
|
||||
"afterCompletionDelta": 4180
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2604,
|
||||
"afterCompletionDelta": 2360
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 5176,
|
||||
"afterCompletionDelta": 7104
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 1884,
|
||||
"afterCompletionDelta": 1640
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 2744,
|
||||
"afterCompletionDelta": 2676
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1652,
|
||||
"afterCompletionDelta": 1664
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": -3344,
|
||||
"afterCompletionDelta": -3332
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 1884,
|
||||
"afterCompletionDelta": 1652
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 8004,
|
||||
"afterCompletionDelta": 6992
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 752,
|
||||
"afterCompletionDelta": 508
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 672,
|
||||
"afterCompletionDelta": 1464
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 520,
|
||||
"afterCompletionDelta": 532
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 1548,
|
||||
"afterCompletionDelta": 1560
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 776,
|
||||
"afterCompletionDelta": 544
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const Module = require('node:module')
|
||||
const { createHash } = require('node:crypto')
|
||||
const { build } = require('esbuild')
|
||||
|
||||
const root = path.resolve(__dirname, '../../..')
|
||||
const sha = (value) => createHash('sha256').update(value).digest('hex')
|
||||
const read = (file) => fs.readFileSync(file, 'utf8').replaceAll('\r\n', '\n')
|
||||
const versionsText = read(path.join(__dirname, 'source-versions.json'))
|
||||
const versions = JSON.parse(versionsText)
|
||||
|
||||
async function loadSource(fixed) {
|
||||
const evaluatedSources = {}
|
||||
const built = await build({
|
||||
stdin: {
|
||||
contents: [
|
||||
"export { TerminalKittyKeyboardModeTracker } from './src/shared/terminal-kitty-keyboard-mode-tracker'",
|
||||
"export { TerminalMouseModeMirror } from './src/main/daemon/terminal-mouse-mode-mirror'",
|
||||
"export { ownRetainedString, resetOwnRetainedStringCopier } from './src/shared/own-retained-string'"
|
||||
].join('\n'),
|
||||
resolveDir: root
|
||||
},
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
format: 'cjs',
|
||||
write: false,
|
||||
plugins: [
|
||||
{
|
||||
name: 'hash-fenced-retained-mode-tails',
|
||||
setup(builder) {
|
||||
builder.onLoad({ filter: /\.ts$/ }, ({ path: filename }) => {
|
||||
const relative = path.relative(root, filename).split(path.sep).join('/')
|
||||
const version = versions.sources[relative]
|
||||
assert.ok(version, `Unreviewed source: ${relative}`)
|
||||
let contents = read(filename)
|
||||
assert.equal(sha(contents), version.fixedSha256, `Fixed source changed: ${relative}`)
|
||||
if (!fixed && version.reverse) {
|
||||
for (const { from, to, count } of version.reverse) {
|
||||
assert.equal(contents.split(from).length - 1, count)
|
||||
contents = contents.replaceAll(from, to)
|
||||
}
|
||||
}
|
||||
const expected = fixed ? version.fixedSha256 : version.baselineSha256
|
||||
assert.equal(sha(contents), expected, `Evaluated source changed: ${relative}`)
|
||||
evaluatedSources[relative] = sha(contents)
|
||||
return { contents, loader: 'ts' }
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
assert.deepEqual(Object.keys(evaluatedSources).sort(), Object.keys(versions.sources).sort())
|
||||
const filename = path.join(__dirname, fixed ? 'fixed-bundle.cjs' : 'baseline-bundle.cjs')
|
||||
const loaded = new Module(filename, module)
|
||||
loaded.filename = filename
|
||||
loaded.paths = Module._nodeModulePaths(root)
|
||||
loaded._compile(built.outputFiles[0].text, filename)
|
||||
return {
|
||||
...loaded.exports,
|
||||
evaluatedSources,
|
||||
bundleSha256: sha(built.outputFiles[0].text),
|
||||
sourceVersionsSha256: sha(versionsText)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { loadSource, sha, read }
|
||||
@@ -0,0 +1,467 @@
|
||||
{
|
||||
"node": "v26.6.0",
|
||||
"electron": null,
|
||||
"v8": "14.6.202.34-node.26",
|
||||
"platform": "darwin",
|
||||
"runnerSha256": "b78dc54a08345732235d4fcc1f72ca1534b00f89280981beda00667d22b8f291",
|
||||
"loaderSha256": "730da9091abba3997432657aef4a2e6e7a3dd0e9218ea78f1838806b6c0fa789",
|
||||
"pendingTailCodeUnits": 18,
|
||||
"clearedRegexStatics": true,
|
||||
"bundles": {
|
||||
"baseline": {
|
||||
"bundleSha256": "6023f5d6868f5db601f6883acda5d56990d204084a6a3c812799aff1e533a5b0",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
},
|
||||
"fixed-buffer": {
|
||||
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
},
|
||||
"fixed-fallback": {
|
||||
"bundleSha256": "bbae10735fc999eea9dafffcd0e1fc4c47e1a8f1daf0045ed074b08cf3a93d45",
|
||||
"evaluatedSources": {
|
||||
"src/shared/own-retained-string.ts": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
|
||||
"src/shared/owned-utf16-suffix.ts": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
},
|
||||
"sourceVersionsSha256": "29cde88cb882b2d7726a2d4a077484217076090acc009f47a805e368f597e51a"
|
||||
}
|
||||
},
|
||||
"reports": [
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2120952,
|
||||
"afterCompletionDelta": 31112
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33557944,
|
||||
"afterCompletionDelta": 3840
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2107000,
|
||||
"afterCompletionDelta": 9336
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33557928,
|
||||
"afterCompletionDelta": 3752
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 3320,
|
||||
"afterCompletionDelta": 3288
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3176,
|
||||
"afterCompletionDelta": 4792
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3176,
|
||||
"afterCompletionDelta": 4904
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 33557944,
|
||||
"afterCompletionDelta": 3232
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 2111984,
|
||||
"afterCompletionDelta": 13904
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 33557072,
|
||||
"afterCompletionDelta": 2272
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 1232,
|
||||
"afterCompletionDelta": 1232
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 992,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 992,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "baseline",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 33555840,
|
||||
"afterCompletionDelta": 1048
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 18376,
|
||||
"afterCompletionDelta": 25472
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 3448,
|
||||
"afterCompletionDelta": 3144
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 9400,
|
||||
"afterCompletionDelta": 8216
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 3696,
|
||||
"afterCompletionDelta": 3392
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 3320,
|
||||
"afterCompletionDelta": 3240
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3176,
|
||||
"afterCompletionDelta": 4648
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3128,
|
||||
"afterCompletionDelta": 3144
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 3448,
|
||||
"afterCompletionDelta": 3152
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 15112,
|
||||
"afterCompletionDelta": 14376
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 1312,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 1232,
|
||||
"afterCompletionDelta": 1200
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 992,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 992,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "fixed-buffer",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 1344,
|
||||
"afterCompletionDelta": 1048
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 9480,
|
||||
"afterCompletionDelta": 8216
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 3448,
|
||||
"afterCompletionDelta": 4048
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 13216,
|
||||
"afterCompletionDelta": 11952
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scanReplay",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 3448,
|
||||
"afterCompletionDelta": 3144
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 3320,
|
||||
"afterCompletionDelta": 3240
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3176,
|
||||
"afterCompletionDelta": 3192
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 3912,
|
||||
"afterCompletionDelta": 3928
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "kitty",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 3448,
|
||||
"afterCompletionDelta": 3152
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 65536,
|
||||
"count": 32,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 9336,
|
||||
"afterCompletionDelta": 8072
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 18,
|
||||
"heapDelta": 1312,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 2,
|
||||
"heapDelta": 1232,
|
||||
"afterCompletionDelta": 8184
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 992,
|
||||
"afterCompletionDelta": 1008
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 0,
|
||||
"heapDelta": 2280,
|
||||
"afterCompletionDelta": 2296
|
||||
},
|
||||
{
|
||||
"variant": "fixed-fallback",
|
||||
"kind": "mouse",
|
||||
"method": "scan",
|
||||
"inputChars": 4194304,
|
||||
"count": 8,
|
||||
"expectedTailLength": 17,
|
||||
"heapDelta": 1312,
|
||||
"afterCompletionDelta": 1016
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
const assert = require('node:assert/strict')
|
||||
const fs = require('node:fs')
|
||||
const path = require('node:path')
|
||||
const { loadSource, sha, read } = require('./load-source.cjs')
|
||||
|
||||
assert.equal(process.env.ORCA_BACKGROUND_LAUNCH, '1')
|
||||
assert.equal(typeof global.gc, 'function', 'Run with --expose-gc')
|
||||
const pending = '\x1b[?1049;2004;1000;'
|
||||
const sizes = [
|
||||
[64 * 1024, 32],
|
||||
[4 * 1024 * 1024, 8]
|
||||
]
|
||||
|
||||
async function heap() {
|
||||
// Isolate owner storage from V8's process-wide last successful regexp input.
|
||||
;/reset/.test('reset')
|
||||
for (let round = 0; round < 4; round++) {
|
||||
await new Promise((resolve) => setImmediate(resolve))
|
||||
global.gc()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function createOwner(Owner, method, chars, index, suffix) {
|
||||
const prefix = `${index}:`
|
||||
const data = `${prefix}${'x'.repeat(chars - prefix.length - suffix.length)}${suffix}`
|
||||
const owner = new Owner()
|
||||
owner[method](data)
|
||||
return owner
|
||||
}
|
||||
|
||||
async function measure(Owner, variant, kind, method, inputChars, count, suffix = pending) {
|
||||
const beforeHeap = await heap()
|
||||
const owners = Array.from({ length: count }, (_, index) =>
|
||||
createOwner(Owner, method, inputChars, index, suffix)
|
||||
)
|
||||
const heapDelta = (await heap()) - beforeHeap
|
||||
const expectedTailLength = suffix.length > 4096 || suffix.endsWith('h') ? 0 : suffix.length
|
||||
assert.ok(owners.every((owner) => owner.scanTail.length === expectedTailLength))
|
||||
const retainsParent = variant === 'baseline' && expectedTailLength >= 13
|
||||
assert.ok(
|
||||
retainsParent ? heapDelta > inputChars * count * 0.75 : heapDelta < 768 * 1024,
|
||||
JSON.stringify({ variant, kind, method, inputChars, count, expectedTailLength, heapDelta })
|
||||
)
|
||||
|
||||
for (const owner of owners) {
|
||||
owner[method]('1006h')
|
||||
if (kind === 'kitty') {
|
||||
if (suffix === pending) {
|
||||
assert.equal(owner.isAlternateScreen, true)
|
||||
}
|
||||
owner[method]('\x1b[>3u')
|
||||
assert.equal(owner.flags, 3)
|
||||
owner.scan('\x1b[<u')
|
||||
assert.equal(owner.flags, 0)
|
||||
} else if (expectedTailLength >= 13) {
|
||||
assert.equal(owner.mouseTrackingMode, 'vt200')
|
||||
assert.equal(owner.sgrMouseMode, true)
|
||||
}
|
||||
assert.equal(owner.scanTail, '')
|
||||
}
|
||||
const afterCompletionDelta = (await heap()) - beforeHeap
|
||||
assert.ok(
|
||||
afterCompletionDelta < 768 * 1024,
|
||||
JSON.stringify({ variant, kind, method, afterCompletionDelta })
|
||||
)
|
||||
for (const owner of owners) {
|
||||
if (kind === 'kitty') {
|
||||
owner.resetForSnapshot()
|
||||
assert.equal(owner.snapshotFlags, undefined)
|
||||
} else {
|
||||
owner.scan('\x1bc')
|
||||
assert.equal(owner.mouseTrackingMode, 'none')
|
||||
assert.equal(owner.sgrMouseMode, false)
|
||||
}
|
||||
}
|
||||
return {
|
||||
variant,
|
||||
kind,
|
||||
method,
|
||||
inputChars,
|
||||
count,
|
||||
expectedTailLength,
|
||||
heapDelta,
|
||||
afterCompletionDelta
|
||||
}
|
||||
}
|
||||
|
||||
function configureCopier(api, fallback) {
|
||||
api.resetOwnRetainedStringCopier()
|
||||
const originalBuffer = globalThis.Buffer
|
||||
try {
|
||||
if (fallback) {
|
||||
globalThis.Buffer = undefined
|
||||
}
|
||||
assert.equal(api.ownRetainedString(pending), pending)
|
||||
} finally {
|
||||
globalThis.Buffer = originalBuffer
|
||||
}
|
||||
}
|
||||
|
||||
function behavior(Tracker, Mirror) {
|
||||
const replay = new Tracker()
|
||||
for (let index = 0; index < 70; index++) {
|
||||
replay.scanReplay('\x1b[>3u')
|
||||
}
|
||||
assert.equal(replay.mainStack.length, 0)
|
||||
assert.equal(replay.flags, 3)
|
||||
replay.scan('\x1b[<u')
|
||||
assert.equal(replay.flags, 0)
|
||||
const live = new Tracker()
|
||||
for (let index = 0; index < 70; index++) {
|
||||
live.scan('\x1b[>3u')
|
||||
}
|
||||
assert.equal(live.mainStack.length, 16)
|
||||
live.scan('\x1b[?1049h\x1b[>5u')
|
||||
assert.equal(live.altStack.length, 1)
|
||||
live.scan('\x1b[?1049l')
|
||||
assert.equal(live.flags, 3)
|
||||
live.scan(`\x1bc${pending}`)
|
||||
assert.equal(live.flags, 0)
|
||||
assert.equal(live.scanTail, pending)
|
||||
live.scan('1006h')
|
||||
assert.equal(live.isAlternateScreen, true)
|
||||
live.reset()
|
||||
assert.equal(live.scanTail, '')
|
||||
assert.equal(live.snapshotFlags, 0)
|
||||
|
||||
const mouse = new Mirror()
|
||||
mouse.scan('\x1b[?1003;1016h')
|
||||
assert.equal(mouse.mouseTrackingMode, 'any')
|
||||
assert.equal(mouse.sgrMousePixelsMode, true)
|
||||
mouse.scan('\x9b?1002;1006h')
|
||||
assert.equal(mouse.mouseTrackingMode, 'drag')
|
||||
assert.equal(mouse.sgrMouseMode, true)
|
||||
assert.equal(mouse.sgrMousePixelsMode, false)
|
||||
mouse.scan(`\x1bc${pending}`)
|
||||
assert.equal(mouse.mouseTrackingMode, 'none')
|
||||
assert.equal(mouse.scanTail, pending)
|
||||
mouse.scan('1006h')
|
||||
assert.equal(mouse.mouseTrackingMode, 'vt200')
|
||||
assert.equal(mouse.sgrMouseMode, true)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const reports = []
|
||||
const bundles = {}
|
||||
for (const variant of ['baseline', 'fixed-buffer', 'fixed-fallback']) {
|
||||
const api = await loadSource(variant !== 'baseline')
|
||||
const { TerminalKittyKeyboardModeTracker: Tracker, TerminalMouseModeMirror: Mirror } = api
|
||||
bundles[variant] = {
|
||||
bundleSha256: api.bundleSha256,
|
||||
evaluatedSources: api.evaluatedSources,
|
||||
sourceVersionsSha256: api.sourceVersionsSha256
|
||||
}
|
||||
configureCopier(api, variant === 'fixed-fallback')
|
||||
behavior(Tracker, Mirror)
|
||||
for (const [kind, Owner, methods] of [
|
||||
['kitty', Tracker, ['scan', 'scanReplay']],
|
||||
['mouse', Mirror, ['scan']]
|
||||
]) {
|
||||
for (const method of methods) {
|
||||
for (const [chars, count] of sizes) {
|
||||
reports.push(await measure(Owner, variant, kind, method, chars, count))
|
||||
}
|
||||
}
|
||||
for (const suffix of [
|
||||
'\x1b[',
|
||||
'\x1b[?1049;2004;1000;1006h',
|
||||
`\x1b[${'1'.repeat(4095)}`,
|
||||
pending.replace('\x1b[', '\x9b')
|
||||
]) {
|
||||
reports.push(await measure(Owner, variant, kind, 'scan', 4 * 1024 * 1024, 8, suffix))
|
||||
}
|
||||
}
|
||||
}
|
||||
const report = {
|
||||
node: process.version,
|
||||
electron: process.versions.electron ?? null,
|
||||
v8: process.versions.v8,
|
||||
platform: process.platform,
|
||||
runnerSha256: sha(read(__filename)),
|
||||
loaderSha256: sha(read(path.join(__dirname, 'load-source.cjs'))),
|
||||
pendingTailCodeUnits: pending.length,
|
||||
clearedRegexStatics: true,
|
||||
bundles,
|
||||
reports
|
||||
}
|
||||
const output = path.join(
|
||||
__dirname,
|
||||
`${process.versions.electron ? 'electron' : 'node'}-results.json`
|
||||
)
|
||||
fs.writeFileSync(output, `${JSON.stringify(report, null, 2)}\n`)
|
||||
console.log(
|
||||
JSON.stringify({ output, passed: reports.length, node: report.node, electron: report.electron })
|
||||
)
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
setTimeout(() => {
|
||||
console.error('fixture deadline')
|
||||
process.exit(2)
|
||||
}, 30000).unref()
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"publicationTopic": "np-oom-scan-retained-text-slices",
|
||||
"publicationBaseCommit": "8d599520e44654a5c28e9930e3070c00d6499931",
|
||||
"historicalParserRef": "v1.4.198",
|
||||
"historicalScope": "Both parser modules and kitty flag parser match this ref; owned-string helpers come from the publication topic. This is not a historical app binary.",
|
||||
"sources": {
|
||||
"src/shared/terminal-kitty-keyboard-mode-tracker.ts": {
|
||||
"baselineSha256": "2ee0bad47d4575046f71c16e0334a8ae8be531f0cfc67133abc287f8ae5aa403",
|
||||
"fixedSha256": "a8da704564b9ab97aa344f380940021d3fa98f80af817751824d14271acb8a84",
|
||||
"reverse": [
|
||||
{
|
||||
"from": "import { ownRetainedString } from './own-retained-string'\n",
|
||||
"to": "",
|
||||
"count": 1
|
||||
},
|
||||
{ "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 1 }
|
||||
]
|
||||
},
|
||||
"src/main/daemon/terminal-mouse-mode-mirror.ts": {
|
||||
"baselineSha256": "5bf8a94ad7ec3fdd151ba7559f49a229b2156818aba88f7b562bef258048acf1",
|
||||
"fixedSha256": "e8f4888dabee5c4cc50b0f15584b3b1b5e2da4db1ba25695dd5762ef9c4a6fb3",
|
||||
"reverse": [
|
||||
{
|
||||
"from": "import { ownRetainedString } from '../../shared/own-retained-string'\n",
|
||||
"to": "",
|
||||
"count": 1
|
||||
},
|
||||
{ "from": "? ownRetainedString(tail) : ''", "to": "? tail : ''", "count": 2 }
|
||||
]
|
||||
},
|
||||
"src/shared/own-retained-string.ts": {
|
||||
"baselineSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2",
|
||||
"fixedSha256": "addea88bc11c725192be2ff617b91315a875113844c95de03e5529cf3d7ef2e2"
|
||||
},
|
||||
"src/shared/owned-utf16-suffix.ts": {
|
||||
"baselineSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475",
|
||||
"fixedSha256": "1ca0bb35dfff2d9f3c02caee3d05b0e0c97378df71c878622acf703524d2c475"
|
||||
},
|
||||
"src/shared/terminal-kitty-keyboard-flags.ts": {
|
||||
"baselineSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225",
|
||||
"fixedSha256": "ee8ca63afdca8356fa12878e74973ff92d807aa8d99750b2415b42f4bccb0225"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
AgentSessionBackgroundTaskRunState
|
||||
} from '../../shared/agent-session-wire'
|
||||
import { backgroundTaskFallbackText } from '../../shared/native-chat-background-task-row'
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
|
||||
const MAX_TASK_ID_LENGTH = 512
|
||||
const MAX_TASK_TEXT_LENGTH = 512
|
||||
@@ -39,7 +40,7 @@ function boundedTaskText(value: unknown): string | undefined {
|
||||
return undefined
|
||||
}
|
||||
const trimmed = value.trim().replace(/\s+/g, ' ')
|
||||
return trimmed.length > 0 ? trimmed.slice(0, MAX_TASK_TEXT_LENGTH) : undefined
|
||||
return trimmed.length > 0 ? ownRetainedString(trimmed.slice(0, MAX_TASK_TEXT_LENGTH)) : undefined
|
||||
}
|
||||
|
||||
export function taskDescription(value: unknown): string | undefined {
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { setImmediate } from 'node:timers/promises'
|
||||
import { expect, it } from 'vitest'
|
||||
import { ClaudeBackgroundTaskTracker } from './claude-background-task-tracker'
|
||||
import { taskDescription, taskName } from './claude-background-task-frames'
|
||||
|
||||
type Retention = 'live' | 'settled' | 'removed'
|
||||
type Field = 'description' | 'name'
|
||||
const TASKS = 8
|
||||
const INPUT_CHARS = 1024 * 1024
|
||||
|
||||
function collectHeap(): number {
|
||||
const collect = globalThis.gc
|
||||
if (typeof collect !== 'function') {
|
||||
throw new Error('global.gc unavailable: run with the repository Vitest --expose-gc config')
|
||||
}
|
||||
for (let index = 0; index < 3; index++) {
|
||||
collect()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function populate(field: Field, retention: Retention, count = TASKS): ClaudeBackgroundTaskTracker {
|
||||
const tracker = new ClaudeBackgroundTaskTracker(() => 1)
|
||||
const keeper = {
|
||||
type: 'system',
|
||||
subtype: 'task_started',
|
||||
task_id: 'keeper',
|
||||
task_type: 'local_bash',
|
||||
is_backgrounded: true
|
||||
}
|
||||
tracker.observe(keeper)
|
||||
for (let index = 0; index < count; index++) {
|
||||
tracker.observe(
|
||||
JSON.parse(
|
||||
JSON.stringify({
|
||||
...keeper,
|
||||
task_id: `task-${index}`,
|
||||
[field]: String.fromCharCode(65 + index).repeat(INPUT_CHARS)
|
||||
})
|
||||
)
|
||||
)
|
||||
if (retention === 'settled') {
|
||||
tracker.observe({
|
||||
type: 'system',
|
||||
subtype: 'task_notification',
|
||||
task_id: `task-${index}`,
|
||||
status: 'completed'
|
||||
})
|
||||
}
|
||||
}
|
||||
if (retention === 'removed') {
|
||||
tracker.observe({ type: 'system', subtype: 'background_tasks_changed', tasks: [keeper] })
|
||||
}
|
||||
return tracker
|
||||
}
|
||||
|
||||
it.each([
|
||||
['description', 'live'],
|
||||
['description', 'settled'],
|
||||
['description', 'removed'],
|
||||
['name', 'live'],
|
||||
['name', 'settled'],
|
||||
['name', 'removed']
|
||||
] as const)('owns bounded %s text retained by %s tasks', async (field, retention) => {
|
||||
populate(field, retention, 1).clear()
|
||||
await setImmediate()
|
||||
const before = collectHeap()
|
||||
const tracker = populate(field, retention)
|
||||
await setImmediate()
|
||||
try {
|
||||
expect(collectHeap() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
if (retention === 'live') {
|
||||
expect(tracker.state?.tasks?.find((task) => task.id === 'task-0')?.[field]).toBe(
|
||||
'A'.repeat(512)
|
||||
)
|
||||
} else if (retention === 'settled') {
|
||||
expect(tracker.state?.settledTasks?.find((task) => task.id === 'task-0')?.[field]).toBe(
|
||||
'A'.repeat(512)
|
||||
)
|
||||
} else {
|
||||
expect(tracker.state?.tasks?.map((task) => task.id)).toEqual(['keeper'])
|
||||
}
|
||||
} finally {
|
||||
tracker.clear()
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves normalization, name fallback, and the UTF-16 clipping boundary', () => {
|
||||
expect(taskDescription(' \t run\n the\r\n build ')).toBe('run the build')
|
||||
expect(taskDescription(' \t\r\n ')).toBeUndefined()
|
||||
expect(taskDescription(null)).toBeUndefined()
|
||||
expect(taskName({ name: ' ', agent_type: '\t reviewer\nagent ' })).toBe('reviewer agent')
|
||||
const value = `${'漢'.repeat(511)}\ud83d\ude00\udfff`
|
||||
expect(taskDescription(value)).toBe(value.slice(0, 512))
|
||||
expect(taskName({ subagent_type: value })).toBe(value.slice(0, 512))
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
import type { TerminalModes } from './types'
|
||||
|
||||
type MouseTrackingMode = NonNullable<TerminalModes['mouseTrackingMode']>
|
||||
@@ -105,10 +106,10 @@ export class TerminalMouseModeMirror {
|
||||
return tail
|
||||
}
|
||||
if (tail.startsWith('\x1b[?')) {
|
||||
return this.isIncompleteParams(tail.slice(3)) ? tail : ''
|
||||
return this.isIncompleteParams(tail.slice(3)) ? ownRetainedString(tail) : ''
|
||||
}
|
||||
if (tail.startsWith('\x9b?')) {
|
||||
return this.isIncompleteParams(tail.slice(2)) ? tail : ''
|
||||
return this.isIncompleteParams(tail.slice(2)) ? ownRetainedString(tail) : ''
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { TerminalMouseModeMirror } from './terminal-mouse-mode-mirror'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
// Isolate mirror ownership from V8's process-wide last successful regexp input.
|
||||
void /reset/.test('reset')
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
describe('mouse mode scan tail retention', () => {
|
||||
it.each(['\x1b[', '\x9b'])(
|
||||
'retains a split %j mode sequence without retaining consumed output',
|
||||
(introducer) => {
|
||||
const before = heapAfterGc()
|
||||
const mirrors = Array.from({ length: 8 }, (_value, index) => {
|
||||
const mirror = new TerminalMouseModeMirror()
|
||||
mirror.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${introducer}?1049;2004;1000;`)
|
||||
return mirror
|
||||
})
|
||||
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
for (const mirror of mirrors) {
|
||||
expect(mirror.mouseTrackingMode).toBe('none')
|
||||
mirror.scan('1006h')
|
||||
expect(mirror.mouseTrackingMode).toBe('vt200')
|
||||
expect(mirror.sgrMouseMode).toBe(true)
|
||||
mirror.scan('\x1b[?1016h')
|
||||
expect(mirror.sgrMouseMode).toBe(false)
|
||||
expect(mirror.sgrMousePixelsMode).toBe(true)
|
||||
mirror.scan('\x1bc')
|
||||
expect(mirror.mouseTrackingMode).toBe('none')
|
||||
expect(mirror.sgrMousePixelsMode).toBe(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Readable } from 'node:stream'
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
|
||||
type PluginWorkerOutputSink = (level: 'info' | 'warn' | 'error', line: string) => void
|
||||
|
||||
@@ -21,9 +22,11 @@ export function pipePluginWorkerOutput(
|
||||
if (line.trim().length > 0) {
|
||||
log(
|
||||
level,
|
||||
truncated
|
||||
? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`
|
||||
: line
|
||||
ownRetainedString(
|
||||
truncated
|
||||
? `${line.slice(0, PLUGIN_WORKER_OUTPUT_LINE_LIMIT - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`
|
||||
: line
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +52,7 @@ export function pipePluginWorkerOutput(
|
||||
buffered = ''
|
||||
discarding = newline === -1
|
||||
} else {
|
||||
buffered += segment
|
||||
buffered += newline === -1 ? ownRetainedString(segment) : segment
|
||||
if (newline !== -1) {
|
||||
emit(buffered)
|
||||
buffered = ''
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { once } from 'node:events'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PluginLogBuffer } from './plugin-log-buffer'
|
||||
import { pipePluginWorkerOutput } from './plugin-worker-output-buffer'
|
||||
|
||||
async function heapAfterGc(): Promise<number> {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
for (let round = 0; round < 3; round++) {
|
||||
await new Promise<void>((resolve) => setImmediate(resolve))
|
||||
globalThis.gc()
|
||||
}
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
async function endStream(stream: PassThrough): Promise<void> {
|
||||
const ended = once(stream, 'end')
|
||||
stream.end()
|
||||
await ended
|
||||
}
|
||||
|
||||
function writeTail(stream: PassThrough, index: number): void {
|
||||
stream.write(`${' '.repeat(4 * 1024 * 1024)}\nretained output ${index}`)
|
||||
}
|
||||
|
||||
function writeLine(stream: PassThrough, index: number, truncated: boolean): void {
|
||||
const prefix = String(index).padStart(4, '0')
|
||||
stream.write(
|
||||
truncated
|
||||
? `${prefix}${'x'.repeat(64 * 1024)}\n`
|
||||
: `${' '.repeat(64 * 1024)}\nretained output ${prefix}\n`
|
||||
)
|
||||
}
|
||||
|
||||
describe('plugin worker retained output', () => {
|
||||
it('keeps unfinished output after consuming a large chunk without retaining the parent', async () => {
|
||||
const lines: string[] = []
|
||||
const before = await heapAfterGc()
|
||||
const streams = Array.from({ length: 8 }, (_value, index) => {
|
||||
const stream = new PassThrough()
|
||||
pipePluginWorkerOutput(stream, 'info', (_level, line) => lines.push(line))
|
||||
writeTail(stream, index)
|
||||
return stream
|
||||
})
|
||||
|
||||
expect((await heapAfterGc()) - before).toBeLessThan(2 * 1024 * 1024)
|
||||
expect(lines).toEqual([])
|
||||
for (const stream of streams) {
|
||||
await endStream(stream)
|
||||
}
|
||||
expect(lines).toEqual(Array.from({ length: 8 }, (_value, index) => `retained output ${index}`))
|
||||
})
|
||||
|
||||
it.each([false, true])(
|
||||
'owns emitted log text without retaining consumed chunks (truncated=%s)',
|
||||
async (truncated) => {
|
||||
const logs = new PluginLogBuffer()
|
||||
const stream = new PassThrough()
|
||||
pipePluginWorkerOutput(stream, 'error', (level, line) => logs.append('plugin', level, line))
|
||||
const before = await heapAfterGc()
|
||||
for (let index = 0; index < 205; index++) {
|
||||
writeLine(stream, index, truncated)
|
||||
}
|
||||
await endStream(stream)
|
||||
|
||||
// Compare text after the heap check: comparisons can flatten concatenated strings.
|
||||
expect((await heapAfterGc()) - before).toBeLessThan(5 * 1024 * 1024)
|
||||
expect(logs.get('plugin')).toHaveLength(200)
|
||||
for (const [index, row] of logs.get('plugin').entries()) {
|
||||
const prefix = String(index + 5).padStart(4, '0')
|
||||
expect(row.level).toBe('error')
|
||||
expect(row.line).toBe(
|
||||
truncated
|
||||
? `${prefix}${'x'.repeat(8192 - 4 - '… [truncated]'.length)}… [truncated]`
|
||||
: `retained output ${prefix}`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable no-control-regex -- Terminal control-sequence parsing intentionally matches raw control bytes. */
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
import type {
|
||||
AdvertisedUrl,
|
||||
AdvertisedUrlChangeEvent,
|
||||
@@ -45,7 +46,7 @@ export class PtyBuffer {
|
||||
const chunkHasLineBreak = chunk.includes('\n') || chunk.includes('\r')
|
||||
// Keep the suffix directly so oversized chunks never materialize a throwaway full concatenation.
|
||||
if (chunk.length >= PER_PTY_BUFFER_LIMIT) {
|
||||
this.raw = chunk.slice(-PER_PTY_BUFFER_LIMIT)
|
||||
this.raw = ownRetainedString(chunk.slice(-PER_PTY_BUFFER_LIMIT))
|
||||
} else if (this.raw.length + chunk.length > PER_PTY_BUFFER_LIMIT) {
|
||||
this.raw = `${this.raw.slice(-(PER_PTY_BUFFER_LIMIT - chunk.length))}${chunk}`
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AdvertisedUrlWatcher } from './advertised-url-watcher'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function ingestOversizedOutput(watcher: AdvertisedUrlWatcher, bound: boolean): void {
|
||||
for (let index = 0; index < 8; index++) {
|
||||
const ptyId = `pty-${index}`
|
||||
if (bound) {
|
||||
watcher.bindPty(ptyId, 'workspace')
|
||||
}
|
||||
watcher.ingest(
|
||||
ptyId,
|
||||
`${index}:${'x'.repeat(4 * 1024 * 1024)}\nhttp://localhost:${4100 + index}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
describe('advertised URL output retention', () => {
|
||||
it.each([true, false])('releases oversized parents with PTYs bound=%s', (bound) => {
|
||||
const watcher = new AdvertisedUrlWatcher()
|
||||
const before = heapAfterGc()
|
||||
ingestOversizedOutput(watcher, bound)
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
|
||||
for (let index = 0; index < 8; index++) {
|
||||
const ptyId = `pty-${index}`
|
||||
watcher.bindPty(ptyId, 'workspace')
|
||||
watcher.ingest(ptyId, '/\n')
|
||||
expect(watcher.lookup('workspace', 4100 + index)?.origin).toBe(
|
||||
`http://localhost:${4100 + index}`
|
||||
)
|
||||
watcher.unbindPty(ptyId)
|
||||
expect(watcher.lookup('workspace', 4100 + index)).toBeUndefined()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
lookupBestAdvertisedUrl,
|
||||
shouldEvictAdvertisedUrlAfterScan
|
||||
} from './advertised-url-reconciliation'
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
export type HostKind = 'custom' | 'loopback' | 'private-ip' | 'public-ip'
|
||||
|
||||
export type AdvertisedUrl = {
|
||||
@@ -140,7 +141,11 @@ export class AdvertisedUrlWatcher {
|
||||
if (!worktreeId) {
|
||||
// Why: daemon PTY data can arrive before the spawn handler resolves the worktreeId (src/main/ipc/pty.ts:1318-1323); buffer until bindPty replays.
|
||||
const prior = this.pending.get(ptyId) ?? ''
|
||||
const merged = (prior + chunk).slice(-PENDING_PRE_BIND_LIMIT)
|
||||
const combined = prior + chunk
|
||||
const merged =
|
||||
combined.length > PENDING_PRE_BIND_LIMIT
|
||||
? ownRetainedString(combined.slice(-PENDING_PRE_BIND_LIMIT))
|
||||
: combined
|
||||
// Why: drop+reinsert refreshes Map insertion order (LRU) so the eviction below drops the oldest unbound PTY.
|
||||
this.pending.delete(ptyId)
|
||||
this.pending.set(ptyId, merged)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { ownRetainedString } from '../../shared/own-retained-string'
|
||||
|
||||
export const RECENT_PTY_OUTPUT_LIMIT = 64 * 1024
|
||||
|
||||
// Compact the backing array once this many fully-dropped head slots accumulate,
|
||||
@@ -42,7 +44,7 @@ export class RecentPtyOutputBuffer {
|
||||
return
|
||||
}
|
||||
if (data.length >= this.limit) {
|
||||
this.chunks = [data.slice(-this.limit)]
|
||||
this.chunks = [data.length > this.limit ? ownRetainedString(data.slice(-this.limit)) : data]
|
||||
this.headIndex = 0
|
||||
this.headOffset = 0
|
||||
this.totalLen = this.limit
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { RECENT_PTY_OUTPUT_LIMIT, RecentPtyOutputBuffer } from './recent-pty-output-buffer'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
describe('recent PTY output retention', () => {
|
||||
it.each([true, false])(
|
||||
'releases oversized parent strings with boundary preservation=%s',
|
||||
(preserveChunkBoundaries) => {
|
||||
const count = 8
|
||||
const before = heapAfterGc()
|
||||
const buffers = Array.from({ length: count }, (_value, index) => {
|
||||
const buffer = new RecentPtyOutputBuffer({ preserveChunkBoundaries })
|
||||
buffer.append(`${index}:${'x'.repeat(4 * 1024 * 1024)}`)
|
||||
return buffer
|
||||
})
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(growth).toBeLessThan(count * RECENT_PTY_OUTPUT_LIMIT * 4)
|
||||
for (const buffer of buffers) {
|
||||
expect(buffer.read()).toBe('x'.repeat(RECENT_PTY_OUTPUT_LIMIT))
|
||||
expect(buffer.retainedChunks().headChunkIsPartial).toBe(true)
|
||||
buffer.append('next')
|
||||
expect(buffer.read()).toBe(`${'x'.repeat(RECENT_PTY_OUTPUT_LIMIT - 4)}next`)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PtyDataMeta } from './pty-dispatcher'
|
||||
import { flattenRetainedSlice } from '../../lib/flatten-retained-slice'
|
||||
|
||||
export const MAX_DEFERRED_REATTACH_LIVE_CHARS = 512 * 1024
|
||||
export const MAX_DEFERRED_REATTACH_LIVE_CHUNKS = 1_024
|
||||
@@ -35,7 +36,9 @@ export class DeferredReattachLiveDataQueue {
|
||||
const oversized = chunk.data.length > MAX_DEFERRED_REATTACH_LIVE_CHARS
|
||||
const queuedChunk = {
|
||||
...chunk,
|
||||
data: oversized ? chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS) : chunk.data
|
||||
data: oversized
|
||||
? flattenRetainedSlice(chunk.data.slice(-MAX_DEFERRED_REATTACH_LIVE_CHARS))
|
||||
: chunk.data
|
||||
}
|
||||
this.chunks.push(queuedChunk)
|
||||
this.retainedChars += queuedChunk.data.length
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { clampUtf8TextTail } from '../../../../shared/utf8-byte-limits'
|
||||
import { flattenRetainedSlice } from '../../lib/flatten-retained-slice'
|
||||
|
||||
export type EagerBufferChunk = {
|
||||
data: string
|
||||
@@ -7,5 +8,8 @@ export type EagerBufferChunk = {
|
||||
|
||||
export function clampUtf8Tail(data: string, maxBytes: number): EagerBufferChunk {
|
||||
const tail = clampUtf8TextTail(data, maxBytes)
|
||||
return { data: tail.text, bytes: tail.bytes }
|
||||
return {
|
||||
data: tail.text.length < data.length ? flattenRetainedSlice(tail.text) : tail.text,
|
||||
bytes: tail.bytes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { capTerminalScrollbackSessionBuffer } from '../../../../shared/workspace-session-terminal-buffers'
|
||||
import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from '../../../../shared/terminal-scrollback-limits'
|
||||
import { clampUtf8Tail } from './pty-eager-buffer-clamp'
|
||||
import { PtyShutdownOutputQueue } from './pty-shutdown-output-queue'
|
||||
import { DeferredReattachLiveDataQueue } from './deferred-reattach-live-data-queue'
|
||||
import { appendPaneTerminalError, type TerminalErrorsByPaneId } from './terminal-error-accumulation'
|
||||
|
||||
const LIMIT = TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT
|
||||
const PARENT_CHARS = 4 * 1024 * 1024
|
||||
const COUNT = 8
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function createPaneErrors(): TerminalErrorsByPaneId {
|
||||
let errors: TerminalErrorsByPaneId = {}
|
||||
for (let index = 0; index < COUNT; index++) {
|
||||
errors = appendPaneTerminalError(errors, 0, `${'x'.repeat(PARENT_CHARS)}:${index}`)
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
||||
describe('capped terminal buffer retention', () => {
|
||||
it.each([
|
||||
['persisted scrollback', capTerminalScrollbackSessionBuffer],
|
||||
['eager/pre-handler output', (text: string) => clampUtf8Tail(text, LIMIT).data]
|
||||
] as const)('detaches %s from oversized incoming strings', (_label, cap) => {
|
||||
const before = heapAfterGc()
|
||||
const retained = Array.from({ length: COUNT }, (_value, index) =>
|
||||
cap(`${index}:${'x'.repeat(PARENT_CHARS)}`)
|
||||
)
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(retained.every((text) => text === 'x'.repeat(LIMIT))).toBe(true)
|
||||
expect(growth).toBeLessThan(COUNT * LIMIT * 2)
|
||||
})
|
||||
|
||||
it('keeps shutdown queue heap storage near its byte ledger after clamping', () => {
|
||||
const before = heapAfterGc()
|
||||
const queues = Array.from({ length: COUNT }, (_value, index) => {
|
||||
const queue = new PtyShutdownOutputQueue()
|
||||
queue.enqueue({ kind: 'replay', data: `${index}:${'x'.repeat(PARENT_CHARS)}` })
|
||||
return queue
|
||||
})
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(queues.every((queue) => queue.getStorageForTest().retainedBytes === LIMIT)).toBe(true)
|
||||
expect(growth).toBeLessThan(COUNT * LIMIT * 2)
|
||||
for (const queue of queues) {
|
||||
expect(queue.takeAll()).toEqual([{ kind: 'replay', data: 'x'.repeat(LIMIT) }])
|
||||
}
|
||||
})
|
||||
|
||||
it('detaches oversized chunks while a reattach queue waits for its consumer', () => {
|
||||
const before = heapAfterGc()
|
||||
const queues = Array.from({ length: COUNT }, (_value, index) => {
|
||||
const queue = new DeferredReattachLiveDataQueue()
|
||||
queue.enqueue({
|
||||
data: `${index}:${'x'.repeat(PARENT_CHARS)}`,
|
||||
ptyId: 'p',
|
||||
streamGeneration: 1
|
||||
})
|
||||
return queue
|
||||
})
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(queues.every((queue) => queue.getStorageForTest().retainedChars === LIMIT)).toBe(true)
|
||||
expect(growth).toBeLessThan(COUNT * LIMIT * 2)
|
||||
for (const queue of queues) {
|
||||
expect(queue.takeAll()[0]?.data).toBe('x'.repeat(LIMIT))
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps capped pane errors without retaining the original error payloads', () => {
|
||||
const before = heapAfterGc()
|
||||
const errors = createPaneErrors()
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(errors[0]).toHaveLength(COUNT)
|
||||
expect(
|
||||
errors[0].every((text, index) => text.length === 4000 && text.endsWith(`:${index}`))
|
||||
).toBe(true)
|
||||
expect(growth).toBeLessThan(PARENT_CHARS)
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,5 @@
|
||||
import { flattenRetainedSlice } from '../../lib/flatten-retained-slice'
|
||||
|
||||
// The toast still consumes newline-joined copy, so legacy tab-wide messages need
|
||||
// whole-run dedup even though pane errors remain structurally separate until render.
|
||||
function containsWholeLineRun(accumulated: string, message: string): boolean {
|
||||
@@ -22,12 +24,12 @@ export function boundTerminalErrorSurface(
|
||||
const lines = surface.split('\n')
|
||||
let bounded = lines.length > maxLines ? lines.slice(-maxLines).join('\n') : surface
|
||||
if (bounded.length <= maxChars) {
|
||||
return bounded
|
||||
return flattenRetainedSlice(bounded)
|
||||
}
|
||||
const suffix = bounded.slice(-maxChars)
|
||||
const firstNewline = suffix.indexOf('\n')
|
||||
bounded = firstNewline === -1 ? suffix : suffix.slice(firstNewline + 1) || suffix
|
||||
return bounded
|
||||
return flattenRetainedSlice(bounded)
|
||||
}
|
||||
|
||||
export function appendPaneTerminalError(
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { PR_CHECK_LOG_TAIL_BYTES, sliceCheckLogTail } from './check-job-log-tail-slice'
|
||||
import { gitLabJobTraceToLogExcerpt } from './gitlab-job-log-excerpt'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
const PARENT_CHARS = 2 * 1024 * 1024
|
||||
const COUNT = 8
|
||||
|
||||
describe('retained CI log excerpts', () => {
|
||||
it.each([
|
||||
[
|
||||
'GitHub long line',
|
||||
(index: number) => `${index}:${'x'.repeat(PARENT_CHARS)}`,
|
||||
sliceCheckLogTail
|
||||
],
|
||||
[
|
||||
'GitHub earlier error',
|
||||
(index: number) => `error: ${index}:${'界'.repeat(PARENT_CHARS)}\n${'recent\n'.repeat(100)}`,
|
||||
sliceCheckLogTail
|
||||
],
|
||||
[
|
||||
'GitLab raw trace',
|
||||
(index: number) => `${index}:${'x'.repeat(PARENT_CHARS)}`,
|
||||
gitLabJobTraceToLogExcerpt
|
||||
]
|
||||
] as const)('releases the parent of a %s', (_label, makeLog, excerpt) => {
|
||||
const before = heapAfterGc()
|
||||
const retained = Array.from({ length: COUNT }, (_value, index) => excerpt(makeLog(index)))
|
||||
// V8's legacy RegExp statics can otherwise keep the final input independently of our cache.
|
||||
void /probe/.test('probe')
|
||||
const growth = heapAfterGc() - before
|
||||
|
||||
expect(retained).toHaveLength(COUNT)
|
||||
expect(retained.every((text) => Buffer.byteLength(text) <= PR_CHECK_LOG_TAIL_BYTES)).toBe(true)
|
||||
expect(growth).toBeLessThan(PARENT_CHARS * 2)
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getUtf8ByteLength,
|
||||
isUtf8ByteLengthWithinLimit
|
||||
} from './utf8-byte-limits'
|
||||
import { ownRetainedString } from './own-retained-string'
|
||||
|
||||
export const PR_CHECK_LOG_TAIL_LINES = 200
|
||||
export const PR_CHECK_LOG_TAIL_RECENT_LINES = 100
|
||||
@@ -57,7 +58,7 @@ function collectEarlierErrorLineIndexes(lines: string[], recentStart: number): n
|
||||
return [...indexes].sort((left, right) => left - right)
|
||||
}
|
||||
|
||||
export function sliceCheckLogTail(logText: string): string {
|
||||
function buildCheckLogTail(logText: string): string {
|
||||
const lines = logText.split(/\r?\n/)
|
||||
const recentStart = Math.max(0, lines.length - PR_CHECK_LOG_TAIL_RECENT_LINES)
|
||||
const recentLines = lines.slice(recentStart)
|
||||
@@ -80,3 +81,8 @@ export function sliceCheckLogTail(logText: string): string {
|
||||
recentLines
|
||||
)
|
||||
}
|
||||
|
||||
export function sliceCheckLogTail(logText: string): string {
|
||||
// Cached excerpts must not pin the downloaded log behind a small V8 slice.
|
||||
return ownRetainedString(buildCheckLogTail(logText))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createCommandCodeOutputStatusDetector } from './command-code-output-status'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
describe('Command Code output retention', () => {
|
||||
it('keeps small boundary carries without pinning oversized output on ordinary panes', () => {
|
||||
const before = heapAfterGc()
|
||||
const detectors = Array.from({ length: 8 }, (_value, index) => {
|
||||
const detector = createCommandCodeOutputStatusDetector({ onWorking: () => {} })
|
||||
detector.observe(`${index}:${'x'.repeat(4 * 1024 * 1024)}`)
|
||||
return detector
|
||||
})
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
for (const detector of detectors) {
|
||||
expect(detector.observe('\nordinary shell output\n')).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from './command-code-prompt-text'
|
||||
import { stripTerminalControl } from './terminal-control-stripping'
|
||||
import { escapeRegex } from './string-utils'
|
||||
import { ownRetainedString } from './own-retained-string'
|
||||
|
||||
export { stripTerminalControl } from './terminal-control-stripping'
|
||||
|
||||
@@ -159,7 +160,7 @@ function rawChunkMayContainCommandCodeBanner(previousRawText: string, data: stri
|
||||
|
||||
function appendRecentRawText(previousRawText: string, data: string): string {
|
||||
if (data.length >= RECENT_TEXT_LIMIT) {
|
||||
return data.slice(-RECENT_TEXT_LIMIT)
|
||||
return ownRetainedString(data.slice(-RECENT_TEXT_LIMIT))
|
||||
}
|
||||
return (previousRawText + data).slice(-RECENT_TEXT_LIMIT)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ownRetainedString } from './own-retained-string'
|
||||
import { parseTerminalKittyKeyboardFlags } from './terminal-kitty-keyboard-flags'
|
||||
|
||||
// Why: PTY/SSH chunks can split an escape sequence before its final byte.
|
||||
@@ -308,7 +309,7 @@ export class TerminalKittyKeyboardModeTracker {
|
||||
if (body === null) {
|
||||
return ''
|
||||
}
|
||||
return this.isIncompleteSequenceBody(body) ? tail : ''
|
||||
return this.isIncompleteSequenceBody(body) ? ownRetainedString(tail) : ''
|
||||
}
|
||||
|
||||
private isIncompleteSequenceBody(body: string): boolean {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { TerminalKittyKeyboardModeTracker } from './terminal-kitty-keyboard-mode-tracker'
|
||||
|
||||
const INCOMPLETE_MODE = '\x1b[?1049;2004;1000;'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
// Isolate tracker ownership from V8's process-wide last successful regexp input.
|
||||
void /reset/.test('reset')
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
describe('kitty keyboard scan tail retention', () => {
|
||||
it.each(['scan', 'scanReplay'] as const)(
|
||||
'%s retains a split mode sequence without retaining consumed output',
|
||||
(method) => {
|
||||
const before = heapAfterGc()
|
||||
const trackers = Array.from({ length: 8 }, (_value, index) => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker[method](`${index}:${'x'.repeat(4 * 1024 * 1024)}${INCOMPLETE_MODE}`)
|
||||
return tracker
|
||||
})
|
||||
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
for (const tracker of trackers) {
|
||||
expect(tracker.isAlternateScreen).toBe(false)
|
||||
tracker[method]('1006h\x1b[>3u')
|
||||
expect(tracker.isAlternateScreen).toBe(true)
|
||||
expect(tracker.flags).toBe(3)
|
||||
tracker.scan('\x1b[<u')
|
||||
expect(tracker.flags).toBe(0)
|
||||
tracker.resetForSnapshot()
|
||||
expect(tracker.snapshotFlags).toBeUndefined()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,109 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ownRetainedString, resetOwnRetainedStringCopier } from './own-retained-string'
|
||||
import { createOsc133CommandFinishedScanner } from './terminal-osc133-command-finished'
|
||||
|
||||
const FISH_PROMPT = '\x1b]133;A;click_events=1'
|
||||
const FISH_COMMAND = '\x1b]133;C;cmdline_url=npx'
|
||||
const UTF16_CARRY = '\x1b]133;D;1234567890;\ud800a\udfff\u0000漢'
|
||||
|
||||
function heapAfterGc(): number {
|
||||
if (!('gc' in globalThis) || typeof globalThis.gc !== 'function') {
|
||||
throw new Error('The test runner must enable --expose-gc')
|
||||
}
|
||||
// Isolate scanner ownership from V8's last successful regexp input.
|
||||
void /reset/.test('reset')
|
||||
globalThis.gc()
|
||||
globalThis.gc()
|
||||
return process.memoryUsage().heapUsed
|
||||
}
|
||||
|
||||
function selectCopier(withoutBuffer: boolean): void {
|
||||
resetOwnRetainedStringCopier()
|
||||
try {
|
||||
if (withoutBuffer) {
|
||||
vi.stubGlobal('Buffer', undefined)
|
||||
}
|
||||
expect(ownRetainedString(UTF16_CARRY)).toBe(UTF16_CARRY)
|
||||
} finally {
|
||||
vi.unstubAllGlobals()
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
resetOwnRetainedStringCopier()
|
||||
})
|
||||
|
||||
describe.each([false, true])('OSC 133 carry with Bufferless copying=%s', (withoutBuffer) => {
|
||||
// Syntax from the captured fish 4.7.1 fixture in terminal-mode-2031-final-state.test.ts.
|
||||
it.each([FISH_PROMPT, FISH_COMMAND])('owns a retained fish suffix %j', (suffix) => {
|
||||
selectCopier(withoutBuffer)
|
||||
const started = vi.fn()
|
||||
const finished = vi.fn()
|
||||
const before = heapAfterGc()
|
||||
const scanners = Array.from({ length: 8 }, (_value, index) => {
|
||||
const scanner = createOsc133CommandFinishedScanner(finished, started)
|
||||
scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${suffix}`)
|
||||
return scanner
|
||||
})
|
||||
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
expect(started).not.toHaveBeenCalled()
|
||||
expect(finished).not.toHaveBeenCalled()
|
||||
for (const scanner of scanners) {
|
||||
scanner.scan('\x07\x1b]133;D;137\x1b\\')
|
||||
}
|
||||
expect(started).toHaveBeenCalledTimes(suffix === FISH_COMMAND ? scanners.length : 0)
|
||||
expect(finished.mock.calls).toEqual(Array.from({ length: scanners.length }, () => [137]))
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
})
|
||||
|
||||
it('preserves UTF-16 carry at every split and retires a reset prefix', () => {
|
||||
selectCopier(withoutBuffer)
|
||||
for (let cut = 1; cut < UTF16_CARRY.length; cut += 1) {
|
||||
const finished = vi.fn()
|
||||
const scanner = createOsc133CommandFinishedScanner(finished)
|
||||
scanner.scan(UTF16_CARRY.slice(0, cut))
|
||||
scanner.scan(`${UTF16_CARRY.slice(cut)}\x1b\\`)
|
||||
expect(finished.mock.calls).toEqual([[1234567890]])
|
||||
scanner.scan(FISH_COMMAND)
|
||||
scanner.reset()
|
||||
scanner.scan('\x07')
|
||||
expect(finished.mock.calls).toEqual([[1234567890]])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('short command-finished carry does not retain consumed output and completes once', () => {
|
||||
const finished = vi.fn()
|
||||
const before = heapAfterGc()
|
||||
const scanners = Array.from({ length: 8 }, (_value, index) => {
|
||||
const scanner = createOsc133CommandFinishedScanner(finished)
|
||||
scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}\x1b]133;D;0`)
|
||||
return scanner
|
||||
})
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
for (const scanner of scanners) {
|
||||
scanner.scan('\x07')
|
||||
scanner.scan('\x07')
|
||||
}
|
||||
expect(finished.mock.calls).toEqual(Array.from({ length: scanners.length }, () => [0]))
|
||||
})
|
||||
|
||||
it('reset releases a pending parent before its terminator arrives', () => {
|
||||
const finished = vi.fn()
|
||||
const started = vi.fn()
|
||||
const before = heapAfterGc()
|
||||
const scanners = Array.from({ length: 8 }, (_value, index) => {
|
||||
const scanner = createOsc133CommandFinishedScanner(finished, started)
|
||||
scanner.scan(`${index}:${'x'.repeat(4 * 1024 * 1024)}${FISH_COMMAND}`)
|
||||
scanner.reset()
|
||||
return scanner
|
||||
})
|
||||
expect(heapAfterGc() - before).toBeLessThan(2 * 1024 * 1024)
|
||||
for (const scanner of scanners) {
|
||||
scanner.scan('\x07')
|
||||
}
|
||||
expect(started).not.toHaveBeenCalled()
|
||||
expect(finished).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -8,6 +8,8 @@
|
||||
* terminators, best-effort exit codes) must be identical in both.
|
||||
*/
|
||||
|
||||
import { ownRetainedString } from './own-retained-string'
|
||||
|
||||
type OscTerminator = {
|
||||
index: number
|
||||
length: number
|
||||
@@ -91,6 +93,7 @@ export function createOsc133CommandFinishedScanner(
|
||||
if (carry.length > MAX_OSC_CARRY_LENGTH) {
|
||||
carry = carry.slice(carry.length - MAX_OSC_CARRY_LENGTH)
|
||||
}
|
||||
carry = ownRetainedString(carry)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getRepoIdFromWorktreeId } from './worktree/id'
|
||||
import { TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT } from './terminal-scrollback-limits'
|
||||
import { clampUtf8TextTail, isUtf8ByteLengthWithinLimit } from './utf8-byte-limits'
|
||||
import { parseExecutionHostId } from './execution-host'
|
||||
import { ownRetainedString } from './own-retained-string'
|
||||
|
||||
export type RepoConnection = Pick<Repo, 'id' | 'connectionId' | 'executionHostId'>
|
||||
|
||||
@@ -53,7 +54,9 @@ export function capTerminalScrollbackSessionBuffer(buffer: string): string {
|
||||
if (isUtf8ByteLengthWithinLimit(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT)) {
|
||||
return buffer
|
||||
}
|
||||
return clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text
|
||||
return ownRetainedString(
|
||||
clampUtf8TextTail(buffer, TERMINAL_SCROLLBACK_SESSION_BUFFER_BYTE_LIMIT).text
|
||||
)
|
||||
}
|
||||
|
||||
function capTerminalScrollbackLeafBuffers(buffers: Record<string, string> | undefined): {
|
||||
|
||||
Reference in New Issue
Block a user