test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude
, '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

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement) - #13899

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement
Aug 31, 2026
Merged

test(service-automation): pin that a node failing mid-resume strands the run (#13807 step 1 measurement)#13899
os-steve merged 1 commit into
mainfrom
claude/issue-13807-strand-reach-measurement

Conversation

@claude

@claudeclaudeBot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Part of #13807 — step 1 only (the measurement slice). No repair is included, deliberately.

The card's step 1 asks one question: does any workflow node failing mid-resume strand the run, or is stranded specific to the approval resume path? The card is explicit that the generalisation was a hypothesis, not its premise. This PR carries the instrument that answers it; the full written answer, with all four deliverables, is posted as a comment on #13807.

Verdict: general to the workflow engine, not approval-path-specific

AutomationEngine.resumeInternal consumes the suspension before it runs any downstream work:

await this.forgetSuspendedRun(run, 'resumed'); // packages/services/service-automation/src/engine.ts
...
await this.traverseNext(node, flow, variables, context, steps, signal?.branchLabel);

So a downstream node that throws throws with the pause already gone. The catch arm records the run failed and returns; nothing puts the suspension back. The word "stranded" never appears in the engine — it is prose in a plugin-approvals error message wrapped around this engine outcome.

What the new test measures

packages/services/service-automation/src/resume-node-failure-strands-run.test.ts reproduces the strand with zero approvals involvement: a plain pausing node declaring resumeAuthority: 'any' (the deliberate opposite of the approval node's resumeAuthority: 'service'), continued through the generic engine.resume() door — the same door POST /:name/runs/:runId/resume serves — with a downstream node that throws the reporter's own update_record ... not found shape.

Characterization assertions, all passing against main today:

  1. the suspension is gone (hasSuspendedRun is false);
  2. the run is recorded failed;
  3. a second resume answers RUN_NOT_FOUND;
  4. cancelRun returns false — a no-op, since it needs a suspended run to consume.

Two reverse controls keep those from being constants. A resume refused before the consumption point (INVALID_SIGNAL) leaves the pause intact and still resumable, so the same probes read true where they read false above. And a clean resume also ends unsuspended — so the strand is the failed status, not merely the missing pause.

These are characterization assertions, not desired behaviour. A repair for #13807 should turn the first test red. That is why it is pinned now: the repair then has to state which of these facts it changed.

Scope

Gates

Re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, both output sections read whole — 22 path-matched families plus the convention-triggered set for adding a test file.

Union run at 69d9b14f8 (the final commit): 23 families green, including every convention-triggered ratchet — check:engine-double-contract, check:where-matcher, check:cross-package-test-inputs, check:query-options-erasure, check:type-check-coverage, check:test-source-alias, check:published-files.

check:type-check-debt — the ledger entry for @objectstack/service-automation reads errors: 3; tsc --noEmit on the package at 69d9b14f8 measures exactly 3, all three the ledgered TS2341s in nested-region-parity.test.ts, and 0 from the new file. --listFiles confirms the new test is in the tsc program (so this is a measurement, not an exclusion reading).

Two gates returned exit 3 = NOT MEASURED, by their own printed verdict lines, neither a pass nor a red: check-test-completeness.mjs (needs a saved turbo run test log, which CI tees and a local run cannot produce) and check:dual-build-cjs-loads (PREREQUISITE NOT MET — needs a full pnpm build; 49 packages have no dist/). Both are CI's to run.

Vitest: Test Files 1 passed (1) · Tests 3 passed (3).

Generated by Claude Code


Generated by Claude Code

…the run
A measurement instrument, not a repair. The reported strand came from the
approvals reject door, whose error prose is the only place the word "stranded"
appears. This pins the same outcome through the GENERIC resume door with no
approvals involvement: `resumeInternal` calls `forgetSuspendedRun(run,
'resumed')` before `traverseNext`, so a downstream node that throws throws with
the pause already consumed.
Characterization assertions: the suspension is gone, the run is recorded
`failed`, a second resume answers RUN_NOT_FOUND, and cancelRun is a no-op. Two
reverse controls keep those from being constants -- a resume refused before the
consumption point (INVALID_SIGNAL) leaves the pause intact and resumable, and a
clean resume also ends unsuspended, so the strand is the FAILED status rather
than the missing pause.
A repair should turn the first test red; that is why it is pinned now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016ZC5rNQj3WEet5HAmmAkMs
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 31, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 73c84668743991a38d0016767175933138a376d3packageMentionDocs.

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

Labels

size/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@os-steve@claude