Commit Graph

8402 Commits

Author SHA1 Message Date
diegosouzapw
480f4a2bbb chore(skills): regenerate the cli-tunnel SKILL.md after the positional-arg fix
#13009 taught the CLI registry parser to read positionals declared with
.addArgument() but did not re-run the generator, so check:agent-skills-sync
has been red on the base tip ever since. Regenerated with the official
generator (--apply --only=cli-tunnel); the only delta is 'tunnel create'
gaining its [type] positional.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-11 13:50:33 -03:00
diegosouzapw
39300717ab docs(changelog): record the base-red orphan fixes for v3.8.51 2026-09-11 13:25:20 -03:00
diegosouzapw
ea7ba79a73 fix(sse): restore credential refresh, failure persistence and codex image errors
Three more regressions from the #12867 pipeline extraction, all red on the
base tip:

- the non-streaming leg stopped refreshing credentials after a 401 and
  stopped persisting failure state, so a Copilot token was never retried
  and per-model quota locks lost their helper references;
- handleImageGeneration (codex) crashed reading fields off a body that
  #12506 now sanitizes before it reaches the classifier;
- the pipeline discarded the raw upstream body, which provider-error
  classification needs (quota vs rate-limit vs ban) — surfaced as
  rawMessage/upstreamBody, both internal to the classifier.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-11 12:56:54 -03:00
diegosouzapw
3603a7b246 test(sse): pin the pipeline error-classification and Kiro stream contracts
Regression guards for the two defects above: the pipeline must surface the
upstream error code/type, and a Kiro error frame now terminates the stream
after forwarding response.failed (the #12506 contract the sibling
stream-passthrough suite already encodes).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-11 12:56:15 -03:00
diegosouzapw
be40cadd01 fix(sse): carry the upstream error code/type through the provider pipeline
parseUpstreamError() already returns errorCode/errorType; the pipeline was
dropping both on the floor. Gates that key on the pair — notably
isAntigravityMissingProjectError — never fired, so a config-class 422 turned
into a generic account cooldown instead of a fail-closed answer.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-11 12:55:42 -03:00
diegosouzapw
9e4955b069 Merge branch 'fix/v3851-basereds-orphans-g1' into fix/release-v3.8.51-basereds-orphans 2026-09-11 12:55:06 -03:00
diegosouzapw
47b6fb837a Merge branch 'fix/v3851-basereds-orphans-g4' into fix/release-v3.8.51-basereds-orphans 2026-09-11 12:55:06 -03:00
diegosouzapw
e25b706c56 test(lease): re-inventory hard-lease call sites after the pipeline extraction
Four drifts, all from PRs merged into the tip on 2026-09-07:

- #12867 extracted chatCore.ts's streaming execution loop into
  chatCore/providerExecutionPipeline.ts. Its two getProviderCredentials() sites
  now go through the injected `connection.getProviderCredentials` handle, which
  the bare-identifier AST scan never saw — the sites would have left the
  inventory unnoticed. Count property-access calls too and inventory the new file.
- #12867 also re-expressed the codex 429 managed-lease fence: the inline
  `provider === "codex" && !managedLease` became `allowAccountRotation:
  !managedLease && …` in chatCore.ts, gated in the pipeline as `canRotateAccount`.
  Assert both halves of that seam instead of the vanished inline form.
- #12746 moved combo.ts's getProviderConnectionById into
  combo/executeTargetGates.ts (class B, unchanged).
- #12805 added the Grok Build reset-credit path: src/lib/usage/grokResetCredits.ts
  (class B — same isConnectionUnavailableToAuxiliaryActivity fence as its codex
  sibling, so it also joins auxiliaryIsolationSources) and
  src/app/api/usage/codex-reset-credit/route.ts (class C — resolves the
  connection's provider to pick a library, never to serve a request).

Also pin CATALOG_BUILD_TIMEOUT_MS in models-catalog-route.test.ts: #12627's 8s
cold-build bound is sized for a warm production process, and a tsx test runner
building the full catalog from a fresh SQLite file crosses it (10-13s observed),
returning a `catalog_build_timeout` error body with no `data` array. The bound's
own behavior stays covered by 12627-catalog-inflight-timeout.test.ts.
2026-09-10 19:13:05 -03:00
diegosouzapw
7edda9c30a test(combo): follow the provider-cooldown source guard into the split modules
#12746 (executeTarget → gates/attempt/loop) and #12811 (round-robin) moved the
combo dispatcher out of open-sse/services/combo.ts. The #5976 invariant moved
with it intact — executeTargetAttempt.ts:1169 and roundRobinCombo.ts:1049 both
still exclude a 500/429 on a per-model-quota provider before recording a provider
cooldown — but the guard only read combo.ts and went red on an empty file.

Scan the whole combo surface instead of one filename and require the skip in
EVERY module that calls recordProviderCooldown(), with the pattern matched on
whitespace-normalized source so a prettier rewrap cannot silently disarm it. The
invariant asserted is unchanged; the guard is now stronger than the single
substring check it replaces.
2026-09-10 19:12:46 -03:00
diegosouzapw
b4616e4316 fix(catalog): keep operator custom models out of the live-sync reader
#12934 unioned customModels into getAllActiveSyncedModels() alongside the
dispatch-time readers it was actually fixing (#12597: getActiveSyncedCatalog,
reconcileProvidersWithActiveSyncedCatalog, getActiveProvidersWithSyncedModel).

getAllActiveSyncedModels() is not a dispatch reader. Its three consumers read it
as "what the provider's live sync reported": /v1/models feeds its synced-emission
loop (and has a separate custom-model pass right after, which owns the
specialty-registry dedupe, hidePaid and the vision overrides), /api/models uses it
to decide whether an exclusive-listing provider suppresses a static row, and
getSyncedAutoAliases derives tier aliases from it. Blurring custom rows into
"synced" made a custom embedding/rerank model on jina-ai come out as the alias
row `jina/<id>` (parentless primary) with the registry's canonical
`jina-ai/<id>` demoted to its child — the inverse of the identity every other
specialty model of that provider carries, and it also dropped the registry's
`dimensions`.

Drop the union there only; the dispatch trio keeps it, so #12597's tests and the
400-on-picker-model fix are untouched. Locked by a new case in
custom-models-live-catalog-12597.test.ts and by both jina cases in
models-catalog-route.test.ts ("does not duplicate imported/custom Jina specialty
models"), which encode the two identities side by side.
2026-09-10 19:12:06 -03:00
diegosouzapw
b3d3d9524c fix(combo): survive a malformed customModels row when building auto/* pools
prepareVirtualAutoComboInputs() reads getCustomModels(providerId) straight into
`for (const m of customModels) if (m.id ...)`. That blob is operator-writable and
is returned as raw parsed JSON, so a null / non-object row threw "Cannot read
properties of null (reading 'id')" and EVERY auto/* combo failed to materialize
("[catalog] Could not materialize built-in auto model auto/<id>"), silently
degrading the whole zero-setup routing surface to its minimal catalog entries.

Filter the rows to objects first, the same way the /v1/models custom-model pass
already does. Regression test in combo-auto-pool-visible-only.test.ts fails with
the original TypeError before the guard.
2026-09-10 19:11:36 -03:00
diegosouzapw
ed44f4ae12 fix(db): create the call_logs provider-stats index after legacy healing
#12832 declared `idx_cl_request_provider ON call_logs(request_type, provider)`
inside SCHEMA_SQL. That block runs before ensureCallLogsColumns() heals a legacy
call_logs table, so on any lineage predating the request_type column the CREATE
INDEX aborted the whole schema exec with "no such column: request_type" and the
server never finished opening the database.

Move the index next to the other request_type/combo indexes in
ensureCallLogsColumns(), which runs after the ALTER TABLE healing (and is also
called on the in-memory path), so both fresh and upgraded databases get it.

Proven by tests/unit/db-core-init.test.ts, "legacy call_logs schemas are upgraded
before combo target indexes are created" — failing on the release tip, green now.
2026-09-10 19:11:08 -03:00
diegosouzapw
2724ad07e1 fix(sse): restore upstream error parsing + rate-limit lock in the provider pipeline
#12867 lifted the non-2xx branch out of chatCore.ts into
providerExecutionPipeline.ts::toOutcome, but reimplemented it instead of
delegating, dropping three behaviors the non-streaming failure path relies on:

1. A non-JSON upstream body fell into the inline JSON.parse catch and surfaced
   as the (empty) statusText — "upstream error" — discarding the text the
   client needs. Now parseUpstreamError() supplies the message, as chatCore did.
2. The body-derived retry-after ("Please retry after 20s") was never parsed:
   retryAfterMs was hard-coded null into applyStatusRestatement/createErrorResult.
3. recordRateLimitBody was plumbed through PipelineStateHooks and wired at both
   chatCore call sites, but never called — so updateFromResponseBody(), which
   drains the runtime reservoir on a body-derived 429, silently stopped running
   for every request routed through this pipeline.

restatement rules also now match against the real upstream payload rather than
the request body that transformedBody carried whenever the parse failed.

Regression guard: tests/unit/chat-rate-limit-body-lock.test.ts (2 base-red
failures on the release/v3.8.51 tip) — both green; provider-execution-pipeline
13/13; typecheck:core clean.
2026-09-10 18:39:42 -03:00
Nguyen Thanh Dat
af49d4972e fix(stream): accept the buffer size glm.ts has been passing since #12179 (#12925)
Rebased onto the tip and completed, per the maintainer's call to finish the wiring rather than merge the capability alone.

What changed since your version:

The tip had already cleared the TS2554 by deleting the 16th argument, leaving a comment that the highWaterMark stays at the helper default. So the base-red you found is gone, but the 64 KB #12179 asked for was still not applied and your new parameter had no caller. glm.ts now passes it, which is what turns the capability into the fix.

Your test file also hung the runner: every stream createSSEStream builds arms a 10s idle watchdog via setInterval in start, and nothing cancelled them, so node:test waited on a non-empty event loop long after the assertions passed. Cancelling each readable in an after hook runs the cancel handler that clears the timer — the file now reports in about 7 seconds. Worth knowing for future stream tests.

Your five assertions are unchanged and all pass. Reading the writable's desiredSize to measure the queue budget the stream was actually built with, rather than standing in for it, is the detail that makes this testable at all — and the 0-budget case pinning `??` against `||` is the kind of thing that silently rots otherwise.

Thank you also for separating your own red checks from the base's and reporting what you found there. That is how #12919's identical failures got explained instead of chased.
2026-09-10 18:25:31 -03:00
Nguyen Thanh Dat
22473dee50 feat(providers): add EURouter as an OpenAI-compatible gateway (#12985) (#13025)
Rebased onto the release tip after #13024 landed: both PRs extend the same three registration files, so the sibling merge turned this into a conflict. The resolution is additive — both catalog entries kept, both registry imports kept, both base URLs kept — and EURouter stays in AGGREGATOR_PROVIDER_IDS while GreenPT stays out, exactly as each PR argued. 14 provider tests pass on the rebased branch and the file-size gate is green under the annotated rebaseline.

Thank you for re-checking the endpoint live instead of trusting the report, and for the sovereignty caveat. Naming the upstreams from EURouter's own catalog — Claude Sonnet served by AWS Bedrock, 19 models owned by openai — and then writing an apiHint that says routing rather than residency is the kind of care that keeps a provider entry honest. The test asserting the copy contains none of "residency", "stays in the EU", "EU-hosted" or "sovereign" is a good guard against that drifting later.
2026-09-10 18:16:32 -03:00
Nguyen Thanh Dat
2b9e7fb3ec feat(providers): add GreenPT as an OpenAI-compatible provider (#13024)
Merged with a rebaseline commit added on top of your branch: check:file-size freezes the gateways catalog at 1462 lines, so any new entry fails the gate on arrival. The annotation covers this entry and EURouter's (#13025) together, following the route every previous gateway entry took (#11786 seekai, #10987 logfare, #10668 tabitoken, #10531 freebuff, #11631 1min.ai) — the file is declarative data already split into six family files, so splitting it for two entries would break the semantic-families rule.

Validated in a combined worktree with 13 sibling PRs: 132 focused tests pass, typecheck:core clean, file-size green after the rebaseline.

Thank you for stating plainly what you did not verify. "The endpoint exists and is key-gated; catalog, streaming and tool calls not exercised" is worth more than a confident entry that turns out to be guesswork, and the conservative entry that follows from it — empty models, no capability declared, hasFree false with the billing shape spelled out — is exactly right.
2026-09-10 18:14:04 -03:00
Nguyen Thanh Dat
9a56147019 fix(skills): read positionals declared with .addArgument() (#13009)
Approved by the maintainer for the agent-instruction surface it touches: the SKILL.md change is regenerated output from the corrected parser (`resilience set` -> `resilience set <name>`), restoring the required argument the published page had been hiding. No hand-written directive was added.

Boarded with 13 sibling PRs and validated as a set: 132 focused tests pass, typecheck:core clean, changelog integrity and file-size gates green.

Thank you — the table contrasting the declared argument against the published page is what made the second case (an agent told to run `resilience set` with no argument) visible as more than cosmetic.
2026-09-10 18:13:37 -03:00
Nguyen Thanh Dat
751247a143 fix(security): scan both ends of an oversized body, not just the front (#13104)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:20 -03:00
Nguyen Thanh Dat
567abb5d68 fix(security): scan the text a tool_result carries (#13101)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:16 -03:00
Nguyen Thanh Dat
403a1a697d fix(guardrails): mask PII inside a tool_result's nested content (#12930)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:12 -03:00
Nguyen Thanh Dat
f2d5728cfd fix(dashboard): test Responses nodes on /v1/responses, not chat completions (#13070) (#13087)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:08 -03:00
Nguyen Thanh Dat
0a314c84de fix(translator): treat contentSchema and unevaluatedItems as schema slots (#13110)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:05 -03:00
Nguyen Thanh Dat
1929aa656a fix(validation): accept a null dailyQuotaResetTimezone (#13066) (#13083)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:01 -03:00
Nguyen Thanh Dat
4edc3d57d0 fix(azure): match the generation, not one release, for max_completion_tokens (#13007)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:57 -03:00
Nguyen Thanh Dat
a6f28210de fix(logs): match the in-memory call-log filter to the SQL one it re-applies (#12896)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:54 -03:00
Nguyen Thanh Dat
5df94f8b05 fix(bedrock): resolve context limits for every vendor prefix, not just anthropic (#12921)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:49 -03:00
Nguyen Thanh Dat
e1a1290fde fix(compression): keep tool_result blocks first when aging annotates a turn (#12920)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:45 -03:00
Nguyen Thanh Dat
ee21e7d2c9 fix(a2a): build the status agent card from the request that asked for it (#12918)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:41 -03:00
Diego Rodrigues de Sa e Souza
fd27ff08c7 chore(deps): drain the Dependabot queue — 10 of 13 alerts (#13213)
* chore(deps): drain the Dependabot queue — 7 of 10 alerts

Lockfile-only bumps; no manifest touched, so nothing changes for consumers.

Root package-lock.json:
  hono      4.13.0 -> 4.13.7  (#215 #216 #217, medium, patched 4.13.5)
  csv-parse 7.0.1  -> 7.0.2   (#213, medium)
  joi       18.2.3 -> 18.2.8  (#211 #212, low, patched 18.2.4/18.2.5)

@omniroute/opencode-plugin:
  toml      4.1.1  -> 4.3.0   (#209, HIGH, patched 4.1.2)

@omniroute/opencode-plugin-v2:
  esbuild   0.28.1 -> 0.28.2  (#210, low) — the direct copy only; see below.

The plugin-v2 diff looks large but is one package: esbuild ships 27 platform
binaries, each carrying version + resolved + integrity.

Three alerts stay open, deliberately:

  #218 extract-zip (HIGH) and #214 adm-zip (medium) have NO published patch.
  Both are dev-scope. Closing them needs an upstream release or a decision to
  replace the dependency — neither belongs in a lockfile bump.

  #210 esbuild is only half-closed. `node_modules/esbuild` is on 0.28.2, but
  `tsup` pins `esbuild: ^0.27.0`, so its nested copy stays at 0.27.7 — inside the
  vulnerable range (>= 0.27.3, < 0.28.1). Updating tsup does not move it (8.5.1
  is already current). Forcing it would take an `overrides` entry pushing a major
  of esbuild inside the bundler, which is exactly the change that breaks a build
  silently, for a LOW dev-only alert. Left for an upstream tsup release.

check:lockfile passes on all three, including the workspace lock/manifest
consistency check. check:tracked-artifacts OK.

* chore(deps): bump js-yaml to 4.3.2 (root + electron)

Two more HIGH alerts arrived after the first sweep:

  #220 js-yaml (root package-lock.json)     >= 4.0.0, < 4.3.2
  #219 js-yaml (electron/package-lock.json) >= 4.0.0, < 4.3.2

The root's own js-yaml was already on 5.4.1; the vulnerable copies were the ones
nested under @yarnpkg/parsers, lockfile-lint, xmlbuilder2 (root) and the direct
dependency in electron. All now 4.3.2. Four version lines, nothing else.

#221 smol-toml (HIGH, <= 1.7.0) is NOT closed here. The root is on 1.8.0; the
vulnerable 1.6.1 sits under @openai/codex-security, which pins it as an EXACT
version rather than a range, so `npm update` cannot move it. Bumping
codex-security itself (0.1.24 -> 0.1.26) does not help — 0.1.26 pins the same
1.6.1 — so that bump was reverted rather than carried along for no benefit.

Closing #221 needs an upstream codex-security release or an `overrides` entry,
the same trade already declined for #210/tsup: forcing a transitive pin from
outside is how a build breaks silently. Note that @openai/codex-security is also
the package carrying the unpatched extract-zip (#218), so one upstream release
would likely clear both.

* chore(deps): override smol-toml to 1.8.0 and raise the js-yaml floor

Closes #221 (smol-toml, HIGH, DoS via malformed TOML, vulnerable <= 1.7.0).

@openai/codex-security pins smol-toml at 1.6.1 as an EXACT version, so no
`npm update` reaches it. This repo already uses `overrides` as its standard tool
for exactly that situation — the block carries 20+ entries, including the
scoped-by-parent form and the `qs`/`fast-uri`/`ip-address` entries that back
earlier security bumps — so a scoped override is the idiomatic fix here, not a
new mechanism:

    "@openai/codex-security": { "smol-toml": "^1.8.0" }

The nested copy deduplicates to the root's existing 1.8.0, which two other
consumers (the root itself and knip) already run, so the version is proven in
this tree. The whole lockfile diff is the 14 lines of the removed 1.6.1 entry.

Also raised the `@yarnpkg/parsers` js-yaml floor from ^4.3.1 to ^4.3.2, so the
override documents the patched version rather than permitting the vulnerable one
it was written against.

Not fixed, and not fixable by version — verified against the npm registry rather
than trusting the advisory metadata:

  #218 extract-zip — latest published IS 2.0.1, the vulnerable version. Dev
       scope, via @openai/codex-security. No release to move to.
  #214 adm-zip — latest published IS 0.6.0, the top of the vulnerable range
       (>= 0.5.9, <= 0.6.0). RUNTIME scope, via onnxruntime-node's ^0.5.16, and
       the repo already overrides adm-zip to ^0.6.0. No release to move to.

Both need an upstream fix or a decision to replace the dependency; neither is a
lockfile change. adm-zip being runtime rather than dev makes it the one worth
tracking.

#210 esbuild stays open too. A flat `overrides: { esbuild: ^0.28.2 }` in
opencode-plugin-v2 does close it — npm then reports 0 vulnerabilities — but it
requires regenerating that lockfile from scratch: 823 lines, 96 packages moved,
for a LOW dev-only alert, and a major esbuild bump inside tsup cannot be
validated here without a real install of that package. Tried, measured,
reverted. Left for an upstream tsup release.

check:lockfile OK on all lockfiles including the workspace consistency check;
check:tracked-artifacts OK; prettier clean.
2026-09-10 13:37:55 -03:00
Diego Rodrigues de Sa e Souza
0549dcfc36 fix(api): scope batch bulk-delete to the calling API key (#13211)
GHSA-wvxc-jp3v-5mg5: `DELETE /api/v1/batches/delete-completed` deleted the
completed batches of EVERY api key on the instance and nulled the contents of
every file those batches referenced. Any ordinary inference key reached it —
including one with `scopes: []` — and no victim key, batch id or file id was
needed.

Two defects stacked in one endpoint:

  - `deleteCompletedBatches()` carried no `api_key_id` predicate. The file
    SELECT, the checkpoint DELETE and the batch DELETE were all instance-wide.
  - The route only checked that SOME key was present (`!scope.apiKeyId` → 401),
    never that the caller owned anything, and called the helper bare.

The helper now takes `apiKeyId` and scopes all three statements to it; the route
passes the caller's key and omits it only for session auth, so the operator's own
dashboard keeps its instance-wide cleanup and an API key clears only its own
completed batches.

None of this is a new pattern. `listBatches(apiKeyId?)` and
`countBatches(apiKeyId?)` in the same module already scope by `api_key_id`, and
`batches/[id]/route.ts` already gates per-record access with `scopeCheck` —
session auth sees everything, a key sees only its own. This one helper was the
one that never got it, which is why the fix reuses the shape instead of
inventing a second convention.

tests/unit/batch-delete-completed-ownership-wvxc.test.ts — 5 tests, 4 red before
the fix, including the two that prove the cross-tenant destruction (another
key's batch survives; another key's file content survives). It also pins the
instance-wide dashboard sweep so the fix cannot be "tightened" into breaking the
operator's own cleanup, and a source guard that the route never calls the helper
bare again.

Reported privately via GHSA-wvxc-jp3v-5mg5.

Closes GHSA-wvxc-jp3v-5mg5
2026-09-10 13:27:10 -03:00
Diego Rodrigues de Sa e Souza
393cfdd660 fix(test): make the ToS heading guard actually require the parentheses (#13228)
CodeQL js/useless-regexp-character-escape (#994-#997) on one line, and it is a
real defect rather than the usual query noise.

The assertion built its pattern in a TEMPLATE literal:

    new RegExp(`\(\s*${String(tos?.actual)}\s*\)`)

JavaScript resolves the escapes before RegExp ever sees the string: `\(` becomes
"(" and `\s` becomes the LETTER "s". The compiled pattern was `(s*16s*)` — a
capture group around optional "s" characters — so it matched any heading merely
CONTAINING the number. The literal parentheses this guard exists to require were
never checked, and it passed on exactly the headings it was written to reject:

    /(s*16s*)/.test("### Caution — clauses worth checking 16")   // true

Doubled the backslashes so they survive the template literal, and routed the
interpolated value through an `escapeRegExp` helper — the count is a number
today, but interpolating an unescaped value into a regex source is the same
class of bug one refactor away.

Added a second test that pins the behaviour rather than the spelling: the
pattern must REJECT a heading carrying the count without parentheses, and accept
it with them (including inner whitespace). Before this fix that test fails.

4/4 green against the real docs/reference/FREE_TIERS.md heading.
2026-09-10 13:27:02 -03:00
Diego Rodrigues de Sa e Souza
d86cf75aef fix(quality): register 4 drifted covering tests in stryker tap.testFiles (#13229)
`check:mutation-test-coverage --strict` has been failing Fast Quality Gates on
every open PR against release/v3.8.51. It grew from 2 missing entries to 5 in
roughly an hour, so it is drifting faster than PRs land.

Four test files cover a mutated module without being listed, so their mutant
kills do not count:

  open-sse/services/accountFallback.ts      <- openai-compatible-per-upstream-402-health
  src/sse/services/auth.ts                  <- openai-compatible-per-upstream-402-health
                                            <- quota-window-label
  src/shared/utils/circuitBreaker.ts        <- combo/execute-target-gates
  open-sse/services/combo/comboStructure.ts <- combo-pin-implicit-allowlist

Registration only — no test or module is touched, and no gate is weakened; the
listing is what makes those kills count in the first place.

Inserted in place, never through a JSON round-trip: re-serializing this file
reorders the ~10 curated entries that are already out of alphabetical order
(learned the hard way in #11438).

check:mutation-test-coverage now reports no drift. check:tracked-artifacts OK,
prettier clean.

Worth noting for whoever adds the next test: this gate fires whenever a NEW test
happens to cover one of the 31 mutated modules, which is easy to do without
realising. Registering it in the same commit is cheaper than a CI round-trip.
2026-09-10 13:26:53 -03:00
Dizzle
81bf3cc36e docs(checks): keep doc counts honest — headings, rankings, catalog, weights, quality gate and scoring diagram now covered (#12507)
Estender o gate de contagens para headings, rankings, catálogo, pesos, quality gate e o diagrama de scoring é exatamente o tipo de trabalho que evita a classe inteira em vez de um caso.

Falo por experiência desta campanha: o `check:docs-counts` caiu **duas vezes** hoje pela mesma causa — contagem de migration escrita à mão em três arquivos mais 41 mirrors, desatualizando a cada migration nova (#12970 e #13209). Cada superfície que este PR passa a cobrir é uma que deixa de virar base-red na mão de quem vier depois.

Revalidei sobre o tip: **19/19**, `check:docs-counts-sync` com 0 drifts, `check:docs-all` PASS, `check:doc-links` PASS.

**Integração:** dois conflitos.

1. `scripts/check/check-docs-counts-sync.mjs` — o bloco de leitura de fatos conflitou com os imports de free-tier que entraram pelo #12786/#12744 nesta campanha. Aditivo, os dois conjuntos ficaram.
2. `docs/diagrams/auto-combo-scoring.mmd` — o seu rótulo dizia `reliability (0.0000)`, mas o #12731 mergeou horas antes e passou a dar peso de reliability a todo mode pack. Ficou o rótulo do tip, `reliability (0.0000 DEFAULT, 0.03 packs, 0.04 reliable)`, que é o número real agora.
2026-09-10 10:54:38 -03:00
Markus Hartung
955b28ef5c feat(dashboard): badge a conversation that never reached a clean stop (#12717)
Uma conversa que nunca chegou a parada limpa e não sinaliza nada é o pior estado possível de UI: indistinguível de uma que terminou. O incidente que você cita no comentário do teste — stream pesado em reasoning estourando o cap do coletor no meio, deixando a conversa presa sem sinal — é exatamente o caso que justifica o badge.

Separar `resolveTurnCompletionState` de `resolveConversationStalledState` também está certo: `tool_call_pending` é um estado legítimo em voo, não uma conversa travada.

Revalidei sobre o tip: **29/29**, typecheck:core limpo.

**Nota de integração.** O `tests/unit/responses-continuation-store.test.ts` conflitou com o #12854, que anexa a própria bateria ao mesmo arquivo. Reconstruí o arquivo como append limpo — versão do tip mais o seu bloco de 184 linhas, verificado por `esbuild` antes de rodar. Registro por que importa: na primeira tentativa eu apenas retirei os marcadores de conflito, e isso enfiou os seus testes **dentro** de um objeto literal não terminado do #12854. Compilava como erro de transform, não como conflito — só apareceu ao rodar. Resolver JSON e teste "aditivamente" sem verificar a sintaxe depois é armadilha; ficou a lição.
2026-09-10 10:52:03 -03:00
Dizzle
a152eb92db fix(resilience): stop unbounded queue that hangs 6min until Aborted (#12715)
Fila sem teto que segura a request seis minutos até o cliente abortar é pior que 503 imediato: consome slot, mascara a saturação e ainda entrega erro no fim. Um orçamento `maxWaitMs` por conexão compartilhado entre gate, slot padrão do provider e fila do Bottleneck é a forma certa — o teto tem que ser um só, senão cada camada espera o seu.

O `max(perConn, upstream)` no `executionMaxWaitMs` é o detalhe que evita a correção matar request em voo, que seria trocar um defeito por outro.

Registro a atribuição: você manteve o #12635 aberto para o @Tushar49 e creditou a percepção dele (providers lentos precisam de 2min→10min por conexão) enquanto adiciona o encanamento que faltava. É o jeito certo de construir sobre PR de outra pessoa sem tomar o crédito.

Sobre o `npm run lint` desmarcado com a nota do eslint quebrado no ambiente: deixar em branco e explicar vale mais que marcar sem ter rodado. Rodei aqui: limpo.

Revalidei sobre o tip: **13/13**, typecheck:core limpo, check-file-size OK. O `file-size-baseline.json` conflitou com os rebaselines desta campanha — resolvido aditivamente, JSON revalidado com `json.load`.
2026-09-10 10:49:34 -03:00
Ravi Tharuma
d6a61074dc fix(quota): align AUTH window labels with usage API and clarify 503 (#12884)
Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar.

Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK.

**Dois consertos meus na sua branch.**

1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no #12746/#12811, mergeados nesta mesma campanha depois que você cortou a branch.

2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o #12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
2026-09-10 10:47:11 -03:00
Dizzle
4c10baa644 fix(dashboard): explain silent Radar cells on hover and gate them (#12937)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Sentinela que não se explica (`—`, `?`) faz o leitor inventar a razão. Explicar no hover é metade; o `check:radar-sentinels` é a outra — sem o gate, a explicação apodrece na primeira coluna nova.
2026-09-10 10:42:48 -03:00
Markus Hartung
b516e95262 fix(conversations): show a pending spinner for unresolved tool nodes instead of "(empty)" (#12727)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

"(empty)" para um nó de ferramenta ainda não resolvido é informação errada, não ausência de informação — o usuário lê como "não retornou nada". Spinner de pendente diz a verdade.
2026-09-10 10:42:38 -03:00
Ravi Tharuma
3e2a6d8f35 fix(credentialHealth): do not poison multi-upstream openai-compat conn on one model 402 (#12875)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Envenenar a conexão inteira por um 402 de **um** modelo é o erro clássico de granularidade em provider openai-compatible com múltiplos upstreams — derruba modelos que estavam saudáveis. Restringir ao modelo afetado é o comportamento correto, e é a mesma distinção que o guia de resiliência faz entre cooldown de conexão e lockout de modelo.
2026-09-10 10:42:34 -03:00
Ravi Tharuma
6029515402 fix(api): page and stream-complete GET /v1/models for large catalogs (#12882)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Paginar e completar o stream em `GET /v1/models` é a correção certa para catálogo grande: um payload único que cresce com o número de providers vira timeout silencioso no cliente, não erro.
2026-09-10 10:42:30 -03:00
Ravi Tharuma
0ddb47228b fix(monitoring): expose failed connection ids on credentialHealth (#12876)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Um health que diz "falhou" sem dizer **qual** conexão obriga o operador a cruzar logs para achar o óbvio. Expor os ids das que falharam é o que transforma o endpoint em ferramenta de diagnóstico.
2026-09-10 10:42:26 -03:00
Diego Rodrigues de Sa e Souza
a0c69ca25e refactor(ui): orchestration canvas fase 3 — theme-aware status tokens across flow surfaces (#12378) (#13203)
* refactor(ui): move shared flow colors to the orchestration status tokens

FLOW_EDGE_COLORS and TokenHealthBadge were pinned to the fixed dark-mode
hexes in STATUS_HEX, so both rendered dark-theme green/amber/red on a light
background. They now read the theme-aware --orch-status-{success,warning,
error,muted} custom properties introduced in Fase 2. The dark values of
those tokens are exactly the old hexes, so dark mode is unchanged and only
light mode gains contrast. `idle` was already a CSS var, which is the
precedent proving a var() resolves in a ReactFlow edge stroke.

Five call-sites built translucent variants by concatenating an 8-bit alpha
suffix onto the palette hex (`${FLOW_EDGE_COLORS.error}40`), which cannot
work with a var(). They move to a new documented helper, flowColorAlpha(),
that wraps color-mix() — the same approach orchStateBadgeBg() already uses
in the orchestration model. Percentages mirror the old suffixes
(20 -> 13%, 30 -> 19%, 40 -> 25%).

STATUS_HEX stays exported as the dark-mode mirror; it now has no production
consumer. globals.css needed no change — all five tokens already existed in
both themes.

The colour assertions in the topology, combo-live and design-grid suites
were aligned to the tokens, never weakened: every hex equality became an
equality against the corresponding var(). design-grid additionally now
asserts each token is defined in BOTH themes.

Refs #12378

* refactor(ui): finish the status-token migration across flow surfaces

Sweeps the five state hexes across the remaining flow surfaces, following D1:

- ComboLiveStudio: active/error provider pills and the run-outcome tri-state.
- CompressionCockpit / WaterfallInspector / IoNode: the savings readouts and the
  savings quality ramp (>=30 success, >=15 warning, else muted).
- EngineNode: the same ramp, plus the running state, whose glow moved to
  flowColorAlpha — the literal #f59e0b40 suffix is invalid once the value is a var().
- WaterfallInspector: a skipped step now reads as muted rather than a bare grey hex.

Deliberately NOT migrated, because they are categorical or brand palettes rather
than state: STRATEGY_COLORS (routing-strategy hues), LAYER_COLORS (compression
layer pills), the provider brand color in ProviderTopology, and IoNode's
indigo/green input-output identity pair. The new test asserts both halves — what
became a token AND what stays hex — so a later sweep cannot silently swallow a
categorical palette.

Refs #12378
2026-09-10 10:29:55 -03:00
Diego Rodrigues de Sa e Souza
590582711c fix(dashboard): orchestration canvas fase 3 — follow-ups da review final (#12639) (#12988)
* feat(api): hydrate memoryHits from the persisted history event

`GET /api/a2a/tasks/[id]` falls back to the persisted history row once a task
leaves the in-memory TTL window, and `reconstituteHistoricalTask` hard-coded
`metadata: {}` — so the drawer's "Memory used" section vanished for any
historical task, even though `executeA2ATaskWithState` had already written a
`memory_hits` event with the hits.

The fallback now reads that event: `data_json` is parsed and, when it yields at
least one well-formed hit, exposed as `metadata.memoryHits`. The event itself is
filtered out of `events` — it is observability, not a state transition, and
without the filter it leaked into the timeline as a duplicate of the row's
current state.

Reading is defensive throughout, mirroring `DrawerMemory`'s own validation: the
payload is caller-influenced and unvalidated end to end, so `JSON.parse` runs
inside `safeJsonParse`, non-arrays are rejected, and each entry must carry `id`,
`key`, `type` and `snippet` as strings (a non-string field would be rendered as
a React child and take the drawer down). Malformed input degrades to
`metadata: {}` and a 200 — never a 500.

Refs #12639

* fix(a2a): bound the memory recall with its own deadline

collectMemoryHits() runs BEFORE the skill handler and had no deadline at all,
so a slow memory backend delayed the start of every A2A task — the HTTP
genericBackend alone defaults to a 30s timeout.

The search now races a MEMORY_RECALL_TIMEOUT_MS (1500ms) deadline. Overshooting
degrades exactly like any other recall failure: empty hits, a warn log, and the
task proceeds normally (best-effort contract unchanged, nothing propagates).
The deadline timer is cleared in a finally on BOTH paths so no handle is left
holding the event loop open, and MemoryHitsDeps.timeoutMs makes it injectable
so the tests cost milliseconds instead of 1.5s of wall clock.

Refs #12639

* fix(dashboard): carry conductor requirements and focus the repeated task

The drawer's "Repeat" for a Conductor task dropped the runner/model pinning and
left the operator staring at the finished run:

- `hubTaskSchema` now parses the hub's `requirements` (`.catch(null)` so an odd
  shape never fails the whole task parse), and `ConductorTaskDetail` exposes
  `cli`/`model` (`null` when the hub sends none).
- `repeatReqForConductor` carries `cli`/`model` when present and OMITS them
  otherwise — the route's Zod takes both as optional strings, so a `null` would
  400. The two fields are independent.
- `performAction` reads the response body once and returns it, so the repeat can
  report the CANVAS id of the created task (`task_id` / `data.id` /
  `result.task.id`, each with its node prefix). `OrchestrationPageClient` then
  refetches and focuses it via `?node=`; History keeps its current behavior.
- `conductor-routes-auth.test.ts` covers the creation route through its `ROUTES`
  array; the duplicated source assertion left `conductor-create-route.test.ts`.

Refs #12639

* chore(a2a): follow-ups changelog

Changelog fragment for the five items PR-C delivers from #12639.

The sixth item on the issue — an authenticated panel path for A2A task
creation — stays deliberately out of scope and is recorded as such in a
comment on the issue rather than silently dropped: the JSON-RPC endpoint
accepts API keys only, and widening that endpoint's auth surface to serve a
UI convenience is the operator's call, not the implementation's.

Closes #12639
2026-09-10 10:23:52 -03:00
Markus Hartung
7d4189fd78 fix(responses-continuation): bridge the write-in-flight window with an in-memory pending store (#12854)
Diagnóstico por captura de pacote em tráfego real, com o `400 previous_response_not_found` reassemblado do tcpdump três vezes no mesmo loop de tool-calling — isso é evidência, não hipótese. A causa é limpa: `detail_state` só vira `'ready'` depois de uma escrita fire-and-forget enfileirada num worker único, e o cliente já tem o id de resposta antes disso. Semear a ponte **antes do primeiro await** é o que faz a correção não custar latência.

Revalidei sobre o tip: **19/19**, typecheck:core limpo, check-file-size OK.

**Estava draft e eu marquei como ready.** Não havia gate declarado — nem RFC pendente, nem decisão de produto em aberto — e passou na validação; a diretiva permanente do dono para esta campanha é avaliar draft como qualquer PR e promover quando passa. Se a intenção era segurar por outro motivo, me avise que eu reverto.

**Um conserto meu na sua branch.** O `typecheck:core` falhava com `TS2345` em `callLogs.ts:489` — e falhava **na sua branch sozinha**, não por interação com a onda; confirmei isolando. O call site fazia cast para `{ clientRawRequest?: unknown; clientResponse?: unknown }`, mais frouxo que o `ContinuationPipeline` que o parâmetro exige, e `unknown` não assina para os membros tipados. Exportei o `ContinuationPipeline` do próprio store e usei ele no cast, em vez de alargar o tipo do parâmetro: o contrato passa a ter um nome só, no lugar onde ele já vivia.

**Sobre a sua Reviewer Note do Map sem limite de contagem:** concordo que vale registrar. Entradas pequenas com TTL de 60s auto-expirando não justificam sizing agora, mas se aparecer burst sustentado o sintoma será memória, não erro — e aí a nota está aqui.

Também carreguei o rebaseline de `chatCore.ts` (6021→6026) e `stream.ts` (3080→3098), que a onda de streaming inteira faz crescer.
2026-09-10 10:23:26 -03:00
Dizzle
12c7895bbd fix(sse): rotate opencode accounts on geo-blocked 403 (#12941)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Estender a rotação que já existe para 429 ao 403 de bloqueio geográfico é a generalização certa, e manter a rejeição de fingerprint (Cloudflare 1010) fora dela é o que impede a rotação de queimar todas as contas contra uma recusa que não é de egresso.

Nota: os checkboxes de validação do corpo ficaram em branco, mas o diff traz dois arquivos de teste — vale marcar da próxima para o revisor não precisar conferir.
2026-09-10 10:19:14 -03:00
Markus Hartung
1216cff05b fix(streaming): fail fast when an upstream stream produces only lifecycle/heartbeat events, never real content (#12741)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Um stream que só emite eventos de ciclo de vida e heartbeat, sem conteúdo nenhum, é falha disfarçada de sucesso: o cliente espera até o timeout dele. Falhar rápido devolve o controle.
2026-09-10 10:19:10 -03:00
Markus Hartung
2b1c57485c fix(sse): stop rebuilding a truncated summary from the collector's cap-dropped event array (#12718)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Reconstruir o resumo a partir de um array que o próprio coletor já truncou por cap produz um resumo que parece completo e não é — pior que resumo ausente, porque não se distingue. Parar de reconstruir dali é a correção.
2026-09-10 10:19:06 -03:00
Dizzle
91990d606b fix(sse): emit trailing usage estimate in translate streams when upstream stays silent (#12828)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

O #12151 cobriu só metade: passthrough emitia o chunk final de usage, translate calculava a estimativa **depois** de fechar o stream, então o número só chegava ao log do servidor e nunca ao cliente. Fechar essa metade é o que faz a feature existir de fato.

Não emitir segundo chunk quando o upstream já mandou usage real é o detalhe que impede a correção de virar contagem dobrada.
2026-09-10 10:19:02 -03:00
Ravi Tharuma
3272bedd1b fix(translator): drop or map agent_message on Chat Completions fallback (#12880)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

`agent_message` chegando num fallback de Chat Completions é um item que o cliente não sabe interpretar; mapear ou descartar é a escolha certa, e escolher por item em vez de derrubar a resposta inteira mantém o fallback útil.
2026-09-10 10:18:58 -03:00
Diego Rodrigues de Sa e Souza
a1b260146d fix(dashboard): orchestration canvas fase 3 — canvas polish (#12392) (#12983)
* fix(dashboard): keep the first failure timestamp in sourceStale

buildSourceStatuses stamped nowIso on every failing source at every poll, so
the stale indicator reported "since the last poll" instead of the first
failure — and, because snapshotContentKey serializes sources, the snapshot
identity churned on every tick while any source was down.

The failing branches now reuse the staleSince already held by that source in
the previous status list, via the functional setStatuses updater (no ref read
during render, no setState inside an effect body).

Refs #12392

* fix(dashboard): flag a source that starts failing after it had data

buildRootAndSourceEdges only materialized a placeholder SourceNode when the
failing source had no node at all. A source that already had work nodes and
then started failing (or went offline) kept its healthy-looking SourceNode
forever: no ⚠, no stale styling, no `sourceStale` line — the operator saw a
normal source while it was actually broken.

Now every non-ok/offline source is flagged: when its SourceNode is missing the
placeholder is created as before; when it exists, the node is replaced by a
copy carrying `sourceIssue` and `staleSince`. The copy (never a mutation) keeps
the function pure — the original object is still referenced by the caller's
`parts`, the same trap the droppedByState aliasing fix covered.

Tests: three cases in tests/unit/ui/orchestrationModel.test.ts — existing node
starting to fail (flags set, work nodes kept, no duplicate node, input object
untouched), existing node going offline (no invented staleSince), and a healthy
source staying free of both fields.

Refs #12392

* fix(dashboard): canvas polish batch (#12392)

Seven pointwise fixes on the Orchestration Canvas, each covered by a test:

1. Debounce x chip race: every chip/clear write in OrchestrationToolbar now cancels
   the pending search timer first. Left armed, it fired ~300ms later with a setParams
   closed over the pre-chip query string and silently reverted the chip.
2. The search input carries an aria-label (searchPlaceholder) — the placeholder alone
   is not an accessible name.
3. parseCsvSet trims each token, so `?state=running, failed` parses like the unpadded
   form instead of dropping the padded value.
4. toggleCsv was duplicated in the toolbar and the page client; both now import the
   single definition from the new model/urlParams.ts (pure, never mutates its inputs).
5. AgentsTab tells "nothing running" apart from "the filter matched nothing": with an
   active filter and no work node it renders noMatches + a clear-filters button instead
   of the setup CTAs, which would be wrong advice there.
6. Particle cap: orchestrationToFlow stamps `particles` on every edge and turns it off
   above PARTICLE_EDGE_CAP (40) simultaneously active edges — StatusEdge then renders
   the colored stroke without its 3 SMIL particles per edge.
7. The drawer's error banner clears when an action succeeds, so a recovered failure
   does not stay on screen.

Only `noMatches` is added to en.json here; the other locales are task B4.

Refs #12392

* chore(dashboard): canvas polish i18n + changelog

Real translations for orchestration.noMatches in the 41 non-English locales,
each one written against that file's own neighbouring keys (emptyTitle,
stateRunning, searchPlaceholder) so the wording for "task" and "filter"
matches what the locale already uses. No i18n:sync-ui, no __MISSING__ left.

Adds the changelog fragment for the nine PR-B fixes.

Closes #12392
2026-09-10 10:17:04 -03:00