Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading
, '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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .changeset/trycatch-returned-failure-step-record.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
---
"@objectstack/service-automation": patch
---

fix(service-automation): a `try_catch` with no `catch` region keeps the record of the writes its try region already made (#14184)

The returned-failure half of the engine's `childSteps` asymmetry. #13803 closed
the **throw** half — a dying `loop` brands its thrown error with the body steps
it completed and the engine's `catch` arm folds them into the run log — and
deliberately left the `if (!result.success)` branch alone, because at that
moment no executor returned `childSteps` on a failing result and a fold for
zero producers is speculative.

`try_catch` is the producer that makes it real. It does not throw: it catches
the try region's failure and RETURNS it, and on that return it withheld its
`childSteps` on purpose — correct while the engine spliced them only after a
successful result, and stale the moment the failing branch learned to fold. So
for a `try_catch` with **no** `catch` region, the try region's completed steps
were recorded nowhere: the run log kept no step for them, and the #4354 summary
folded over that log reported `acted: 0` for a region that had genuinely
written rows.

That is wrong in the one direction that causes harm. `acted: 0` on a failed run
reads as "nothing happened, safe to re-run", and for a non-idempotent region
(notifications, counters, external calls) that misread invites double-execution.

Two halves, mirroring #13803:

- `try_catch`'s no-`catch` failing return now carries `childSteps`.
- The engine folds `result.childSteps` in its `if (!result.success)` branch, in
the same position the throw arm and the success path use — right behind the
container's own step, ahead of any `fault` handler's steps.

**A record fix only; accept/reject is untouched and measured so.** `try_catch`
still returns failure with the same error text, still produces a `NODE_FAILURE`
step with the same message, still writes the same `$error`, still routes down
the same `fault` edge, and the run still ends `failed`. Those four were green
before this change and are green after it. The contained (with-`catch`) path,
the all-succeeding path and the failing-`catch` path are unchanged, and a try
region that fails before writing anything still reports `acted: 0` — there 0 is
the honest answer.

Every folded step carries a `parentNodeId`, so the ADR-0044 runaway guard, which
counts only top-level visits, does not see them. Nesting was measured for
double-folding: a container's sink already absorbs an inner container's steps,
so each step object still reaches the run log exactly once.
Original file line numberDiff line numberDiff line change
Expand Up@@ -182,12 +182,32 @@ export function registerTryCatchNode(engine: AutomationEngine, ctx: PluginContex
}

// No catch handler — surface the failure to the flow's fault edge / error
// handling. No `childSteps` here on purpose: the engine splices them only
// on a SUCCESSFUL node result, so attaching them to a failing one would
// be dead weight. That path is not the gap #7546 closes either — an
// unhandled failure already terminates the run `failed` with both
// run-level and step-level errors, which is loud by construction.
return { success: false, error: `try_catch '${node.id}': try region failed — ${lastError}` };
// handling.
//
// #14184 — and the try region's steps ride out WITH it. They used to be
// withheld here on purpose, because the engine spliced `childSteps` only
// on a SUCCESSFUL node result and attaching them to a failing one really
// was dead weight. #13803 taught the engine's THROW arm to fold a dying
// container's carried steps, and this card teaches its returned-failure
// arm the same, so the sink now has a reader on both channels.
//
// Withholding them was the #13803 defect one construct over. The try
// region's nodes may have written rows before one of them failed; the run
// log kept no step for any of them, so the #4354 summary folded over that
// log reported `acted: 0` for a region that had genuinely written. `acted:
// 0` on a failed run reads as "nothing happened, safe to re-run", which
// for a non-idempotent region invites double-execution.
//
// Purely additive to the RECORD: this return already reported failure,
// already produced a `NODE_FAILURE` step, already set `$error` and was
// already routable by a `fault` edge. Adding `childSteps` moves none of
// that — unlike the rejected "make `loop` swallow its throw and return"
// shape (see `partial-steps.ts`), which would have changed all four.
return {
success: false,
error: `try_catch '${node.id}': try region failed — ${lastError}`,
childSteps: failedAttemptSteps,
};
},
});

Expand Down
Loading
Loading