Commit graph

4139 commits

Author SHA1 Message Date
Bryan Helmkamp
c464e1b91c
feat(agent): implement Read, Write, and Bash to Kimi Code's contract
Three Kimi Code tools differ from fabro's built-ins in what their parameters
mean, not just what they are called. Renaming fabro's parameters would have
advertised behavior fabro does not have, so these are separate tools:

- Bash takes `timeout` in SECONDS where fabro takes milliseconds, and accepts a
  `cwd`. A rename alone would have made every timeout 1000x wrong -- silently,
  since nothing validates the magnitude.
- Read accepts a NEGATIVE `line_offset`, meaning "read the last N lines".
  Fabro's `offset` has no such meaning, so the tool counts the file's lines and
  converts to an absolute start.
- Write takes a `mode`, so it can append. The Sandbox trait has no append, so
  append is read-modify-write, which keeps every provider working and stays
  inside path policy.

Everything reaches the environment through the same Sandbox methods the
built-ins use, so sandbox behavior, path policy, and the read-before-write
guard are unchanged. Tools register under their canonical names and the
registry's vocabulary renames them, so the Kimi profile does not special-case
naming twice.

Edit needed no new tool: `old_string`, `new_string`, and `replace_all` already
match Kimi Code exactly, and `file_path` versus `path` is a pure rename.

Grep and Glob are not converted. Their shared parameters already behave
identically; the gap is optional capability fabro lacks -- Grep's `type`,
`multiline`, and `include_ignored`, and Glob's `include_dirs` and
`include_ignored` -- which needs new Sandbox trait methods implemented across
the local, Docker, and Daytona providers. Omitting an optional parameter is
honest; renaming one whose semantics differ is not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:28:47 -04:00
Bryan Helmkamp
21b90bad00
test(agent): pin that Kimi tool descriptions stay scoped to the Kimi profile
The Kimi profile rewrites several built-in tool descriptions. Every profile
builds its registry from the same factories, so a change made in the wrong
place would reword tools for models that were never meant to see it, and
nothing would fail.

Assert the isolation directly: for each shared built-in, Kimi's description
differs from Anthropic's, OpenAI and Gemini match Anthropic's stock wording,
and the read-before-write phrasing appears nowhere but Kimi.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:19:28 -04:00
Bryan Helmkamp
5349e9d99c
feat(agent): give the Kimi profile Kimi Code's tool descriptions
Edit and Write already carried Kimi-specific descriptions; the other four
built-ins were still fabro's one-liners, roughly 150-220 characters against
Kimi Code's 1-5KB. Port Bash, Read, Grep, and Glob the same way.

Bash is the largest and the most useful: most of its length is an explicit
translation table steering shell usage to the dedicated tools -- cat to Read,
sed to Edit, find to Glob, grep to Grep -- under the names this profile
exposes. It also states that each call runs in a fresh bash process, so `cd`
and environment variables do not persist, and that a command which timed out
needs a raised `timeout_ms` rather than a retry. Two of the observed K3 tool
failures were shell timeouts.

The port stays subtractive. Kimi Code's Bash documents background execution,
TaskOutput, TaskStop, and a `cwd` argument; fabro's shell has none of those, so
none of it is claimed. Read drops Kimi Code's media and paging specifics that
do not match fabro's offset/limit, and gains the fact that reading a file is
what clears it for writing. Grep deliberately does not promise ripgrep syntax:
fabro falls back to POSIX grep when rg is absent, so the description asks for
portable patterns instead.

Bash quotes the timeouts this profile actually enforces by interpolating them
from NativeToolOptions, so the description cannot drift from behavior. Tests
assert the interpolation rendered, that the translation table names the exposed
tools, and that no background-execution guidance leaked in.

Parameter names stay fabro's. Kimi Code's differ (`path` and `line_offset`
where fabro has `file_path` and `offset`), but across roughly 1200 tool calls
in two observed K3 runs there were no schema or missing-parameter errors, so
the model reads the schema it is given. Renaming parameters would be churn
against a hypothesis the data does not support.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 21:14:23 -04:00
Bryan Helmkamp
9604d2ac9d
fix(agent): apply the Kimi vocabulary to every tool, not just the early ones
Renaming ran as a pass at the end of profile construction, so it only covered
tools registered by that point. Subagent tools arrive later via
`register_subagent_tools`, and the skill tool is registered when a session
discovers skills, so a Kimi profile actually exposed a mixed set:

  Read Write Edit Bash Grep Glob FetchURL TodoList     renamed
  spawn_agent send_input close_agent wait use_skill    missed

