Describe the bug
A long-lived session's events.jsonl grew past V8's maximum string length. After that, the CLI could no longer resume the session. The session still appears in the /resume list, its row is intact in session-store.db, and the file itself is intact and readable, but no supported CLI path can reach it.
Rewriting the file to a smaller size restored it immediately, with no other change.
Stage 2 first, because the evidence is strongest. After the process exited cleanly, resume failed:
[WARNING] Deferred resume: session <id> could not be loaded
[ERROR] Failed to switch session: Error: Failed to get session
Both the /resume picker and --resume=<id> failed the same way. The file was 559,508,763 bytes. I rewrote it to 429,424,287 bytes by truncating long strings nested under .data.result, and the session resumed normally on the next attempt.
Stage 1, earlier and less certain. While the session was running, the terminal stopped updating:
[ERROR] TerminalRenderer React uncaught error: Error: Failed to convert rust `String` into napi `string`
A React error boundary caught this and the pane never painted again. The agent underneath kept working for another 45 minutes. It executed tool calls, wrote assistant.message events, and completed turns normally.
Input still reached the agent while the display was dead. Typed messages were accepted and answered. Slash commands were not: /quit produced no event in events.jsonl at all, which suggests the crashed UI layer intercepts it before the agent sees it. I found no in-band way to exit, so I terminated the process from another shell.
Affected version
Copilot CLI 1.0.77, which is the current release. Node.js v24.16.0, x86-64 Linux. The session was originally created under 1.0.75.
Steps to reproduce the behavior
I cannot offer a cheap reproduction from scratch, since mine took six days of real use to build a 533 MiB log. A synthetic path should work: take an existing session's events.jsonl, pad it past 536,870,888 bytes with valid event records, then run --resume=<id> against it.
For reference, my log grew at roughly 60 MB/hour under sustained use with large tool outputs.
Expected behavior
A session should not become unreachable because of its own size. Specifically:
- Session state should be read incrementally, so no file size makes a session unloadable.
- If an aggregate string must be built, the failure should be contained rather than making the whole session unreachable.
- The error should name the real cause. "Failed to get session" gave no indication that size was involved.
Additional context
What I measured, versus what I inferred.
The measurement is a bracket, not an exact threshold. The session failed to load at 559,508,763 bytes and loaded at 429,424,287 bytes. The true threshold lies somewhere in that range, and I did not narrow it further.
V8's maximum string length falls inside that bracket. Node.js v24.16.0 bundles V8 13.6, which defines the x64 limit as (1 << 29) - 24, or 536,870,888. I could not run a standalone node on the affected machine, so I verified the constant on Node v26.4.0 with V8 14.6 instead, where buffer.constants.MAX_STRING_LENGTH is 536,870,888 and allocating one more character throws ERR_STRING_TOO_LONG. That value has been unchanged across those versions.
One unit caveat: Node documents this constant in UTF-16 code units. The Stage 1 error text originates in napi-rs, which passes a Rust string's UTF-8 byte length to napi_create_string_utf8. That call forwards to v8::String::NewFromUtf8, which compares the byte length against the same numeric limit before decoding. So a byte-to-constant comparison is the right one for that path.
I cannot see the CLI source, so I am inferring the code path from the error string. What I can say is that size is the only variable I changed, and it decided whether the session loaded.
Alternatives I considered.
A 1.0.75 to 1.0.77 format change is the obvious competing hypothesis, since the session was created under the older version. I do not think it explains this. The successful load used the same 1.0.77 binary on the same session directory, and file size was the only difference. I did not test the unmodified file under 1.0.75, so I cannot fully exclude a version interaction.
Memory pressure is a second candidate. Resident memory of the resumed process was 1.7 GB, against 24 GB for the process that died. Those are different lifetimes with different data, so that comparison is suggestive at best, and an allocation failure at 24 GB is independently plausible.
Stage 1 and Stage 2 may not share a cause. I reconstructed the file's size over time from event offsets. It crossed 536,870,888 bytes at 18:41, but the renderer did not fail until 21:53, more than three hours later, with the file then around 530 MiB. Being over the limit was therefore not sufficient on its own. Something has to attempt the oversized conversion. In my case the display froze shortly after I opened the /tasks view, so that may be the trigger. Treating both stages as one root cause is an inference, and only Stage 2 is backed by a deterministic fix.
Where the volume comes from. Tool results dominate. Events larger than 8 KB accounted for 317 MiB of the file, and .data.result was by far the largest field. Two individual events reached 3.5 MB each. Those two are a small share of the total, but they show that no per-event cap applies. One large event came from an HTTP 502 response whose HTML error page was logged verbatim, including a base64-encoded image of about 45 KB.
Workaround, for anyone who hits this. Trim only while the session is stopped. The process holds events.jsonl open in append mode, so replacing the file underneath a live session sends later writes to the orphaned inode. walk requires jq 1.6 or newer.
cp events.jsonl events.jsonl.bak
jq -c 'if (.data|type)=="object" and (.data|has("result"))
then .data.result = (.data.result | walk(
if type=="string" and length>4000
then .[0:4000] + " ...[TRUNCATED]" else . end))
else . end' events.jsonl > events.jsonl.trimmed
mv events.jsonl.trimmed events.jsonl
The rewritten file had the same number of JSONL records, 148,787. Only strings nested under .data.result were shortened. The first and last events were still session.start and session.shutdown.
Related issues.
#4251 is the closest relative and, taken together with this report, brackets the threshold from
both sides. It describes a roughly 532 MB session that still resumes, but only after heavy memory
use and a long CPU grind. If that figure is decimal, it is about 507 MiB, which sits just under
536,870,888. Mine is 533.6 MiB, just over, and it does not resume at all. A degradation just below
the limit and a hard failure just above it is what an engine ceiling would look like. That reporter
attributes the slowdown to a 1.0.74 change, so the two effects may be independent, and I have not
verified their exact byte count.
#3695 reports the same napi-rs conversion error from /fork, with no diagnosis attached. If that
path also builds one large string, this limit would explain it, but I cannot confirm the two share
a cause.
#2209 and #2490 both describe long-lived sessions becoming unresumable and recoverable only by
trimming events.jsonl by hand. Both were closed as not planned. I believe this report is
different in kind rather than another instance. Those sessions were 18 MB and 20 MB, and the
diagnosis there was raw U+2028 breaking JSON.parse (#2012). Mine is more than an order of
magnitude larger, contains no such corruption, and points at a fixed engine ceiling that no amount
of valid, well-formed content can avoid.
#4102 reports a different V8 ceiling, a fatal invalid array length, on much smaller tool-heavy
sessions. It is not the same limit, but it suggests session growth meets more than one engine
bound.
#3366 and #4098 cover other events.jsonl durability failures, if the surrounding pattern is
useful.
Suggested mitigations, in rough order of value:
- Avoid materialising the event log or aggregate session state as a single JavaScript string, on both the resume path and the N-API boundary.
- Keep a session resumable when one payload is too large. Skipping or lazily loading it with a warning is better than failing the whole session.
- Cap or externalise oversized tool results at write time.
- Segment the log, provided resume also reads segments incrementally.
- Surface the underlying size or conversion error, so the failure is self-diagnosing.
I still have the original 559 MB file. I would rather not attach it publicly, since it contains my working content, but I am happy to share metadata, offsets, or a synthetic equivalent if that helps.
Describe the bug
A long-lived session's
events.jsonlgrew past V8's maximum string length. After that, the CLI could no longer resume the session. The session still appears in the/resumelist, its row is intact insession-store.db, and the file itself is intact and readable, but no supported CLI path can reach it.Rewriting the file to a smaller size restored it immediately, with no other change.
Stage 2 first, because the evidence is strongest. After the process exited cleanly, resume failed:
Both the
/resumepicker and--resume=<id>failed the same way. The file was 559,508,763 bytes. I rewrote it to 429,424,287 bytes by truncating long strings nested under.data.result, and the session resumed normally on the next attempt.Stage 1, earlier and less certain. While the session was running, the terminal stopped updating:
A React error boundary caught this and the pane never painted again. The agent underneath kept working for another 45 minutes. It executed tool calls, wrote
assistant.messageevents, and completed turns normally.Input still reached the agent while the display was dead. Typed messages were accepted and answered. Slash commands were not:
/quitproduced no event inevents.jsonlat all, which suggests the crashed UI layer intercepts it before the agent sees it. I found no in-band way to exit, so I terminated the process from another shell.Affected version
Copilot CLI 1.0.77, which is the current release. Node.js v24.16.0, x86-64 Linux. The session was originally created under 1.0.75.
Steps to reproduce the behavior
I cannot offer a cheap reproduction from scratch, since mine took six days of real use to build a 533 MiB log. A synthetic path should work: take an existing session's
events.jsonl, pad it past 536,870,888 bytes with valid event records, then run--resume=<id>against it.For reference, my log grew at roughly 60 MB/hour under sustained use with large tool outputs.
Expected behavior
A session should not become unreachable because of its own size. Specifically:
Additional context
What I measured, versus what I inferred.
The measurement is a bracket, not an exact threshold. The session failed to load at 559,508,763 bytes and loaded at 429,424,287 bytes. The true threshold lies somewhere in that range, and I did not narrow it further.
V8's maximum string length falls inside that bracket. Node.js v24.16.0 bundles V8 13.6, which defines the x64 limit as
(1 << 29) - 24, or 536,870,888. I could not run a standalonenodeon the affected machine, so I verified the constant on Node v26.4.0 with V8 14.6 instead, wherebuffer.constants.MAX_STRING_LENGTHis 536,870,888 and allocating one more character throwsERR_STRING_TOO_LONG. That value has been unchanged across those versions.One unit caveat: Node documents this constant in UTF-16 code units. The Stage 1 error text originates in napi-rs, which passes a Rust string's UTF-8 byte length to
napi_create_string_utf8. That call forwards tov8::String::NewFromUtf8, which compares the byte length against the same numeric limit before decoding. So a byte-to-constant comparison is the right one for that path.I cannot see the CLI source, so I am inferring the code path from the error string. What I can say is that size is the only variable I changed, and it decided whether the session loaded.
Alternatives I considered.
A 1.0.75 to 1.0.77 format change is the obvious competing hypothesis, since the session was created under the older version. I do not think it explains this. The successful load used the same 1.0.77 binary on the same session directory, and file size was the only difference. I did not test the unmodified file under 1.0.75, so I cannot fully exclude a version interaction.
Memory pressure is a second candidate. Resident memory of the resumed process was 1.7 GB, against 24 GB for the process that died. Those are different lifetimes with different data, so that comparison is suggestive at best, and an allocation failure at 24 GB is independently plausible.
Stage 1 and Stage 2 may not share a cause. I reconstructed the file's size over time from event offsets. It crossed 536,870,888 bytes at 18:41, but the renderer did not fail until 21:53, more than three hours later, with the file then around 530 MiB. Being over the limit was therefore not sufficient on its own. Something has to attempt the oversized conversion. In my case the display froze shortly after I opened the
/tasksview, so that may be the trigger. Treating both stages as one root cause is an inference, and only Stage 2 is backed by a deterministic fix.Where the volume comes from. Tool results dominate. Events larger than 8 KB accounted for 317 MiB of the file, and
.data.resultwas by far the largest field. Two individual events reached 3.5 MB each. Those two are a small share of the total, but they show that no per-event cap applies. One large event came from an HTTP 502 response whose HTML error page was logged verbatim, including a base64-encoded image of about 45 KB.Workaround, for anyone who hits this. Trim only while the session is stopped. The process holds
events.jsonlopen in append mode, so replacing the file underneath a live session sends later writes to the orphaned inode.walkrequires jq 1.6 or newer.The rewritten file had the same number of JSONL records, 148,787. Only strings nested under
.data.resultwere shortened. The first and last events were stillsession.startandsession.shutdown.Related issues.
#4251 is the closest relative and, taken together with this report, brackets the threshold from
both sides. It describes a roughly 532 MB session that still resumes, but only after heavy memory
use and a long CPU grind. If that figure is decimal, it is about 507 MiB, which sits just under
536,870,888. Mine is 533.6 MiB, just over, and it does not resume at all. A degradation just below
the limit and a hard failure just above it is what an engine ceiling would look like. That reporter
attributes the slowdown to a 1.0.74 change, so the two effects may be independent, and I have not
verified their exact byte count.
#3695 reports the same napi-rs conversion error from
/fork, with no diagnosis attached. If thatpath also builds one large string, this limit would explain it, but I cannot confirm the two share
a cause.
#2209 and #2490 both describe long-lived sessions becoming unresumable and recoverable only by
trimming
events.jsonlby hand. Both were closed as not planned. I believe this report isdifferent in kind rather than another instance. Those sessions were 18 MB and 20 MB, and the
diagnosis there was raw U+2028 breaking
JSON.parse(#2012). Mine is more than an order ofmagnitude larger, contains no such corruption, and points at a fixed engine ceiling that no amount
of valid, well-formed content can avoid.
#4102 reports a different V8 ceiling, a fatal invalid array length, on much smaller tool-heavy
sessions. It is not the same limit, but it suggests session growth meets more than one engine
bound.
#3366 and #4098 cover other
events.jsonldurability failures, if the surrounding pattern isuseful.
Suggested mitigations, in rough order of value:
I still have the original 559 MB file. I would rather not attach it publicly, since it contains my working content, but I am happy to share metadata, offsets, or a synthetic equivalent if that helps.