Repository navigation
feat: shared log for the property lenses (#1271) - #1338
mrthankyou wants to merge 7 commits into
Conversation
A prompt that names the new {{shared_log_command}} variable gets the run's
ultrafuzz-shared-log command: `post` appends an entry, `read` prints every
entry and `read --new` only those since the task's last read. The command
stamps each entry with the task, retry count and time from the workspace it
runs in, records every read, and keeps one log per workflow, so each fork
starts an empty one. run, resume and fork install it only when a rendered
prompt names it, so a fork whose refreshed prompts name it gets a log without
relaunching, and runs whose prompts do not are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The eight built-in property-lens prompts now name the shared log command. Each lens is told that other lenses work on the same code from different angles, how to post and read, to read the log when it starts and again with `read --new` before it finalizes, and to treat entries as untrusted data. What to post is left to the lens. The fan-in prompt is unchanged. Refs #1271 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| const age = Date.now() - (fs.statSync(lock, { throwIfNoEntry: false })?.mtimeMs ?? Date.now()); | ||
| if (age > STALE_LOCK_MS) fs.rmSync(lock, { recursive: true, force: true }); |
There was a problem hiding this comment.
Stale-lock recovery can remove another writer's new lock. If two callers both observe an old .lock, the first can remove it and acquire a fresh lock before the second performs its already-decided removal. Both callers can then write at once and choose the same entry number. Later read --new calls can skip entries with duplicate numbers.
Use a lock implementation with safe ownership checks, and add a test with competing stale-lock recoveries.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/shared-log.ts
Line: 86-87
Comment:
**Recovery lets writers overlap**
Stale-lock recovery can remove another writer's new lock. If two callers both observe an old `.lock`, the first can remove it and acquire a fresh lock before the second performs its already-decided removal. Both callers can then write at once and choose the same entry number. Later `read --new` calls can skip entries with duplicate numbers.
Use a lock implementation with safe ownership checks, and add a test with competing stale-lock recoveries.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed. This code has since been replaced, so the comment is outdated.
- 1e05701: every lock now carries a unique owner token and is renamed into place whole, so a lock is never seen without an owner. A writer only releases a lock that still carries its own token.
- ba80da3: a stale lock is cleared by a single atomic rename to
.lock-stale-<owner token>. Every waiter that judged the same lock stale aims at that one name, and renaming onto a non-empty directory fails, so only the first rename moves anything. A lock taken since then is never moved. It's one step, so a waiter that dies mid-way can't leave the log locked (the follow-up in #4449394608). - e02483c: a writer releases by renaming its lock aside before deleting it, so
.lockis never briefly an empty directory that another waiter's lock could replace.
Tests in packages/runtime/test/shared-log.test.ts:
- waiters that all find a dead writer's lock clear it once and write one at a time
- a lock that isn't stale yet is left alone
- 10 rounds of 24 parallel posts with no crashes and unique entry numbers
Knip flagged SHARED_LOG_ROOT_PATH, sharedLogWrapper, SHARED_LOG_SCRIPT and sharedLogReferenced as unused exports; only installSharedLogIfReferenced and SHARED_LOG_COMMAND_PATH are used outside the module. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion (#1278) GitHub withdrew GHSA-ch52-4w7c-c8xp on 2026-10-28, so the registry no longer reports it and the advisory gate fails the exception as stale. The exception said to remove it once the advisory was withdrawn. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ale-lock recovery in the shared log
- Launch and resume now also install the command when a prompt template
snapshot under dynamic-prompt-templates/ names {{shared_log_command}}.
A dynamic task's prompt is rendered only after its source task finishes,
so before this a run whose only reference was in a dynamic task handed
its agents a command that did not exist.
- {{shared_log_command}} renders as one shell word, single-quoted when the
run path holds characters the shell treats specially, such as spaces.
- Each lock now carries its owner's token and is renamed into place whole.
Only one waiter can clear a stale lock (by claiming it from inside), and
only while it still carries the owner judged stale, so two waiters can no
longer both clear it and write at once. A writer releases only its own lock.
- Satisfy strict lint: no numbers in template literals, no non-null assertion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clearing a stale lock used to claim it by creating "reaping" inside it and then removing it. A waiter that died between those two steps left a claim nobody could clear, so every later post and read timed out. A waiter now renames the stale lock aside to .lock-stale-<owner token> in a single atomic rename, so there is no half-done state to abandon. Every waiter that judged the same lock stale aims at that one name, and renaming onto a non-empty directory fails, so only the first moves anything and a lock retaken since is never moved. The moved locks stay as a record of the crashed writers. A lock's age now counts from when it was taken, not from when its waiter staged it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A writer released its lock by deleting it in place, which removes the owner file before the directory. For that moment .lock was an empty directory, and on Linux a waiter's staged lock renamed onto an empty directory replaces it. The releasing writer's rmdir then failed on the new, non-empty lock and the post exited 1 (seen in CI), or it could delete the new owner's lock. The writer now renames its lock aside in one step and deletes it there, so .lock is never empty. A new test runs ten rounds of 24 parallel posts; it caught the old release in 3 of 4 runs, and a 360-post stress run that crashed 3 posts before passes cleanly three times over. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Considering that #1339 mentions v0.2 will not be backwards compatible, I am ok if we did not merge this. One goal of this PR was to determine if shared logs is a viable prompt request for sub-agents to rely on when doing parallel work and I think I've resolved that goal here. |
Summary
Closes #1271.
The eight property lenses run in parallel and never see each other's work until the fan-in merges their catalogs. This PR gives them a shared, append-only log so they can build on each other instead of rediscovering the same ground eight times.
{{shared_log_command}}variable gets anultrafuzz-shared-logcommand.post <text>(or text on stdin) appends an entry,readprints every entry, andread --newprints only entries since the task's last read. Each entry is stamped with the posting task, its retry count and the time, and every read is recorded. Each workflow (the launch and each fork) gets its own log under<run>/shared-log/<workflow run id>/.runandresumeinstall the command, reportingSHARED_LOG_INSTALLED, only when a rendered prompt names it, so runs whose prompts don't mention it are unchanged.read --newbefore it finalizes, and says to treat entries as untrusted data. What to post is left to the lens: no format, categories or size limits. The fan-in prompt is unchanged, so the merge step never sees the log.The section added to each lens prompt:
How we tested it
Targets
Two Solidity codebases built with Foundry, chosen to differ in size and domain:
Before testing, both were censored:
very-liquid-vaults shipped its own property list and three of the lenses' reference suites, so without censoring we would have measured copying, not deriving. Renaming also makes it harder for a model to recognise the project from training data. Storage layouts and every function, event and error signature were verified identical to the originals.
Setup
Results
Raw per-trial numbers
"Claims" are distinct claims after blind matching. Cost is the lens and fan-in work for one trial.
very-liquid-vaults
Wildcat (15-trial matching)
Summary
Caveats
Test plan
Run in a Linux container (Ubuntu, Node 24):
pnpm install --frozen-lockfile,pnpm -r build,pnpm typecheckpnpm docs:check@ultrafuzz/promptstests (render, including the new variable)shared-log.test.ts: 1,179 pass, 1 fail. The failure (generated Smithers pinned source proof counts hidden unreachable commits…) assumes git's default branch ismaster; the container defaults tomain. It passes with the global git config ignored and touches no code in this PR.@ultrafuzz/cliand@ultrafuzz/artifactstests🤖 Generated with Claude Code


Confidence Score: 5/5
No new blocking issue was found; the earlier stale-lock recovery concern remains non-blocking.Fix with agent prompt
Summary
Adds a shared log that property lenses can post to and read during a workflow.
http-cache-semantics.Reviews (5) · Last reviewed commit: "fix(runtime): release the shared log loc..." · Reviewed by Greptile