Skip to content

feat: add verified self-evolving Experience memory - #100

Open
al2wang wants to merge 27 commits into
mainfrom
alex/verified-self-evolving-experience
Open

al2wang wants to merge 27 commits into
mainfrom
alex/verified-self-evolving-experience

Conversation

@al2wang

@al2wang al2wang commented Sep 21, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

This PR adds the first landing slice of S-Code's self-evolving agent stack:

corrective execution → distilled Experience → evaluation → approval → verified reuse

The feature is local-first, scoped to the actor and project that produced the Experience, and
off by default. Shared Skills, the Skill Shop, the Forum and all research results are
deliberately out of scope and live in follow-up PRs (#101 → #109 → #111); this PR's review
surface is the verified local Experience memory only.

When an agent runs a verifier that fails, edits the workspace, and later passes the identical
verifier, the daemon preserves that recovery as bounded corrective evidence. After the turn
completes it may distil that evidence into a short reusable Experience candidate. Candidates stay
quarantined until an explicit decision admits them — manually, or through an immutable evaluation
on held-out work that the approval is bound to — and only an approved Experience of the same actor
and project is ever retrieved, as advisory derived-untrusted context below the system rules, the
user, policy, permissions, the sandbox and tool results.

What the review changed (all on this head)

finding fix regression (fail-before / pass-after)
edits from the current files[] and Patch patch + revisions formats are observed; a partial batch records only the paths the runtime wrote ff9f565 edits_are_observed_in_every_current_editing_form, a_partial_batch_records_only_the_paths_the_runtime_wrote
the file-protection reset is respected when retrieving Experience 693bf84 a_privacy_reset_keeps_a_stale_experience_out_of_the_prompt
distillation goes through the protected, observed provider a82c1e5 distillation_is_observed_with_its_own_purpose, a_protection_change_before_dispatch_refuses_distillation_and_persists_nothing
both distillation privacy windows: a reset while the distiller streams discards the lesson; a reset before dispatch is privacy invalidation, never a fallback candidate 7cec287 a_reset_while_the_distiller_streams_discards_the_lesson_it_produced, a_reset_while_the_distiller_streams_is_never_downgraded_to_a_fallback, a_reset_between_admission_and_dispatch_discards_the_evidence
approval resume observes the executed approved call exactly once from the checkpointed arguments; a denial is not a verifier failure or an edit; the wrong pending call cannot resume the checkpoint 421871c approved_recovery_is_observed_exactly_once_and_becomes_evidence, an_approved_edit_is_observed_with_the_outcome_it_actually_had, a_denied_approval_is_never_a_verifier_failure_or_an_edit, a_failed_approval_resume_is_reported_to_clients, corrective_trace_is_seeded_across_a_resumed_turn
a tool result is classified from the raw execution result before it becomes an artifact handle 28b5ba8 classification_fields_carry_the_outcome_and_no_content, an_externalised_large_success_is_never_recovery_evidence
an evaluated approval selects the newest applicable evaluation, validates a named id, recomputes eligibility, binds the evaluation and transitions status in one BEGIN IMMEDIATE transaction f21f518, 1aba109 an_evaluation_landing_during_an_approval_cannot_be_superseded_after_the_fact, newer_ineligible_evidence_supersedes_an_older_eligible_evaluation, an_approval_keeps_its_binding_when_the_audit_event_fails
the decision, its evaluation binding and its durable audit record commit together; automatic promotion commits both of its records or neither 5ec1b02 an_approval_rolls_back_when_its_audit_record_cannot_be_written, an_approval_and_its_audit_record_survive_a_restart_before_delivery, automatic_promotion_commits_both_audit_records_or_neither

Durable audit guarantee

An Experience decision is written in one storage transaction with its approval-evaluation binding
and its audit event: the event is sequenced and chained by the daemon first and inserted inside
the decision's own transaction. If the audit row cannot be written the transaction rolls back and no
approved or retrievable Experience remains. Failing to notify an in-memory subscriber after that
transaction is a delivery concern and never undoes the decision.

Post-turn work and shutdown

Candidate extraction and distillation run after turn.completed, so they never delay or fail a
turn. A graceful shutdown gives in-flight Experience work a bounded opportunity to finish — up to
twenty seconds, the fifteen-second distillation deadline plus the server's five-second drain —
and aborts tasks still in flight after that timeout. This reduces but does not eliminate candidate
loss; no persistent recovery queue is introduced in this PR, and forced termination ends the work
without that opportunity.

Storage

crates/storage/migrations: 0053_experiences, 0054_experience_evaluations and
0057_experience_approval_binding (the durable approval-evaluation binding). 0055 and 0056
are reserved by #101's shared-skill layer, which stacks on this PR; the skill registry keeps an
independent migration namespace. Validated with the real migrator on disposable databases:

path applied migrations ≥ 0050
fresh current main (eb7bd9f) 0050, 0051, 0052
current-main database opened by this branch 0050–0054, 0057
fresh database on this branch 0050–0054, 0057
this branch's database opened by the prospective #101 0050–0057
fresh database on the prospective #101 0050–0057
reopen after each migration succeeds

A #101-migrated database opened by this branch's binary is refused with VersionMissing(55), as
expected for a downgrade. No applied migration file was edited.

Configuration

[daemon]
experience_mode = "off"          # off | observe | verified
experience_promotion = "manual"  # manual | evaluated | automatic

(S_CODE_DAEMON_EXPERIENCE_MODE, S_CODE_DAEMON_EXPERIENCE_PROMOTION.)

Review fixes (Wei, 2026-10-11 review of cd44c18)

  1. Privacy check and candidate insert are indivisible (2b37818). record_experience_evidence now holds the existing
    per-owner protection gate (protection_gate(scope), the same in-process lock replace_file_protection takes for writing)
    from the final check of the turn's original boundary through the candidate INSERT, and releases it before
    experience.created is published. Distillation, stream consumption and event fan-out stay outside it. The distilled and
    deterministic-fallback paths share the block. replace_file_protection also places its cutoff after the newest committed
    experience for the same owner, so a candidate that wins the race always lands before the cutoff. No migration; created_at
    is never adjusted. Tests: a_reset_before_the_final_check_discards_a_distilled_lesson,
    a_reset_before_the_final_check_discards_the_fallback_lesson,
    a_reset_cannot_commit_between_the_final_check_and_a_distilled_insert,
    a_reset_cannot_commit_between_the_final_check_and_a_fallback_insert,
    protection::tests::file_protection_activation_covers_committed_experiences.
  2. Corrective evidence follows execution, not delivery (98bae0a, 6817ea3, 04a7094). The executor records a bounded execution
    outcome as soon as the execution service returns, before publication or artifact externalization. Agent core observes that
    outcome once per call (execute_prepared_observed, ToolExecution), and the model still receives the delivery error. Exit 0
    is a pass; a nonzero exit is a failure (even when the output says "not found"); denied, awaiting-approval, never-run calls and
    failed run_command calls without an exit status (spawn errors, timeouts) are not observed. Error text is never read to decide
    whether a call ran. Edits keep only actually written
    paths. Approval resume observes the real outcome once; if publishing tool.completed/approval.resolved fails, the turn now
    resumes with that error instead of staying in AwaitingApproval; the approval decision and the execution outcome are durably
    committed before any publication, and a failed durable write never resumes. Tests:
    a_publication_failure_after_a_passing_verifier_is_never_recovery_evidence,
    an_externalisation_failure_after_a_passing_verifier_is_never_recovery_evidence,
    a_real_verifier_failure_survives_a_delivery_failure_and_its_repair_is_evidence,
    an_unexecuted_verifier_is_never_the_failure_half_of_a_recovery,
    a_verifier_that_never_spawned_is_never_the_failure_half_of_a_recovery,
    an_approved_verifier_keeps_its_outcome_once_through_a_publication_failure,
    a_written_edit_keeps_its_paths_through_a_publication_failure,
    a_partial_edit_keeps_only_its_written_paths_through_a_publication_failure,
    a_failed_command_is_a_verifier_failure_only_when_its_process_ran,
    a_verifier_killed_at_its_timeout_leaves_no_observation,
    a_shell_that_ran_and_reported_not_found_is_still_a_verifier_failure,
    an_approval_delivery_failure_resumes_once_and_never_re_executes,
    a_failed_durable_approval_or_outcome_write_never_resumes_the_turn,
    a_denied_approval_whose_publication_fails_resumes_unobserved,
    the_trace_follows_the_reported_execution_not_the_delivered_result,
    a_completed_execution_keeps_only_bounded_classification_fields.

Known limitation (not changed in this PR): two approval tests added in 04a7094
(an_approval_delivery_failure_resumes_once_and_never_re_executes, a_denied_approval_whose_publication_fails_resumes_unobserved)
can time out under heavy load because of a pre-existing race, in which a decision that lands before the turn records its pause is
dropped. The fix is a separate, reviewed follow-up and is not part of this PR.

Validation on 04a7094

Local, on a Linux compute node with the command sandbox available (bubblewrap), Rust 1.89.0:

check result
cargo fmt --all -- --check pass
cargo clippy --locked --workspace --all-targets -- -D warnings pass
cargo test --locked --workspace 937 passed, 0 failed, 0 ignored
community tree, secret history scan (pushed refs + this head), doc links, DCO, community candidate, verification suite, docs-site generation check pass

The new regressions were written first and failed on cd44c18. The earlier note that docs/source-gate were red is
obsolete: CI was green on cd44c18. 04a7094 passed GitHub CI against the previous base (run 38189221818). The run against current main (which now includes #125) is reported in the review thread. 04a7094 adds sqlx as a daemon dev-dependency only (an existing workspace crate; no new package).

Non-goals

No sharing between agents, no registry, no Skills, no challenge/fork/supersession, no demo, no
learned routing or per-attempt allowlists, and no research results. Those are follow-up PRs or
research instrumentation and are not part of this review.

🤖 Generated with Claude Code

@sl-7qx sl-7qx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I found three issues that should be fixed before merging: normal edits are missing from the corrective trace, and two Experience paths bypass existing privacy protections. I reproduced each issue with focused local tests. This was a targeted review, not a full project verification run.

Comment thread crates/agent-core/src/lib.rs Outdated
Comment thread crates/daemon/src/lib.rs
Comment thread crates/daemon/src/lib.rs Outdated
@weiddeng

Copy link
Copy Markdown
Collaborator

On “8. Graceful post-turn draining”: My understanding is that completed corrective turns can spawn independent, potentially parallel distillation tasks, and shutdown waits up to 20 seconds total for them. Any tasks still running are then aborted and cannot resume. This reduces—but does not prevent—Experience loss, while allowing remote model latency to delay daemon restart.

Given that this is best-effort post-turn work, would it be simpler to cancel active distillations and exit promptly? Only candidates in flight at shutdown would be lost, and similar corrections could be learned again from later qualifying turns. That seems a modest tradeoff for avoiding up to 20 seconds of Experience-specific shutdown latency.

@weiddeng weiddeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the substantial work here and for addressing the initial review. I did another end-to-end pass across corrective capture, distillation, promotion, and retrieval. I found five remaining correctness gaps that affect the feature's core guarantees and should be resolved before merge. Four are noted inline; I added the remaining protection-race detail to the existing privacy thread so that discussion stays together.

Comment thread crates/agent-core/src/lib.rs Outdated
Comment thread crates/agent-core/src/lib.rs
Comment thread crates/daemon/src/lib.rs Outdated
Comment thread crates/daemon/src/lib.rs Outdated
@al2wang
al2wang force-pushed the alex/verified-self-evolving-experience branch from 06c543a to 655a9a6 Compare October 1, 2026 01:51
al2wang added 11 commits October 1, 2026 15:09
An agent that corrects itself inside a turn currently leaves nothing
behind: the recovery is in the trace, and the next turn starts from the
same footing.

This adds the durable half of verified experience memory. An experience
record holds the lesson, the bounded evidence it was derived from, the
actor and project it belongs to, its lifecycle status and the decision
that last moved it; an evaluation record holds the immutable outcome of
assessing one. Records are scoped like every other row here, so one
actor's memory is never visible to another, and a record starts as a
quarantined candidate that nothing retrieves.

Two migrations follow main's current high-water mark, 0052.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Verified experience memory is off unless a deployment asks for it, and
promotion is manual unless it asks for that too. Both settings are
validated at load time with the rest of the daemon configuration, so an
unknown value fails the start instead of silently disabling a control.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Experience must be derived from what the agent actually did, not from
the prose it produced. The execution loop now keeps a bounded corrective
trace: the verifier that failed, the edits that followed and the same
verifier passing afterwards, with every captured string length-bounded
and the raw arguments kept as a digest.

The context engine gains the item kind those experiences enter a prompt
under, so a retrieved lesson is always distinguishable from source
evidence and from instructions.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
This is the working half of verified experience memory, behind the
configuration added earlier and off by default.

After a turn completes, the daemon extracts the bounded corrective
evidence the execution loop recorded and stores a quarantined candidate.
A separate, tool-free model call distils that evidence into one short
reusable lesson and the condition it applies under; the distiller sees
only the bounded evidence object, never the workspace, the trace or the
model history, and its output is length-bounded, screened for secrets,
repository-specific references and unsafe advice, and falls back to a
deterministic evidence-derived lesson when it fails.

Promotion is explicit. A candidate becomes approved only through a
scope-checked decision by the owning actor, and under evaluated
promotion only when an eligible evaluation is on record: evaluation
evidence is immutable, strictly validated, pre-registered at five
repeats per arm, and its gates are recomputed by the daemon from raw
counts rather than trusted from the submission. The daemon records and
gates; it never runs evaluation jobs.

Retrieval is narrow. Only approved, unexpired records owned by the same
actor for the same project are read, and only in verified mode. They
enter the context as clearly delineated, advisory, derived-untrusted
items that never outrank the user, the system rules, policy or tool
results, and every retrieval is audited as an event.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Evaluated promotion required a human decision for every candidate, even
when the evidence already satisfied every pre-registered gate. A
deployment that trusts its evaluation protocol had no way to say so.

Automatic promotion is a third, opt-in setting. Under it the daemon
promotes a candidate only when an evaluation it already accepted is
eligible on its own recomputed gates, and it does so transactionally:
the decision, the audit event and the evaluation binding are written
together or not at all, so a crash cannot leave an experience approved
without the evidence that approved it. Manual and evaluated promotion
are unchanged, and nothing promotes without evidence under any setting.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Distillation runs after a turn completes, as best-effort background
work. A graceful shutdown cancelled that work at an arbitrary point, so
a candidate whose evidence had already been extracted could vanish
without ever being written, and nothing in the logs said it had.

Post-turn experience work is now started through a registry the server
drains on a graceful shutdown, bounded by the distillation deadline plus
the same few seconds the rest of the drain allows. Once shutdown has
begun the registry refuses new work instead of leaking it. A forced
termination still loses best-effort work, as it always did; a graceful
one no longer does.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Sessions now carry a mode, and Chat refuses a workspace. The experience
lifecycle tests all work against a workspace, so they say Work
explicitly rather than leaning on a default.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
The corrective trace read a top-level `path` from an editing call, which
is the legacy single-file form. The editing tools now send `files[]` for
Lines and Text edits and `patch` with `revisions` for Patch edits, so a
perfectly normal repair recorded no edit at all, and
verifier-fail -> edit -> verifier-pass never became a recovery segment.

The observation boundary now reads the runtime's own report rather than
the request: a successful call records the paths its result names, and a
batch that stops part-way records exactly the files the error says were
applied. Paths that were asked for but not written are recorded as
unsuccessful, never as writes; a failure that names no applied file
records no write at all. Paths are deduplicated in a deterministic
order and stay under the same bounds as the rest of the captured
evidence.

Tests cover the current `files[]` form, the Patch form, the legacy
single path, a partial batch, an opaque failure, and the end-to-end
case: a verifier failure, a current-format edit and the same verifier
passing now yields corrective evidence naming the edited file.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
A file-protection change is a context reset: `collect_context_items`
refuses cached instructions, memory and editor diagnostics across it,
because content acquired before the change has no trustworthy
provenance afterwards. Verified experience memory is content of exactly
that kind, and retrieval did not share the boundary: an approved row
kept re-entering the prompt after the reset, carrying whatever it had
learned from the files that are now protected.

Retrieval now applies the same boundary the rest of the history does. A
record acquired at or before the reset is not offered; one acquired
after it is, so ordinary use after a protection change keeps working.
No new privacy model is introduced, and nothing about file protection is
weakened: the existing cutoff is simply respected in one more place.

The reviewer's reproduction is a test: an approved Experience carrying a
marker reaches the provider before the reset, does not reach it
afterwards, no retrieval event claims otherwise for that turn, and an
Experience acquired after the reset is retrieved normally.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
Distillation received the raw provider, so the one model call the
mechanism makes on its own behalf skipped the checks every other call
goes through: the current file protections were not applied and the
dispatch left no trace in the privacy record.

It now goes through the same observed provider as the agent's own calls,
under its own purpose, `experience_distillation`. Because the provider
checks at stream time, the boundary that applies is the one in force
when the background task dispatches - not the one that held when the
turn ended.

Refusal is fail-closed. If protection moved after the turn, the evidence
is not distilled, not screened and not stored at all: the deterministic
fallback lesson would otherwise carry the same material past the reset
that refused the dispatch. The user's turn is untouched either way,
since this work only ever ran after the turn completed.

Tests: a permitted distillation dispatches and stores its distilled
lesson under the right purpose; a protection change before dispatch
leaves the raw provider with no distillation request, persists no
lesson, and leaves the completed turn exactly as it was.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
A recovery can straddle the tool-call-limit pause: the verifier fails, the edit
lands, the budget runs out, the user resumes, and the same verifier passes. The
resume path started the new runner with an empty corrective trace, so the turn
ended holding only the final success. Extraction needs the failure and the edit
that preceded it, so the episode produced no Experience at all.

The checkpoint already carries the trace, exactly as it does for an approval or
a question resume; the tool-call-limit resume now hands that trace to the
resumed runner instead of a fresh one. Nothing about the pause, the question,
the budget or the resume semantics changes.

The regression drives the whole path through the API: one verifier run fails,
an edit creates the file it checks, a batch trips the limit, the user resumes,
the same verifier passes, and the completed turn must still yield evidence
naming the pre-pause failure and the edited path. It fails on the previous
behaviour with only the post-pause success on record.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
@al2wang
al2wang force-pushed the alex/verified-self-evolving-experience branch from 0a2844a to b2c964c Compare October 1, 2026 19:13
al2wang added 10 commits October 2, 2026 17:03
Main threaded `transient_tool_media` through the resume and turn-execution
signatures while this branch threaded `corrective_trace` through the same ones,
so every conflict is the same shape: keep both fields, in both the constructions
and the destructuring, and keep main's post-resume media discard. The
distillation request carries no tool media — it sends the sealed evidence object
and nothing else.

No behaviour of either side is changed: a resumed turn still continues its
corrective observations, and a resumed approval still discards the cached
transient media once the resume succeeds.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
The same integration point as the request itself: this test asserts that the
observed provider refuses after a protection reset, and its probe request now
carries the empty tool-media map main's ModelRequest requires.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
An approved tool call executes out of band, after the agent loop has
already paused and returned its checkpoint, so the loop never sees the
result and never observes it. A recovery whose verifier and edit both
went through approval therefore left an empty corrective trace and no
experience evidence at all, which is exactly the shape a human-supervised
session has.

The resume now observes that one call itself, from the arguments the
checkpoint recorded when the approval was requested and from the outcome
the executor actually produced:

* The checkpoint carries the approved arguments, so the observation is
  the paused turn's own call rather than whatever a later record holds.
* The checkpoint also names its tool call, and a resolution for any other
  call of the same turn no longer resumes it. A turn can leave more than
  one call awaiting approval, and delivering the wrong result under the
  checkpointed model call id would both mislead the model and observe the
  wrong command.
* The observation follows the executor's report, not the message the
  model receives: expired transient media is reported to the model as a
  retryable error while the call itself succeeded.
* A denial executes nothing and is not observed. A rejected verifier is
  not a verifier failure and a rejected edit is not an edit, so neither
  can become the failure half or the recovery half of evidence.
* The trace mutated is the decoded checkpoint's own copy, so a resume
  that somehow ran twice would still append exactly once.

Three regressions fail before this change: an approved
failure-edit-pass recovery now yields evidence with exactly one
observation per approved call; a denied verifier followed by a real edit
and a real pass stays unevidenced; and an approved edit that the executor
rejected is observed as not written and never joins the edited paths.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
A result too large to inline is replaced by a handle to a stored
artifact, and a handle carries no exit code. The corrective trace
classified the handle instead of the result it stands for, and an absent
exit code reads as a failing one, so every externalised success was
observed as a verifier failure. A large success, an edit, and a small
success of the same command therefore looked exactly like a repaired
failure, and the extractor had no way to tell that nothing had ever
failed.

The raw result is now classified before it is externalised:
`trace_classification_fields` names the parts of a result that
`CorrectiveTrace::observe` reads -- an exit code, an error string, and
the paths an edit reports writing -- and the handle carries those
forward. Captured output and file contents stay in the artifact; the
handle gains only bounded, non-content fields, and the model can now
also tell whether a large command succeeded.

The agent-core unit test shows the classification of a handle built this
way is identical to the classification of the result it replaced, and
that no edited content travels with it. The daemon regression runs a
verifier whose output is too large to inline, shrinks the file it reads,
and runs the same verifier again: before this change the trace held a
failure, an edit and a pass, which is admissible evidence for a recovery
that never happened; now both runs are successes and nothing is
extracted.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
… under

The file-protection check that admitted a trajectory ran once, at the top
of the recording path, and nothing checked again before the candidate was
written. Two windows followed from that.

A reset that landed while the distiller's request was in flight left the
dispatch admitted and the lesson distilled from material the reset had
since withdrawn, and that lesson was persisted. A reset that landed
between the admission check and the dispatch made the observed provider
refuse the request -- and a refused distillation fell back to the
deterministic lesson, which is built from the same evidence, so the
material the reset refused to send was stored anyway.

Evidence is now bound to the boundary it was admitted under. The
recording path stamps the boundary -- the protection revision and its
cutoff -- onto the evidence before anything is measured or sent, and
`record_experience_evidence` takes it as an argument, so a caller cannot
omit it. Immediately before the candidate is written, the boundary in
force is read again and must still be that one. Revisions only increase,
so an inequality is a reset and not a transient read.

An invalidated trajectory is discarded, never downgraded: the fallback
lesson is not stored either, because it carries the same evidence. The
revalidation is a point-in-time read, so no lock is held across the
remote request and a reset still never waits on a model dispatch. The
budget measures the widest boundary a record can carry rather than the
one in force, so how many resets an actor has made cannot change what
fits.

Three regressions fail before this change: a reset mid-request discards
the distilled lesson it produced; the same reset followed by a failed
request stores no fallback lesson either; and a reset between admission
and dispatch leaves nothing persisted and nothing dispatched.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
An evaluated-mode approval selected the newest evaluation, validated it,
and then transitioned the candidate's status in a separate statement. An
evaluation submitted between those two steps superseded the evidence the
approval was granted on, so an older pass could mask a newer failure --
which is the one thing selecting the newest evaluation exists to prevent.

The decision is now one `BEGIN IMMEDIATE` transaction in storage: the
candidate check, the newest-evaluation selection, the named-id and
eligibility validation, the durable binding and the status transition.
The handler still previews the decision without a write transaction, so a
request that is already refusable is refused without one, but the preview
is advisory and the transaction decides. Both render their refusals
through the same function, so they cannot explain themselves differently.

The approval event named the evaluation, but it is published after the
decision commits: when that publication failed, an approved -- and so
retrievable -- lesson was left with nothing on record saying what
approved it. `experiences.approval_evaluation_id` now carries that
binding, written in the same transaction as the transition by both the
explicit approval and automatic promotion, so no evidence-gated approval
can be read without the evidence that granted it.

Two regressions fail before this change. A barrier holds an approval
between its preview and its decision while a regressed re-run lands in
exactly that window: the approval is refused as superseded, the record
stays a candidate, and nothing is announced -- before, it was approved
with a 200. With the binding dropped from the transition, an approval
whose audit event fails leaves an approved record with no binding and no
retrievable lesson that explains itself.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
"Only a graceful shutdown is drained" reads as a promise that a graceful
shutdown loses nothing. It does not: a task still pending when the
twenty-second bound expires is abandoned, along with the candidate it
would have recorded, and the only record is the log line the shutdown
writes. What the bound guarantees is that the loss is bounded and
visible.

Whether to replace the bound with durable, resumable work is a separate
maintainer decision; this change only stops the documentation from
claiming more than the code does.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
`clippy -D warnings` rejects the decision enum: the approved variant
carries both records and the refusals carry almost nothing, so the
records set the size of every variant. The two records are boxed.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
The daemon stores a turn's completed status before it publishes the
turn's usage and terminal events. The helper returned as soon as the
stored status read completed, so every test that then reads the event
trail could race the publication: under six concurrent runs of the suite
this failed oversized_evidence_never_reaches_the_distiller,
distilled_experience_is_quarantined_until_approved_then_retrieved_with_audit
and distillation_provider_error_and_timeout_fall_back_without_failing_turns,
each with a usage or event assertion that read a trail the daemon had not
finished writing.

The helper now waits for the stored status and for the turn's own
terminal event, and its bound is raised to thirty seconds so a loaded
runner does not turn a slow turn into a failure. This is the same repair
already made on alex/experience-distillation as 704f8c3, which this
branch predates.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
An approval, a rejection and an evaluation each leave an audit record,
and each of those records was published after the decision's transaction
had committed. When that publication failed, an approved -- and
therefore retrievable -- lesson was left with nothing on record saying
what approved it. The binding added earlier fixed what the record would
have said; it did not make the record exist.

The record now commits with the decision. The daemon sequences and chains
the event before the transaction and hands it in; storage writes it with
`insert_audit_event` inside the same `BEGIN IMMEDIATE` transaction as the
candidate checks, the evidence binding and the status transition. This
covers the evaluated approval, automatic promotion -- which owes two
records, at consecutive sequence numbers -- and an ordinary manual
decision.

Three properties follow, and each is tested:

* The record cannot describe a decision that was not made. It names the
  evidence, and the transaction refuses unless the evidence it selects is
  that one. One timestamp serves the row and the record.
* A rollback leaves neither a gap nor an orphan in the hash chain,
  because the event ordering is held across the transaction and released
  without advancing when the transaction fails.
* Subscribers are notified after the commit, and that notification is a
  broadcast whose failure is ignored by construction, so a decision that
  committed correctly is never undone because nobody was listening.

A restart reads the sequence and the chain head back from the database,
so a decision that committed before the process ended is recovered with
its own record and is refused rather than decided again.

No pending state and no queue: with one transaction there is nothing to
recover or finalise later, so there is no window in which an approved row
is unusable, nothing to resurrect, and no second decision to make
idempotent.

The approval-binding migration moves to 0057. The shared-skill layer
already holds 0055 and 0056 in development databases; two migrations at
one version is not a build error but makes every `Store::open` fail with
SQLite 1555, and renumbering that layer instead gives an applied database
`VersionMismatch(55)`. A database that applied 0057 first accepts 0055
and 0056 afterwards, which is measured.

Four regressions fail before this change: a forced failure of the audit
insert inside the transaction rolls the approval back with nothing
retrievable; a committed approval survives a restart with its original
record and refuses the retry; automatic promotion commits both records or
neither; and a manual approval without its record does not take effect.

The documentation and its mirror now also say what the bounded post-turn
drain guarantees: the wait is bounded, not the loss.

AI-assisted drafting was used; the resulting changes were reviewed and verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
@al2wang

al2wang commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator Author

On “8. Graceful post-turn draining”

Thanks — I agree that this is bounded best-effort work rather than a durability guarantee. The
implementation still gives graceful shutdown a bounded opportunity to finish in-flight distillation
(up to twenty seconds: the fifteen-second distillation deadline plus the server's five-second
drain), then aborts remaining tasks; the documentation now states explicitly that this reduces but
does not eliminate candidate loss, and that no persistent recovery queue is introduced in this PR.
Regressions: graceful_shutdown_waits_for_an_in_flight_distillation_and_keeps_the_candidate,
no_experience_task_is_accepted_once_shutdown_began,
failed_experience_tasks_are_reaped_and_shutdown_stays_healthy,
finished_experience_tasks_never_delay_shutdown. Please let me know whether you consider the
bounded drain itself a remaining merge blocker or prefer immediate cancellation as the product
contract.

al2wang and others added 3 commits October 11, 2026 18:09
…ible

The final file-protection check before an Experience candidate is written
was a point-in-time read, and the insert was a separate operation. A
protection reset could commit between the two; the candidate then received
a post-reset created_at, so evidence from the old boundary passed the
timestamp-based retrieval filter as if it were fresh.

Every protection replacement commits under the per-owner protection gate's
exclusive guard (Store::replace_file_protection is the only writer). The
recording path now holds that gate's shared guard from the final boundary
check until the insert has committed, and releases it before the
experience.created fan-out. A reset therefore either commits first, and the
check discards the evidence, or waits until the row is durable. The guard
never spans the distiller's request or stream. Both the distilled lesson and
the deterministic fallback go through this one critical section.

A reset's cutoff now also covers this actor's committed experiences, as it
already covers turns, inputs, tasks and goals, so a row that won the race
stays behind the new boundary even if the wall clock stepped backwards.

Regressions use test-only pause points rather than sleeps: a reset landing
before the final check discards both distilled and fallback lessons, and a
reset requested while the insert is paused after the check is shown queued
on the gate, commits only after the insert, and leaves the approved
candidate out of a later scripted provider request, while a post-reset
experience is still retrieved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: al2wang <lightever131@gmail.com>
A completed call was turned into AgentToolResult::Failed when publishing
its event or externalising its large result failed afterwards, and the
agent loop then recorded that delivery error as a failed verifier. A pass,
a delivery failure, an edit and a pass of the same command looked like a
repaired failure, and could become an experience candidate.

The corrective trace now reads only a separate, bounded execution outcome
(agent-core ToolExecution: NotExecuted, Unknown, Completed with the trace
classification fields and the bounded failure excerpt, or Failed with the
executor's own error). AgentToolExecutor gains execute_prepared_observed,
which returns it next to the delivered result; the default keeps the old
reading for executors whose delivery cannot fail after a call ran. The
daemon fixes the outcome from the execution service's report before it
publishes or externalises anything, keeps it through every later delivery
error, records nothing for vetoed, refused or approval-pending calls, and
records nothing when submission failed without a report. The model still
sees "tool event publication failed" or the artifact error.

Approval resume observes the same outcome. When publishing an approval's
outcome fails, the decision and the call have already happened, so the
paused turn now resumes with the delivery error as the model-facing result
and the real outcome in the trace, instead of being stranded; the HTTP
caller still gets the error.

Regressions inject one-shot publication, externalisation and approval
publication faults through the real runner and executor: a passing
verifier stays a pass and yields no candidate; a real failure keeps its
own excerpt and its repair is still evidence; an unexecuted verifier and
a denial leave nothing; written and partially written edits keep exactly
the paths they wrote.

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed run_command carries no exit status; its status reaches the daemon
only in a completed result. Almost every failed command never started:
argument parsing, file protection, program resolution, executable and
shebang trust, staging, sandbox setup and the spawn itself all return
ToolCallOutcome::Failed before a process exists, and the remaining wait,
capture and drain errors leave the status unknown. Recording such an error
as a verifier failure turned a spawn failure, an edit and the same verifier
passing into a recovery that never happened.

A failed run_command is now an unknown execution and leaves no
observation, with one exception: the exact "process timed out" error,
which the runtime raises only after killing a process that was running,
stays a verifier failure. Failed edits keep their error and so the files a
stopped batch wrote. The execution crate is unchanged.

The regression fails the first spawn through a one-shot platform runtime,
exactly as the native runtime reports a spawn error, then edits and runs
the identical verifier for real: the turn completes, the model still sees
the spawn error, and no failure or recovery is recorded. A unit test pins
the timeout message to the error chain that produces it.

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
al2wang added a commit that referenced this pull request Oct 11, 2026
Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
al2wang added a commit that referenced this pull request Oct 11, 2026
…n branch

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
al2wang added a commit that referenced this pull request Oct 11, 2026
Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@al2wang

al2wang commented Oct 11, 2026

Copy link
Copy Markdown
Collaborator Author

@weiddeng Thanks, both remaining gaps were reproducible. Replying to your 2026-10-11 review of cd44c18.

1. Privacy persistence: fixed in 2b37818

Protection is owned by one writer: Store::replace_file_protection. Every reset path goes through it: the HTTP POST/DELETE on /v1/sessions/{id}/privacy/protections and the chat command. It takes the per-owner in-process gate protection_gate(scope) for writing, inside one Store per daemon, so the existing gate is the right serialization domain.

  • The critical section: record_experience_evidence now holds that gate for reading from the final check of the turn's original boundary through the candidate INSERT commit. It releases the gate before experience.created is published.
  • What stays outside: distillation, stream consumption and event fan-out. The distilled and deterministic-fallback paths share the same block.
  • Cutoff: replace_file_protection now also places its cutoff after the newest committed experience for that owner. A candidate that wins the race therefore always lands before the cutoff, even if the wall clock steps back.
  • No migration was needed, and created_at is never adjusted.

The regression uses test-only pause points and barrier acknowledgments, never sleeps. It pauses after the final check and before the INSERT, starts a real reset in a separate task, and asserts:

  • the reset is queued on the gate and the revision is unchanged during the pause;
  • once released, the insert commits and then the reset commits;
  • after explicit approval, a later scripted-provider request does not contain the old marker;
  • a control experience created after the reset does appear.

The reset-first case discards both the distilled and the fallback lesson.

On cd44c18, both critical-section variants leaked old evidence into a later request, with candidate created_at 21:36:18.874 against cutoff 21:36:18.873.

Tests:

  • a_reset_before_the_final_check_discards_a_distilled_lesson
  • a_reset_before_the_final_check_discards_the_fallback_lesson
  • a_reset_cannot_commit_between_the_final_check_and_a_distilled_insert
  • a_reset_cannot_commit_between_the_final_check_and_a_fallback_insert
  • protection::tests::file_protection_activation_covers_committed_experiences

2. Execution versus delivery: fixed in 98bae0a and 6817ea3

Corrective observations now come from the executor's bounded outcome, recorded as soon as the execution service returns. That is before event publication or artifact externalization, so delivery failures can't change it.

  • Recording: agent core observes it once per call (execute_prepared_observed, ToolExecution).
  • What the model sees: the model still gets the delivery error.
  • What the trace keeps: only the classification fields and the bounded failure excerpt. No raw output, and the trace never reads the exit status from an artifact handle.

Behaviour:

  • exit-zero verifier → publication or externalization failure → edit → exit-zero verifier: no recovery and no candidate, and the turn completes;
  • a genuine nonzero failure → delivery failure → repair → exit zero: still learnable;
  • denied, awaiting-approval and never-run calls are unobserved. A failed run_command counts as a failure only when the process provably ran: the timeout raised after the process is killed. A spawn failure is not a failure;
  • edits keep only actually written paths;
  • approval resume observes the real outcome exactly once.

One behaviour change in this scope: if publishing tool.completed or approval.resolved fails in resolve_approval, the paused turn now resumes with that error as the model-facing result. Previously it stayed in AwaitingApproval indefinitely.

On cd44c18, the new tests failed with records such as Verifier{succeeded: false, failure_excerpt: "tool event publication failed"}, Edit, Verifier{succeeded: true}.

Tests:

  • a_publication_failure_after_a_passing_verifier_is_never_recovery_evidence
  • an_externalisation_failure_after_a_passing_verifier_is_never_recovery_evidence
  • a_real_verifier_failure_survives_a_delivery_failure_and_its_repair_is_evidence
  • an_unexecuted_verifier_is_never_the_failure_half_of_a_recovery
  • a_verifier_that_never_spawned_is_never_the_failure_half_of_a_recovery
  • an_approved_verifier_keeps_its_outcome_once_through_a_publication_failure
  • a_written_edit_keeps_its_paths_through_a_publication_failure
  • a_partial_edit_keeps_only_its_written_paths_through_a_publication_failure
  • agent core: the_trace_follows_the_reported_execution_not_the_delivered_result, a_completed_execution_keeps_only_bounded_classification_fields
  • unit: a_failed_command_is_a_verifier_failure_only_when_its_process_ran

Validation

Local, on a Linux compute node with the command sandbox (bubblewrap) available:

  • cargo fmt --all -- --check: clean.
  • cargo clippy --locked --workspace --all-targets -- -D warnings: clean.
  • cargo test --locked --workspace: 932 passed, 0 failed.
  • Source checks: community tree, secret history scan, doc links, DCO, community candidate, verification suite and the docs-site check all pass.

Current head: 6817ea305ebd2f69b0958da2d75fb1323f1d8dfe.

GitHub CI: run 38181874312 passed on this head (linux, macos, source-gate, docs, web, vscode, jetbrains, benchmarks, codeql); windows was skipped by the path plan.

The pre-existing, intermittent fast_durable_question_resume_keeps_tool_limit_fail_closed (inherited from main) passed in these runs and is unchanged.

@al2wang
al2wang requested a review from weiddeng October 11, 2026 23:08
A failed run_command carries no exit status, and the execution path gives
no typed signal that its process ever started: a spawn error, a sandbox
setup error and a timeout kill all arrive as the same untyped failure.
The previous change still recorded a timeout as a verifier failure by
matching its exact error message. Error text is not evidence of
execution, so a failed command is now always unknown and leaves no
observation; a timeout, an edit and the same verifier passing are no
recovery. A command that ran and exited nonzero is a completed result
with an exit status and stays a verifier failure, whatever its output
says.

Adding a typed timeout signal would need a new runtime error variant
carried through the tool runtime and stored on the tool call, so it is
left out.

Regressions cover the approval contract through the real HTTP path:
- A tool.completed, approval.resolved or tool.denied publication failure
  happens after the decision and the outcome are committed. It resumes
  the turn once with the delivery error and observes the outcome once.
- Repeating the resolution, or repeating it after a restart that reloads
  the database, neither executes, observes nor resumes again.
- A durable write failure, forced with a trigger on the approval decision
  or on the executed call's outcome, never resumes the turn and observes
  nothing.
sqlx becomes a daemon dev-dependency for that trigger.

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
weiddeng
weiddeng previously approved these changes Oct 12, 2026

@weiddeng weiddeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed current head 04a7094 against the two requested invariants. The privacy-boundary check and candidate persistence are now serialized against protection replacement with deterministic race coverage, and corrective tracing now follows the actual bounded execution outcome rather than later event or artifact delivery failures. No remaining blockers within this review scope; required CI should continue to gate merge.

al2wang added a commit that referenced this pull request Oct 12, 2026
…esume contract tests) into the Skill Shop branch

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
al2wang added a commit that referenced this pull request Oct 12, 2026
…anch

Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@al2wang

al2wang commented Oct 12, 2026

Copy link
Copy Markdown
Collaborator Author

@weiddeng One narrow follow-up on item 2, pushed as 04a7094 on top of 6817ea3. Nothing else changed.

  • Error text never decides whether a command ran. The previous head still treated the exact "process timed out" error as a verifier failure. The execution path has no typed signal that the process ran, so a failed run_command without an exit status (spawn error, setup error, timeout) now leaves no observation.
    • A shell that really ran and exited nonzero stays a failure, even when its output says "not found".
    • Tests: a_verifier_killed_at_its_timeout_leaves_no_observation, a_shell_that_ran_and_reported_not_found_is_still_a_verifier_failure, and the updated a_failed_command_is_a_verifier_failure_only_when_its_process_ran.
  • Approval resume after a delivery failure. This is the behaviour change I disclosed in my previous reply. Its contract is now tested, not just described:
    • The approval decision (UPDATE ... WHERE status='pending'), the running mark and the execution outcome are all durably committed before any publication. resolve_approval returns on any of those errors before it can resume.
    • Only a later tool.completed/approval.resolved publication failure resumes the turn, and the model still sees that error.
    • Tests:
      • an_approval_delivery_failure_resumes_once_and_never_re_executes: one execution, one observation, one resume; a duplicate resolution is refused; a restart reloads a consistent checkpoint without re-executing;
      • a_failed_durable_approval_or_outcome_write_never_resumes_the_turn: forced SQLite write failures; adds sqlx as a daemon dev-dependency only, an existing workspace crate;
      • a_denied_approval_whose_publication_fails_resumes_unobserved.
    • Not covered: a process crash between the durable commit and the in-process resume leaves the turn paused, without re-execution. That was already the case before this change.

Local validation on 04a7094, on a Linux node with the command sandbox: fmt and clippy clean; cargo test --locked --workspace 937 passed, 0 failed; source, secret-history, docs and DCO checks pass.

GitHub CI: run 38189221818 passed on 04a70949132d88cc26f94c5b4d422d2760e68552; windows was skipped by the path plan.

Your re-review request from my previous reply still stands; I'm not re-requesting.

al2wang added a commit that referenced this pull request Oct 12, 2026
Signed-off-by: al2wang <lightever131@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
al2wang added a commit that referenced this pull request Oct 12, 2026
The Skill Shop branch was based on #100 before its review. Twenty-two
review commits moved underneath it, and this merge brings them in.

Conflicts, resolved:

* `AppState`'s builder: the shop's `with_skill_shop` and
  `with_skill_shop_registry` and the review's test-only
  `experience_decision_barrier` are independent additions; both kept.
* `maybe_resume_turn`: the shop carried an older variant of the
  approval-resume observation. The reviewed one supersedes it -- it
  classifies the approved call's execution before anything is
  externalised, guards the checkpoint's tool call id, takes the
  approved arguments from the checkpoint rather than from the message
  history, and carries transient tool media -- so the function is now
  byte-identical to #100's, and the shop's orphaned
  `checkpointed_tool_call_arguments` helper is gone.

`skills.rs`, ported to the reviewed APIs:

* publishing cites the evaluation the approval was bound to
  (`approval_evaluation_id`), falling back to the newest eligible one
  for a record approved before bindings existed, and refuses when
  neither is eligible;
* `ExperienceEvidence` gained a privacy boundary;
* the test helpers write evaluations and decisions through the audit
  path, as production does, and share the test's own `AppState` so one
  sequencer owns the chain -- two states seeded at zero on one store
  collided on `audit_events.sequence`.

`Cargo.lock` is refreshed minimally: the workspace now carries two
`sha2` versions and two entries needed the version to disambiguate,
which is what `cargo ... --locked` had been failing on in CI.

Gate on the merged tree: `cargo fmt --all -- --check`,
`cargo clippy --locked --workspace --all-targets -- -D warnings`, and
the daemon's experience (15) and skill (19) suites pass. Three
approval-resume tests fail here exactly as they do on pristine #100 on
this host -- they need a sandbox-capable runner, and #100's CI passes
them.

AI-assisted drafting was used; the resulting changes were reviewed and
verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
al2wang added a commit that referenced this pull request Oct 12, 2026
Brings #100's review, already merged into the Skill Shop branch, down one
level. Two conflicts, both additive: `AppState`'s builder keeps this
branch's `with_skill_shop_lineage` beside the review's test-only
decision barrier, and the empty-evidence literal the review extended had
already been removed here, so this branch's side stands.

Two test helpers that still took a bare store now take the test's
`AppState`, for the reason recorded upstream: one audit sequencer per
store.

Gate on the merged tree: `cargo fmt --all -- --check`,
`cargo clippy --locked --workspace --all-targets -- -D warnings`, daemon
experience (15) and skill (22) suites pass. The same three
approval-resume tests fail on this host as on pristine #100, and pass in
CI.

AI-assisted drafting was used; the resulting changes were reviewed and
verified by the author.

Signed-off-by: al2wang <lightever131@gmail.com>
@weiddeng

Copy link
Copy Markdown
Collaborator

Could we keep PR #100 as the standalone Experience change that was already reviewed and restore its head to 04a70949?

I verified that this commit still merges cleanly into current main. The current head (5bca7891) also absorbs #101, #109, and #111, even though those follow-up areas remain explicitly out of scope in this PR's description. Merging the current head would therefore merge the whole stack rather than just the reviewed PR #100.

Please also leave out 3d4e8b4; its approval-resolution handoff still has an unresolved race. Once the branch is back at 04a70949 and required CI is green, I can promptly re-approve that exact scope for merge.

@al2wang

al2wang commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

Thanks, that makes sense. The child PRs got propagated into #100 unintentionally while I was updating the stacked branches. I’ll restore #100 exactly to 04a70949, leave out 3d4e8b4, and keep #101 → #109 → #111 as separate follow-ups. I’ll rerun the required CI on that exact scope and ping you once it’s green. Thanks for catching this.

@al2wang

al2wang commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator Author

@weiddeng Thanks for catching this. #100 is restored exactly to 04a70949132d88cc26f94c5b4d422d2760e68552, with 3d4e8b4 excluded. The PR is back to 26 commits / 16 files against current main, and #101, #109 and #111 are again separate follow-ups.

Required CI on the restored head (run 38314503799): every job passed except linux, which stopped at Dependency and license policy.

One disclosure for this scope: two approval tests added in 04a7094 (an_approval_delivery_failure_resumes_once_and_never_re_executes, a_denied_approval_whose_publication_fails_resumes_unobserved) can time out under heavy CI load. The cause is the pre-existing early-decision race, which 04a7094 does not change. In this run they passed on macOS; linux's test step was skipped after the policy failure, so linux has not run them on this base yet. The real fix stays out of #100 and will come as a separate PR that also closes the time-of-check gap you pointed out in 3d4e8b4.

Edited: corrected the CI statement above. Linux tests did not run in this run.

@al2wang

al2wang commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Closing and immediately reopening only to trigger a fresh CI run against current main (now including #126) without changing the head commit 04a70949.

@al2wang al2wang closed this Oct 13, 2026
@al2wang al2wang reopened this Oct 13, 2026
@al2wang

al2wang commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks, #126 has landed, and #100 remains exactly at 04a70949132d88cc26f94c5b4d422d2760e68552, with 3d4e8b4 excluded.

Fresh required CI against current main is green: run 38329066046, tested integration c7ff1df37e75807e50209a813a19e199e4f2f95c. Linux reached and ran the full Rust suite this time: 937 passed, 0 failed.

#101, #109 and #111 remain outside this PR.

Re-requesting review of exactly 04a70949.

@al2wang
al2wang requested a review from weiddeng October 13, 2026 01:08

@weiddeng weiddeng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-reviewed the restored head 04a70949 against the previously agreed merge scope. The standalone Experience change excludes #101, #109, #111, and 3d4e8b4; the privacy-boundary serialization and execution-result tracing fixes remain intact. Fresh required CI against current main is fully green. No remaining blockers within this review scope.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants