What happened
The installed macOS T3 Code Nightly stopped starting after updating to 0.0.41-nightly.20260908.1400. Its embedded backend repeatedly exhausted the JavaScript heap and aborted. Restoring official signed 0.0.41-nightly.20260908.1387 restored access to the same existing projects and threads without deleting history or editing projection cursors.
Diagnosis
The attachment-cleanup bootstrap added by #9871, commit 7220dfe2c949476eaa7d21eccbcd3a0ce0eddb49, forces a full historical event replay when its new cursor is absent or zero, even when all ordinary projections are caught up.
On the stopped-app recovery backup:
- All nine ordinary projection cursors were at event
1131083; projection.attachment-cleanup was 0.
- The event store held 1,131,083 events and 3,441,699,990 serialized payload bytes. The largest event was 67,627,295 bytes; the largest 500-event page was 95,024,354 bytes.
- Only 24
thread.deleted events, totaling 2,160 payload characters, were relevant to cleanup. There were no thread.reverted events.
ProjectionPipeline.ts persists the starting cursor, runs readFromSequence(cleanupStart, Number.MAX_SAFE_INTEGER), filters deletions/reverts in JavaScript, and advances the cleanup cursor only after the complete scan and successful cleanup. An OOM leaves the next launch starting from zero again.
The existing event-store reader uses 500-row SQL pages but recursively concatenates them inside Stream.flatMap. A synthetic comparison using Effect 4.0.0-rc.112 reproduces retention of consumed pages even with a discard consumer. Non-recursive Stream.unfold pagination over the same generated rows stays flat in that test.
The native crash stack reaches node::sqlite::StatementExecutionHelper::All; the Node SQL adapter calls statement.all. This is paged SQL with cumulative retention, not a single SQL query returning the entire database. The shipped 1400 ASAR contains the same cleanup and recursive reader code.
The evidence strongly connects the newly forced full replay to a pre-existing stream-retention problem. A full-app heap profile has not been captured, so the exact retained-object graph and contribution of other startup allocations remain unproven.
Steps to reproduce
Observed on the affected profile, not repeated against live data during this diagnosis:
- Start with an established 1387 profile containing a large orchestration event history, with ordinary projections caught up.
- Update the desktop app to Nightly 1400 and launch it.
- Observe the embedded backend abort with V8 OOM during startup and retry without advancing the cleanup cursor.
- Restore 1387; the existing environment becomes usable again.
The portable synthetic test below can be saved as stream-retention.mjs. It uses 100 pages of 500 generated events with 2,048-character text payloads, a discard consumer, forced GC every 10,000 events, and count assertions. Run against an existing Effect 4.0.0-rc.112 package:
node --expose-gc stream-retention.mjs recursive /absolute/path/to/effect
node --expose-gc stream-retention.mjs flat /absolute/path/to/effect
stream-retention.mjs
import { pathToFileURL } from "node:url";
import path from "node:path";
const effectRoot = process.argv[3];
if (!effectRoot) throw new Error("Pass variant (recursive|flat) and absolute path to effect package directory");
const Effect = await import(pathToFileURL(path.join(effectRoot, "dist/Effect.js")));
const Stream = await import(pathToFileURL(path.join(effectRoot, "dist/Stream.js")));
import assert from "node:assert/strict";
const pages = 100;
const pageSize = 500;
const readRows = (cursor) => Array.from({length: pageSize}, (_,i) => ({sequence: cursor+i+1, payload: JSON.parse(JSON.stringify({text: String(cursor+i).padEnd(2048,"x")}))}));
const recursive = (cursor, remaining) => Stream.fromEffect(Effect.sync(() => readRows(cursor))).pipe(Stream.flatMap(events => {
const nextRemaining = remaining - events.length;
if(nextRemaining <= 0) return Stream.fromIterable(events);
return Stream.concat(Stream.fromIterable(events), recursive(events.at(-1).sequence,nextRemaining));
}));
const flat = Stream.unfold(0, cursor => Effect.sync(() => cursor >= pages*pageSize ? undefined : [readRows(cursor), cursor+pageSize])).pipe(Stream.flattenIterable);
let count=0;
await Effect.runPromise(Stream.runForEach(process.argv[2] === "recursive" ? recursive(0,pages*pageSize) : flat, () => Effect.sync(() => {
count++;
if(count % 10000 === 0) { global.gc(); console.log(JSON.stringify({variant:process.argv[2],count,heapMB:Math.round(process.memoryUsage().heapUsed/1024/1024)})); }
})));
assert.equal(count,pages*pageSize);
Both consume exactly 50,000 events. This reproduces stream retention only; it is not a minimal full-application reproduction. No private database or event contents are attached.
Version
Failed: 0.0.41-nightly.20260908.1400, release commit eb115063634c416c6362cc407f8572cb0c136ddf.
Working rollback: 0.0.41-nightly.20260908.1387.
Environment
macOS 27.0 (26A5425a), arm64, desktop app with its embedded local Electron backend. Triage launched with npx t3@nightly triage; its Node v24.15.0 describes the triage process, not a verified version of the failed Electron Node runtime. Source lockfile pins Effect 4.0.0-rc.112.
Evidence
2026-09-08T13:01:20.803Z
Mark-Compact (reduce) 3693.7 (3751.1) -> 3693.5 (3749.1) MB
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
2026-09-08T13:03:01Z macOS crash report, version 1400:
EXC_CRASH (SIGABRT), Abort trap: 6
node::OOMErrorHandler
v8::String::NewFromUtf8
node::sqlite::StatementExecutionHelper::ColumnToValue
node::sqlite::ExtractRowValues
node::sqlite::StatementExecutionHelper::All
Synthetic retained heap after forced GC (MiB):
events 10000 20000 30000 40000 50000
recursive 29 50 71 91 112
unfold 10 10 10 10 10
Desktop traces record backend restart scheduling after OOM. The source awaits projection bootstrap before starting the orchestration worker. The 1387-to-1400 comparison changes the cleanup path but not the event-store implementation or schema migrations.
Related issues
No confirmed duplicate found in targeted searches on September 8, 2026. #8648 concerns long-uptime OOM on 0.0.35; #7075 concerns provider resume payloads. Neither establishes this new cleanup-startup trigger.
Merged #8992 bounds WebSocket replay payloads, but its guard does not cover cleanup bootstrap. Open #10512 addresses a V2 compatibility RPC rather than this startup path. #7537 documents why silently capping projection catch-up is unsafe.
At investigation time 1400 was the newest published release. Main at 061543e9e5b54ec0048725c37d52fef2962df173 had no subsequent cleanup fix.
Fix applied or workaround
Recovery restored official signed Nightly 1387 and verified existing projects and threads were accessible. User history and attachments were preserved. This triage only queried the recovery backup read-only and inspected source/logs; it made no live changes.
Suggested upstream direction: non-retaining pagination in the shared event reader, plus a cleanup query that reads only necessary deletion/revert metadata. Preserve full catch-up and retry-safe cleanup semantics. Raising the heap limit or manually skipping cleanup is not a verified fix.
Filed by
Diagnosed and filed with GPT-6 Astra in the Codex harness via T3 Code triage, including a synthetic retention experiment and read-only database measurements.
What happened
The installed macOS T3 Code Nightly stopped starting after updating to
0.0.41-nightly.20260908.1400. Its embedded backend repeatedly exhausted the JavaScript heap and aborted. Restoring official signed0.0.41-nightly.20260908.1387restored access to the same existing projects and threads without deleting history or editing projection cursors.Diagnosis
The attachment-cleanup bootstrap added by #9871, commit
7220dfe2c949476eaa7d21eccbcd3a0ce0eddb49, forces a full historical event replay when its new cursor is absent or zero, even when all ordinary projections are caught up.On the stopped-app recovery backup:
1131083;projection.attachment-cleanupwas0.thread.deletedevents, totaling 2,160 payload characters, were relevant to cleanup. There were nothread.revertedevents.ProjectionPipeline.ts persists the starting cursor, runs
readFromSequence(cleanupStart, Number.MAX_SAFE_INTEGER), filters deletions/reverts in JavaScript, and advances the cleanup cursor only after the complete scan and successful cleanup. An OOM leaves the next launch starting from zero again.The existing event-store reader uses 500-row SQL pages but recursively concatenates them inside
Stream.flatMap. A synthetic comparison using Effect 4.0.0-rc.112 reproduces retention of consumed pages even with a discard consumer. Non-recursiveStream.unfoldpagination over the same generated rows stays flat in that test.The native crash stack reaches
node::sqlite::StatementExecutionHelper::All; the Node SQL adapter callsstatement.all. This is paged SQL with cumulative retention, not a single SQL query returning the entire database. The shipped 1400 ASAR contains the same cleanup and recursive reader code.The evidence strongly connects the newly forced full replay to a pre-existing stream-retention problem. A full-app heap profile has not been captured, so the exact retained-object graph and contribution of other startup allocations remain unproven.
Steps to reproduce
Observed on the affected profile, not repeated against live data during this diagnosis:
The portable synthetic test below can be saved as
stream-retention.mjs. It uses 100 pages of 500 generated events with 2,048-character text payloads, a discard consumer, forced GC every 10,000 events, and count assertions. Run against an existing Effect 4.0.0-rc.112 package:stream-retention.mjs
Both consume exactly 50,000 events. This reproduces stream retention only; it is not a minimal full-application reproduction. No private database or event contents are attached.
Version
Failed:
0.0.41-nightly.20260908.1400, release commiteb115063634c416c6362cc407f8572cb0c136ddf.Working rollback:
0.0.41-nightly.20260908.1387.Environment
macOS 27.0 (
26A5425a), arm64, desktop app with its embedded local Electron backend. Triage launched withnpx t3@nightly triage; its Node v24.15.0 describes the triage process, not a verified version of the failed Electron Node runtime. Source lockfile pins Effect 4.0.0-rc.112.Evidence
Desktop traces record backend restart scheduling after OOM. The source awaits projection bootstrap before starting the orchestration worker. The 1387-to-1400 comparison changes the cleanup path but not the event-store implementation or schema migrations.
Related issues
No confirmed duplicate found in targeted searches on September 8, 2026. #8648 concerns long-uptime OOM on 0.0.35; #7075 concerns provider resume payloads. Neither establishes this new cleanup-startup trigger.
Merged #8992 bounds WebSocket replay payloads, but its guard does not cover cleanup bootstrap. Open #10512 addresses a V2 compatibility RPC rather than this startup path. #7537 documents why silently capping projection catch-up is unsafe.
At investigation time 1400 was the newest published release. Main at
061543e9e5b54ec0048725c37d52fef2962df173had no subsequent cleanup fix.Fix applied or workaround
Recovery restored official signed Nightly 1387 and verified existing projects and threads were accessible. User history and attachments were preserved. This triage only queried the recovery backup read-only and inspected source/logs; it made no live changes.
Suggested upstream direction: non-retaining pagination in the shared event reader, plus a cleanup query that reads only necessary deletion/revert metadata. Preserve full catch-up and retry-safe cleanup semantics. Raising the heap limit or manually skipping cleanup is not a verified fix.
Filed by
Diagnosed and filed with GPT-6 Astra in the Codex harness via T3 Code triage, including a synthetic retention experiment and read-only database measurements.