Move the vocabulary into ToolRegistry instead of applying it as a pass.
`register` renames built-ins on the way in, so registration order stops
mattering and a late registration cannot slip through. `ToolRegistry::new`
keeps the fabro vocabulary, so no other profile changes.

`use_skill` now exposes as `Skill`, matching Kimi Code, which has the same
semantics. The subagent tools stay under fabro's names on purpose: Kimi Code's
`Agent` launches a subagent and returns its result, while fabro's spawn_agent
returns a handle that send_input, wait, and close_agent drive. Borrowing the
name without the semantics would promise a result the tool does not return --
the same mistake as exposing incremental task tools under a whole-list name.

The skills prompt section hardcoded `use_skill`, which under this vocabulary
names a tool the model was not given. It takes the exposed name now, threaded
through EmbeddedPrompt so a profile's prompt and its registry cannot disagree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:27:53 -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
fbff6f5774
refactor(agent): model built-in tools as an enum with per-profile vocabularies
Tool names were string literals matched in several places, which made
renaming a tool for one profile unsafe: `tool_category` falls back to `Shell`
for an unrecognized name, so exposing `Read` instead of `read_file` would have
silently demanded shell-level approval for every file read.

Introduce `NativeTool`, the closed set of tools fabro implements, with strum
string conversions per the repo convention. A tool is an identity; a name is
one rendering of it. `ToolVocabulary` names the renderings -- fabro's own, and
Kimi Code's -- and `NativeTool::from_any_name` resolves a name in any
vocabulary back to the identity. Permissions, categories, and telemetry go
through that resolution, so behavior no longer depends on which profile is
running.

`known_tool_category` is now an exhaustive match on the enum rather than a
string match, so a new built-in tool has to state its category instead of
silently inheriting the unknown-tool default. Tools that are uncategorized
today stay uncategorized: giving them a category would change the CLI
permission gate, which is a behavior change rather than a cleanup.

MCP, skill, and run-scoped tools keep arbitrary string names, so
`ToolDefinition.name` and the registry keys stay `String`. The enum covers the
closed set only.

With that in place, the Kimi profile exposes its tools under Kimi Code's
vocabulary -- Read, Write, Edit, Bash, Grep, Glob, WebSearch, FetchURL -- and
its prompt and tool descriptions use those names. Tools with no Kimi Code
counterpart of the same shape keep fabro's names. Ask Fabro's tool policy
resolves through the canonical name so a Kimi-model run is not denied its
whole tool set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 20:04:11 -04:00
Bryan Helmkamp
c08e5c5490
feat(agent): add a Kimi agent profile for Moonshot and gateway routes
Kimi models ran on the OpenAI profile, which exists to look like Codex. Give
them their own profile derived from Kimi Code's system prompt.

Routing is per model, not per provider, because Kimi models are served both
directly by Moonshot and through gateways. `kimi` sets agent_profile at the
provider level; the Kimi model rows on `openrouter` set it individually, so a
gateway route behaves like the direct one while other OpenRouter models keep
the provider's OpenAI profile.

The profile targets a measured failure. Across two observed K3 implementation
stages, 32 of 35 tool failures were the same thing: writes to files the model
had not read, rejected by the workspace read-before-write guard, or
`old_string` values reconstructed from memory rather than taken from a read.
Kimi Code drills this rule in its own tool descriptions, so the profile does
too -- `edit_file` and `write_file` carry Kimi-specific descriptions naming the
guard and the failure text the model will see, alongside a "Reading Before
Writing" section in the system prompt. Profiles own their tool registries, so
this re-describes the tools for Kimi only; every other profile is untouched and
the executors and JSON schemas are shared unchanged.

Tool names stay fabro's existing snake_case. Whether Kimi Code's PascalCase
vocabulary measurably helps is untested, and renaming would also mean updating
the name-keyed categories in tool_permissions.rs, where an unknown tool falls
back to Shell. That is a separate change to make on evidence.

The prompt is a subtractive port: capabilities fabro does not have -- plan
mode, background tasks, cron, subagent swarms, the cwd tree listing -- are
dropped rather than promised. The shell timeout default matches Kimi Code's 60s
and memory discovery reads AGENTS.md, which is the only instruction file Kimi
Code looks for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 19:50:58 -04:00
Bryan Helmkamp
63c952e380
Merge pull request #628 from fabro-sh/refactor/agent-md-prompts
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run
refactor(agent): render profile system prompts with minijinja templates
2026-07-24 19:31:48 -04:00
Bryan Helmkamp
fd6f14f107
refactor(agent): simplify prompt template rendering 2026-07-24 19:10:48 -04:00
Bryan Helmkamp
1d685cbea4
refactor(agent): render profile prompts with minijinja via fabro-template
The previous commit introduced render_prompt and splice_optional_section, a
second templating mechanism in a workspace that already standardizes on
MiniJinja behind fabro-template. Drop both and render the profile prompts the
same way fabro-workflow and fabro-manifest render theirs.

Expressing the conditionals as {% if %} lets every profile collapse to a single
template, since the optional blocks no longer need to be separate files spliced
in from Rust:

  before: 6 files + 2 splice helpers, prompt prose split across .md and .rs
  after:  3 files, one per profile, all prose in the template

Rust now passes only facts -- provider name, which file-edit tool is active,
and whether web search and subagents are available. Values land under `vars`,
so templates read {{ vars.env_block }}. Booleans are passed as "true"/"false"
and compared explicitly via the bool_var helper, because the shared
TemplateContext types vars as strings and a bare {% if %} on the string
"false" would be truthy.

Also converts fabro-server's Ask Fabro prompt, which is assembled at runtime.
Its tool guidance now arrives as a template variable instead of being
interpolated into the template text. That guidance carries tool names and
descriptions that can originate from MCP servers, and MiniJinja does not
re-render substituted values, so a tool description containing {{ ... }} stays
inert rather than being evaluated.

Output is unchanged. Verified by diffing all ten prompt variants against the
same unmodified origin/main worktree used for the previous commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:23:47 -04:00
Bryan Helmkamp
2c13e2da5e
refactor(agent): load profile system prompts from .md files
The three profile prompts lived as multi-line Rust string literals with
backslash continuations, which made them awkward to read, diff, and review.
Move them to profiles/prompts/*.md loaded via include_str!, matching the
existing convention in fabro-workflow's pipeline prompts and the server's
playground prompt.

Two helpers back the templates, both following the {env_block} convention
already used by assemble_system_prompt rather than adding a template engine:

- render_prompt substitutes {name} placeholders and leaves the rest intact,
  for values spliced inline (provider name, the web-search bullet)
- splice_optional_section handles whole blocks that come and go, dropping the
  blank line ahead of the placeholder when the block is empty

Gemini already carried a {web_search_section} placeholder in its literal, so
that one maps onto render_prompt unchanged. Anthropic's per-section functions
collapse into a single template plus a subagent fragment. OpenAI keeps its two
one-line file-edit failure hints inline, since they are bound to the tool name
and would not read well as standalone files; the multi-line usage blocks they
pair with become fragments.

Output is unchanged. Verified by capturing all ten prompt variants -- Anthropic
across subagent x web-search, OpenAI across apply_patch/edit_file x web-search,
Gemini across web-search -- from an unmodified worktree at origin/main, then
diffing them byte for byte against this branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:55:08 -04:00
Bryan Helmkamp
8921fc533f
Merge pull request #622 from fabro-sh/feat/stage-chat-view
feat(web): add Chat view for agent stages
2026-07-24 17:54:19 -04:00
Bryan Helmkamp
7044dd57fa
Merge pull request #627 from fabro-sh/fix/openai-compatible-stream-usage
fix(llm): request streaming usage on openai_compatible providers
2026-07-24 17:53:45 -04:00
Bryan Helmkamp
ae3b6702e2
Merge pull request #626 from fabro-sh/feat/passive-reasoning-capture
feat(reasoning): passive reasoning capture in agent.message
2026-07-24 17:49:46 -04:00
Bryan Helmkamp
53a92f75f4
test(llm): model streamed usage in OpenAI twin 2026-07-24 17:47:05 -04:00
Release Repro
512ab50f9c
fix(reasoning): align stream and client invariants 2026-07-24 17:42:44 -04:00
Bryan Helmkamp
e739f86f6a
Merge remote-tracking branch 'origin/main' into feat/stage-chat-view
# Conflicts:
#	apps/fabro-web/app/routes/run-stages.test.ts
#	apps/fabro-web/app/routes/run-stages.tsx
2026-07-24 17:39:48 -04:00
Bryan Helmkamp
2c6fd5d798
Merge pull request #625 from fabro-sh/fix/brave-search-profile-config
Fix Brave Search tool secret propagation
2026-07-24 17:36:48 -04:00
Bryan Helmkamp
470db44ba2
Merge pull request #624 from fabro-sh/fix/tool-group-boundaries-dna-attribution
fix(web): correct tool-group boundaries and DNA timeline attribution
2026-07-24 17:35:28 -04:00
Bryan Helmkamp
4ce57f8aae
refactor: simplify profile builder and drop dead tool plumbing
Follow-up cleanup on the profile-builder refactor.

AgentProfileBuilder::build now borrows instead of consuming, removing the
builder.clone().build() dance at all seven call sites. Deletes
with_command_timeouts, which had no caller but its own test, and the
with_summarizer constructors on all three profiles, whose only remaining
caller was each profile's own new().

Replaces the fifth copy of the profile-kind match (guardrails.rs) with the
builder, and swaps the parity matrix's hand-maintained provider list for
Catalog::effective_agent_profile so a new catalog provider cannot silently
skip the matrix. Collapses web_search_provider_test! into a secrets = arm
on provider_test! and uses EnvVars::BRAVE_SEARCH_API_KEY over a literal.

Drops the Brave key from the Ask Fabro session: AskFabroToolAccessPolicy
denies web_search, and both tools() and the prompt are filtered through
that policy, so the vault read only registered an uncallable tool.

Makes NativeToolOptions::for_profile match exhaustively so a new profile
kind must state its timeout, restores Anthropic's borrowed prompt sections
and Gemini's static prompt (placeholder substitution rather than format!
over 110 lines with doubled braces), and introduces WEB_SEARCH_TOOL_NAME
for the registry lookups that keep tool availability and prompt guidance
in sync.

Updates the product docs, which still described web_search as always
registered and as erroring at call time when unconfigured.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:32:33 -04:00
Release Repro
4d3de5f564
fix(reasoning): tighten capture normalization 2026-07-24 17:31:14 -04:00
Bryan Helmkamp
b1bf868c9d
refactor(web): simplify stage chat projection 2026-07-24 17:29:04 -04:00
Bryan Helmkamp
7b82a150ad
refactor(web): simplify thread DNA selection identity 2026-07-24 17:23:08 -04:00
Bryan Helmkamp
0169725b4e
fix(llm): request streaming usage on openai_compatible providers
Chat Completions only emits the trailing usage chunk when the request sets
`stream_options: {"include_usage": true}`. The openai_compatible codec never
sent it, so providers that follow the spec strictly returned no usage at all
on streamed responses. Every message came back with zero tokens, and the
catalog cost estimate multiplied those zeros into $0.

Kimi is the visible case: a run's kimi-k3 stages report 0 tokens and no
dollars, while an openrouter stage in the same run bills normally because
OpenRouter volunteers usage (and an in-band cost) without being asked.

Send the opt-in whenever we stream. Providers that already volunteer usage
accept the field and are unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:21:00 -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
e15defe4c5
refactor: centralize agent profile tool configuration 2026-07-24 14:06:29 -04:00
Bryan Helmkamp
110058bb4b
fix(web): correct tool-group boundaries and DNA timeline attribution
Empty-text `agent.message` events were discarded, erasing the boundaries
between batches of tool calls. Eight short shell calls issued across five
model responses collapsed into one `Bash x8` group whose DNA bar spanned
the model-response gaps between them, showing a misleading six-minute
duration. Filtering could recreate the same artificial adjacency.

- Always emit an assistant turn for `agent.message`, carrying
  `tool_call_count` so a text-free response renders as
  "Requested N tool calls" instead of a blank row.
- Derive grouping and DNA timing from the complete turn stream, then
  apply kind/search filters as a pure visibility pass over display
  items. Hiding a tool can no longer inflate an adjacent Agent bar, and
  hiding an Agent can no longer merge the tool groups on either side.
- Give a tool group the wall-clock envelope of its children (earliest
  start to latest end) rather than the sum of their durations or the
  span to the last array element. Row, details header, DNA bar, and
  tooltip all read the same values.
- Advance the DNA previous-activity cursor by the maximum observed end
  so out-of-order or overlapping completions cannot move it backward.

Frontend only: no event, persistence, or API schema changes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 13:56:57 -04:00
Bryan Helmkamp
2ecf185aeb
Merge pull request #623 from fabro-sh/feat/anthropic-opus-5-catalog
Add Claude Opus 5 to the Anthropic model catalog
2026-07-24 13:50:31 -04:00
Release Repro
0b58d087ee
feat(model): add Claude Opus 5 2026-07-24 13:43:08 -04:00
Bryan Helmkamp
fec5021a29
fix: pass tool secrets to agent profiles 2026-07-24 13:36:45 -04:00
Bryan Helmkamp
2539e3a661
feat(web): add Chat view for agent stages
Adds a Chat tab to agent stage pages alongside Thread and Debug, styled
after the Ask Fabro sidebar: agent messages render as first-class chat
bubbles (the narration between tool batches is the content that matters),
the stage prompt is a collapsed user-side card, and each run of
consecutive tool calls collapses to a wrench-icon count chip. While the
stage is running, in-flight tool calls (agent.tool.started without a
completed event) show as a live spinner line with the tool name and input
preview — data the Thread view drops today.

Thread remains the default tab; Chat becomes the default only after
production testing.

Also fixes the demo dataset: detect-drift carries the agent-flavored
stage events (prompt, agent messages, tool calls) but was labeled a
command stage, so its Thread/Chat views were unreachable in demo mode.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 13:17:57 -04:00
Bryan Helmkamp
280afbb649
Merge pull request #621 from fabro-sh/docs/refresh-product-docs-2026-07-24
docs: refresh changelog and product documentation
2026-07-24 11:55:59 -04:00
Bryan Helmkamp
d0d1dac3ac
docs: sync product documentation 2026-07-24 11:39:16 -04:00
Bryan Helmkamp
7ad164c45d
Merge remote-tracking branch 'origin/main' into docs/refresh-product-docs-2026-07-24 2026-07-24 11:37:32 -04:00
fabro-releases[bot]
ffee6ed29e Bump version to 0.304.0-nightly.1 2026-07-24 14:47:39 +00:00
Bryan Helmkamp
39664cd966
docs(changelog): refresh recent product changes 2026-07-24 10:47:14 -04:00
Bryan Helmkamp
678e75e2f3
Merge pull request #612 from fabro-sh/feat/backward-event-pagination
Add backward event history pagination
2026-07-24 10:30:59 -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
2f16836022
Merge pull request #620 from fabro-sh/feat/stage-execution-identity-on-resume
Treat resumed in-flight nodes as new stage executions
2026-07-24 10:13:51 -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
17cf8d710e
Merge pull request #618 from fabro-sh/fix/shared-run-projection-cache
Reuse shared projections for current run state
2026-07-24 09:47:29 -04:00
Bryan Helmkamp
8394eb2723
Merge pull request #619 from fabro-sh/feat/fireworks-provider
feat(llm): add Fireworks AI as an opt-in provider
2026-07-24 09:46:03 -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
ad9b3810c2
docs(fireworks): fix CLI examples flagged in review
- The catalog comment showed `fabro provider login fireworks`, but
  `--provider` is a required flag: `fabro provider login --provider fireworks`.
- The remote-server `fabro model test` example omitted `--provider fireworks`,
  which could resolve the slug against a different provider.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:37:18 -04:00
Bryan Helmkamp
78fea736e3
fix: harden stage execution identity on resume 2026-07-24 09:37:05 -04:00