Skip to content

fix(harness): give the workflow registry's temp file a per-process name - #659

Open
memosr wants to merge 1 commit into
sapiom:mainfrom
memosr:fix/harness-workflow-registry-tmp-collision
Open

fix(harness): give the workflow registry's temp file a per-process name#659
memosr wants to merge 1 commit into
sapiom:mainfrom
memosr:fix/harness-workflow-registry-tmp-collision

Conversation

@memosr

@memosr memosr commented Aug 17, 2026

Copy link
Copy Markdown

Primary change type

  • Bug fix

Problem and motivation

WorkflowRegistry.persist() wrote to a fixed temp path:

// Mirrors the pattern used by SessionManager.persist().
const tmpPath = `${this.registryPath}.tmp`;

The comment is not accurate. Every other atomic writer in the package uses a per-process name:

File Temp name
core/session-manager.ts:1217 .tmp-${pid}-${seq}
core/workspace-context.ts:145 .tmp-${pid}-${uuid}
core/collector/store-retention.ts:127 -tmp-${pid}-${Date.now()}
core/record-archive.ts:420 -tmp-${pid}-${now}-${counter}

This was the only one left on a shared name.

writeQueue serializes writers within one instance, but the default registry path is machine-wide: expandHome(HARNESS_PATHS.workflows), i.e. ~/.sapiom/harness/workflows.json. A CLI and a desktop app, or two Studio servers for two projects, run two registries over that one file.

When two /api/workflows/connect or /scan calls overlap, both write into the same temp. Either one renames it away and the other fails, or the rename publishes a half-written file. ensureLoaded() swallows broken JSON with catch { this.workflows = [] }, so the visible symptom is connected workflows silently disappearing. The rename is atomic, but sharing the temp voids the guarantee the comment claims.

Summary and scope

Add a pid + uuid suffix to the temp name, and remove the temp file if the write or rename throws.

Out of scope: the other atomic writers are already correct and are not touched. The broader question of whether two harness instances should share one registry file at all is left alone; this only makes the existing atomic-write claim hold.

Related work

Related issue or discussion: N/A — direct PR, focused bug fix with a reproduction, under the direct-PR policy.

Validation

pnpm --filter @sapiom/harness exec vitest run src/core   — pass (1075/1075)
pnpm --filter @sapiom/harness typecheck                  — pass
pnpm --filter @sapiom/harness lint                       — pass (0 errors)
pnpm build                                               — pass

The new test fails on the unpatched code:

Error: ENOENT: no such file or directory, rename '.../workflows.json.tmp' -> '.../workflows.json'
  at WorkflowRegistry.persist src/core/workflow-registry.ts

I verified this by reverting only the temp-name line and re-running the file.

Tests and documentation

Tests: added a case where two registries over one path scan concurrently, asserting the persisted file is intact and no temp artefact is orphaned.

The existing C4 atomicity test asserted fs.access(${registryPath}.tmp) rejects. That would pass for free once the name changed, and would not catch a temp orphaned under a different name, so it now matches on the .tmp prefix across the directory instead.

Documentation: N/A, internal behavior only. The inaccurate code comment is corrected in place.

Compatibility and release impact

  • Breaking or externally visible changes: None. The temp file is an implementation detail and is renamed away in the same operation.
  • Changeset: Added (patch, @sapiom/harness).

Security

  • I have not included secrets, credentials, private data, or unsanitized logs.
  • This pull request does not publicly disclose a suspected vulnerability. This is a data-integrity race between the user's own processes, not a trust-boundary issue.

AI assistance

  • I used AI assistance and have described it below.

I used Claude (Claude Code and the Claude app). Claude surveyed the package for atomic writers and reported the inconsistency; I checked each of the four call sites myself and confirmed the registry path is machine-wide before accepting the finding. I directed the fix and the regression test, and Claude wrote them. I validated the test by reverting only the temp-name line and confirming the test fails with the ENOENT above, so it is a real regression test rather than one that passes either way.

Checklist

  • I read CONTRIBUTING.md, and this contribution follows the direct-PR or issue-first policy.
  • This pull request addresses one focused problem and contains no unrelated cleanup.
  • I added or updated tests, or explained above why tests are not applicable.
  • I ran the relevant build, typecheck, lint, and test commands, or explained any N/A checks above.
  • I updated documentation for user-facing changes, or marked it N/A above.
  • I added a Changeset for a published-package change, or explained why it is not applicable.
  • I can explain and maintain every submitted change, including any AI-assisted work.

persist() wrote to a fixed `${registryPath}.tmp`, with a comment claiming it mirrors SessionManager.persist(). It does not: that one uses .tmp-${pid}-${seq}, and so do workspace-context, store-retention and record-archive. This was the only atomic writer left on a shared temp name.

writeQueue serializes writers within one instance, but the default registry path is machine-wide (~/.sapiom/harness/workflows.json), so a CLI and a desktop app, or two Studio servers, run two registries over the same file. Concurrent writes then land in the same temp: one renames it away and the other fails, or worse, publishes a half-written file that load() swallows via catch { this.workflows = [] } - silent loss of connected workflows.

Add a pid + uuid suffix and clean the temp up on failure. The new test fails on the old code with ENOENT on rename and passes on the new one. The existing atomicity test asserted against the fixed temp name, which would have passed regardless once the name changed, so it now matches on the prefix instead.
@github-actions github-actions Bot added contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: small Review size is at most 100 changed lines area: studio Changes to Agent Studio or harness applications labels Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studio Changes to Agent Studio or harness applications contribution: incomplete Required pull request information is incomplete or ambiguous contributor: external Pull request author does not have write, maintain, or admin access to sapiom-js needs-triage Awaiting maintainer review and classification review: manual External pull request requires maintainer review before automation size: small Review size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant