Skip to content

Spec: deepen Command log ownership to prevent same-second collisions #794

Closed
2 of 2 issues completed

Description

@JacobStephens2

Problem Statement

Two Commands started on the same issue within one second keep separate Command records but choose the same Command log. Starting the second Command truncates the first Command's output, before Issue branch selection, Claim creation or Worktree acquisition can refuse duplicate work. Session log names also omit the record's unique identity, so the same naming scheme cannot guarantee the exact Command ownership promised for Session logs and Model evidence.

The Day shift needs each Command's output and evidence to remain its own, including when another Command fails early or starts concurrently.

Solution

Deepen Command log ownership into one module shared by recording, Session logs, child Runs and Command inspection. Give each top-level Command one immutable storage owner, distinct from its display timestamp and human Command name. The module owns allocation, filename encoding and decoding, and exact ownership matching. Existing logs remain readable where they are.

User Stories

  1. As the Day shift, I want two Commands on the same issue in one second to retain separate Command logs, so that the second cannot erase the first.
  2. As the Day shift, I want an early duplicate-work refusal to leave earlier output intact, so that I can diagnose both Commands.
  3. As the Day shift, I want separate Session logs for separate Commands, so that one transcript cannot replace another.
  4. As the Day shift, I want each Command record to name its own Command log, so that status and show report the right output.
  5. As the Day shift, I want Session Model evidence attributed to the exact Command, so that a notification never borrows another Command's Models.
  6. As the Day shift, I want Ticket Runs to retain their parent's storage owner, so that a Spec run collects only its own Sessions.
  7. As the Day shift, I want nested Base fixes to retain that same owner, so that evidence remains with the originating Command.
  8. As the Day shift, I want a Pass and the Run or Spec run it dispatches to share ownership, so that the Command still has one Command log.
  9. As the Day shift, I want Resume and Repair logs to retain their existing kinds, so that Session selection remains familiar.
  10. As the Day shift, I want review reports kept beside the correct Session log, so that evidence survives concurrent Commands.
  11. As the Day shift, I want status and show to read old and new records together, so that upgrading loses no history.
  12. As the Day shift, I want logs to select and follow the correct Command and Session files, so that another same-second Command cannot redirect observation.
  13. As the Day shift, I want Stop to continue using exact process and parent-record ownership, so that filename changes do not change which Command ends.
  14. As the Day shift, I want an allocation collision to preserve the existing file, so that collision handling itself cannot destroy output.
  15. As the Day shift, I want genuinely owned partial logs retained after a write failure, so that diagnostics remain available.
  16. As the Day shift, I want skipped Passes to remain quiet and keep no Command log, so that frequent scheduling stays useful.
  17. As the Day shift, I want human Command names and local start times preserved, so that existing CLI usage remains valid.
  18. As a maintainer, I want one filename decoder and one ownership interface, so that logging changes have locality across writers and readers.

Implementation Decisions

The design tree is settled in dependency order; each round resolves the whole frontier.

Round 1: scope and ownership

  1. Deepen Command log ownership, rather than only renaming files. Allocation, encoding, decoding, inheritance and ownership checks move behind one interface because the demonstrated bug comes from those facts being distributed across callers. Deleting the resulting module would force the invariants back into those callers: it passes the deletion test.
  2. The top-level Command owns one immutable storage identity containing its original start stamp and an opaque random token. An issue number, second-resolution stamp, PID or process-start value alone is not a persistent uniqueness guarantee. Human Command names and process-liveness facts remain separate and unchanged.
  3. Allocate only once work starts, before any Command record or log is written. Preserve held startup output, quiet skips and the existing no-log behavior before work starts. Dispatched work and child Runs reuse the owner rather than allocating another top-level Command log.
  4. Keep the existing repository and Command-kind directories. Append a tilde and the owner token immediately after the existing twenty-character start stamp in new artifact names. Preserve the existing subject prefixes and Session kind suffixes. This gives every artifact one parseable identity without introducing directory migration.

Round 2: allocation and records

  1. Generate opaque tokens using the existing tempfile randomness support and reserve new Command logs with exclusive creation. A pre-existing path causes selection of a fresh token and another exclusive attempt; it is never opened for truncation. Keep this policy inside the module and its file-effects adapter, with a deterministic adapter for tests; add no dependency.
  2. Distinguish allocation failure from failure after acquiring a file. Never adopt a pre-existing file as this Command's partial output. Preserve genuinely owned partial files, warning suppression and nonfatal Command-log failure handling after acquisition. The owner remains immutable once exposed to records or Sessions.
  3. Derive new Command record paths from the exact owned Command-log stem. The owner token replaces the separate root-record uniqueness suffix. Keep process identity, exact parent-record links, dispatch links and Stop request placement as they are; these express process and Run ownership rather than log-name uniqueness.
  4. Keep persisted Command record and CLI JSON version 1. Ownership is encoded in filenames; no persisted field or new public option is required. Decode both new token-bearing names and the current legacy record suffixes, including deriving the legacy shared log path exactly as before.

Round 3: Sessions, inheritance and readers

  1. Capture the same owner in Session logging and Model recording. Session paths include the owner token; Model sidecars and review reports continue deriving their paths from the exact Session log. Retain current Session kinds, Resume behavior and Repair budgets. This work separates different Commands; it does not add repeated-session execution policy.
  2. Carry the captured owner through child-process inheritance. Use one internal serialized owner value for the stamp, token and optional owning Command-log path, replacing independent log-owner reconstruction. Keep the existing parent-record and parent-dispatch links and hidden start-stamp argument for their current purposes. A top-level Command always captures its own owner; only an actual child Run consumes inherited ownership, and absence clears stale inherited state. Local ownership takes precedence.
  3. Preserve safe failure behavior without falling back to a shared timestamp-only target. A Command-log writer failure does not discard an already allocated identity; Sessions may still use its distinct names. If no safe owner can be allocated, emit the existing logging warning and return a causal error when a Session requires a log path. Do not invent ownership of an existing file.
  4. All readers use the module's decoder and exact owner matching. Command inspection, Session inventory, Session-kind selection, history reconstruction and Model collection must stop splitting fixed filename offsets independently. New token-bearing ownership is never matched to tokenless legacy ownership just because the stamp agrees. Model collection retains exact owning Command-log comparison.
  5. Keep legacy reads, not legacy writes, after the final Ticket. Existing repository/kind logs, Activity-log reconstruction, old uniquely suffixed Command records, Session-only history and tokenless Session names remain readable without moving or rewriting them. Historical collisions cannot be repaired or assigned invented ownership. Do not expand support to the older pre-repository flat layout.
  6. Preserve human selection, Stop and output contracts. Issue URLs, repository/issue names and Pass names keep their existing meaning, including existing live/newest selection and ambiguity behavior. The owner token is a storage detail, not a new user selection requirement. Activity starts and notifications use the actual allocated paths. Child-owned files and retained-resource trees still follow exact parent links.
  7. Update existing domain wording and navigation through the implementation Tickets. Clarify that stamped Command and Session logs have distinct storage ownership even when start times agree; update the existing logging ADR's filename examples and user-guide examples. No new domain term or new ADR is needed: this is a reversible storage implementation change that preserves repository ownership, scheduler behavior and CLI/Console reading decisions.

Round 4: delivery

  1. Publish two sequential tracer-bullet Tickets. The first delivers distinct Command logs/records through CLI observation with legacy compatibility; Session writers remain temporarily tokenless. The second uses the captured owner for Session writing, inheritance and inventories, completing evidence isolation. Reader support for both name forms is present before Session writers switch. Each Ticket is independently verifiable and fits one fresh implementation context; the whole refactor spans recording, Session execution and compatibility tests.
  2. Remove distributed naming logic as each caller migrates. This adds depth to one module rather than another shallow pass-through. Retain existing effect adapters for the production filesystem and deterministic fixtures; add no generic storage framework. No open issue proposes this deepening. The newly published Deepen the Command reader around selection and ownership #793 deepens Command reading and explicitly preserves its current persisted formats; this Spec supplies storage identity and filename decoding underneath that reader, without redoing its observation/tree/Stop work. The Console Spec consumes the logs, and log terminology and dead-Claim issues address different work. Deepen the Command reader around selection and ownership #793 is related work, not a blocking prerequisite.

Testing Decisions

  1. Test across the owned recording interface using the existing fixed-clock file adapter. Two owners share the same repository, issue, process facts and second; assert distinct paths and survival of both output markers after interleaved writes. This exercises the reproduced bug without sleeps or Worktree contention.
  2. Test the module's file seam with actual temporary directories as well. The production filesystem adapter and deterministic adapter are both real uses of the seam. Force an allocation collision, verify the old bytes are unchanged and a different path is chosen, and separately verify retention after a partial write failure. Assert outcomes, not random token values.
  3. Test the CLI seam with fixture records and fake GitHub/Harness processes. Cover new and legacy records together through status, show and logs; verify displayed paths exist and return the selected files. Preserve existing Stop, parent/dispatch and retained-resource coverage. Do not introduce a Console implementation.
  4. Test Session ownership across two captured owners at the same stamp. Give them different Session and Model markers, then verify isolated transcript/report files, Session inventories and notification lines. Include child Ticket Runs, a nested Base fix, Pass-dispatched work, Resume and Repair kinds, and interrupted stream evidence.
  5. Retain existing execution and compatibility suites. Prior art is the recording tests, Command-log scenarios, status/show/logs/Stop scenarios, Session recording tests and Run-notification tests. Migrate filename-shape assertions to ownership assertions where exact spelling is not the behavior under test; retain a focused decoder compatibility matrix. Remove obsolete tests of distributed naming helpers once the shared interface covers the behavior.
  6. Review evidence at base commit de3c1ed: the existing same-second-record test passes. A temporary regression using the same Record interface and Scripted file adapter failed with “starting the second Command erased the first Command output.” The probe was removed; no source change is part of this review. Both report diagrams passed Mermaid parsing before publication. This confirms Command-output loss, not a separately executed concurrent-Harness reproduction of Session evidence loss.
  7. Verify each Ticket's relevant tests and final repository-required checks. The new regressions must first fail for the ownership defect and then pass. Use fixture-owned processes and wait for each by its own handle; do not test against real provider accounts.

Out of Scope

  • Changes to Claims, Pickup reservations, Worktree exclusion or how many Commands may run.
  • New Command selection syntax, a Console, log terminology changes or new tracing/eval storage.
  • Automatic migration, repair or deletion of existing logs.
  • New scheduling, retention, cleanup or repeated-session policy.
  • Recovering evidence already overwritten by an historical collision.
  • Changes to Harness selection, Session supervision or notification recipients.

Further Notes

Strength: Strong · Dependencies: local-substitutable

Files: src/logs.rs, src/logs/recording.rs, src/logs/command_log.rs, src/logs/effects.rs, src/logs/command_record.rs, src/logs/status.rs, src/logs/status/history.rs, src/logs/status/inspect.rs, src/session.rs, src/session/recording.rs, src/session/review_reports.rs, src/child_run.rs, src/logs/recording/tests.rs, src/session/recording/tests.rs, tests/command_logs.rs, tests/status.rs, tests/show_logs.rs, tests/stop.rs, tests/run_notification.rs, CONTEXT.md, docs/code-navigation.md, docs/adr/0011-thirdshift-keeps-a-per-repository-activity-log.md, docs/guide/observing.md

Before

flowchart LR
  R["Command recording"] --> A["Command log allocation"]
  R --> N["Command record naming"]
  S["Session logs"] --> T["Session log naming"]
  C["Child Runs"] --> S
  A -. "stamp only" .-> F["Log files"]
  T -. "stamp only" .-> F
  N --> F
  I["Command inspection"] -. "reconstruct names" .-> F
  classDef leak stroke:#dc2626,stroke-width:2px;
  class A,T,I leak
Loading

After

flowchart LR
  R["Command recording"] --> O
  S["Session logs"] --> O
  C["Child Runs"] --> S
  I["Command inspection"] --> O
  subgraph O["Command log owner module"]
    A["Command log allocation"]
    N["Command record naming"]
    T["Session log naming"]
  end
  O --> F["Log files"]
  classDef deep stroke-width:3px;
  class O deep
Loading

Problem: Separate record identity and shallow filename rules leak storage ownership across the seam, allowing a second Command to erase the first Command's output.

Solution: One deep Command log owner module hides allocation, naming and decoding behind the interface shared by recording, Session logs, child inheritance and inspection.

Wins

  • Locality: ownership rules concentrate.
  • Leverage: writers and readers agree.
  • Adapter tests exercise one seam.
  • Depth replaces shallow filename rules.

Metadata

Metadata

Assignees

No one assigned

    Labels

    architect-planAn Architect plan: the Spec or Ticket an Architecture review published

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions