Commit graph

64 commits

Author SHA1 Message Date
Scott Werner
62ed7cb8a2 Rename RunBlobId to BlobHash 2026-08-12 11:33:41 -04:00
Scott Werner
2ee6109006 Warn on malformed blob keys and drop structural sharing test
Skipping a malformed key under blobs/sha256 during listing now emits a
warn! so operators get a signal when the CAS namespace contains garbage,
matching the projection-cache warmup skip path. Also removes the
runs_share_database_blob_store test, which asserted Arc pointer identity
of internal wiring rather than any observable behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-12 11:26:35 -04:00
Scott Werner
f773a24758 Simplify run handle construction and blob store test fixtures
- Collapse RunDatabase::open_writer/open_reader wrappers into one
  pub(crate) build, with a Database::open_run_database helper that
  gathers the shared-store dependencies in one place
- Stop fetching the blob store on open_run's active-cache hit path
- Share a raw-db test fixture between the two BlobStore raw-key tests
- Evict the cached writer in open_run_reader_is_read_only so the test
  exercises the real reader construction path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 10:51:19 -04:00
Scott Werner
10499e707c Share one blob store across run handles 2026-08-10 15:32:47 -04:00
Bryan Helmkamp
6dfe1c49d2
Merge remote-tracking branch 'origin/main' into feat/async-pr-create
# Conflicts:
#	lib/foundation/fabro-api/src/lib.rs
#	lib/foundation/fabro-client/src/client.rs
2026-08-04 15:04:24 -04:00
Bryan Helmkamp
5c6289df80
Simplify async pull request creation
Structural cleanup of the durable pull request creation feature, from a
three-agent review (reuse, quality, efficiency) of the branch:

- Move the supervisor out of handler/ into server/pull_request_supervisor.rs,
  collapse its double bookkeeping into one task-id map, and fold the five
  copy-pasted failure arms into attempt_pull_request_creation.
- Tag pull_request.failed events with the creation id they resolve, so a
  publish-stage failure can never fail an unrelated explicit creation. The
  reducer gains PullRequestCreation::succeed/fail transition methods.
- Scan pending creations through a narrow projection-cache accessor instead
  of materializing every run summary, raise the scan interval to 30s (notify
  covers the live path), and cap retries for runs whose worker cannot even
  record a failure.
- Answer "creation already pending" POSTs before taking the per-run create
  lock, which a worker can hold for the whole creation.
- Replace the hand-rolled per-run lock map with fabro_store::KeyedMutex.
- Reuse cheap Arc'd projections (cached_run_projection) on the poll endpoint
  and in the worker instead of deep-cloning run summaries and diffs.
- Merge ExistingPullRequest into fabro_github::CreatedPullRequest and
  extract one reconcile_existing_pull_request helper for both call sites.
- Give the client poll loop a 15-minute deadline; document that Retry-After
  and the poll interval are the same constant.
- Resolve a wedged pending creation (run already has a pull request) as a
  durable failure instead of skipping it forever.
- Tests: shared wait_for_pull_request_creation helper, a pinned generation-
  failure assertion, and a new pipeline test proving reconciliation adopts
  an existing PR without an LLM call or create request.

Verified: cargo build --workspace, cargo nextest run --workspace (7,767
passed), nightly clippy -D warnings, fmt --check, insta (no pending), bun
typecheck in fabro-api-client.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 14:51:29 -04:00
Scott Werner
3d77d371c6 Remove the recorded pre-run push outcome, keep the push
The manifest builder's best-effort pre-run push converted every result
into a PreRunPushOutcome that was serialized into GitContext, expanded
into five OpenAPI union arms, and generated into API clients — but no
production path ever read it; every field read was a test.

Delete the concept while preserving the behavior:

- Drop the PreRunPushOutcome enum and GitContext.push_outcome from
  fabro-types; GitContext keeps origin_url, branch, optional sha, and
  dirty, which remain real execution inputs and provenance.
- Rename the manifest outcome builder to push_manifest_branch_best_effort,
  a side-effect-only helper with the same decision rules: skip without an
  origin, skip on configured-repository mismatch, skip when the branch is
  already synced, otherwise push noninteractively and discard the result
  without failing manifest creation or logging raw Git stderr.
- Prove the push through repository state instead of the deleted enum: a
  branch ahead of a local bare origin is pushed during manifest build, a
  mismatched configured repository is not, and a failing remote helper
  still cannot fail manifest creation.
- Remove push_outcome from GitContext in OpenAPI, delete the five-arm
  union schemas, and drop the fabro-api type replacement and re-export.
- Keep one regression proving historical run.created events with a nested
  push_outcome still deserialize through ordinary unknown-field tolerance
  and reserialize to the reduced shape. No migration or event rewrite.

Old JSON carrying the removed field stays readable. Newly generated
clients omit a field older servers required, so new-client-to-old-server
compatibility is intentionally not promised for this pre-1.0 contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-03 19:27:33 -04:00
Scott Werner
2ecb8e0366 Test unknown run event properties generically 2026-08-03 16:23:00 -04:00
Scott Werner
4194ff4a26 Update run creation event fixtures and docs 2026-08-03 15:09:32 -04:00
Scott Werner
cff0a2461a Trim unused run creation event fields 2026-08-03 15:09:25 -04:00
Scott Werner
7104570049 Remove duplicate-run handling from store creation 2026-08-03 12:34:32 -04:00
Bryan Helmkamp
30ce02f2a8
Make pull request creation durable and asynchronous 2026-08-01 19:28:52 -04:00
Bryan Helmkamp
f212594875
Merge pull request #694 from fabro-sh/feat/reusable-subagent-sessions
Reuse completed subagent sessions
2026-08-01 10:21:41 -04:00
Bryan Helmkamp
1bb70adafd
refactor: simplify subagent session reuse
Review pass over the reuse change. No intended behavior changes.

- share one definition of the initial generation from fabro-types instead
  of three copies across fabro-types, fabro-agent, and the supervisor
- give each child one SubAgentHandle instead of threading the supervisor's
  state, callback, and notification sender through five functions, and
  collapse the repeated signal-then-drain pairs into publish()
- move `reusable` inside SubAgentStatus::Finished so a closed agent can no
  longer be marked reusable
- clear the lifecycle draining flag with an RAII guard, so one panicking
  callback cannot silence every later lifecycle event
- tear down a session that failed to initialize right away rather than
  holding it and its sandbox until the parent closes the agent
- look agents up through SupervisorState::agent/agent_mut instead of five
  copies of the same not-found error
- drop the unreachable cleanup_started branch and the test-only emit_event
  whose only caller was its own test
- render subagent starts from one ProgressEvent and one display method,
  deriving the spawn/turn distinction from the generation
- set projected subagent status through one helper instead of four
  identical reducer arms
- drive the generation-pinned wait test through spawn/send_input rather
  than hand-writing private supervisor state

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 10:09:07 -04:00
Bryan Helmkamp
20d3aa5cdd
Merge pull request #705 from fabro-sh/feat/download-all-artifacts
Download latest artifacts as a ZIP
2026-08-01 09:28:16 -04:00
Bryan Helmkamp
daaca3f479
refactor: simplify artifact ZIP download
Path safety now lives in one place. The NUL-byte and drive-letter rules
move from a server-only helper into the store's own filename validation,
so uploads reject those paths at write time instead of only the ZIP read
path catching them. The download still re-checks, because artifacts
stored before the rule existed can still carry an unsafe path, but it
now skips a bad path rather than failing the whole archive.

Promote is_boundary_stage to RunProjection and drop the three identical
private copies. The ZIP download used a node-name match instead, which
would have dropped artifacts from a working node that happened to be
named "start".

Compress the archive. Entries were Stored while the response was also
excluded from transfer compression, so text artifacts moved at full
size. async_zip gains the deflate feature; async-compression and flate2
were already in the lock file.

Log archive failures unconditionally. The send-succeeded guard meant a
client that had already disconnected left no record at all, which is the
case where the log is the only evidence.

