Skip to content

feat: persist session submissions and reconcile delivery - #709

Draft
zxch3n wants to merge 6 commits into
refactor/attachment-effect-resourcesfrom
feat/attachment-submit-recovery
Draft

zxch3n wants to merge 6 commits into
refactor/attachment-effect-resourcesfrom
feat/attachment-submit-recovery

Conversation

@zxch3n

@zxch3n zxch3n commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Problem / pressure

A lost write receipt can leave a message authored but invite a duplicate append. Route, account and app teardown also need a durable owner for messages that have not reached their target transport.

Summary

Stack 3/4, based on #707. Introduce account/workspace-scoped IndexedDB submission records, separate per-session submission and delivery locks, and recovery controls. The existing writers prepare history or queue operations on a temporary fork; exact bytes are saved before live import. Recovery replays those bytes and merges the original replica baseline. The saved source is the window that actually prepares the operations, including cross-window takeover. Explicit target synchronization publishes imported operations. Guide uncertainty reconciles the original turn without another offer.

Electron asks before draining renderer work and stopping the CLI. Browser navigation, logout and cache/reset paths retain unresolved messages. Transfer timing remains unchanged; complete attachment drafts follow in layer 4.

Visual explanation

New / continuation / queue
  → strict local admission
  → prepare exact operations (existing writers)
  → save operation bytes
  → import + flush
  → serialized target delivery → target sync receipt
          ↘ uncertain: retain record, reconcile same identity

Exit → check every renderer → confirm → join owners → stop CLI

Before / after

Before After
Local acceptance could be confused with durable delivery Saved, prepared, committed and delivered stages are separate
Retrying list append could duplicate a message Recovery imports the same saved operations
Queue writes had a separate lifetime Queue admission shares the journal and preserves queue format
Quit could stop the CLI before renderer work settled All renderer drains precede CLI shutdown

Test plan

  • Real Loro tests cover no publication before the storage receipt, repeated recovery and absent baseline dependencies.
  • Actual IndexedDB-compatible transactions cover reopen, sequence allocation and account isolation.
  • Current Streams adapter test reproduces imported updates bypassing the local-edit listener, then verifies explicit export reaches a second real document exactly once.
  • Deterministic tests cover failed-head ordering, per-session delivery exclusion, queue replay, and multi-window exit/drain ordering.
  • TMPDIR=/private/tmp NODE_ENV=test pnpm check passes completely (components: 479 files / 3,671 tests). pnpm format and pnpm run docs check completed; docs report zero errors. Packaged desktop/mobile acceptance is still outstanding.

Context handoff

Instructions for reviewing agents

  • Review focus: journal receipt boundaries, original-replica restoration, queue promotion, Guide uncertainty, and shutdown ordering.
  • Decisions to challenge: Exact operation replay, separate submission/delivery locks, and explicit synchronization after imported operations.
  • Plausible failures / evidence gaps: Uninterruptible IPC can delay shutdown. Unavailable original replicas retain recovery records. Transport sync is not proof of Agent execution. Real-device acceptance remains outstanding.

Authoring context

  • Origin: Maintainer-directed stacked implementation of the draft attachment Spec.
  • Scope: Reliable submission and recovery prerequisite; uploading on Send and pending attachment presentation are the next layer.
  • Unknowns / confidence: Deterministic local evidence does not establish packaged-device or distributed exactly-once Agent execution guarantees.

@zxch3n zxch3n left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Layer 3’s journal receipt boundary is sound: HistoryWriter prepares on a fork, IndexedDB put of exact bytes happens before live import, replay is idempotent, original-replica snapshot import is a Loro merge (not a replace), submission/delivery use separate per-session locks, Guide checkpoints offered before RPC and will not send a second offer, logout/cache-clear retain unresolved records, and Electron drains renderers before CLI shutdown.

One P1 remains. Queue admission now journals under the queue item’s userTurnId. Native queue-steer still promotes by calling addHistory with that same id and a pending_apply entry. insert treats the different entry JSON as an identity conflict, so steer-capable agents cannot promote a queued message.

Issues

bug — queued userTurnId cannot be promoted to history

pushMessageQueue admits a journal record keyed by userTurnId with default status: 'pending' and a queue payload. handleNativeSteerQueuedMessage then calls useSessionDoc.addHistory with the same id and status: 'pending_apply'. All user addHistory calls now go through acceptSessionUserTurnstorage.insert, which throws Submission identity conflicts with saved content when the entry JSON differs. Native queue-steer (acknowledgedSteer) therefore fails every time instead of activate/retry on the original record. CLI mq promotion is unaffected.

Suggestion: If sendJournal.read(userTurnId) already exists, activate to guide/dispatch and retry instead of admitting a second history write.

throw new Error('Runtime not ready');
}
const entry = { ...item, id: item.id ?? uuidv4() } as SessionHistory;
if (entry.role === 'user') {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: This new user-turn gate journals every addHistory under entry.id. pushMessageQueue already admitted that id as a queue record (status: 'pending' plus queue payload). Native queue-steer (handleNativeSteerQueuedMessage) reuses the same userTurnId with status: 'pending_apply' and no queue payload, so storage.insert throws Submission identity conflicts with saved content.

Steer-capable agents therefore cannot promote a visible queued message; retry hits the same conflict. Reuse the existing record via activate({ kind: 'guide', expectedTurnId }) + retry instead of a second admission.

@zxch3n
zxch3n force-pushed the feat/attachment-submit-recovery branch from 4effefc to ef0faa6 Compare September 16, 2026 07:18
@zxch3n
zxch3n force-pushed the feat/attachment-submit-recovery branch from ef0faa6 to 95a68c7 Compare September 16, 2026 07:25
@zxch3n
zxch3n force-pushed the feat/attachment-submit-recovery branch from 786a877 to 8bcd872 Compare September 16, 2026 10:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant