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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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

@memosrmemosr 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().consttmpPath=`${this.registryPath}.tmp`;

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

FileTemp 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-actionsgithub-actionsBot 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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: studioChanges to Agent Studio or harness applicationscontribution: incompleteRequired pull request information is incomplete or ambiguouscontributor: externalPull request author does not have write, maintain, or admin access to sapiom-jsneeds-triageAwaiting maintainer review and classificationreview: manualExternal pull request requires maintainer review before automationsize: smallReview size is at most 100 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@memosr