Also: collapse the duplicate 500 arms, drop the dead stage-ID tiebreaker
and the cached order in the selection map, name the accessible label
after the visible one, share the run URL prefix between the two download
href builders, and document the mid-stream truncation behavior in the
OpenAPI description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 09:09:54 -04:00
Bryan Helmkamp
980aabc543
feat: download latest artifacts as zip 2026-07-31 13:23:35 -04:00
Bryan Helmkamp
8b5106902c
feat: reuse completed subagent sessions 2026-07-31 08:19:10 -04:00
Fabro
a94233407b fabro(01KYQMV1VW6139EGNHEM1RGF2G): simplify_sol (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 7
Fabro-Checkpoint: edcf7467e9

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-30 00:22:06 +00:00
Fabro
6ce5ea76a2 fabro(01KYQMV1VW6139EGNHEM1RGF2G): simplify_fable (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 6
Fabro-Checkpoint: b9c14c247e

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 22:31:26 +00:00
Fabro
4746d143fd fabro(01KYQMV1VW6139EGNHEM1RGF2G): implement (succeeded)
Fabro-Run: 01KYQMV1VW6139EGNHEM1RGF2G
Fabro-Completed: 5
Fabro-Checkpoint: c84d4147ba

⚒️ Generated with [Fabro](https://fabro.sh)
2026-07-29 20:35:54 +00:00
Bryan Helmkamp
a369ea7fc4
Merge remote-tracking branch 'origin/main' into feat/for-each-item-injection
# Conflicts:
#	apps/fabro-web/app/components/stage-renderers/parallel-children.tsx
2026-07-28 20:03:25 -04:00
Bryan Helmkamp
503e21fbc1
Merge origin/main into feat/parallel-branch-live-status
Conflicts were between this branch's parallel-branch identity work and
main's stage billing, review targets, and live stage timing.

- Stage fixtures: main added `billing` to each per-file `makeStage`; this
  branch had hoisted one builder into `lib/test-utils`. Kept the hoisted
  builder and gave it `billing: makeBilledTokenCounts()`, so both intents
  hold and the field list stays in one place. `stage-sidebar.test.ts` also
  builds raw `RunStage` wire payloads, so it keeps importing
  `makeBilledTokenCounts` directly.
- Import lists (`run_projection.rs`, `fabro-api/src/lib.rs`,
  `run_state.rs`, `stage_projection_round_trip.rs`): unioned both sides —
  `ParallelBranchId` alongside `timing`, `ReviewTarget`,
  `ReviewTargetKind`, `AttrValue`, `Node`, and
  `StageToolBatchProjection`.
- `fabro-server` tests: git interleaved two unrelated new tests into one
  body. Split them back into
  `list_run_stages_exposes_parallel_branch_identity` and
  `run_billing_includes_live_stage_timing_in_rows_and_totals`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:50:36 -04:00
Bryan Helmkamp
690ddd2a96
Fix live parallel branch refresh and simplify branch rendering
Branches bypass the engine's stage.started/stage.completed lifecycle, so
no SWR key invalidated the stages list while a fork ran. The new live
branch rows stayed frozen at their first observed state until an
incidental refetch. Map parallel.* events to the stages list, run events,
and graph keys.

Also:
- Label branch rows with formatStageLabel so a re-entered branch renders
  as `review_glm@2`, matching the sidebar and waterfall.
- Build branch rows in one pass and count live outcomes in one loop.
- Name ParallelBranchId in the OpenAPI spec and reuse fabro_types::
  ParallelBranchId, replacing two copies of an inline string format.
- Hoist makeStage and textContent into lib/test-utils so widening Stage
  cannot leave per-file fixtures stale (tests are excluded from
  typecheck, so the two component-test copies had already gone stale).
- Query stat tiles by data-stat instead of an exact Tailwind class.
- Reuse append_scoped_stage_event's body via append_event_with_scope and
  add test_branch_event instead of poking envelope fields.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 17:26:05 -04:00
Bryan Helmkamp
1aa7a153b0
Merge pull request #652 from fabro-sh/fix/publish-failures
Make publish failures terminal
2026-07-28 17:03:18 -04:00
Bryan Helmkamp
e6cf174578
Merge origin/main into feat/human-review-target
Conflict in apps/fabro-web/app/components/interview-dock.tsx. Main moved
the dock onto the shared collapsible `RunDockShell` and replaced the
local button constants with shared ones.

Kept main's structure whole and re-applied the review target rendering
onto it: the question paragraph in the shell's `body` becomes the linked
`ReviewTargetQuestion` when the target passes `safeReviewTarget`, and
plain text otherwise. Both now share main's paragraph classes through
`QUESTION_TEXT`, so the two renderings stay visually identical.

`peek` keeps using `question.text`, which is the correct plain-text
collapsed summary for a review target question.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-28 16:48:02 -04:00
Bryan Helmkamp
96f70a9aea
fix(timing): simplify live timing projections 2026-07-28 14:20:59 -04:00
Bryan Helmkamp
010c8d50c1
Add structured review targets to human gates 2026-07-28 13:24:12 -04:00
Release Repro
c812274db8
Show live status for parallel branches 2026-07-27 17:08:08 -04:00
Bryan Helmkamp
b53045a1ac
Add runtime for_each item injection 2026-07-27 11:55:55 -04:00
Bryan Helmkamp
1c82bd9008
fix(workflow): make publish failures terminal 2026-07-27 11:25:18 -04:00
Bryan Helmkamp
dd9f75fb05
fix(timing): harden live active projections 2026-07-25 23:43:43 -04:00
Bryan Helmkamp
c4971b93d3
fix(timing): accumulate active time for in-flight stages
`active_time_ms` was only ever computed from terminal stage events, so a
stage still running contributed zero to the run rollup. A run parked in one
long agent stage reported 2m 8s of active time against 16m 53s of wall
clock — the two finished stages — while the running stage had been doing
continuous inference and tool work for over 14 minutes.

`live_run_timing` summed `filter_map(|stage| stage.timing)`, and
`stage.timing` is only written at finalization. Wall time ticked live off
`start_time`; active time did not tick at all.

Stage projections now accumulate brackets from the event log:

- Closing an inference bracket folds its span into `live_inference_ms`
  instead of discarding it, including across retries, matching the
  in-process stopwatch.
- Tool calls open a batch on the first outstanding call and close it when
  the last one drains, so tools running concurrently within a turn count
  once — the same span `execute_tool_calls` is bracketed by. Summing
  per-call durations would over-count parallel tool use. Subagent tool
  events are excluded; they run inside the root call's span already.
- `StageProjection::live_timing(now)` composes accumulators with any open
  bracket, per handler: agent stages use the brackets, prompt and command
  stages count elapsed time as inference and tool respectively, and
  handlers that wait on a human, timer, condition, or child branches
  report zero.

Active is clamped to wall per stage. A worker killed mid-turn leaves its
bracket open forever, and without the clamp it would tick up unbounded.
The clamp does not need to detect the dead worker: a stage cannot have been
active longer than it has existed. `watchdog.timeout` remains the authority
on whether a run is stuck. The clamp is deliberately not applied at run
level, where concurrent branches can legitimately sum past run wall time.

Timing is derived from events rather than emitted by the worker, so this
needs no event-schema change and applies to runs already stored.
`StageProjection.timing` keeps its terminal-only meaning, and the
authoritative breakdown still replaces the live estimate at terminal
events.

The billing endpoint had the same hole behind its `wall_only` fallback:
running stages reported zero inference/tool/active. Not visible in the
product, which renders only `wall_time_ms`, but wrong for any other
consumer of `GET /runs/{id}/billing`.

Parallel branch stages lose their breakdown permanently, even after
completion, because `parallel.branch.completed` carries only `duration_ms`.
That is a separate data-loss bug, tracked in #644.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 14:54:38 -04:00
Bryan Helmkamp
c81ea69c73
Merge origin/main into feat/inference-observability 2026-07-24 22:55:15 -04:00
Bryan Helmkamp
d4f619bc2a
fix: clean up inference observability 2026-07-24 22:50:00 -04:00
Bryan Helmkamp
eddee10b35
fix(agent): harden Kimi profile tool contracts 2026-07-24 22:05:48 -04:00
Bryan Helmkamp
6659ae768a
feat(events): make inference in-flight state observable
During a long LLM turn the durable event stream was silent: between
`agent.tool.completed` and the next `agent.message` nothing was emitted,
so "the model is generating" and "the worker is wedged" were
indistinguishable from the run store, SSE, or the UI.

The signal already existed. `AssistantTextStart` fired at exactly the
right point — after `build_request()`, after compaction, immediately
before the stream opens — then was classified as streaming noise and
thrown away. This promotes it rather than inventing a new one.

Two events, each asserting only what is provable when it is emitted:

- `agent.llm.started` carries the *requested* provider/model. No usage,
  no cost, no context window: none of it exists yet, and failover can
  re-target, so `agent.message` stays authoritative for what answered.
- `agent.llm.first_output` is edge-triggered on the first output of an
  attempt and names what arrived. `ToolCall` is required, not optional:
  a turn that opens with a tool call produces no text or reasoning
  delta, so a latch keyed on those two would stay silent for exactly
  the tool-heavy rounds where liveness matters most.

`agent.llm.retry` now also fires on the one previously invisible
mid-turn path — a stream that ends without a finish event, which
replays the turn and discards its output with nothing to show for it.
Its `attempt` field was already fed by two independent counters, so an
optional `phase` (open | consume) names which loop it counts.

`StageProjection.inference` projects the open bracket. `Some` means
"the event log contains an unclosed inference bracket", not "the model
is computing now" — a SIGKILLed worker leaves it open, which is the
truthful statement of what we know, and `watchdog.timeout` remains the
authority on actually-stuck.

The close is the subtle part. Terminal cancel and wall-clock timeout
tear the session down through `discard_session` without emitting a
message, error, or interrupt, so a session-lifecycle backstop is
required. It has to be `agent.session.ended`, not
`agent.session.deactivated`: deactivation is emitted by `lease.release()`
*before* the forwarder drains queued agent events, so a queued
`agent.llm.started` can arrive after it and re-open the bracket. But
`agent.session.ended` carries no stage identity, so the close takes
ordering from the event and identity from the projection, scanning for
brackets the ending session opened. A normal stage lookup there finds
no target and silently no-ops.

Presentation states what the log proves and nothing more: no progress
bar or ETA (no completion estimate exists), "reasoning" only when the
provider sent reasoning output, elapsed counted since the request
opened, and no live animation once the run is terminal.

Scope is session-backed agent stages. One-shot completions call
`client.complete` directly and never build a session; covering them
means moving the emit point into `fabro-llm`, filed as a follow-up.

`agent.output.start` was never persisted — it existed in a name map,
an `unreachable!` arm, and docs — so the rename carries no migration
risk. Corrects `events.md`, which documented it as a real emitted
event, and the v2 proposal, which mapped it to `message.part.started`
despite it firing before the request opens.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:47:51 -04:00
Bryan Helmkamp
ddddc5bb33
feat(agent): give the Kimi profile Kimi Code's TodoList tool
The Kimi profile was registering the Anthropic task tools. Both persist
through the same TodoRuntime, but they model opposite interactions: TaskCreate
and TaskUpdate mutate individual tasks against tracked ids, while Kimi Code's
TodoList replaces the whole list in one call. Of the two surfaces fabro already
had, Kimi was given the one furthest from what its models are trained on.

Add TodoListKind::KimiTodos and a TodoList tool matching Kimi Code's contract
exactly:

  TodoList({ todos?: [{ title, status: pending | in_progress | done }] })

Omitting `todos` reads the list, an empty array clears it, and a list replaces
it. Reconciliation mirrors update_plan -- items are identified by their text,
so re-submitting a list preserves identity for unchanged entries -- and the
runtime, projections, and events are unchanged.

Two differences from the existing surfaces were behavioral rather than
cosmetic. Items carry only `title`, where TaskCreate requires both `subject`
and `description`, so a model with nothing to say for a description had to
invent one. And the terminal status is spelled `done`; `completed` is the
Anthropic and Codex spelling, and a model emitting `done` against the old
schema got a validation error rather than a todo. The internal representation
stays TodoStatus::Completed; only the wire vocabulary differs.

Kimi todo lists are session-scoped like OpenAI plans, so the root-agent
projection excludes subagent lists the same way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:18:29 -04:00
Bryan Helmkamp
e7740b4acb
feat(reasoning): capture provider reasoning in agent.message
Normalize the readable reasoning providers already return into a
canonical `ReasoningOutput` and carry it through the `agent.message`
run event to storage, SSE, and JSONL.

The shape is derived from the final response's canonical message
content rather than stored a second time, so there is no duplicate
source of truth and retried or replaced streaming buffers never
become durable reasoning. OpenAI-compatible `reasoning_details` are
now preserved verbatim as an opaque content part; only known readable
members are normalized out of them, leaving encrypted entries for a
later provider-aware replay phase.

This phase is passive: no request parameters change, no capability
guessing, and no newly observed provider field is replayed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:21:15 -04:00
Bryan Helmkamp
71549f0917
Merge remote-tracking branch 'origin/feat/backward-event-pagination' into feat/backward-event-pagination 2026-07-24 10:21:40 -04:00
Bryan Helmkamp
b77116994f
Harden event pagination bounds and scope desc params to the run route
Guard EventScan seeks against sequences past MAX_EVENT_SEQ: a
seven-digit start prefix sorts below six-digit event keys, so an
unvalidated since_seq like 5000000 returned an incorrect slice of
history instead of an empty page. An end bound past MAX_EVENT_SEQ now
delegates to the unbounded scan, which is equivalent because no stored
sequence exceeds it.

Clamp the descending exclusive end to just past the newest stored
event, so an oversized before_seq cursor pages from the newest event
instead of probing empty key space and returning nothing.

Split RunEventListParams out of EventListParams so before_seq and
order are only accepted by /runs/{id}/events; the session, stage,
pair transcript, and demo endpoints go back to ignoring them instead
of accepting order=desc while returning ascending results.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:21:25 -04:00
Bryan Helmkamp
c9900b2bfa
Merge branch 'main' into feat/backward-event-pagination 2026-07-24 10:14:57 -04:00
Bryan Helmkamp
eb83539a18
Merge origin/main into feat/stage-execution-identity-on-resume
Resolves conflicts with the shared-checkout parallel rewrite (#607) and the
cached-run/billing dedup (de60eb900):

- handler/parallel.rs: rebuilt on main's shared-checkout version. Branch
  ordinals are still reserved inside the branch task right before
  ParallelBranchStarted (with graph_visit/resumed_from_stage_id), and the
  reserved StageScope is shared with post-await error paths via a OnceLock
  slot instead of main's dispatch-time visit=1 scope, so completion events
  are never emitted under a guessed ordinal.
- billing.rs: keep this branch's run_stage_from_projection (RunStage grew
  graph_visit/resumed_from_stage_id and a typed id), adopt main's
  state.cached_run() and drop the removed run_stage_from_stage_id import.
- run_projection.rs: adopt main's typed parallel_results
  (Option<Vec<ParallelBranchResult>>).
- run_event/misc.rs: union of both sides' imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:07:35 -04:00
Bryan Helmkamp
7e7d7e4457
Recover latest event seq with bounded probes instead of a full scan
On a projection-cache miss, descending pagination recovered the latest
sequence by scanning the run's entire event prefix, making a cold-cache
order=desc request O(total_events). Binary-search the zero-padded
sequence key space with single-entry probes instead, bounding recovery
to O(log MAX_EVENT_SEQ) reads. The probe predicate (smallest stored
sequence at or above a bound) stays monotone across gaps left by
failed appends.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:05:01 -04:00
Bryan Helmkamp
ec100fca2b
Merge remote-tracking branch 'origin/main' into feat/backward-event-pagination
Resolved conflicts against main's shared-projection-cache rework:
- projection_cache.rs: kept main's projection_snapshot and dropped this
  branch's last_seq accessor, which it subsumes; latest_event_seq now
  reads the sequence from projection_snapshot.
- run_store.rs: kept main's EventScan cursor and added a seek_before
  constructor so the backward-pagination range scan bounds its end key
  through the same abstraction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:57:15 -04:00
Bryan Helmkamp
b886f82622
Clamp backward pagination end bound to the event key-order limit
Event keys zero-pad seq to six digits, so an exclusive end bound past
MAX_EVENT_SEQ formatted as a seven-digit prefix that sorts before real
event keys, producing an inverted scan range. This made the newest page
come back empty once a run reached MAX_EVENT_SEQ, and let a client
supplied before_seq beyond MAX_EVENT_SEQ garble the range. Clamp the
bound and treat anything past MAX_EVENT_SEQ as unbounded; no stored
sequence exceeds it, so the results are equivalent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:50:40 -04:00
Bryan Helmkamp
94e4a38375
Merge remote-tracking branch 'origin/main' into fix/shared-run-projection-cache
# Conflicts:
#	lib/components/fabro-store/src/slate/run_store.rs
2026-07-24 09:41:58 -04:00
Bryan Helmkamp
78fea736e3
fix: harden stage execution identity on resume 2026-07-24 09:37:05 -04:00
Bryan Helmkamp
396f75578a
Merge remote-tracking branch 'origin/main' into feat/backward-event-pagination
Resolved conflict in run_store.rs tests: kept both the new
list_events_before_with_limit tests from this branch and the
append_event_rejects_sequences_beyond_key_order_limit test from main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:35:34 -04:00
Bryan Helmkamp
3c6a26e8c2
Merge pull request #607 from fabro-sh/feat/shared-checkout-parallel
Shared-checkout parallel execution
2026-07-24 09:33:03 -04:00