From b8811b0557a28869ecdd5f21193b70bca253eff7 Mon Sep 17 00:00:00 2001 From: Actual Operator Date: Thu, 1 Oct 2026 23:02:28 +0000 Subject: [PATCH 1/2] Sync context files with ADRs - Update .actual/rules/cross-cutting-message-ingestion-synchronization-layers-e283.md (claude) - Update .actual/rules/cross-cutting-session-history-compaction-routines-reve-df90.md (claude) - Update .actual/rules/cross-cutting-session-message-timelines-transcript-exp-f4fc.md (claude) - Update AGENTS.md (agents) - Update docs/adr/e2838170-cdcd-4036-ae98-5e997b53d7cd-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-message-ingestion-synchronization-layers-not-assume.md (docs) - Update docs/adr/df903f58-c272-4a3f-b640-a93922945540-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-history-compaction-routines-revert-boundary.md (docs) - Update docs/adr/f4fcb389-bd12-4d7e-8adc-0d32c57f98c4-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-message-timelines-transcript-exports-sync.md (docs) --- ...e-ingestion-synchronization-layers-e283.md | 24 ++++ ...n-history-compaction-routines-reve-df90.md | 22 ++++ ...n-message-timelines-transcript-exp-f4fc.md | 25 ++++ AGENTS.md | 83 ++++++++++++ ...ory-compaction-routines-revert-boundary.md | 120 ++++++++++++++++++ ...stion-synchronization-layers-not-assume.md | 120 ++++++++++++++++++ ...ssage-timelines-transcript-exports-sync.md | 120 ++++++++++++++++++ 7 files changed, 514 insertions(+) create mode 100644 .actual/rules/cross-cutting-message-ingestion-synchronization-layers-e283.md create mode 100644 .actual/rules/cross-cutting-session-history-compaction-routines-reve-df90.md create mode 100644 .actual/rules/cross-cutting-session-message-timelines-transcript-exp-f4fc.md create mode 100644 docs/adr/df903f58-c272-4a3f-b640-a93922945540-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-history-compaction-routines-revert-boundary.md create mode 100644 docs/adr/e2838170-cdcd-4036-ae98-5e997b53d7cd-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-message-ingestion-synchronization-layers-not-assume.md create mode 100644 docs/adr/f4fcb389-bd12-4d7e-8adc-0d32c57f98c4-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-message-timelines-transcript-exports-sync.md diff --git a/.actual/rules/cross-cutting-message-ingestion-synchronization-layers-e283.md b/.actual/rules/cross-cutting-message-ingestion-synchronization-layers-e283.md new file mode 100644 index 000000000000..0d05f3b93871 --- /dev/null +++ b/.actual/rules/cross-cutting-message-ingestion-synchronization-layers-e283.md @@ -0,0 +1,24 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Message Ingestion Synchronization Layers Not Assume + +These rules are ALWAYS ACTIVE for all session message ingestion, timeline aggregation, sync hydration reducers, transcript exports, and history revert calculations across packages/app, packages/tui, packages/opencode, and packages/web. + +### Rules + +- **R-MSG-001** MUST_NOT: Message ingestion and synchronization layers MUST NOT assume or depend upon network transport arrival order for message sequencing. + +### Verify + +```bash +# Run test suite for session synchronization, timeline aggregation, and revert calculation +npx turbo test --filter=app --filter=tui --filter=opencode --filter=web +# Run linter and type-checker across packages +npx turbo check +``` + +**Accept when:** +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + + +Claude Code MUST NOT skip or defer verification. + \ No newline at end of file diff --git a/.actual/rules/cross-cutting-session-history-compaction-routines-reve-df90.md b/.actual/rules/cross-cutting-session-history-compaction-routines-reve-df90.md new file mode 100644 index 000000000000..fc8f2e476a59 --- /dev/null +++ b/.actual/rules/cross-cutting-session-history-compaction-routines-reve-df90.md @@ -0,0 +1,22 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Session History Compaction Routines Revert Boundary + +These rules are ALWAYS ACTIVE for all files matching the configured scope. + +### Rules + +- **R-CHRON-001** MUST: Session history compaction routines and revert boundary calculations MUST determine message sequence and cutoffs strictly by persistent creation timestamps. + +### Verify + +```bash +# Discover and run the project's test suite for session synchronization, timeline aggregation, and revert calculation +# Discover and run the repository linter and type-checker across packages/app, packages/tui, packages/opencode, and packages/web. +``` + +**Accept when:** +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + + +Claude Code MUST NOT skip or defer verification. + \ No newline at end of file diff --git a/.actual/rules/cross-cutting-session-message-timelines-transcript-exp-f4fc.md b/.actual/rules/cross-cutting-session-message-timelines-transcript-exp-f4fc.md new file mode 100644 index 000000000000..7bf3b9133ce7 --- /dev/null +++ b/.actual/rules/cross-cutting-session-message-timelines-transcript-exp-f4fc.md @@ -0,0 +1,25 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Session Message Timelines Transcript Exports Sync + +These rules are ALWAYS ACTIVE for all files matching the configured scope across packages/app, packages/tui, packages/opencode, and packages/web. + +### Rules + +- **R-ORD-001** MUST: Session message timelines, transcript exports, and sync hydration reducers MUST order messages deterministically by persistent creation timestamp rather than array insertion sequence. +- **R-ORD-002** MUST: Incorporate a secondary deterministic tie-breaker (such as unique message ID) when creation timestamps are equal to prevent non-deterministic sorting order across clients. + +### Verify + +```bash +# Run the project's test suite for session synchronization, timeline aggregation, and revert calculation +npx jest --testNamePattern="session|timeline|revert|sync" +# Run repository linter and type-checker across packages +npx turbo run lint typecheck +``` + +**Accept when:** +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + + +Claude Code MUST NOT skip or defer verification. Automated unit and integration test suites validating out-of-order message hydration and timeline sorting must pass successfully. + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index cd2327e88811..a260d05b1c06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,86 @@ + +# Project ADRs + +This project's conventions are encoded as ADRs under `.actual/rules/`. **The ADRs ARE the pattern.** Follow them verbatim instead of reading existing implementations to figure out how to do something. + +> **Note:** this directive is calibrated for one-shot tasks (a single discrete feature). For multi-task interactive sessions, consult the ADRs for each task transition rather than holding to the per-session caps below. + +## Workflow (follow in order) + +1. **Identify topic.** Match the files you'll edit against the path-glob table below. Pick the 1-3 topics that match. Do not pre-emptively pick "related" topics; pick only what the file paths actually match. + +2. **Select ADRs by filename — the filename is the index.** Run `ls .actual/rules/`. Each filename is `--.md`; the `` (middle segment) names the ADR's specific concern — e.g., `database-schema-defined`, `zod-input-validation`, `cache-key-format`. + + **Scan ALL filenames first, then pick only the ones whose aspect-slug directly names a noun or verb in your task.** Select by filename; never read a body to decide relevance. **Hard cap: read at most 5 ADR files total.** If more than 5 look relevant, you are over-matching — keep the 5 most specific. + + **If the path-glob table below is a single `**/*` → `cross-cutting-` row** (one big bucket, no per-area topics), this filename scan is your ONLY filter. Do **not** read the bucket exhaustively — treat the filenames as a menu, match aspect-slugs to your task, read ≤5, and ignore the rest. Reading every ADR in the bucket is the exact failure this directive exists to prevent. + +3. **Locate insertion points (one read per file, max 3 files).** You may read source files ONLY to (a) find where to add code (which directory, which barrel export to update) or (b) look up an exact identifier you must import. **Do not read source files as pattern examples — the ADRs already encode the pattern.** If you find yourself reading a file because "I want to see how X is done elsewhere," stop. The ADR you already read tells you how. + +4. **Implement.** Write the code following the rule statements verbatim. If two ADRs seem to conflict, follow the more specific one (longer topic prefix wins). + +5. **Verify after implementing.** Only after the code is written, re-read the `verify_commands` or `accept_criteria` sections of the ADRs you applied and check your work against them. Run the verify commands if any. + +## Anti-patterns to avoid + +- Reading the first N rules alphabetically because they're cheap. Filter by aspect-slug first, then read only the relevant ones. +- Reading >5 ADR files for a single feature. If you're tempted, you're over-scoping the topic match. +- Reading the entire `cross-cutting-` bucket because "every rule is always active." Selection is by filename (step 2); you apply the ≤5 you selected, not all of them. +- Reading existing similar features to "see the pattern" — the ADRs encode the pattern. Trust them. +- Re-reading the same ADR multiple times. Cache it mentally. +- Continuing to browse the codebase after step 3. By step 4 you should be writing, not reading. + +Each rule file at `.actual/rules/--.md` contains the full ADR with rule statements, verify commands, and accept criteria. + +## Verification Protocol + +These rules are ALWAYS ACTIVE. Apply every rule **from the ADRs you selected in step 2** that governs the files you touch — to all code generation, modification, and review. "Always active" does **not** mean read every ADR: you apply the handful you selected by filename, within the read cap above. + +Every rule follows a **Verify → Fix → Repeat** loop. After generating or modifying code for any rule you MUST: + +1. **RUN** the rule's `### Verify` command(s). +2. **CAPTURE** the full output (stdout + stderr). +3. **EVALUATE** the output against the rule's **Accept when** criteria. +4. **IF FAILING:** diagnose the root cause, apply a fix, and re-run from step 1. +5. **IF PASSING:** keep the passing output as evidence before moving on. +6. **MAX ITERATIONS:** 5 attempts per rule. If still failing after 5 attempts, STOP and report the failure with all captured output. + +Compliance is not optional. Do not skip verification, assume correctness, or defer it to a later task. Every change to a governed area must be accompanied by a passing verification run. + +## Mandatory Dependency Grounding + +Before writing or modifying implementation code that uses an external dependency: + +1. Identify every affected external dependency. +2. Locate the repository's manifest and lock or resolution artifact. +3. Determine the exact repository-resolved version from the lock artifact. +4. Verify that the active environment matches that version. +5. Verify each API being introduced or changed against evidence applicable to that exact version (official documentation or public API reference). +6. Produce a dependency-grounding record. +7. Stop if any version, environment, or API cannot be verified. + +Do not begin implementation until dependency grounding has passed. + +The repository lock or resolution artifact is authoritative. A manifest range or model recollection is not sufficient. + +### Integrated workflow + +1. Stabilize the workspace. +2. Inspect the task and relevant architecture decisions. +3. Identify affected dependencies. +4. Run dependency grounding. +5. Produce the implementation plan. +6. Implement the change. +7. Validate grounding coverage, build, types, lint, and tests. +8. Report evidence, assumptions, and blockers. + +## Path glob → topic + +| You're editing | Topic prefix | +|---|---| +| `packages/**/*` | `cross-cutting-` _(3 ADRs)_ | + + - To regenerate the legacy JavaScript SDK, run `./packages/sdk/js/script/build.ts`. - After changing the public Protocol or Server `HttpApi`, run `bun run generate` from `packages/client`. Do not edit `src/generated` or `src/generated-effect` directly. - Keep runtime dependencies directed from Schema to Core and Protocol, then from Core and Protocol to Server. Client runtime code may depend on Schema and Protocol but never Core or Server; `sdk-next` composes Client, Core, and Server. diff --git a/docs/adr/df903f58-c272-4a3f-b640-a93922945540-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-history-compaction-routines-revert-boundary.md b/docs/adr/df903f58-c272-4a3f-b640-a93922945540-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-history-compaction-routines-revert-boundary.md new file mode 100644 index 000000000000..a5a1c809ab2a --- /dev/null +++ b/docs/adr/df903f58-c272-4a3f-b640-a93922945540-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-history-compaction-routines-revert-boundary.md @@ -0,0 +1,120 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Session History Compaction Routines Revert Boundary + +Status: proposed +Date: 2026-10-01 +Deciders: AI (signal conversion) + +## Context + +- Session message delivery and state synchronization across client applications (including packages/app, packages/tui, packages/opencode, and packages/web) occur over asynchronous transport channels such as WebSocket and SSE connections. +- Previously, timeline aggregation, sync hydration reducers, transcript exports, and history revert calculations relied on array ingestion order (the sequence in which messages arrived at the client or were inserted into arrays). +- Out-of-order network arrival caused message sequence corruption, desynchronized transcripts across frontend surfaces, and broken session revert points when updates arrived non-sequentially. + +## Problem Statement + +Relying on implicit arrival sequence and array insertion order for session messages leads to transcript desynchronization, corrupt timelines, and broken history revert boundaries during out-of-order network delivery. + +## Decision + +1. MUST: Session history compaction routines and revert boundary calculations MUST determine message sequence and cutoffs strictly by persistent creation timestamps. + +## Policy Block + +- MUST Session history compaction routines and revert boundary calculations MUST determine message sequence and cutoffs strictly by persistent creation timestamps. + +In scope: +- packages/app message timelines, layout helpers, server session context, and sync reducers +- packages/tui session sync context, session routes, and transcript utilities +- packages/opencode session message models, prompt compaction, revert logic, and session state +- packages/web shared session components + +Out of scope: +- Ephemeral real-time byte chunk streaming prior to boundary message creation +- Non-message system event logs that do not participate in session revert or transcript export + +## Rationale + +- Persistent creation timestamps provide an immutable, deterministic ordering baseline that is independent of transport latency or packet reordering. +- Enforcing timestamp-based sorting across all frontends and transcript storage layers ensures timeline consistency across packages/app, packages/tui, packages/opencode, and packages/web. +- Calculating session revert boundaries based on creation timestamps ensures that rollback operations target the true chronological history rather than an arbitrary arrival sequence. + +## Consequences + +Positive: +- Eliminates message timeline sequence corruption caused by out-of-order network delivery over SSE and WebSocket connections. +- Ensures consistent transcript exports and synchronization state across web, app, and TUI clients. +- Provides deterministic and accurate session history revert operations. + +Negative: +- Introduces explicit sorting overhead at sync hydration and timeline rendering boundaries. +- Requires consistent timestamp generation precision across all message producer boundaries. + +## Alternatives + +- Implicit array appending and ingestion sequence for session message order and history revert boundaries (rejected) + Rejected because: Relying on implicit arrival order caused message sequence corruption, transcript desynchronization, and broken session revert points when updates arrived out of order over WebSocket or SSE connections across multiple client surfaces. +- Server-assigned monotonic sequence numbers for timeline ordering (deferred) + When valid: May be evaluated if sub-millisecond timestamp collisions occur across distributed message producers. + +## Risks + +- Messages generated with identical creation timestamps may experience non-deterministic sorting order across clients. + Mitigation: Incorporate a secondary deterministic tie-breaker (such as unique message ID) when creation timestamps are equal. + Owner: Core Architecture Team + +## Implementation Notes + +- Sorting logic should be implemented at hydration and reducer ingestion boundaries to prevent propagation of unordered arrays. +- Refactoring applies across packages/app (context/global-sync, context/server-session, pages/session/timeline), packages/tui (context/sync, routes/session, util/transcript), packages/opencode (session/message-v2, session/revert, session/session), and packages/web (components/Share). + +## Continuation Context + + +Verify commands: +- Discover and run the project's test suite for session synchronization, timeline aggregation, and revert calculation to ensure chronological ordering under out-of-order input payloads. +- Discover and run the repository linter and type-checker across packages/app, packages/tui, packages/opencode, and packages/web. + +Accept when: +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + +## Enforcement + +- Verified by: Automated unit and integration test suites validating out-of-order message hydration and timeline sorting. +- Verified by: Code review of sync hydration reducers, transcript generators, and timeline model components. +- Violation handling: Pull requests introducing array-append ordering or unsorted ingestion boundaries will fail automated tests or code review. +- Violation handling: Discrepancies in timeline synchronization between packages will be logged as sequencing regressions. +- Exception process: Exceptions for ephemeral, non-persisted streaming event buffers must be reviewed and approved by the Core Architecture Team. + +## References + +- file:packages/app/src/context/server-session.ts +- file:packages/app/src/context/global-sync/event-reducer.ts +- file:packages/app/src/context/sync.tsx +- file:packages/app/src/utils/session-message.ts +- file:packages/app/src/pages/session/timeline/message-timeline.tsx +- file:packages/app/src/pages/session/timeline/model.ts +- file:packages/app/src/pages/session/timeline/rows.ts +- file:packages/app/src/pages/layout/helpers.ts +- file:packages/tui/src/context/sync.tsx +- file:packages/tui/src/routes/session/index.tsx +- file:packages/tui/src/util/transcript.ts +- file:packages/opencode/src/session/message-v2.ts +- file:packages/opencode/src/session/prompt.ts +- file:packages/opencode/src/session/revert.ts +- file:packages/opencode/src/session/session.ts +- file:packages/web/src/components/Share.tsx +- commit:5aa5cb35235509c7bcb206179cf29ee11627276e +- commit:91132551141aeb93ca3053a64295a294312c78a7 +- commit:23cc677108069e4a7e5ae914d508fde9671b9431 +- commit:28bcc0e4f4d4679946542e05412cb96d737a0428 +- commit:20750c332e75dd68a10e88f9a0c6b1ca9ac41213 +- commit:db581e47a3a6f4900a6289ad7fddec60fec44e1c +- commit:a54a693af242108b0b5c9db6ae498c10b2d8843b +- pr:#40990 +- pr:#40991 +- pr:#40994 +- pr:#40995 +- pr:#41000 +- pr:#41001 +- pr:#41006 \ No newline at end of file diff --git a/docs/adr/e2838170-cdcd-4036-ae98-5e997b53d7cd-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-message-ingestion-synchronization-layers-not-assume.md b/docs/adr/e2838170-cdcd-4036-ae98-5e997b53d7cd-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-message-ingestion-synchronization-layers-not-assume.md new file mode 100644 index 000000000000..2da3ee1fe9de --- /dev/null +++ b/docs/adr/e2838170-cdcd-4036-ae98-5e997b53d7cd-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-message-ingestion-synchronization-layers-not-assume.md @@ -0,0 +1,120 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Message Ingestion Synchronization Layers Not Assume + +Status: proposed +Date: 2026-10-01 +Deciders: AI (signal conversion) + +## Context + +- Session message delivery and state synchronization across client applications (including packages/app, packages/tui, packages/opencode, and packages/web) occur over asynchronous transport channels such as WebSocket and SSE connections. +- Previously, timeline aggregation, sync hydration reducers, transcript exports, and history revert calculations relied on array ingestion order (the sequence in which messages arrived at the client or were inserted into arrays). +- Out-of-order network arrival caused message sequence corruption, desynchronized transcripts across frontend surfaces, and broken session revert points when updates arrived non-sequentially. + +## Problem Statement + +Relying on implicit arrival sequence and array insertion order for session messages leads to transcript desynchronization, corrupt timelines, and broken history revert boundaries during out-of-order network delivery. + +## Decision + +1. MUST_NOT: Message ingestion and synchronization layers MUST NOT assume or depend upon network transport arrival order for message sequencing. + +## Policy Block + +- MUST_NOT Message ingestion and synchronization layers MUST NOT assume or depend upon network transport arrival order for message sequencing. + +In scope: +- packages/app message timelines, layout helpers, server session context, and sync reducers +- packages/tui session sync context, session routes, and transcript utilities +- packages/opencode session message models, prompt compaction, revert logic, and session state +- packages/web shared session components + +Out of scope: +- Ephemeral real-time byte chunk streaming prior to boundary message creation +- Non-message system event logs that do not participate in session revert or transcript export + +## Rationale + +- Persistent creation timestamps provide an immutable, deterministic ordering baseline that is independent of transport latency or packet reordering. +- Enforcing timestamp-based sorting across all frontends and transcript storage layers ensures timeline consistency across packages/app, packages/tui, packages/opencode, and packages/web. +- Calculating session revert boundaries based on creation timestamps ensures that rollback operations target the true chronological history rather than an arbitrary arrival sequence. + +## Consequences + +Positive: +- Eliminates message timeline sequence corruption caused by out-of-order network delivery over SSE and WebSocket connections. +- Ensures consistent transcript exports and synchronization state across web, app, and TUI clients. +- Provides deterministic and accurate session history revert operations. + +Negative: +- Introduces explicit sorting overhead at sync hydration and timeline rendering boundaries. +- Requires consistent timestamp generation precision across all message producer boundaries. + +## Alternatives + +- Implicit array appending and ingestion sequence for session message order and history revert boundaries (rejected) + Rejected because: Relying on implicit arrival order caused message sequence corruption, transcript desynchronization, and broken session revert points when updates arrived out of order over WebSocket or SSE connections across multiple client surfaces. +- Server-assigned monotonic sequence numbers for timeline ordering (deferred) + When valid: May be evaluated if sub-millisecond timestamp collisions occur across distributed message producers. + +## Risks + +- Messages generated with identical creation timestamps may experience non-deterministic sorting order across clients. + Mitigation: Incorporate a secondary deterministic tie-breaker (such as unique message ID) when creation timestamps are equal. + Owner: Core Architecture Team + +## Implementation Notes + +- Sorting logic should be implemented at hydration and reducer ingestion boundaries to prevent propagation of unordered arrays. +- Refactoring applies across packages/app (context/global-sync, context/server-session, pages/session/timeline), packages/tui (context/sync, routes/session, util/transcript), packages/opencode (session/message-v2, session/revert, session/session), and packages/web (components/Share). + +## Continuation Context + + +Verify commands: +- Discover and run the project's test suite for session synchronization, timeline aggregation, and revert calculation to ensure chronological ordering under out-of-order input payloads. +- Discover and run the repository linter and type-checker across packages/app, packages/tui, packages/opencode, and packages/web. + +Accept when: +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + +## Enforcement + +- Verified by: Automated unit and integration test suites validating out-of-order message hydration and timeline sorting. +- Verified by: Code review of sync hydration reducers, transcript generators, and timeline model components. +- Violation handling: Pull requests introducing array-append ordering or unsorted ingestion boundaries will fail automated tests or code review. +- Violation handling: Discrepancies in timeline synchronization between packages will be logged as sequencing regressions. +- Exception process: Exceptions for ephemeral, non-persisted streaming event buffers must be reviewed and approved by the Core Architecture Team. + +## References + +- file:packages/app/src/context/server-session.ts +- file:packages/app/src/context/global-sync/event-reducer.ts +- file:packages/app/src/context/sync.tsx +- file:packages/app/src/utils/session-message.ts +- file:packages/app/src/pages/session/timeline/message-timeline.tsx +- file:packages/app/src/pages/session/timeline/model.ts +- file:packages/app/src/pages/session/timeline/rows.ts +- file:packages/app/src/pages/layout/helpers.ts +- file:packages/tui/src/context/sync.tsx +- file:packages/tui/src/routes/session/index.tsx +- file:packages/tui/src/util/transcript.ts +- file:packages/opencode/src/session/message-v2.ts +- file:packages/opencode/src/session/prompt.ts +- file:packages/opencode/src/session/revert.ts +- file:packages/opencode/src/session/session.ts +- file:packages/web/src/components/Share.tsx +- commit:5aa5cb35235509c7bcb206179cf29ee11627276e +- commit:91132551141aeb93ca3053a64295a294312c78a7 +- commit:23cc677108069e4a7e5ae914d508fde9671b9431 +- commit:28bcc0e4f4d4679946542e05412cb96d737a0428 +- commit:20750c332e75dd68a10e88f9a0c6b1ca9ac41213 +- commit:db581e47a3a6f4900a6289ad7fddec60fec44e1c +- commit:a54a693af242108b0b5c9db6ae498c10b2d8843b +- pr:#40990 +- pr:#40991 +- pr:#40994 +- pr:#40995 +- pr:#41000 +- pr:#41001 +- pr:#41006 \ No newline at end of file diff --git a/docs/adr/f4fcb389-bd12-4d7e-8adc-0d32c57f98c4-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-message-timelines-transcript-exports-sync.md b/docs/adr/f4fcb389-bd12-4d7e-8adc-0d32c57f98c4-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-message-timelines-transcript-exports-sync.md new file mode 100644 index 000000000000..b2bfbec7d770 --- /dev/null +++ b/docs/adr/f4fcb389-bd12-4d7e-8adc-0d32c57f98c4-standardize-chronological-ordering-for-session-message-ingestion-and-timelines-session-message-timelines-transcript-exports-sync.md @@ -0,0 +1,120 @@ +# Standardize Chronological Ordering for Session Message Ingestion and Timelines: Session Message Timelines Transcript Exports Sync + +Status: proposed +Date: 2026-10-01 +Deciders: AI (signal conversion) + +## Context + +- Session message delivery and state synchronization across client applications (including packages/app, packages/tui, packages/opencode, and packages/web) occur over asynchronous transport channels such as WebSocket and SSE connections. +- Previously, timeline aggregation, sync hydration reducers, transcript exports, and history revert calculations relied on array ingestion order (the sequence in which messages arrived at the client or were inserted into arrays). +- Out-of-order network arrival caused message sequence corruption, desynchronized transcripts across frontend surfaces, and broken session revert points when updates arrived non-sequentially. + +## Problem Statement + +Relying on implicit arrival sequence and array insertion order for session messages leads to transcript desynchronization, corrupt timelines, and broken history revert boundaries during out-of-order network delivery. + +## Decision + +1. MUST: Session message timelines, transcript exports, and sync hydration reducers MUST order messages deterministically by persistent creation timestamp rather than array insertion sequence. + +## Policy Block + +- MUST Session message timelines, transcript exports, and sync hydration reducers MUST order messages deterministically by persistent creation timestamp rather than array insertion sequence. + +In scope: +- packages/app message timelines, layout helpers, server session context, and sync reducers +- packages/tui session sync context, session routes, and transcript utilities +- packages/opencode session message models, prompt compaction, revert logic, and session state +- packages/web shared session components + +Out of scope: +- Ephemeral real-time byte chunk streaming prior to boundary message creation +- Non-message system event logs that do not participate in session revert or transcript export + +## Rationale + +- Persistent creation timestamps provide an immutable, deterministic ordering baseline that is independent of transport latency or packet reordering. +- Enforcing timestamp-based sorting across all frontends and transcript storage layers ensures timeline consistency across packages/app, packages/tui, packages/opencode, and packages/web. +- Calculating session revert boundaries based on creation timestamps ensures that rollback operations target the true chronological history rather than an arbitrary arrival sequence. + +## Consequences + +Positive: +- Eliminates message timeline sequence corruption caused by out-of-order network delivery over SSE and WebSocket connections. +- Ensures consistent transcript exports and synchronization state across web, app, and TUI clients. +- Provides deterministic and accurate session history revert operations. + +Negative: +- Introduces explicit sorting overhead at sync hydration and timeline rendering boundaries. +- Requires consistent timestamp generation precision across all message producer boundaries. + +## Alternatives + +- Implicit array appending and ingestion sequence for session message order and history revert boundaries (rejected) + Rejected because: Relying on implicit arrival order caused message sequence corruption, transcript desynchronization, and broken session revert points when updates arrived out of order over WebSocket or SSE connections across multiple client surfaces. +- Server-assigned monotonic sequence numbers for timeline ordering (deferred) + When valid: May be evaluated if sub-millisecond timestamp collisions occur across distributed message producers. + +## Risks + +- Messages generated with identical creation timestamps may experience non-deterministic sorting order across clients. + Mitigation: Incorporate a secondary deterministic tie-breaker (such as unique message ID) when creation timestamps are equal. + Owner: Core Architecture Team + +## Implementation Notes + +- Sorting logic should be implemented at hydration and reducer ingestion boundaries to prevent propagation of unordered arrays. +- Refactoring applies across packages/app (context/global-sync, context/server-session, pages/session/timeline), packages/tui (context/sync, routes/session, util/transcript), packages/opencode (session/message-v2, session/revert, session/session), and packages/web (components/Share). + +## Continuation Context + + +Verify commands: +- Discover and run the project's test suite for session synchronization, timeline aggregation, and revert calculation to ensure chronological ordering under out-of-order input payloads. +- Discover and run the repository linter and type-checker across packages/app, packages/tui, packages/opencode, and packages/web. + +Accept when: +- Sync reducers and timeline views sort messages strictly by creation timestamp even when supplied out-of-order test events. +- Transcript exports and revert boundary calculations produce deterministic outputs matching creation timestamp ordering across all packages. + +## Enforcement + +- Verified by: Automated unit and integration test suites validating out-of-order message hydration and timeline sorting. +- Verified by: Code review of sync hydration reducers, transcript generators, and timeline model components. +- Violation handling: Pull requests introducing array-append ordering or unsorted ingestion boundaries will fail automated tests or code review. +- Violation handling: Discrepancies in timeline synchronization between packages will be logged as sequencing regressions. +- Exception process: Exceptions for ephemeral, non-persisted streaming event buffers must be reviewed and approved by the Core Architecture Team. + +## References + +- file:packages/app/src/context/server-session.ts +- file:packages/app/src/context/global-sync/event-reducer.ts +- file:packages/app/src/context/sync.tsx +- file:packages/app/src/utils/session-message.ts +- file:packages/app/src/pages/session/timeline/message-timeline.tsx +- file:packages/app/src/pages/session/timeline/model.ts +- file:packages/app/src/pages/session/timeline/rows.ts +- file:packages/app/src/pages/layout/helpers.ts +- file:packages/tui/src/context/sync.tsx +- file:packages/tui/src/routes/session/index.tsx +- file:packages/tui/src/util/transcript.ts +- file:packages/opencode/src/session/message-v2.ts +- file:packages/opencode/src/session/prompt.ts +- file:packages/opencode/src/session/revert.ts +- file:packages/opencode/src/session/session.ts +- file:packages/web/src/components/Share.tsx +- commit:5aa5cb35235509c7bcb206179cf29ee11627276e +- commit:91132551141aeb93ca3053a64295a294312c78a7 +- commit:23cc677108069e4a7e5ae914d508fde9671b9431 +- commit:28bcc0e4f4d4679946542e05412cb96d737a0428 +- commit:20750c332e75dd68a10e88f9a0c6b1ca9ac41213 +- commit:db581e47a3a6f4900a6289ad7fddec60fec44e1c +- commit:a54a693af242108b0b5c9db6ae498c10b2d8843b +- pr:#40990 +- pr:#40991 +- pr:#40994 +- pr:#40995 +- pr:#41000 +- pr:#41001 +- pr:#41006 \ No newline at end of file From 7cee47051653e73eea52126429bc613a27164001 Mon Sep 17 00:00:00 2001 From: Austin Born Date: Thu, 1 Oct 2026 16:11:18 -0700 Subject: [PATCH 2/2] fix(session): delete reverted messages boundary-last and tie-break ids by raw order Revert cleanup removed the boundary message first, so an interrupted cleanup left no marker and the next cleanup cleared the revert while resurrecting the remaining reverted messages. Delete messages and parts newest-first so the boundary is removed last and an interrupted cleanup stays resumable. The TUI and share page tie-broke same-millisecond messages with localeCompare, which disagrees with storage's SQLite BINARY collation (and with the TUI's own binary search) and can treat distinct ids as equal. Compare ids by code unit. Refs anomalyco/opencode#42816 Co-Authored-By: Claude Opus 5.5 (1M context) --- packages/opencode/src/session/revert.ts | 7 +- .../test/session/revert-compact.test.ts | 100 +++++++++++++++++- packages/tui/src/context/sync.tsx | 8 +- packages/tui/test/context/sync.test.ts | 26 +++++ packages/web/src/components/Share.tsx | 4 +- 5 files changed, 138 insertions(+), 7 deletions(-) create mode 100644 packages/tui/test/context/sync.test.ts diff --git a/packages/opencode/src/session/revert.ts b/packages/opencode/src/session/revert.ts index 03e5afd085e0..b5e1615cf038 100644 --- a/packages/opencode/src/session/revert.ts +++ b/packages/opencode/src/session/revert.ts @@ -106,7 +106,10 @@ const layer = Layer.effect( const index = msgs.findIndex((msg) => msg.info.id === messageID) const target = index < 0 ? undefined : msgs[index] const remove = index < 0 ? [] : msgs.slice(index + (session.revert.partID ? 1 : 0)) - for (const msg of remove) { + // Delete newest-first so the revert boundary is removed last. Each removal is its own + // durable write, so an interrupted cleanup must leave the boundary in place for the next + // cleanup to locate the remaining rows; otherwise clearRevert() would resurrect them. + for (const msg of remove.toReversed()) { yield* sessions.removeMessage({ sessionID, messageID: msg.info.id }) } if (session.revert.partID && target) { @@ -115,7 +118,7 @@ const layer = Layer.effect( if (idx >= 0) { const removeParts = target.parts.slice(idx) target.parts = target.parts.slice(0, idx) - for (const part of removeParts) { + for (const part of removeParts.toReversed()) { yield* sessions.removePart({ sessionID, messageID: target.info.id, partID: part.id }) } } diff --git a/packages/opencode/test/session/revert-compact.test.ts b/packages/opencode/test/session/revert-compact.test.ts index f9e8cfd9aee3..a79a360fbbe6 100644 --- a/packages/opencode/test/session/revert-compact.test.ts +++ b/packages/opencode/test/session/revert-compact.test.ts @@ -7,6 +7,7 @@ import path from "path" import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner" import { Effect } from "effect" import { Session } from "@/session/session" +import { EventV2Bridge } from "@/event-v2-bridge" import { SessionRevert } from "../../src/session/revert" import { MessageV2 } from "../../src/session/message-v2" @@ -19,7 +20,14 @@ import { ModelV2 } from "@opencode-ai/core/model" const it = testEffect( LayerNode.compile( - LayerNode.group([Session.node, SessionRevert.node, Snapshot.node, SessionProjector.node, CrossSpawnSpawner.node]), + LayerNode.group([ + Session.node, + SessionRevert.node, + Snapshot.node, + SessionProjector.node, + CrossSpawnSpawner.node, + EventV2Bridge.node, + ]), ), ) @@ -438,6 +446,96 @@ describe("revert + compact workflow", () => { ), ) + it.live( + "cleanup removes the revert boundary last so an interrupted cleanup stays resumable", + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const session = yield* Session.Service + const revert = yield* SessionRevert.Service + const events = yield* EventV2Bridge.Service + + const info = yield* session.create({}) + const sid = info.id + + const u1 = yield* user(sid) + const u2 = yield* user(sid) + const a2 = yield* assistant(sid, u2.id, dir) + const u3 = yield* user(sid) + + const removed: string[] = [] + const unsubscribe = yield* events.listen((event) => { + if (event.type === SessionV1.Event.MessageRemoved.type) + removed.push((event.data as typeof SessionV1.Event.MessageRemoved.data.Type).messageID) + if (event.type === SessionV1.Event.PartRemoved.type) + removed.push((event.data as typeof SessionV1.Event.PartRemoved.data.Type).partID) + return Effect.void + }) + yield* Effect.addFinalizer(() => unsubscribe) + + yield* session.setRevert({ + sessionID: sid, + revert: { messageID: u2.id }, + summary: { additions: 0, deletions: 0, files: 0 }, + }) + yield* revert.cleanup(yield* session.get(sid)) + expect(removed).toEqual([u3.id, a2.id, u2.id]) + expect((yield* session.messages({ sessionID: sid })).map((msg) => msg.info.id)).toEqual([u1.id]) + + const other = yield* session.create({}) + const o1 = yield* user(other.id) + const q1 = yield* text(other.id, o1.id, "first part") + const q2 = yield* text(other.id, o1.id, "second part") + const q3 = yield* text(other.id, o1.id, "third part") + removed.length = 0 + yield* session.setRevert({ + sessionID: other.id, + revert: { messageID: o1.id, partID: q2.id }, + summary: { additions: 0, deletions: 0, files: 0 }, + }) + yield* revert.cleanup(yield* session.get(other.id)) + expect(removed).toEqual([q3.id, q2.id]) + expect((yield* session.messages({ sessionID: other.id }))[0]?.parts.map((part) => part.id)).toEqual([q1.id]) + }), + { git: true }, + ), + ) + + it.live( + "cleanup resumes after an interruption that left the revert boundary in place", + provideTmpdirInstance( + (dir) => + Effect.gen(function* () { + const session = yield* Session.Service + const revert = yield* SessionRevert.Service + + const info = yield* session.create({}) + const sid = info.id + + const u1 = yield* user(sid) + const u2 = yield* user(sid) + const a2 = yield* assistant(sid, u2.id, dir) + const u3 = yield* user(sid) + + yield* session.setRevert({ + sessionID: sid, + revert: { messageID: u2.id }, + summary: { additions: 0, deletions: 0, files: 0 }, + }) + // Newest-first deletion interrupted after its first removal. + yield* session.removeMessage({ sessionID: sid, messageID: u3.id }) + + yield* revert.cleanup(yield* session.get(sid)) + + const ids = (yield* session.messages({ sessionID: sid })).map((msg) => msg.info.id) + expect(ids).toEqual([u1.id]) + expect(ids).not.toContain(a2.id) + expect((yield* session.get(sid)).revert).toBeUndefined() + }), + { git: true }, + ), + ) + it.live( "reverts chronological suffixes on both sides of mixed message ID ordering", provideTmpdirInstance( diff --git a/packages/tui/src/context/sync.tsx b/packages/tui/src/context/sync.tsx index 71e050d11e68..000610f6085a 100644 --- a/packages/tui/src/context/sync.tsx +++ b/packages/tui/src/context/sync.tsx @@ -51,8 +51,10 @@ function search(items: T[], target: string, key: (item: T) => string) { return { found: false, index: left } } -function compareMessage(a: Message, b: Message) { - return a.time.created - b.time.created || a.id.localeCompare(b.id) +// Tie-break by code unit order to match storage (SQLite BINARY collation) and search() below; +// localeCompare can order same-millisecond ids differently and treats some distinct ids as equal. +export function compareMessage(a: Message, b: Message) { + return a.time.created - b.time.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0) } const messageKey = (message: Message) => message.time.created + message.id @@ -170,7 +172,7 @@ export const { function listSessions() { return sdk.client.session .list({ start: Date.now() - 30 * 24 * 60 * 60 * 1000, ...sessionListQuery() }) - .then((x) => (x.data ?? []).toSorted((a, b) => a.id.localeCompare(b.id))) + .then((x) => (x.data ?? []).toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) } event.subscribe((event, { directory, workspace }) => { diff --git a/packages/tui/test/context/sync.test.ts b/packages/tui/test/context/sync.test.ts new file mode 100644 index 000000000000..95b463c34e34 --- /dev/null +++ b/packages/tui/test/context/sync.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test" +import type { Message } from "@opencode-ai/sdk/v2" +import { compareMessage } from "../../src/context/sync" + +const message = (id: string, created: number) => ({ id, time: { created } }) as Message + +test("orders messages by creation time, then by raw id like storage", () => { + const upper = message("msg_A1", 1) + const lower = message("msg_a1", 1) + const earlier = message("msg_z9", 0) + const later = message("msg_00", 2) + const expected = [earlier, upper, lower, later].map((item) => item.id) + + // Storage uses SQLite BINARY collation, where "A" < "a"; locale collation puts "a" first. + expect([later, lower, earlier, upper].toSorted(compareMessage).map((item) => item.id)).toEqual(expected) + expect([upper, later, lower, earlier].toSorted(compareMessage).map((item) => item.id)).toEqual(expected) +}) + +test("never treats distinct ids as equal", () => { + // Canonically equivalent under locale collation, but distinct primary keys. + const composed = message("msg_é", 1) + const decomposed = message("msg_é", 1) + + expect(compareMessage(composed, decomposed)).not.toBe(0) + expect(compareMessage(composed, decomposed)).toBe(-compareMessage(decomposed, composed)) +}) diff --git a/packages/web/src/components/Share.tsx b/packages/web/src/components/Share.tsx index 04514b214e50..da78311de9f8 100644 --- a/packages/web/src/components/Share.tsx +++ b/packages/web/src/components/Share.tsx @@ -76,7 +76,9 @@ export default function Share(props: { messages: {}, }) const messages = createMemo(() => - Object.values(store.messages).toSorted((a, b) => a.time.created - b.time.created || a.id.localeCompare(b.id)), + Object.values(store.messages).toSorted( + (a, b) => a.time.created - b.time.created || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0), + ), ) const [connectionStatus, setConnectionStatus] = createSignal<[Status, string?]>(["disconnected"])