Keep long live session logs stable while they update (#8432)

* Keep long live session logs stable while updating

* fix(editor): normalize content before append sync

* chore(editor): export e2e probe type and link gate motivator

Post-review cleanup: env.d.ts referenced the probe's method shape as an
inline literal, so a probe rename would only surface in the e2e spec;
the new reliability gate's motivatingLinks pointed at the repo root.

---------

Co-authored-by: Brennan Benson <brennanbenson@Brennans-MacBook-Pro.local>
This commit is contained in:
Brennan Benson 2026-07-12 17:56:49 -07:00 committed by GitHub
parent 67cd462b24
commit fcd60a03f8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1135 additions and 9 deletions

View File

@ -1,6 +1,6 @@
{
"schemaVersion": 1,
"updatedAt": "2026-07-11",
"updatedAt": "2026-07-12",
"policy": {
"maturityLevels": [
"experimental",
@ -16,6 +16,107 @@
}
},
"gates": [
{
"id": "editor.live-log-append-stability",
"title": "Long live session logs retain their Monaco viewport while appending",
"maturity": "experimental",
"protection": "partial",
"owner": "editor-runtime",
"layer": "renderer-electron-contract",
"surfaces": ["Agent Session History View Log", "Monaco external-content reconciliation", "renderer crash containment"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local"],
"coverageNotes": "Focused tests and real-Monaco 9/50 MiB benchmarks are platform-independent. Local macOS Electron evidence opens a synthetic 9 MiB transcript through Agent Session History at fixed 900x720 viewport, 13px font, 1x zoom, and the measured 2,775,880px legacy-failure geometry, then verifies three five-second-cadence watcher appends with Find open and closed. Live Windows/Linux evidence remains uncollected.",
"motivatingLinks": ["https://github.com/stablyai/orca/pull/8432"],
"invariant": "Append-only external file growth changes only Monaco's model suffix, retaining the viewport, selection, Find state, and renderer liveness above the append point; arbitrary rewrites continue to replace the model content.",
"oracle": "Focused tests assert one post-mount content owner, actual outer lifecycle remount ordering across retained path models, exact end-of-model suffix edits with one model read, no-op equality, and full replacement for non-appends. With Node forced GC, real-Monaco benchmarks alternate 30 suffix and 30 replacement samples after five warmups on fresh equivalent models at 9 and 50 MiB, forcing GC and event-loop settlement between every arm. The Electron scenario alternates an e2e-only legacy setValue red control and the fixed watcher append from restored equivalent model/geometry at the measured legacy-failure cadence, asserting that the control disrupts anchor state while the fixed path preserves visible ranges, selection, complete Find state, scroll offset, renderer survival, and forced-GC heap/native-memory budgets.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/monaco-content-sync.test.ts src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx",
"node --expose-gc ./node_modules/vitest/vitest.mjs bench src/renderer/src/components/editor/monaco-content-sync.bench.ts --pool=threads",
"pnpm run test:e2e -- tests/e2e/agent-session-log-tail-stability.spec.ts --workers=1"
],
"testFiles": [
"src/renderer/src/components/editor/monaco-content-sync.test.ts",
"src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx",
"src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx",
"src/renderer/src/components/editor/monaco-content-sync.bench.ts",
"tests/e2e/agent-session-log-tail-stability.spec.ts"
],
"assertionRefs": [
{
"file": "src/renderer/src/components/editor/monaco-content-sync.test.ts",
"assertions": [
"append-only drift reads the current model once and inserts only at the previous model end",
"identical content emits no edit and non-append drift retains full replacement plus undo stops",
"a stale retained target model reconciles on mount without explicit undo stops while prior-path content and undo history remain isolated"
]
},
{
"file": "src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx",
"assertions": ["the Monaco wrapper receives defaultValue and no controlled value prop"]
},
{
"file": "src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx",
"assertions": ["a same-pane path switch unmounts the prior outer Monaco before real mount reconciliation refreshes the stale target; the prior retained model content and undo sentinel remain untouched"]
},
{
"file": "src/renderer/src/components/editor/monaco-content-sync.bench.ts",
"assertions": ["with forced GC and deterministic settlement between every arm, fresh real-Monaco 9 MiB and 50 MiB models alternate 30 append and 30 replacement samples after five warmups; append p95 stays below 50/100ms and at least 2x faster"]
},
{
"file": "tests/e2e/agent-session-log-tail-stability.spec.ts",
"assertions": [
"production Agent Session History opens a synthetic 9 MiB View Log at the measured 2,775,880px legacy-failure geometry",
"an executable e2e-only legacy setValue control disrupts selection/Find/anchor state at each fixed-geometry five-second sample, then restores the equivalent model state before the fixed arm",
"three alternating watcher suffix appends preserve visible ranges, selection, scroll offset, Find open/query/active-match state, and exact suffix content",
"the renderer remains responsive with no render-process-gone event and forced-GC JS-heap/working-set/private-memory peak and retained budgets hold against paired legacy controls"
]
}
],
"evidenceRuns": [
{
"date": "2026-07-12",
"runner": "local",
"platform": "macos",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/editor/monaco-content-sync.test.ts src/renderer/src/components/editor/MonacoEditor.content-owner.test.tsx src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx",
"result": "passed",
"durationSeconds": 5,
"summary": "Focused editor ownership, edit-shape, mount reconciliation, lifecycle-key, and actual same-pane retained-model remount tests passed."
},
{
"date": "2026-07-12",
"runner": "local",
"platform": "macos",
"command": "node --expose-gc ./node_modules/vitest/vitest.mjs bench src/renderer/src/components/editor/monaco-content-sync.bench.ts --pool=threads",
"result": "passed",
"durationSeconds": 75,
"summary": "Forced-GC, settled, alternating fresh-model Monaco p95: 9 MiB append 4.02-5.51ms versus replacement 81.42-83.76ms; 50 MiB append 22.72-26.60ms versus replacement 445.11-449.21ms."
},
{
"date": "2026-07-12",
"runner": "local",
"platform": "macos",
"command": "pnpm run test:e2e -- tests/e2e/agent-session-log-tail-stability.spec.ts --workers=1",
"result": "passed",
"durationSeconds": 78,
"summary": "The production View Log journey alternated retained e2e-only legacy-red controls with fixed appends from restored equivalent state; every control detected instability while the fixed path retained viewport, selection, complete Find state, renderer liveness, and normalized forced-GC/native memory budgets."
}
],
"runtimeBudget": { "p95Seconds": 600, "scope": "local focused renderer tests plus one Electron production-journey scenario" },
"flakeHistory": { "status": "unknown", "evidence": "New deterministic gate with local macOS passes; CI soak history is not yet available." },
"redGreenEvidence": { "status": "complete", "evidence": "The retained Electron gate itself executes the former read-only wrapper setValue behavior behind MODE=e2e at fixed 900x720, 13px, 1x zoom, 2,775,880px content height, and five-second cadence. The control retains a flat incoming value like the former controlled IPC prop. Each legacy arm must disrupt selection, Find active-match state, visible range, or scroll anchor, then restore identical model length/tail/geometry and anchor state before the fixed watcher arm; every fixed arm must preserve them. Production builds never install the control. The original exact-file dev repro also recorded renderer exit code 5." },
"performanceBudget": { "required": true, "evidence": "Every update retrieves the model value once and performs at most one equality-or-prefix comparison; a matching append submits only the suffix. The registered Node command requires --expose-gc and --pool=threads so worker GC is available; alternating fresh real-Monaco arms force GC and deterministic event-loop settlement after every operation. Observed p95 ranges: 9 MiB append 4.02-5.51ms versus replacement 81.42-83.76ms; 50 MiB append 22.72-26.60ms versus replacement 445.11-449.21ms. Electron alternates each legacy replacement control and fixed watcher suffix from equivalent restored model state/geometry, samples forced-GC JS heap plus app.getAppMetrics renderer working set and OS private memory before/after/settled, asserts retained memory within max(20MiB,10%), and requires suffix peak deltas for jsHeapMb, workingSetMb, and privateMb not exceed the paired legacy controls." },
"promotionCriteria": [
"Collect stable soak history on macOS, Linux, and Windows.",
"Accumulate 100 consecutive deterministic gate passes or 14 days without unexplained flakes."
],
"knownGaps": [
"No live Windows or Linux View Log evidence yet."
],
"demotionRule": "Quarantine the Electron scenario if it flakes without a product or harness bug; demote if viewport/Find drift, renderer loss, p95 regression, or memory retention exceeds the registered budgets."
},
{
"id": "terminal-session.snapshot-freshness",
"title": "Stale liveness snapshots cannot close newer PTY bindings",

View File

@ -0,0 +1,165 @@
// @vitest-environment happy-dom
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { OpenFile } from '@/store/slices/editor'
const lifecycle = vi.hoisted(() => ({
events: [] as string[],
models: new Map<string, { content: string; undo: string[] }>()
}))
vi.mock('@/lib/lazy-with-retry', async () => {
const React = await import('react')
const { syncContentOnMount } = await import('./monaco-content-sync')
return {
lazyWithRetry: (factory: () => Promise<unknown>) => {
if (!factory.toString().includes('/MonacoEditor.tsx')) {
return () => null
}
return function MockRetainedMonaco(props: { filePath: string; content: string }) {
/* oxlint-disable react-hooks/exhaustive-deps -- Mount-only by design: a prop-effect would hide a missing outer React remount. */
React.useEffect(() => {
lifecycle.events.push(`mount:${props.filePath}`)
const retained = lifecycle.models.get(props.filePath) ?? { content: '', undo: [] }
const model = {
getValue: () => retained.content,
getEOL: () => '\n',
getFullModelRange: () => ({
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: retained.content.length + 1
}),
pushEditOperations: (_selections: unknown[], operations: { text: string }[]) => {
retained.content = operations[0]?.text ?? retained.content
}
}
// Why: exercising the real mount reconciler makes outer key ordering
// observable without replacing Monaco's retained-model semantics.
syncContentOnMount(
{
getModel: () => model,
pushUndoStop: () => {
retained.undo.push('unexpected undo stop')
return true
}
} as never,
props.content
)
lifecycle.models.set(props.filePath, retained)
return () => {
lifecycle.events.push(`unmount:${props.filePath}`)
}
}, [])
/* oxlint-enable react-hooks/exhaustive-deps */
return null
}
}
}
})
vi.mock('@/store', () => ({
useAppStore: Object.assign(
(selector: (state: Record<string, unknown>) => unknown) =>
selector({
worktreesByRepo: {},
openFile: vi.fn(),
openMarkdownPreview: vi.fn(),
openConflictReviewFile: vi.fn(),
openConflictReview: vi.fn(),
closeFile: vi.fn(),
setRightSidebarTab: vi.fn(),
setPendingEditorReveal: vi.fn(),
reloadOpenCheckRunDetailsTab: vi.fn()
}),
{
getState: () => ({
folderWorkspaces: [],
projectGroups: [],
repos: [],
settings: {},
worktreesByRepo: {},
openFile: vi.fn(),
openMarkdownPreview: vi.fn(),
openConflictReviewFile: vi.fn(),
openConflictReview: vi.fn(),
closeFile: vi.fn(),
setRightSidebarTab: vi.fn(),
setPendingEditorReveal: vi.fn(),
reloadOpenCheckRunDetailsTab: vi.fn()
})
}
)
}))
import { EditorContent } from './EditorContent'
function file(filePath: string): OpenFile {
return {
id: filePath,
filePath,
relativePath: filePath.split('/').at(-1) ?? filePath,
worktreeId: 'repo::/repo',
language: 'typescript',
isDirty: false,
mode: 'edit'
}
}
function props(activeFile: OpenFile, content: string) {
return {
activeFile,
viewStateScopeId: 'same-pane',
fileContents: { [activeFile.id]: { content, isBinary: false } },
diffContents: {},
editBuffers: {},
openFiles: [activeFile],
worktreeEntries: [],
resolvedLanguage: 'typescript',
isMarkdown: false,
isMermaid: false,
isCsv: false,
isNotebook: false,
mdViewMode: 'rich' as const,
isChangesMode: false,
sideBySide: false,
pendingEditorReveal: null,
handleContentChange: vi.fn(),
handleContentChangeForFile: vi.fn(),
handleDirtyStateHint: vi.fn(),
handleSave: vi.fn(),
handleSaveForFile: vi.fn(),
reloadContent: vi.fn()
}
}
afterEach(() => {
cleanup()
lifecycle.events.length = 0
lifecycle.models.clear()
})
describe('EditorContent Monaco lifecycle boundary', () => {
it('unmounts the prior path before reconciling a stale retained target model', () => {
const first = file('/repo/first.ts')
const second = file('/repo/second.ts')
lifecycle.models.set(first.filePath, { content: 'first with edits', undo: ['first undo'] })
lifecycle.models.set(second.filePath, { content: 'stale second', undo: ['second undo'] })
const view = render(<EditorContent {...props(first, 'first with edits')} />)
view.rerender(<EditorContent {...props(second, 'fresh second')} />)
expect(lifecycle.events).toEqual([
'mount:/repo/first.ts',
'unmount:/repo/first.ts',
'mount:/repo/second.ts'
])
expect(lifecycle.models.get(second.filePath)).toEqual({
content: 'fresh second',
undo: ['second undo']
})
expect(lifecycle.models.get(first.filePath)).toEqual({
content: 'first with edits',
undo: ['first undo']
})
})
})

View File

@ -313,10 +313,10 @@ export function EditorContent({
// Why: Without a key, React reuses the same MonacoEditor instance when
// switching tabs or split panes, just updating props. That means
// useLayoutEffect cleanup (which snapshots scroll position) never fires.
// Keying on the visible pane identity forces unmount/remount so each split
// tab keeps its own viewport state even when the underlying file is shared.
// Keying on the visible pane and path forces remount before a retained target
// model mounts, so the old path cannot receive the new file's reconciliation.
<MonacoEditor
key={viewStateScopeId}
key={`${viewStateScopeId}\u0000${activeFile.filePath}`}
fileId={activeFile.id}
filePath={activeFile.filePath}
viewStateKey={editorViewStateKey}

View File

@ -0,0 +1,62 @@
// @vitest-environment happy-dom
import { cleanup, render } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
const editorProps = vi.hoisted(() => ({ current: null as Record<string, unknown> | null }))
vi.mock('@monaco-editor/react', () => ({
default: (props: Record<string, unknown>) => {
editorProps.current = props
return null
},
loader: { config: vi.fn() }
}))
vi.mock('@/store', () => ({
useAppStore: (selector: (state: Record<string, unknown>) => unknown) =>
selector({
settings: { theme: 'dark', terminalFontSize: 13, terminalFontFamily: 'monospace' },
editorFontZoomLevel: 0,
setPendingEditorReveal: vi.fn(),
setEditorCursorLine: vi.fn(),
addDiffComment: vi.fn(),
deleteDiffComment: vi.fn(),
updateDiffComment: vi.fn(),
scrollToDiffCommentId: null,
setScrollToDiffCommentId: vi.fn(),
worktreeDiffComments: {}
})
}))
vi.mock('../diff-comments/useDiffCommentDecorator', () => ({
useDiffCommentDecorator: vi.fn()
}))
vi.mock('./useContextualCopySetup', () => ({
useContextualCopySetup: () => ({ setupCopy: vi.fn(), toastNode: null })
}))
import MonacoEditor from './MonacoEditor'
afterEach(() => {
cleanup()
editorProps.current = null
})
describe('MonacoEditor content ownership', () => {
it('initializes the wrapper without a controlled value updater', () => {
render(
<MonacoEditor
fileId="file"
filePath="/repo/file.jsonl"
viewStateKey="pane:file"
relativePath="file.jsonl"
content="initial content"
language="jsonl"
onContentChange={vi.fn()}
onSave={vi.fn()}
readOnly
/>
)
expect(editorProps.current?.defaultValue).toBe('initial content')
expect(editorProps.current).not.toHaveProperty('value')
})
})

View File

@ -58,6 +58,7 @@ import {
getMonacoAutoHeightForContent,
isMonacoAutoHeightCapped
} from './monaco-auto-height'
import { installMonacoE2EProbe } from './monaco-e2e-probe'
type MonacoEditorProps = {
fileId: string
@ -332,6 +333,7 @@ export default function MonacoEditor({
(editorInstance, monaco) => {
editorRef.current = editorInstance
setMountedEditor(editorInstance)
const uninstallE2EProbe = installMonacoE2EProbe(editorInstance, filePath)
let autoHeightSub: { dispose: () => void } | null = null
let autoHeightFrame: number | null = null
const updateAutoHeight = (): void => {
@ -507,6 +509,7 @@ export default function MonacoEditor({
}
conflictDecorationsRef.current?.clear()
conflictDecorationsRef.current = null
uninstallE2EProbe()
editorRef.current = null
setMountedEditor(null)
setCommentPopover(null)
@ -826,7 +829,9 @@ export default function MonacoEditor({
<Editor
height={renderedEditorHeight === null ? '100%' : `${renderedEditorHeight}px`}
language={language}
value={content}
// Why: Orca's mount/layout reconciliation is the sole post-mount content
// owner; the wrapper's controlled read-only path would also call setValue.
defaultValue={content}
theme={isDark ? 'vs-dark' : 'vs'}
onChange={handleChange}
onMount={handleMount}

View File

@ -0,0 +1,85 @@
// @vitest-environment happy-dom
import { bench, expect } from 'vitest'
import * as monaco from 'monaco-editor'
import { syncContentUpdate } from './monaco-content-sync'
const SAMPLE_COUNT = 30
const WARMUP_COUNT = 5
function percentile95(samples: readonly number[]): number {
const sorted = [...samples].sort((left, right) => left - right)
return sorted[Math.ceil(sorted.length * 0.95) - 1] ?? Number.POSITIVE_INFINITY
}
async function settleBetweenArms(): Promise<void> {
const gc = (globalThis as { gc?: () => void }).gc
if (!gc) {
throw new Error('Forced GC unavailable; run the benchmark with node --expose-gc')
}
gc()
await new Promise<void>((resolve) => setTimeout(resolve, 0))
}
async function measureReconciliation(
initial: string,
appended: string,
rewritten: string
): Promise<{ appendSamples: number[]; replacementSamples: number[] }> {
const appendSamples: number[] = []
const replacementSamples: number[] = []
const measure = (next: string): number => {
const model = monaco.editor.createModel(initial, 'plaintext')
const editorInstance = {
getModel: () => model,
pushUndoStop: () => true
} as unknown as monaco.editor.IStandaloneCodeEditor
const startedAt = performance.now()
syncContentUpdate(editorInstance, next)
const duration = performance.now() - startedAt
model.dispose()
return duration
}
for (let index = 0; index < WARMUP_COUNT + SAMPLE_COUNT; index++) {
const appendDuration = measure(appended)
await settleBetweenArms()
const replacementDuration = measure(rewritten)
await settleBetweenArms()
if (index >= WARMUP_COUNT) {
appendSamples.push(appendDuration)
replacementSamples.push(replacementDuration)
}
}
return { appendSamples, replacementSamples }
}
async function runReconciliationBenchmark(
label: string,
bytes: number,
maxAppendP95Ms: number
): Promise<void> {
const base = `header\n${'x'.repeat(bytes - 8)}`
const appended = `${base}\n{"type":"tail"}`
const rewritten = `H${base.slice(1)}`
const { appendSamples, replacementSamples } = await measureReconciliation(
base,
appended,
rewritten
)
const appendP95 = percentile95(appendSamples)
const replacementP95 = percentile95(replacementSamples)
console.log(`[monaco-content-sync] ${label} ${JSON.stringify({ appendP95, replacementP95 })}`)
expect(appendP95).toBeLessThan(maxAppendP95Ms)
expect(replacementP95 / appendP95).toBeGreaterThanOrEqual(2)
}
bench(
'9 MiB append and replacement p95',
() => runReconciliationBenchmark('9 MiB model', 9 * 1024 * 1024, 50),
{ iterations: 1, time: 1, warmupIterations: 0, warmupTime: 0 }
)
bench(
'50 MiB append and replacement p95',
() => runReconciliationBenchmark('50 MiB model', 50 * 1024 * 1024, 100),
{ iterations: 1, time: 1, warmupIterations: 0, warmupTime: 0 }
)

View File

@ -0,0 +1,168 @@
import { describe, expect, it, vi } from 'vitest'
import type { editor } from 'monaco-editor'
import { syncContentOnMount, syncContentUpdate } from './monaco-content-sync'
function createHarness(
initialContent: string,
eol = '\n'
): {
editorInstance: editor.IStandaloneCodeEditor
getValue: ReturnType<typeof vi.fn>
getFullModelRange: ReturnType<typeof vi.fn>
pushEditOperations: ReturnType<typeof vi.fn>
pushUndoStop: ReturnType<typeof vi.fn>
} {
const getValue = vi.fn(() => initialContent)
const getFullModelRange = vi.fn(() => ({
startLineNumber: 1,
startColumn: 1,
endLineNumber: 3,
endColumn: 5
}))
const pushEditOperations = vi.fn()
const model = {
getValue,
getEOL: () => eol,
getFullModelRange,
pushEditOperations
} as unknown as editor.ITextModel
const pushUndoStop = vi.fn()
const editorInstance = {
getModel: () => model,
pushUndoStop
} as unknown as editor.IStandaloneCodeEditor
return { editorInstance, getValue, getFullModelRange, pushEditOperations, pushUndoStop }
}
describe('monaco content sync', () => {
it('inserts only an append-only suffix at the model end', () => {
const harness = createHarness('first\nsecond\nlast')
syncContentUpdate(harness.editorInstance, 'first\nsecond\nlast\nnext')
expect(harness.getValue).toHaveBeenCalledTimes(1)
expect(harness.getFullModelRange).toHaveBeenCalledTimes(1)
expect(harness.pushEditOperations).toHaveBeenCalledWith(
[],
[
{
range: {
startLineNumber: 3,
startColumn: 5,
endLineNumber: 3,
endColumn: 5
},
text: '\nnext'
}
],
expect.any(Function)
)
expect(harness.pushUndoStop).toHaveBeenCalledTimes(2)
})
it('does nothing for identical content', () => {
const harness = createHarness('unchanged')
syncContentUpdate(harness.editorInstance, 'unchanged')
expect(harness.getValue).toHaveBeenCalledTimes(1)
expect(harness.getFullModelRange).not.toHaveBeenCalled()
expect(harness.pushEditOperations).not.toHaveBeenCalled()
expect(harness.pushUndoStop).not.toHaveBeenCalled()
})
it.each([
[
'CRLF file content into an LF model',
'first\nsecond\nlast',
'\n',
'first\r\nsecond\r\nlast\r\nnext',
'\nnext'
],
[
'LF file content into a CRLF model',
'first\r\nsecond\r\nlast',
'\r\n',
'first\nsecond\nlast\nnext',
'\r\nnext'
],
[
'mixed file content into an LF model',
'first\nsecond\nlast',
'\n',
'first\r\nsecond\rlast\nnext',
'\nnext'
]
])(
'keeps %s on the suffix path',
(_label, initialContent, modelEol, nextContent, expectedSuffix) => {
const harness = createHarness(initialContent, modelEol)
syncContentUpdate(harness.editorInstance, nextContent)
expect(harness.pushEditOperations).toHaveBeenCalledWith(
[],
[expect.objectContaining({ text: expectedSuffix })],
expect.any(Function)
)
expect(harness.pushUndoStop).toHaveBeenCalledTimes(2)
}
)
it.each([
['same-length rewrite', 'before', 'after!'],
['prefix mismatch', 'before', 'changed content'],
['truncation', 'longer content', 'short']
])('fully replaces a %s', (_label, initialContent, nextContent) => {
const harness = createHarness(initialContent)
syncContentUpdate(harness.editorInstance, nextContent)
expect(harness.getValue).toHaveBeenCalledTimes(1)
expect(harness.pushEditOperations).toHaveBeenCalledWith(
[],
[{ range: harness.getFullModelRange.mock.results[0]?.value, text: nextContent }],
expect.any(Function)
)
expect(harness.pushUndoStop).toHaveBeenCalledTimes(2)
})
it('reconciles a stale retained model on mount without undo stops', () => {
const harness = createHarness('stale')
expect(syncContentOnMount(harness.editorInstance, 'fresh')).toBe(true)
expect(harness.pushEditOperations).toHaveBeenCalledTimes(1)
expect(harness.pushUndoStop).not.toHaveBeenCalled()
})
it('does nothing on mount when content already matches', () => {
const harness = createHarness('same')
expect(syncContentOnMount(harness.editorInstance, 'same')).toBe(false)
expect(harness.pushEditOperations).not.toHaveBeenCalled()
expect(harness.pushUndoStop).not.toHaveBeenCalled()
})
it('does nothing on mount when only raw file EOLs differ from the model', () => {
const harness = createHarness('first\nsecond')
expect(syncContentOnMount(harness.editorInstance, 'first\r\nsecond')).toBe(false)
expect(harness.pushEditOperations).not.toHaveBeenCalled()
expect(harness.pushUndoStop).not.toHaveBeenCalled()
})
it('reconciles only the retained target model after a path switch', () => {
const priorPath = createHarness('prior path with user edits')
const targetPath = createHarness('stale target')
syncContentOnMount(targetPath.editorInstance, 'fresh target')
expect(targetPath.pushEditOperations).toHaveBeenCalledTimes(1)
expect(targetPath.pushUndoStop).not.toHaveBeenCalled()
expect(priorPath.pushEditOperations).not.toHaveBeenCalled()
expect(priorPath.pushUndoStop).not.toHaveBeenCalled()
})
})

View File

@ -1,5 +1,15 @@
import type { editor } from 'monaco-editor'
function normalizeToModelEol(content: string, model: editor.ITextModel): string {
const eol = model.getEOL()
// Why: Monaco normalizes model line endings, while filesystem content keeps
// its raw EOLs. Compare the representation Monaco can actually retain.
if (eol === '\n' && !content.includes('\r')) {
return content
}
return content.replace(/\r\n|\r|\n/g, eol)
}
/**
* Overwrite a Monaco model's text via pushEditOperations so the change
* participates in the undo stack (unlike setValue, which blows it away).
@ -7,10 +17,11 @@ import type { editor } from 'monaco-editor'
function replaceModelContent(
editorInstance: editor.IStandaloneCodeEditor,
model: editor.ITextModel,
currentContent: string,
content: string,
withUndoStops: boolean
): void {
if (model.getValue() === content) {
if (currentContent === content) {
return
}
const fullRange = model.getFullModelRange()
@ -41,13 +52,15 @@ export function syncContentOnMount(
if (!model) {
return false
}
if (model.getValue() === content) {
const currentContent = model.getValue()
const normalizedContent = normalizeToModelEol(content, model)
if (currentContent === normalizedContent) {
return false
}
// Why: no undo stop on mount — the retained model's text was already the
// user's last-known state, and adding an undo entry here would make Cmd+Z
// revert to the pre-remount text, which is confusing.
replaceModelContent(editorInstance, model, content, false)
replaceModelContent(editorInstance, model, currentContent, normalizedContent, false)
return true
}
@ -67,5 +80,37 @@ export function syncContentUpdate(
if (!model) {
return
}
replaceModelContent(editorInstance, model, content, true)
const currentContent = model.getValue()
const normalizedContent = normalizeToModelEol(content, model)
if (currentContent.length === normalizedContent.length) {
replaceModelContent(editorInstance, model, currentContent, normalizedContent, true)
return
}
if (
normalizedContent.length > currentContent.length &&
normalizedContent.startsWith(currentContent)
) {
// Why: preserving the existing prefix lets Monaco retain viewport,
// selection, find-widget, and tokenization state above a live-file append.
const fullRange = model.getFullModelRange()
editorInstance.pushUndoStop()
model.pushEditOperations(
[],
[
{
range: {
startLineNumber: fullRange.endLineNumber,
startColumn: fullRange.endColumn,
endLineNumber: fullRange.endLineNumber,
endColumn: fullRange.endColumn
},
text: normalizedContent.slice(currentContent.length)
}
],
() => null
)
editorInstance.pushUndoStop()
return
}
replaceModelContent(editorInstance, model, currentContent, normalizedContent, true)
}

View File

@ -0,0 +1,99 @@
import type { editor, IRange, ISelection } from 'monaco-editor'
export type MonacoE2ESnapshot = {
contentHeight: number
scrollHeight: number
scrollTop: number
visibleRanges: IRange[]
selection: ISelection | null
valueLength: number
valueTail: string
find: {
open: boolean
query: string
activeMatch: string
}
}
export type MonacoE2EProbe = {
filePath: string
restoreLegacySetValueControl: () => void
restoreScrollTop: (scrollTop: number) => void
runLegacySetValueAppend: (suffix: string) => void
snapshot: () => MonacoE2ESnapshot
}
export function installMonacoE2EProbe(
editorInstance: editor.IStandaloneCodeEditor,
filePath: string
): () => void {
if (import.meta.env.MODE !== 'e2e') {
return () => {}
}
let legacyControlOriginalValue: string | null = null
let legacyControlIncomingValue: string | null = null
const probe: MonacoE2EProbe = {
filePath,
runLegacySetValueAppend: (suffix: string): void => {
if (legacyControlOriginalValue !== null) {
throw new Error('Legacy control must be restored before running again')
}
legacyControlOriginalValue = editorInstance.getValue()
// Why: the former controlled wrapper retained a flat IPC-delivered prop
// while setValue rebuilt the model; preserve that ownership in the control.
legacyControlIncomingValue = new TextDecoder().decode(
new TextEncoder().encode(`${legacyControlOriginalValue}${suffix}`)
)
// Why: reproduces the former controlled read-only wrapper's unconditional
// whole-model setValue without exposing a switch in production builds.
editorInstance.setValue(legacyControlIncomingValue)
},
restoreLegacySetValueControl: (): void => {
if (legacyControlOriginalValue === null) {
return
}
editorInstance.setValue(legacyControlOriginalValue)
legacyControlOriginalValue = null
legacyControlIncomingValue = null
},
restoreScrollTop: (scrollTop: number): void => {
// Why: the legacy setValue control can perturb Monaco's pixel rounding;
// paired fixed-path measurements must start from the recorded geometry.
editorInstance.setScrollTop(scrollTop)
},
snapshot: (): MonacoE2ESnapshot => {
const container = editorInstance.getContainerDomNode()
const findWidget = container.querySelector<HTMLElement>('.find-widget')
const findInput = findWidget?.querySelector<HTMLInputElement | HTMLTextAreaElement>('.input')
const model = editorInstance.getModel()
const valueLength = model?.getValueLength() ?? 0
const lastLineNumber = model?.getLineCount() ?? 1
const lastLine = model?.getLineContent(lastLineNumber) ?? ''
const lastNonEmptyLine =
lastLine || lastLineNumber === 1
? lastLine
: (model?.getLineContent(lastLineNumber - 1) ?? '')
return {
contentHeight: editorInstance.getContentHeight(),
scrollHeight: editorInstance.getScrollHeight(),
scrollTop: editorInstance.getScrollTop(),
visibleRanges: [...editorInstance.getVisibleRanges()],
selection: editorInstance.getSelection(),
valueLength,
valueTail: lastNonEmptyLine.slice(-256),
find: {
open: findWidget?.getAttribute('aria-hidden') !== 'true',
query: findInput?.value ?? '',
activeMatch:
findWidget?.querySelector<HTMLElement>('.matchesCount')?.textContent?.trim() ?? ''
}
}
}
}
window.__monacoEditorE2E = probe
return () => {
if (window.__monacoEditorE2E === probe) {
delete window.__monacoEditorE2E
}
}
}

View File

@ -3,6 +3,7 @@
import type { PaneManager } from '@/lib/pane-manager/pane-manager'
import type { OnboardingFeatureSetupDeps } from '@/components/onboarding/onboarding-feature-setup'
import type { languages } from 'monaco-editor'
import type { MonacoE2EProbe } from './components/editor/monaco-e2e-probe'
declare module 'monaco-editor/esm/vs/basic-languages/python/python.js' {
export const conf: languages.LanguageConfiguration
@ -71,6 +72,7 @@ declare global {
parkDelayMs: number
parkedTabIds: () => string[]
}
__monacoEditorE2E?: MonacoE2EProbe
}
}

View File

@ -0,0 +1,394 @@
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import path from 'node:path'
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
import { expect, test } from './helpers/orca-app'
const ANCHOR_TOKEN = 'E2E_LIVE_LOG_STABLE_ANCHOR'
const INITIAL_PAYLOAD_BYTES = 9 * 1024 * 1024
const APPEND_CADENCE_MS = 5_000
const SETTLEMENT_MS = 500
type CrashProbe = { processGone: { reason: string; exitCode: number } | null }
type MemorySample = {
jsHeapMb: number
privateMb: number
workingSetMb: number
}
declare global {
// eslint-disable-next-line no-var -- main-process global probe for this spec
var __liveLogCrashProbe: CrashProbe | undefined
}
test.describe('Agent Session History live log', () => {
test.use({
orcaAppExtraArgs: ['--js-flags=--expose-gc', '--enable-precise-memory-info']
})
test('keeps a long View Log viewport stable while the transcript grows', async ({
electronApp,
orcaPage,
testRepoPath
}, testInfo) => {
test.setTimeout(10 * 60_000)
const fixture = await seedSyntheticSession(electronApp, testRepoPath)
await armMainProcessCrashProbe(electronApp)
await electronApp.evaluate(({ BrowserWindow }) => {
BrowserWindow.getAllWindows()[0]?.webContents.setZoomFactor(1)
})
await orcaPage.setViewportSize({ width: 520, height: 720 })
await openSessionHistory(orcaPage)
const title = orcaPage.getByText(fixture.title, { exact: true }).first()
await expect(title).toBeVisible({ timeout: 30_000 })
await title.click()
await orcaPage.getByText('View Log', { exact: true }).click()
await expect
.poll(() => readProbeOrNull(orcaPage), { timeout: 120_000 })
.toMatchObject({ filePath: fixture.filePath, valueLength: fixture.initialLength })
await orcaPage.setViewportSize({ width: 900, height: 720 })
await orcaPage.evaluate(async () => {
const state = window.__store?.getState()
state?.setRightSidebarOpen(false)
state?.setEditorFontZoomLevel(0)
await state?.updateSettings({ terminalFontSize: 13 })
})
let baseline = await restoreAnchorState(orcaPage, true)
console.log(`[live-log-stability] geometry ${JSON.stringify(baseline)}`)
// The legacy whole-model baseline at this fixed viewport/font/zoom moved
// the anchor at 2,775,880px; missing that geometry cannot claim containment.
expect(baseline.contentHeight).toBeGreaterThanOrEqual(2_775_880)
expect(baseline.visibleRanges.length).toBeGreaterThan(0)
expect(baseline.find).toMatchObject({ open: true, query: ANCHOR_TOKEN })
expect(baseline.find.activeMatch).not.toBe('')
const suffixMemory: { before: MemorySample; after: MemorySample; settled: MemorySample }[] = []
const replacementMemory: {
before: MemorySample
after: MemorySample
settled: MemorySample
}[] = []
for (let batch = 0; batch < 3; batch++) {
const equivalentState = baseline
replacementMemory.push(await measureLegacyControl(electronApp, orcaPage, baseline, batch))
baseline = await restoreAnchorState(orcaPage, baseline.find.open, equivalentState.scrollTop)
expect(baseline.contentHeight).toBe(equivalentState.contentHeight)
expect(baseline.valueLength).toBe(equivalentState.valueLength)
expect(baseline.valueTail).toBe(equivalentState.valueTail)
expect(baseline.visibleRanges).toEqual(equivalentState.visibleRanges)
expect(baseline.selection).toEqual(equivalentState.selection)
expect(baseline.find).toEqual(equivalentState.find)
expect(Math.abs(baseline.scrollTop - equivalentState.scrollTop)).toBeLessThanOrEqual(2)
const suffix = `${JSON.stringify({ type: 'e2e_append', batch, text: 'tail-only' })}\n`
await forceGcAndSettle(orcaPage)
const before = await readMemory(electronApp, orcaPage)
appendFileSync(fixture.filePath, suffix)
fixture.initialLength += suffix.length
await orcaPage.waitForTimeout(APPEND_CADENCE_MS)
await expect
.poll(() => readProbe(orcaPage), { timeout: 30_000 })
.toMatchObject({ valueLength: fixture.initialLength })
const after = await readMemory(electronApp, orcaPage)
await forceGcAndSettle(orcaPage)
const settled = await readMemory(electronApp, orcaPage)
suffixMemory.push({ before, after, settled })
const current = await readProbe(orcaPage)
console.log(`[live-log-stability] anchor ${JSON.stringify({ batch, baseline, current })}`)
expect(current.visibleRanges).toEqual(baseline.visibleRanges)
expect(current.valueTail).toBe(suffix.trimEnd())
expect(current.selection).toEqual(baseline.selection)
expect(current.find).toEqual(baseline.find)
expect(Math.abs(current.scrollTop - baseline.scrollTop)).toBeLessThanOrEqual(2)
expect(await orcaPage.evaluate(() => 2 + 2)).toBe(4)
expect((await readMainProcessCrashProbe(electronApp)).processGone).toBeNull()
baseline = current
if (batch === 0) {
await orcaPage.keyboard.press('Escape')
await expect(orcaPage.locator('.monaco-editor .find-widget')).toHaveAttribute(
'aria-hidden',
'true'
)
baseline = await readProbe(orcaPage)
expect(baseline.find.open).toBe(false)
}
}
console.log(
`[live-log-stability] memory ${JSON.stringify({ suffixMemory, replacementMemory })}`
)
assertMemoryBudget(suffixMemory, replacementMemory)
await testInfo.attach('synthetic-live-log-stable', {
body: await orcaPage.screenshot(),
contentType: 'image/png'
})
})
})
async function seedSyntheticSession(
electronApp: ElectronApplication,
cwd: string
): Promise<{ filePath: string; initialLength: number; title: string }> {
const userData = await electronApp.evaluate(({ app }) => app.getPath('userData'))
const title = `Synthetic live log ${Date.now()}`
const sessionId = `e2e-live-log-${Date.now()}`
const sessionsDir = path.join(
userData,
'codex-runtime-home',
'home',
'sessions',
'2026',
'07',
'12'
)
mkdirSync(sessionsDir, { recursive: true })
const filePath = path.join(sessionsDir, `rollout-${sessionId}.jsonl`)
const records = [
JSON.stringify({
timestamp: '2026-07-12T12:00:00.000Z',
type: 'session_meta',
payload: { id: sessionId, cwd }
}),
JSON.stringify({
timestamp: '2026-07-12T12:00:01.000Z',
type: 'response_item',
payload: { type: 'message', role: 'user', content: [{ type: 'text', text: title }] }
}),
JSON.stringify({ type: 'synthetic', text: 'x'.repeat(INITIAL_PAYLOAD_BYTES / 2) }),
JSON.stringify({ type: 'anchor', text: ANCHOR_TOKEN }),
JSON.stringify({ type: 'synthetic', text: 'y'.repeat(INITIAL_PAYLOAD_BYTES / 2) })
]
const content = `${records.join('\n')}\n`
writeFileSync(filePath, content)
return { filePath, initialLength: content.length, title }
}
async function openSessionHistory(page: Page): Promise<void> {
await page.evaluate(() => {
const store = window.__store
if (!store) {
throw new Error('Store unavailable')
}
store.getState().setRightSidebarOpen(true)
store.getState().setRightSidebarTab('vault')
})
await page.evaluate(async () => window.api.aiVault.listSessions({ force: true }))
await page.getByRole('button', { name: 'Refresh Session History' }).click()
}
async function focusAnchorWithFind(page: Page): Promise<void> {
const modifier = process.platform === 'darwin' ? 'Meta' : 'Control'
await page.locator('.monaco-editor').click()
await page.keyboard.press(`${modifier}+f`)
const findInput = page.locator('.monaco-editor .find-widget .input').first()
await expect(findInput).toBeVisible()
await findInput.fill(ANCHOR_TOKEN)
await page.keyboard.press('Enter')
}
async function restoreAnchorState(
page: Page,
findOpen: boolean,
targetScrollTop?: number
): Promise<Awaited<ReturnType<typeof readProbe>>> {
await focusAnchorWithFind(page)
await expect
.poll(
async () => {
const snapshot = await readProbe(page)
return (snapshot.selection as { startLineNumber?: number } | null)?.startLineNumber
},
{ timeout: 60_000 }
)
.toBe(4)
await expect
.poll(async () => {
const range = (await readProbe(page)).visibleRanges[0] as {
startLineNumber?: number
endLineNumber?: number
}
return (range.startLineNumber ?? 0) <= 4 && (range.endLineNumber ?? 0) >= 4
})
.toBe(true)
if (!findOpen) {
await page.keyboard.press('Escape')
await expect(page.locator('.monaco-editor .find-widget')).toHaveAttribute('aria-hidden', 'true')
}
if (targetScrollTop !== undefined) {
await page.evaluate((scrollTop) => {
window.__monacoEditorE2E?.restoreScrollTop(scrollTop)
}, targetScrollTop)
await expect.poll(async () => (await readProbe(page)).scrollTop).toBe(targetScrollTop)
}
return readProbe(page)
}
async function readProbe(page: Page): Promise<{
contentHeight: number
filePath: string
scrollTop: number
selection: unknown
valueLength: number
valueTail: string
visibleRanges: unknown[]
find: { open: boolean; query: string; activeMatch: string }
}> {
return page.evaluate(() => {
const probe = window.__monacoEditorE2E
if (!probe) {
throw new Error('Monaco E2E probe unavailable')
}
return { filePath: probe.filePath, ...probe.snapshot() }
})
}
async function armMainProcessCrashProbe(electronApp: ElectronApplication): Promise<void> {
await electronApp.evaluate(({ BrowserWindow }) => {
const probe: CrashProbe = { processGone: null }
globalThis.__liveLogCrashProbe = probe
for (const win of BrowserWindow.getAllWindows()) {
win.webContents.on('render-process-gone', (_event, details) => {
probe.processGone = { reason: details.reason, exitCode: details.exitCode ?? -1 }
})
}
})
}
async function readMainProcessCrashProbe(electronApp: ElectronApplication): Promise<CrashProbe> {
return electronApp.evaluate(() => globalThis.__liveLogCrashProbe ?? { processGone: null })
}
async function forceGcAndSettle(page: Page): Promise<void> {
await page.evaluate(() => {
const gc = (window as unknown as { gc?: () => void }).gc
if (!gc) {
throw new Error('Forced GC unavailable; launch must include --js-flags=--expose-gc')
}
gc()
gc()
})
await page.waitForTimeout(SETTLEMENT_MS)
}
async function readMemory(electronApp: ElectronApplication, page: Page): Promise<MemorySample> {
const jsHeapMb = await page.evaluate(() => {
const memory = (performance as Performance & { memory?: { usedJSHeapSize?: number } }).memory
?.usedJSHeapSize
if (!memory) {
throw new Error('Precise renderer JS heap metrics unavailable')
}
return memory / 1024 / 1024
})
const metric = await electronApp.evaluate(({ app, BrowserWindow }) => {
const rendererPid = BrowserWindow.getAllWindows()[0]?.webContents.getOSProcessId()
const rendererMetric = app.getAppMetrics().find((candidate) => candidate.pid === rendererPid)
if (!rendererMetric) {
throw new Error(`Renderer app metric unavailable for pid ${rendererPid ?? 'unknown'}`)
}
return {
pid: rendererMetric.pid,
privateMb: rendererMetric.memory.privateBytes
? rendererMetric.memory.privateBytes / 1024
: null,
workingSetMb: rendererMetric.memory.workingSetSize / 1024
}
})
const privateMb = metric.privateMb ?? readPrivateMemoryMb(metric.pid)
return { jsHeapMb, privateMb, workingSetMb: metric.workingSetMb }
}
function readPrivateMemoryMb(pid: number): number {
if (process.platform === 'linux') {
const status = readFileSync(`/proc/${pid}/status`, 'utf8')
const privateKb = /RssAnon:\s+(\d+) kB/.exec(status)?.[1]
if (!privateKb) {
throw new Error(`Unable to parse renderer RssAnon for pid ${pid}`)
}
return Number(privateKb) / 1024
}
if (process.platform !== 'darwin') {
throw new Error('app.getAppMetrics privateBytes unavailable on Windows')
}
// Electron omits MemoryInfo.privateBytes on macOS; footprint's
// phys_footprint is the OS private-memory equivalent for the same renderer PID.
const output = execFileSync('footprint', ['-p', String(pid), '-f', 'bytes', '--noCategories'], {
encoding: 'utf8'
})
const bytes = /phys_footprint:\s+(\d+) B/.exec(output)?.[1]
if (!bytes) {
throw new Error(`Unable to parse renderer private footprint for pid ${pid}`)
}
return Number(bytes) / 1024 / 1024
}
async function measureLegacyControl(
electronApp: ElectronApplication,
page: Page,
baseline: Awaited<ReturnType<typeof readProbe>>,
batch: number
): Promise<{ before: MemorySample; after: MemorySample; settled: MemorySample }> {
await forceGcAndSettle(page)
const before = await readMemory(electronApp, page)
await page.evaluate((index) => {
window.__monacoEditorE2E?.runLegacySetValueAppend(
`${JSON.stringify({ type: 'legacy_set_value_control', index, text: 'tail-only' })}\n`
)
}, batch)
await page.waitForTimeout(APPEND_CADENCE_MS)
const after = await readMemory(electronApp, page)
const disrupted = await readProbe(page)
console.log(
`[live-log-stability] legacy-control ${JSON.stringify({ batch, baseline, disrupted })}`
)
expect(hasInstabilitySignal(baseline, disrupted)).toBe(true)
await forceGcAndSettle(page)
const settled = await readMemory(electronApp, page)
await page.evaluate(() => window.__monacoEditorE2E?.restoreLegacySetValueControl())
await expect.poll(() => readProbe(page)).toMatchObject({ valueLength: baseline.valueLength })
expect((await readMainProcessCrashProbe(electronApp)).processGone).toBeNull()
return { before, after, settled }
}
function hasInstabilitySignal(
baseline: Awaited<ReturnType<typeof readProbe>>,
current: Awaited<ReturnType<typeof readProbe>>
): boolean {
return (
JSON.stringify(current.selection) !== JSON.stringify(baseline.selection) ||
JSON.stringify(current.find) !== JSON.stringify(baseline.find) ||
JSON.stringify(current.visibleRanges) !== JSON.stringify(baseline.visibleRanges) ||
Math.abs(current.scrollTop - baseline.scrollTop) > 2
)
}
function assertMemoryBudget(
suffix: { before: MemorySample; after: MemorySample; settled: MemorySample }[],
replacement: { before: MemorySample; after: MemorySample; settled: MemorySample }[]
): void {
expect(suffix).toHaveLength(replacement.length)
expect(suffix.length).toBeGreaterThan(0)
for (const sample of suffix) {
for (const field of ['jsHeapMb', 'workingSetMb', 'privateMb'] as const) {
const retainedAllowance = Math.max(20, sample.before[field] * 0.1)
expect(sample.settled[field] - sample.before[field]).toBeLessThanOrEqual(retainedAllowance)
}
}
for (const [index, sample] of suffix.entries()) {
const pairedReplacement = replacement[index]
expect(pairedReplacement).toBeDefined()
for (const field of ['jsHeapMb', 'workingSetMb', 'privateMb'] as const) {
const suffixPeak = sample.after[field] - sample.before[field]
const replacementPeak = pairedReplacement.after[field] - pairedReplacement.before[field]
expect(suffixPeak).toBeLessThanOrEqual(replacementPeak)
}
}
}
async function readProbeOrNull(page: Page): Promise<Awaited<ReturnType<typeof readProbe>> | null> {
return page.evaluate(() => {
const probe = window.__monacoEditorE2E
return probe ? { filePath: probe.filePath, ...probe.snapshot() } : null
})
}