Repository navigation
fix(sessions): keep attachment references a rewritten message still points at #2236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fa77d1c
ee1f257
9c08140
b3af551
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| "agents": patch | ||
| --- | ||
|
|
||
| Sessions: keep the attachment references of a message written back with pointers in it. | ||
|
|
||
| A message whose media was already offloaded carries `attachment:sha256:…` pointers. Writing that stored form back (an `updateMessage`, or a copy through `appendMessage` or `importMessage`) derived references only from media extracted on that write, so it recorded none, and the payload could be collected while the row still pointed at it. References now cover every pointer the stored message contains. Rows already orphaned are not repaired. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -940,7 +940,11 @@ export class SessionsCore { | |
| // Inline media leaves the message before it is serialized, so the row | ||
| // holds a pointer and never the payload. Addresses are computed here, out | ||
| // of the transaction; the transaction only writes. | ||
| const { message: staged, attachments } = extractAttachments(message); | ||
| const { | ||
| message: staged, | ||
| attachments, | ||
| references | ||
| } = extractAttachments(message); | ||
| const slices = splitContent(JSON.stringify(staged)); | ||
| const seq = tail.nextSeq; | ||
| this.io.transaction(() => { | ||
|
|
@@ -964,11 +968,7 @@ export class SessionsCore { | |
| ] | ||
| ); | ||
| this.#writeContinuations(sessionId, message.id, slices); | ||
| this.#attachments.addRefs( | ||
| sessionId, | ||
| message.id, | ||
| attachments.map((attachment) => attachment.hash) | ||
| ); | ||
| this.#attachments.addRefs(sessionId, message.id, references); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Pointer copies bypass image token charges When Learn moreThe per-row token estimate is calculated by the write pipeline before attachment ingestion. estimateRowTokens charges inline file data URLs but not existing attachment pointers. This change now allows copies bearing pointers to keep their payloads, yet the copied rows get estimates that omit those images. The threshold in compactAfter depends on those estimates. Example: Copy a stored image message under a new id and call Recommended fix: For pointer-form writes, derive the attachment token charge from referenced payload metadata before stamping Was this helpful? React with 👍 or 👎 to provide feedback.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Out of scope here. |
||
| this.#indexFts(sessionId, staged, false); | ||
| }); | ||
|
|
||
|
|
@@ -1024,7 +1024,11 @@ export class SessionsCore { | |
| (this.#continuations(sessionId, [message.id]).get(message.id) ?? ""); | ||
| // Compare in stored form: a re-sent identical image extracts to the same | ||
| // address, so an unchanged update still writes nothing. | ||
| const { message: staged, attachments } = extractAttachments(message); | ||
| const { | ||
| message: staged, | ||
| attachments, | ||
| references | ||
| } = extractAttachments(message); | ||
| const json = JSON.stringify(staged); | ||
| if (oldContent === json) return "unchanged"; | ||
|
|
||
|
|
@@ -1058,11 +1062,7 @@ export class SessionsCore { | |
| this.#writeContinuations(sessionId, message.id, slices); | ||
| // Payloads are stored before references move, so a hash this message | ||
| // still uses is never momentarily unreferenced and collected. | ||
| this.#attachments.replaceRefs( | ||
| sessionId, | ||
| message.id, | ||
| attachments.map((attachment) => attachment.hash) | ||
| ); | ||
| this.#attachments.replaceRefs(sessionId, message.id, references); | ||
| this.#indexFts(sessionId, staged, true); | ||
| }); | ||
| const memo = this.#pathTokens.get(sessionId); | ||
|
|
@@ -1293,7 +1293,11 @@ export class SessionsCore { | |
| message: SessionMessage, | ||
| options: { parentId: string | null; createdAt: number } | ||
| ): boolean { | ||
| const { message: staged, attachments } = extractAttachments(message); | ||
| const { | ||
| message: staged, | ||
| attachments, | ||
| references | ||
| } = extractAttachments(message); | ||
| const slices = splitContent(JSON.stringify(staged)); | ||
| const tail = this.#tail(sessionId); | ||
| let inserted = 0; | ||
|
|
@@ -1319,11 +1323,7 @@ export class SessionsCore { | |
| ); | ||
| if (inserted === 0) return; | ||
| this.#writeContinuations(sessionId, message.id, slices); | ||
| this.#attachments.addRefs( | ||
| sessionId, | ||
| message.id, | ||
| attachments.map((attachment) => attachment.hash) | ||
| ); | ||
| this.#attachments.addRefs(sessionId, message.id, references); | ||
| this.#indexFts(sessionId, staged, false); | ||
| }); | ||
| if (inserted === 0) return false; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Pointer scan adds a second full traversal
Every write now scans
partsagain after extraction, including text-only messages. Nested tool outputs also allocate anObject.valuesarray per object. Consider measuring this added write cost for large transcripts.Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The scan is bounded by the same depth as extraction and only visits the staged parts.
JSON.stringifyof the same message follows straight after and walks all of it, so the scan is small next to that.