@skaile/workspaces 3.8.0 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +87 -0
- package/dist/app-sdk/index.js +1 -1
- package/dist/{asset-feeds-7HM3HONM.js → asset-feeds-4AEYY5P7.js} +4 -4
- package/dist/{asset-feeds-7HM3HONM.js.map → asset-feeds-4AEYY5P7.js.map} +1 -1
- package/dist/asset-manager/index.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +132 -9
- package/dist/bridge/drivers/claude-sdk.js.map +1 -1
- package/dist/bridge/drivers/codex.js +1 -1
- package/dist/bridge/drivers/continue.js +2 -2
- package/dist/bridge/drivers/echo.js +1 -1
- package/dist/bridge/drivers/gemini.js +3 -3
- package/dist/bridge/drivers/goose.js +2 -2
- package/dist/bridge/drivers/omp.js +1 -1
- package/dist/bridge/drivers/opencode.js +2 -2
- package/dist/bridge/drivers/qwen.js +3 -3
- package/dist/bridge/index.js +2 -2
- package/dist/bridge/src/drivers/claude-sdk.d.ts +58 -1
- package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
- package/dist/{chunk-2GREL3OD.js → chunk-2IVCUCJJ.js} +3 -3
- package/dist/{chunk-2GREL3OD.js.map → chunk-2IVCUCJJ.js.map} +1 -1
- package/dist/{chunk-O3LEV6KB.js → chunk-2KBA5OO2.js} +4 -4
- package/dist/{chunk-O3LEV6KB.js.map → chunk-2KBA5OO2.js.map} +1 -1
- package/dist/{chunk-KNPBMR2P.js → chunk-32MHUAED.js} +3 -3
- package/dist/{chunk-KNPBMR2P.js.map → chunk-32MHUAED.js.map} +1 -1
- package/dist/{chunk-NVYEK4FI.js → chunk-4W2OOVN5.js} +3 -3
- package/dist/{chunk-NVYEK4FI.js.map → chunk-4W2OOVN5.js.map} +1 -1
- package/dist/{chunk-2JW5SWIU.js → chunk-6BHWELKC.js} +6 -6
- package/dist/{chunk-2JW5SWIU.js.map → chunk-6BHWELKC.js.map} +1 -1
- package/dist/{chunk-4WTC6BSV.js → chunk-AQGWIMNV.js} +3 -3
- package/dist/{chunk-4WTC6BSV.js.map → chunk-AQGWIMNV.js.map} +1 -1
- package/dist/{chunk-BGAAZMBK.js → chunk-BIA7TJWH.js} +2 -2
- package/dist/{chunk-BGAAZMBK.js.map → chunk-BIA7TJWH.js.map} +1 -1
- package/dist/{chunk-DGDELPOD.js → chunk-CQMH3VP4.js} +3 -3
- package/dist/{chunk-DGDELPOD.js.map → chunk-CQMH3VP4.js.map} +1 -1
- package/dist/{chunk-IH2KJYAU.js → chunk-DHLCJTDF.js} +29 -17
- package/dist/chunk-DHLCJTDF.js.map +1 -0
- package/dist/{chunk-RVXBOKHK.js → chunk-DZTQI5RC.js} +3 -3
- package/dist/{chunk-RVXBOKHK.js.map → chunk-DZTQI5RC.js.map} +1 -1
- package/dist/{chunk-PRUCNSBI.js → chunk-HNKVHE4B.js} +4 -4
- package/dist/{chunk-PRUCNSBI.js.map → chunk-HNKVHE4B.js.map} +1 -1
- package/dist/{chunk-OZZCPTOG.js → chunk-MFI6ZSN6.js} +6 -6
- package/dist/{chunk-OZZCPTOG.js.map → chunk-MFI6ZSN6.js.map} +1 -1
- package/dist/{chunk-7SHXLJQP.js → chunk-RPZAX3II.js} +3 -3
- package/dist/{chunk-7SHXLJQP.js.map → chunk-RPZAX3II.js.map} +1 -1
- package/dist/{chunk-B7TA2XI7.js → chunk-TVYXXUEN.js} +2 -2
- package/dist/{chunk-B7TA2XI7.js.map → chunk-TVYXXUEN.js.map} +1 -1
- package/dist/{chunk-6GDVGAIZ.js → chunk-UJOTHTKU.js} +4 -4
- package/dist/{chunk-6GDVGAIZ.js.map → chunk-UJOTHTKU.js.map} +1 -1
- package/dist/{chunk-QVQH6GV2.js → chunk-UZGI2IYY.js} +2 -2
- package/dist/{chunk-QVQH6GV2.js.map → chunk-UZGI2IYY.js.map} +1 -1
- package/dist/cli/index.js +30 -30
- package/dist/connectors/index.js +3 -3
- package/dist/{ensure-sources-42LTJ5QI.js → ensure-sources-KHV6C6PZ.js} +4 -4
- package/dist/{ensure-sources-42LTJ5QI.js.map → ensure-sources-KHV6C6PZ.js.map} +1 -1
- package/dist/factory-assets/connectors/box.js +3 -3
- package/dist/factory-assets/connectors/deploy.js +3 -3
- package/dist/factory-assets/connectors/devserver.js +3 -3
- package/dist/factory-assets/connectors/flow/adapter.js +3 -3
- package/dist/factory-assets/connectors/flow/run-flow.js +4 -4
- package/dist/factory-assets/connectors/flow.js +3 -3
- package/dist/factory-assets/connectors/git.js +3 -3
- package/dist/factory-assets/connectors/gmail.js +3 -3
- package/dist/factory-assets/connectors/googledrive.js +3 -3
- package/dist/factory-assets/connectors/local.js +3 -3
- package/dist/factory-assets/connectors/mattermost.js +3 -3
- package/dist/factory-assets/connectors/memory.js +3 -3
- package/dist/factory-assets/connectors/minio.js +3 -3
- package/dist/factory-assets/connectors/postgres.js +3 -3
- package/dist/factory-assets/connectors/s3.js +3 -3
- package/dist/factory-assets/connectors/sharepoint.js +3 -3
- package/dist/factory-assets/connectors/sqlite.js +3 -3
- package/dist/factory-assets/connectors/static-server.js +3 -3
- package/dist/factory-assets/connectors/tunnel.js +3 -3
- package/dist/factory-assets/connectors/webdav.js +3 -3
- package/dist/factory-assets/connectors/xstate-store.js +3 -3
- package/dist/factory-assets/connectors/xstate.js +3 -3
- package/dist/{helpers-BP7FX6CO.js → helpers-BTEJZPZU.js} +4 -4
- package/dist/{helpers-BP7FX6CO.js.map → helpers-BTEJZPZU.js.map} +1 -1
- package/dist/{library-publish-X5Z32WJY.js → library-publish-FRA4ANLM.js} +6 -6
- package/dist/{library-publish-X5Z32WJY.js.map → library-publish-FRA4ANLM.js.map} +1 -1
- package/dist/{open-library-PHUMBYHP.js → open-library-PBWBUXLT.js} +5 -5
- package/dist/{open-library-PHUMBYHP.js.map → open-library-PBWBUXLT.js.map} +1 -1
- package/dist/{project-HAMCXPNE.js → project-H3I5WFCM.js} +5 -5
- package/dist/{project-HAMCXPNE.js.map → project-H3I5WFCM.js.map} +1 -1
- package/dist/runner/index.js +7 -7
- package/dist/runner/src/ai-credential-retry.d.ts +21 -7
- package/dist/runner/src/ai-credential-retry.d.ts.map +1 -1
- package/dist/runner/src/ai-provider-switch.d.ts +8 -1
- package/dist/runner/src/ai-provider-switch.d.ts.map +1 -1
- package/dist/runner/src/serve.d.ts.map +1 -1
- package/dist/sdk/asset-manager.js +1 -1
- package/dist/sdk/bridge.js +2 -2
- package/dist/sdk/index.js +7 -7
- package/dist/sdk/runner.js +7 -7
- package/dist/sdk/session.js +2 -2
- package/dist/sdk/types.js +1 -1
- package/dist/session/index.js +2 -2
- package/dist/{setup-IOOGR3EL.js → setup-ZB6U5MSS.js} +4 -4
- package/dist/{setup-IOOGR3EL.js.map → setup-ZB6U5MSS.js.map} +1 -1
- package/dist/{source-772GLNM6.js → source-B4LAWF4G.js} +6 -6
- package/dist/{source-772GLNM6.js.map → source-B4LAWF4G.js.map} +1 -1
- package/dist/{source-manifest-publish-LAUGDQ5G.js → source-manifest-publish-UO4VLBID.js} +7 -7
- package/dist/{source-manifest-publish-LAUGDQ5G.js.map → source-manifest-publish-UO4VLBID.js.map} +1 -1
- package/dist/tui/index.js +7 -7
- package/dist/types/index.js +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/lifecycle.d.ts +29 -0
- package/dist/types/src/lifecycle.d.ts.map +1 -1
- package/dist/types/src/version.d.ts +23 -1
- package/dist/types/src/version.d.ts.map +1 -1
- package/dist/workspace-plugin/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-IH2KJYAU.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,92 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.9.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#592](https://github.com/skaile-ai/workspaces/pull/592) [`cf74a3d`](https://github.com/skaile-ai/workspaces/commit/cf74a3d5f92fe945e7181e36d8f3170ab60b0ac3) Thanks [@peteralbert](https://github.com/peteralbert)! - protocol: say which rejection drove a `retry-401` on `host.refresh_credential`
|
|
8
|
+
|
|
9
|
+
`reason: 'retry-401'` collapsed two situations the platform has to repair differently.
|
|
10
|
+
Either the credential was rejected — rotate this seat's token — or the seat hit an
|
|
11
|
+
upstream usage limit, in which case the credential is fine and the only repair is
|
|
12
|
+
re-resolving the session onto a peer seat. Nothing on the wire distinguished them, so a
|
|
13
|
+
limit-blocked session pinned to an `AIProviderConfig` that is not opted into
|
|
14
|
+
`autoActivate` never re-resolved: the platform saw a credential problem, rotated a
|
|
15
|
+
perfectly good token, and left the session sitting on the exhausted seat.
|
|
16
|
+
|
|
17
|
+
The `ai-credentials` input gains an optional `rejection: 'auth' | 'usage-limit'`
|
|
18
|
+
(`CredentialRejection`, exported from `@skaile/workspaces/types`), attached only alongside
|
|
19
|
+
`reason: 'retry-401'`. Protocol `3.9.0 → 3.10.0`.
|
|
20
|
+
|
|
21
|
+
It is set only where the runner genuinely knows. `ClaudeSdkDriver` makes auth and limit
|
|
22
|
+
recovery mutually exclusive for a given failure, and each hook has exactly one way in:
|
|
23
|
+
`onAuthError` is reachable only from an `AuthError`, so it sends `'auth'`; `onLimitBlocked`
|
|
24
|
+
is reachable only from a seat-capacity classification, so it sends `'usage-limit'`. The
|
|
25
|
+
two call sites that classify nothing — the connector token mediator, which forwards a
|
|
26
|
+
`reason` the git driver picked because a credential-helper script touched a flag file, and
|
|
27
|
+
the MCP reauthorizer, a different credential domain entirely — send no `rejection` and
|
|
28
|
+
emit exactly the bytes they did before.
|
|
29
|
+
|
|
30
|
+
**`auth` is not a claim that capacity is fine.** `error-classifier.ts` records that
|
|
31
|
+
Anthropic mislabels quota exhaustion on Claude Max OAuth subscriptions as
|
|
32
|
+
`authentication_error`, so `'auth'` asserts only that the rejection was auth-shaped. Only
|
|
33
|
+
`'usage-limit'` is a positive assertion, and only it unlocks new platform behaviour — the
|
|
34
|
+
fallback for `'auth'` is today's, so a mislabel costs nothing new. Consumers must treat an
|
|
35
|
+
unrecognised value as if the field were absent, which is what lets a third rejection be
|
|
36
|
+
added later without another breaking change.
|
|
37
|
+
|
|
38
|
+
The single-flight re-pull in `ai-provider-switch.ts` learned the field too. Coalescing now
|
|
39
|
+
ranks requests — `refresh` < bare-or-`auth` `retry-401` < `usage-limit` `retry-401` — and an
|
|
40
|
+
in-flight re-pull may be adopted only by a request asking for no more than it does, because
|
|
41
|
+
adopting a round trip that never mentioned a limit would report success while the session
|
|
42
|
+
stayed parked. This is forward-compat, not a live fix: no caller produces a rank-1 re-pull
|
|
43
|
+
today (`onAuthError` mints directly rather than through `repull`), so in-flight entries are
|
|
44
|
+
only ever rank 0 or rank 2, and the pre-existing `refresh`/`retry-401` rule is a strict
|
|
45
|
+
special case of the rank. It is there because the type admits an `auth` re-pull and the next
|
|
46
|
+
caller through that seam will make one.
|
|
47
|
+
|
|
48
|
+
Additive in both directions. There is no peer-version gate, so a 3.9 platform does receive
|
|
49
|
+
the key on those two inputs — compatibility rests on a handler ignoring an unknown input
|
|
50
|
+
key, exactly as it already does for `rejectedFingerprint`, not on the key being absent. See
|
|
51
|
+
root `MIGRATION.md` for the wire contract and the one thing to check (strict input
|
|
52
|
+
validation).
|
|
53
|
+
|
|
54
|
+
### Patch Changes
|
|
55
|
+
|
|
56
|
+
- [#595](https://github.com/skaile-ai/workspaces/pull/595) [`af0ca25`](https://github.com/skaile-ai/workspaces/commit/af0ca251faf70068d85eb096031c4bf2400b8e19) Thanks [@peteralbert](https://github.com/peteralbert)! - claude-sdk: stop deleting a successful turn's answer when it merely discusses authentication
|
|
57
|
+
|
|
58
|
+
`ClaudeSdkDriver` drops the Claude CLI's own render of an upstream 401 on the promise that the credential self-heal will retry and re-render the real content. That suppression gated on `classifyClaudeSdkError`'s verdict alone — and the classifier buckets a bare `401`, `403`, `unauthorized` or `api key` as `auth`. An ordinary model answer about authentication ("the endpoint returns 401 Unauthorized when the bearer token is missing") therefore qualified, was dropped, and — when the turn then ended with a normal `{subtype:"success"}` result — no `AuthError` was ever built, no retry fired, and nothing re-emitted it. The user saw an empty turn with their answer deleted.
|
|
59
|
+
|
|
60
|
+
Both suppression sites now require the same CLI marker the stash already required (`Failed to authenticate`, `API Error:`, `authentication_error`, `not logged in`, `please run /login`, `OAuth authentication is currently not supported`) in addition to the classifier's verdict, via one shared predicate. No markers were added: every CLI auth render observed in this repo — the production rotation 401, `Not logged in · Please run /login`, and the OAuth-unsupported branch — already carries one, and the classifier's wide keyword bucket is the only auth path with no marker. Note that `textIsAuthRender`'s `auth`/`401`/`login` prefilter runs ahead of the classifier, so an `API Error: 403 {…"permission_error"…}` render (and a bare `Not logged in`, which has no `login` substring) never reaches the classifier at all and is not suppressed either way — pre-existing, untouched here, and narrowing rather than widening the effective surface.
|
|
61
|
+
|
|
62
|
+
**The streamed site had to move with the message site**, not as a symmetry preference. `normalizer.ts` skips a `message_start` whose text already streamed (`hasStreamedText`). Had the stream kept suppressing on the classifier alone while the assembled message required a marker, benign auth prose would have streamed its pre-keyword prefix, dropped every delta from the keyword onward, and then had its full-text `message_start` discarded by the normalizer — leaving a truncated answer instead of a whole one. The shared predicate also folds in the self-heal-availability check that only the message site performed, so a stream can no longer drop text nothing will ever retry.
|
|
63
|
+
|
|
64
|
+
**[#589](https://github.com/skaile-ai/workspaces/pull/589)'s stash is preserved: every genuine CLI 401 that fed the result path still does, and nothing new does.** `stashSuppressedAuthRender` is reachable only from these two sites and independently re-checks availability and the marker; both sites now compute `available ∧ marker ∧ classifier` (the message site additionally excluding tool calls, as before), which is the conjunction the stash was already applying. On the whole-message path the stashed set is therefore identical. On the streamed path it is _narrowed_ by exactly one class, because suppression there is a latch: the old code could combine a classifier verdict taken on an early prefix with a marker that only arrived later, even when the verdict had since moved off `auth`. Verified against the live classifier — `"Request returned 401. "` is `auth`, but `+ "Rate limit exceeded. API Error: retry later."` is `rate_limit` — the old code stashed that text, the new code requires both conditions on the same text and does not. That is the desired direction: it is rate-limit-shaped text that would otherwise have built an `AuthError` and spent a credential refresh. `stashSuppressedAuthRender`'s own guards are kept as defence in depth for a future third call site.
|
|
65
|
+
|
|
66
|
+
The bounded grace timer from [#498](https://github.com/skaile-ai/workspaces/pull/498) is untouched, and unchanged behaviourally: benign auth prose was never stashed, so a detail-free `is_error` result after it already classified `unknown` and already armed the timer — only the message's visibility changes.
|
|
67
|
+
|
|
68
|
+
**The cost, stated plainly:** for a genuine render whose marker arrives _after_ the classifying keyword, the transient stream prefix shown before suppression engages widens from "text before the keyword" to "text before the marker". That is zero-width for every render observed in this repo — `Failed to authenticate…`, `Not logged in · Please run /login`, and `OAuth authentication is currently not supported` all lead with their marker — and the change simultaneously removes a strictly worse case, where benign prose streamed a prefix and then had its `message_start` swallowed by the normalizer. Showing a moment of a 401 that is about to be recovered beats deleting a real answer.
|
|
69
|
+
|
|
70
|
+
## 3.8.1
|
|
71
|
+
|
|
72
|
+
### Patch Changes
|
|
73
|
+
|
|
74
|
+
- [#589](https://github.com/skaile-ai/workspaces/pull/589) [`14178d0`](https://github.com/skaile-ai/workspaces/commit/14178d0f427d1b8480f7ea370494dfe1192f532c) Thanks [@peteralbert](https://github.com/peteralbert)! - claude-sdk: classify an empty-`errors[]` `is_error` result from the auth render the same turn suppressed
|
|
75
|
+
|
|
76
|
+
When the Claude Agent SDK renders an upstream 401 as assistant text, `ClaudeSdkDriver` drops that text on the promise that the credential self-heal will retry and re-render the real content. On the production rotation shape — `{type:"result", subtype:"success", is_error:true, errors:[], terminal_reason:"completed"}` with no follow-up iterator throw — the self-heal was unreachable: `handleResultErrorPayload` called the classifier with an empty `errors[]`, which can only answer `unknown`, so no `AuthError` was ever constructed. The turn's only user-visible evidence had already been discarded, and the turn died with a detail-free failure.
|
|
77
|
+
|
|
78
|
+
The result path now falls back to the suppressed render's text when `errors[]` is empty, so the render and result paths reach the same verdict on the same text via the same classifier. A populated `errors[]` stays authoritative and is unchanged.
|
|
79
|
+
|
|
80
|
+
The fallback is deliberately narrower than suppression itself, because a wrong verdict here refreshes a credential, bumps that seat's health counters and replays the turn rather than merely hiding a message. Text is carried forward only when the self-heal is actually available _and_ it carries a marker the CLI itself emits (`Failed to authenticate`, `API Error:`, `authentication_error`, `not logged in`, `please run /login`, `OAuth authentication is currently not supported`) — so a model answer that merely discusses 401s cannot trigger a refresh, while the production render matches. Within that set a false positive costs at most one retry, which re-renders content the old behaviour dropped outright.
|
|
81
|
+
|
|
82
|
+
The bounded grace timer remains the backstop for genuinely detail-free failures, including an auth-shaped render that an authoritative `terminal_reason` outranks.
|
|
83
|
+
|
|
84
|
+
- [#587](https://github.com/skaile-ai/workspaces/pull/587) [`31ab187`](https://github.com/skaile-ai/workspaces/commit/31ab18757d81bc855132b4ca8c3263be84695b76) Thanks [@peteralbert](https://github.com/peteralbert)! - claude-sdk: bound the wait for an `is_error` result's follow-up throw so the turn always settles
|
|
85
|
+
|
|
86
|
+
When the Claude Agent SDK reported `{type:"result", subtype:"success", is_error:true, errors:[]}` and the classifier returned `unknown`, `ClaudeSdkDriver` deferred the settle to the consumer-error catch — which only runs if the SDK's iterator subsequently throws. That throw is not guaranteed (observed in production when a seat OAuth rotation revoked the prior access token and the next turn 401'd), so `prompt()` could stay pending forever, wedging the prompt FIFO until the 30-minute stall watchdog fired.
|
|
87
|
+
|
|
88
|
+
The deferral is kept — the follow-up throw carries the richer classification, and with it the 401 self-heal — but is now capped by a grace timer (default 5 s, override with `SKAILE_ERROR_RESULT_GRACE_MS`). On expiry the turn is failed with a plain `Error` carrying the classified retryable detail, never an `AuthError`. Behaviour when the throw does arrive is unchanged.
|
|
89
|
+
|
|
3
90
|
## 3.8.0
|
|
4
91
|
|
|
5
92
|
### Minor Changes
|
package/dist/app-sdk/index.js
CHANGED
|
@@ -65,7 +65,7 @@ async function libraryAssets() {
|
|
|
65
65
|
const domains = [];
|
|
66
66
|
try {
|
|
67
67
|
const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([
|
|
68
|
-
import('./open-library-
|
|
68
|
+
import('./open-library-PBWBUXLT.js'),
|
|
69
69
|
import('./library/index.js'),
|
|
70
70
|
import('./core/index.js')
|
|
71
71
|
]);
|
|
@@ -98,7 +98,7 @@ async function libraryAssets() {
|
|
|
98
98
|
async function storeAssets(projectDir) {
|
|
99
99
|
const notes = [];
|
|
100
100
|
try {
|
|
101
|
-
const { resolveCatalogSource } = await import('./open-library-
|
|
101
|
+
const { resolveCatalogSource } = await import('./open-library-PBWBUXLT.js');
|
|
102
102
|
const { source, close } = await resolveCatalogSource({ projectDir });
|
|
103
103
|
try {
|
|
104
104
|
const assets = await source.listAssets();
|
|
@@ -202,5 +202,5 @@ function dedupeBySlug(domains) {
|
|
|
202
202
|
}
|
|
203
203
|
|
|
204
204
|
export { gatherAssetFeeds };
|
|
205
|
-
//# sourceMappingURL=asset-feeds-
|
|
206
|
-
//# sourceMappingURL=asset-feeds-
|
|
205
|
+
//# sourceMappingURL=asset-feeds-4AEYY5P7.js.map
|
|
206
|
+
//# sourceMappingURL=asset-feeds-4AEYY5P7.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../cli/src/asset-feeds.ts"],"names":[],"mappings":"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","file":"asset-feeds-7HM3HONM.js","sourcesContent":["/**\n * asset-feeds.ts — gather installable assets from every feed the manage TUI\n * surfaces.\n *\n * Four feeds, normalized into `CatalogEntry`:\n * - **Project sources** — `skaile.yaml` `sources:` entries (scanned via\n * `AssetManager.search()`; the new `sources:` field is merged into the\n * legacy repositories map in `AssetManager.loadConfig`).\n * - **Local libraries** — user authoring places from `LocalIndex` (filesystem\n * scan of each library's `path`).\n * - **Factory assets** — the bundled first-party `factory-assets` tree\n * (`factoryAssetRoots()`), scanned from disk. Always present; no network.\n * - **Remote store** — the configured catalog source when `catalog.url` is\n * not `local`. Failures are non-fatal — they surface as notes.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n type AssetKind,\n type CatalogDomain,\n type CatalogEntry,\n factoryAssetRoots,\n parseFrontmatter,\n scanRepo,\n walkDir,\n} from \"@skaile/workspaces/core\";\nimport {\n assignDomain,\n buildDomainIndex,\n DOMAIN_MD_FILENAME,\n type DomainMd,\n parseDomainMd,\n} from \"@skaile/workspaces/discovery\";\n\n/** One non-fatal note returned to the caller for surfacing in the UI. */\nexport interface FeedNote {\n feed: \"sources\" | \"libraries\" | \"store\" | \"factory\";\n message: string;\n}\n\n/**\n * Which feed an asset came from. This is the top-level grouping axis in the\n * manage TUI (\"where is this skill coming from?\") and is intentionally distinct\n * from `publisher` (asset-intrinsic identity): the same publisher can legitimately\n * appear under more than one origin (e.g. a github repo you added as a `source`\n * that is also listed in the remote `store`).\n */\nexport type AssetOrigin = \"source\" | \"library\" | \"factory\" | \"store\";\n\n/** A catalog entry tagged with the feed it was gathered from. */\nexport interface OriginEntry {\n origin: AssetOrigin;\n entry: CatalogEntry;\n /**\n * Second-level grouping label for the manage tree. Defaults (when unset) to the\n * entry's `publisher`; the source feed sets it to the source's short repo name\n * so the Sources band groups by source — \"which repo did this come from\" — not\n * by the asset-intrinsic publisher.\n */\n group?: string;\n}\n\nexport interface AssetFeedResult {\n entries: OriginEntry[];\n /**\n * Display-only domain metadata, keyed (by the consumer) on `slug`. Populated\n * from the store's `catalog.listDomains` when the connected catalog source\n * exposes it; empty otherwise. The manage TUI joins `entry.domainSlug` → a\n * domain for grouping headers, descriptions, and relation badges.\n */\n domains: CatalogDomain[];\n notes: FeedNote[];\n}\n\n// ── Domain stamping (on-disk feeds) ──────────────────────────────────────────\n//\n// `scanRepo` (the source / library / factory scan path) does not read the\n// `DOMAIN.md` sidecars, so its entries arrive with no `domainSlug` and render\n// flat under their publisher band. We re-derive the grouping here from the\n// root's `DOMAIN.md` files (the same sidecars the remote store computes\n// server-side), and fall back to the repo/source name for any entry no domain\n// claims so it still groups instead of landing in the synthetic \"no domain\" pile.\n\n/** Repo-relative POSIX path (DOMAIN.md slugs are matched whole-segment, `/`-joined). */\nfunction toPosix(p: string): string {\n return p.split(path.sep).join(\"/\");\n}\n\n/** Parse every `DOMAIN.md` sidecar under `root` into a {@link DomainMd}. */\nasync function loadDomainMds(root: string): Promise<DomainMd[]> {\n const out: DomainMd[] = [];\n for await (const { path: filePath, name } of walkDir(root)) {\n if (name !== DOMAIN_MD_FILENAME) continue;\n try {\n const { data } = parseFrontmatter(readFileSync(filePath, \"utf8\"));\n out.push(parseDomainMd(toPosix(path.relative(root, filePath)), data));\n } catch {}\n }\n return out;\n}\n\n/**\n * Stamp `domainSlug` onto every entry scanned from `root` using the root's\n * `DOMAIN.md` sidecars. Returns the `CatalogDomain` registry for the real\n * domains, plus a synthetic fallback domain when any entry used the fallback.\n * Mutates `entries` in place (sets `entry.domainSlug`).\n *\n * `synthesizeFallback` (default true): entries no domain claims fall back to\n * `fallbackSlug` (the repo/source name). Pass `false` when the caller already\n * groups these entries one level up (e.g. the source feed groups by source\n * node) — unmatched entries then get no domain (`\"\"`) and hang directly under\n * that parent, instead of nesting under a same-named fallback domain.\n */\nasync function stampDomains(\n root: string,\n fallbackSlug: string,\n entries: CatalogEntry[],\n synthesizeFallback = true,\n): Promise<CatalogDomain[]> {\n const domainMds = await loadDomainMds(root);\n const assignables = entries.map((entry) => ({\n entry,\n ref: `${entry.kind}:${entry.name}`,\n relativePath: toPosix(path.relative(root, entry.source)),\n }));\n\n let usedFallback = false;\n for (const a of assignables) {\n const slug = assignDomain(a.relativePath, domainMds);\n if (slug) a.entry.domainSlug = slug;\n else if (synthesizeFallback) {\n a.entry.domainSlug = fallbackSlug;\n usedFallback = true;\n } else a.entry.domainSlug = \"\";\n }\n\n const { index } = buildDomainIndex(assignables, domainMds);\n const catalog: CatalogDomain[] = Object.values(index).map((d) => ({\n slug: d.slug,\n title: d.title,\n ...(d.description ? { description: d.description } : {}),\n ...(d.relation ? { relation: d.relation } : {}),\n ...(d.order !== undefined ? { order: d.order } : {}),\n ...(d.recommended[0] ? { recommendedRef: d.recommended[0] } : {}),\n }));\n if (usedFallback) catalog.push({ slug: fallbackSlug, title: fallbackSlug });\n return catalog;\n}\n\n/**\n * Pull assets from local libraries (the authoring places under\n * `~/.skaile/libraries/<name>/`, not the `~/.skaile/cache/sources/` cache). Each\n * library's `path` is scanned with `scanRepo` and grouped under the bare library\n * name (`group: <name>`) so the manage TUI shows a clean `<name>` band — no\n * `library:` prefix and no redundant same-named fallback domain (mirrors the\n * source feed).\n */\nasync function libraryAssets(): Promise<{\n entries: OriginEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([\n import(\"./open-library.ts\"),\n import(\"@skaile/workspaces/library\"),\n import(\"@skaile/workspaces/core\"),\n ]);\n const { manager, close } = await openLibraryManager();\n try {\n // Authoring libraries are the rows that are NOT source clones. index.db\n // rows predating the cache migration still carry the legacy\n // ~/.skaile/sources path, so exclude both roots.\n const newRoot = getGlobalCacheDir();\n const legacyRoot = path.join(skaileHomeDir(), \"sources\");\n const libs = (await manager.listLibraries()).filter(\n (l) => !l.path.startsWith(newRoot) && !l.path.startsWith(legacyRoot),\n );\n const entries: OriginEntry[] = [];\n for (const lib of libs) {\n if (!existsSync(lib.path)) continue;\n // Scan + group under the bare library name. `synthesizeFallback: false`\n // (as the source feed does) keeps domainless assets directly under the\n // library band instead of a redundant same-named fallback domain.\n const libEntries = [...(await scanRepo(lib.path, lib.name))];\n domains.push(...(await stampDomains(lib.path, lib.name, libEntries, false)));\n for (const entry of libEntries) entries.push({ origin: \"library\", entry, group: lib.name });\n }\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n notes.push({\n feed: \"libraries\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/**\n * Pull a (best-effort) snapshot of the connected remote store catalog.\n *\n * - Returns no entries when the resolved catalog is local-only or the host\n * is unreachable (the failure becomes a note, not an exception).\n * - Maps `CatalogAsset` → `CatalogEntry`, carrying `publisher`, `repo`, and\n * `category` straight through and using `source = \"store://<id>\"`; per-file\n * `source` paths are not available for remote assets so the canonical ref is\n * a stable placeholder.\n */\nasync function storeAssets(projectDir: string): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n try {\n const { resolveCatalogSource } = await import(\"./open-library.ts\");\n const { source, close } = await resolveCatalogSource({ projectDir });\n try {\n const assets = await source.listAssets();\n const entries: CatalogEntry[] = assets.map((a) => ({\n name: a.name,\n kind: (a.kind as AssetKind) ?? \"skill\",\n description: a.description ?? \"\",\n source: `store://${a.id}`,\n publisher: a.publisher,\n repo: a.repo,\n domainSlug: a.domainSlug,\n category: a.category,\n version: a.version,\n requires: [],\n dependencies: [],\n }));\n // Domains are optional on the catalog source — only the store backend\n // (post Part 4 `catalog.listDomains`) exposes them. Duck-type the call so\n // older sources and the local catalog degrade to no domains, not an error.\n const domains = await loadDomains(source);\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n // Local-only catalog throws a recognizable message — silence it; absence\n // is the user's choice, not an error.\n if (!/local/i.test(msg)) {\n notes.push({ feed: \"store\", message: msg });\n }\n return { entries: [], domains: [], notes };\n }\n}\n\n/** Optional `listDomains` capability — returns `[]` when the source lacks it. */\nasync function loadDomains(source: unknown): Promise<CatalogDomain[]> {\n const fn = (source as { listDomains?: () => Promise<CatalogDomain[]> }).listDomains;\n if (typeof fn !== \"function\") return [];\n const domains = await fn.call(source);\n return Array.isArray(domains) ? domains : [];\n}\n\n/** The slice of `AssetManager` the source feed needs (kept narrow for tests). */\ninterface AssetManagerLike {\n search: () => Promise<CatalogEntry[]>;\n searchRepoRoots: () => { repoName: string; root: string }[];\n}\n\n/**\n * Project-source feed: leans on `AssetManager.search()`, which now merges\n * `skaile.yaml`'s `sources:` field into the repositories map, then stamps\n * `DOMAIN.md`-derived domain grouping onto the scanned entries via\n * `searchRepoRoots()`.\n */\nasync function sourceAssets(\n am: AssetManagerLike,\n): Promise<{ entries: OriginEntry[]; domains: CatalogDomain[] }> {\n // The bundled factory tree is injected into the repositories map as an implicit\n // source, so `am.search()` returns it too. It has its own `factory` origin band,\n // so drop it here — otherwise the same first-party skills list twice under Sources.\n const factoryPrefixes = factoryAssetRoots().map((r) => (r.endsWith(path.sep) ? r : r + path.sep));\n const isFactoryPath = (p: string) =>\n factoryPrefixes.some((fp) => p === fp.slice(0, -1) || p.startsWith(fp));\n\n const all = (await am.search()).filter((e) => !isFactoryPath(e.source));\n const entries: OriginEntry[] = [];\n const domains: CatalogDomain[] = [];\n // Group each source's entries under the source node (its short repo name), not\n // the publisher: under Sources the navigational question is \"which repo did\n // this come from\". Each entry's `source` (absolute manifest path) lives under\n // exactly one declared root, so group + stamp by prefix. The cache slug is\n // URL-keyed `github.com/<org>/<repo>` — take the trailing segment.\n for (const { repoName, root } of am.searchRepoRoots()) {\n if (isFactoryPath(root)) continue;\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n const owned = all.filter((e) => e.source === root || e.source.startsWith(prefix));\n if (owned.length === 0) continue;\n const sourceName = repoName.split(\"/\").pop() || repoName;\n // No synthetic fallback domain — the source node already groups these, so a\n // same-named fallback would just nest the source under itself.\n domains.push(...(await stampDomains(root, sourceName, owned, false)));\n for (const entry of owned) entries.push({ origin: \"source\", entry, group: sourceName });\n }\n return { entries, domains };\n}\n\n/**\n * Factory-asset feed: scan the bundled first-party `factory-assets` tree(s)\n * resolved by `factoryAssetRoots()` (the `SKAILE_FACTORY_ASSETS_DIR` override\n * layer ahead of the bundled tree). Local disk only, no network — the entries are\n * the built-in skills / connectors / bundles that ship with `@skaile/workspaces`.\n * `factoryAssetRoots()` already dedupes, so the env override pointing at the\n * bundled dir collapses to one root.\n */\nasync function factoryAssets(): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const entries: CatalogEntry[] = [];\n for (const root of factoryAssetRoots()) {\n const rootEntries = [...(await scanRepo(root, \"skaile-factory\"))];\n domains.push(...(await stampDomains(root, \"skaile-factory\", rootEntries)));\n entries.push(...rootEntries);\n }\n return { entries, domains, notes };\n } catch (err) {\n notes.push({\n feed: \"factory\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/** Tag a flat entry list with a single origin. */\nfunction tag(origin: AssetOrigin, entries: CatalogEntry[]): OriginEntry[] {\n return entries.map((entry) => ({ origin, entry }));\n}\n\n/**\n * Run all feeds and return a single normalized result.\n *\n * Feeds run in parallel where possible — the store fetch is the slow one\n * (one HTTP call) and is fired alongside the local library walk. The factory\n * scan is local disk only, so it runs inline.\n */\nexport async function gatherAssetFeeds(\n am: AssetManagerLike,\n projectDir: string,\n): Promise<AssetFeedResult> {\n const [libs, store] = await Promise.all([libraryAssets(), storeAssets(projectDir)]);\n const factory = await factoryAssets();\n const src = await sourceAssets(am);\n return {\n entries: [\n ...src.entries,\n ...libs.entries,\n ...tag(\"factory\", factory.entries),\n ...tag(\"store\", store.entries),\n ],\n // Store domains first (server-curated), then the on-disk feeds; dedupe by\n // slug, first-wins, so a store-served domain shadows a same-slug local one.\n domains: dedupeBySlug([...store.domains, ...src.domains, ...libs.domains, ...factory.domains]),\n notes: [...libs.notes, ...store.notes, ...factory.notes],\n };\n}\n\n/** First-wins dedupe of a domain list by `slug`. */\nfunction dedupeBySlug(domains: CatalogDomain[]): CatalogDomain[] {\n const seen = new Set<string>();\n const out: CatalogDomain[] = [];\n for (const d of domains) {\n if (seen.has(d.slug)) continue;\n seen.add(d.slug);\n out.push(d);\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../cli/src/asset-feeds.ts"],"names":[],"mappings":"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","file":"asset-feeds-4AEYY5P7.js","sourcesContent":["/**\n * asset-feeds.ts — gather installable assets from every feed the manage TUI\n * surfaces.\n *\n * Four feeds, normalized into `CatalogEntry`:\n * - **Project sources** — `skaile.yaml` `sources:` entries (scanned via\n * `AssetManager.search()`; the new `sources:` field is merged into the\n * legacy repositories map in `AssetManager.loadConfig`).\n * - **Local libraries** — user authoring places from `LocalIndex` (filesystem\n * scan of each library's `path`).\n * - **Factory assets** — the bundled first-party `factory-assets` tree\n * (`factoryAssetRoots()`), scanned from disk. Always present; no network.\n * - **Remote store** — the configured catalog source when `catalog.url` is\n * not `local`. Failures are non-fatal — they surface as notes.\n */\n\nimport { existsSync, readFileSync } from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n type AssetKind,\n type CatalogDomain,\n type CatalogEntry,\n factoryAssetRoots,\n parseFrontmatter,\n scanRepo,\n walkDir,\n} from \"@skaile/workspaces/core\";\nimport {\n assignDomain,\n buildDomainIndex,\n DOMAIN_MD_FILENAME,\n type DomainMd,\n parseDomainMd,\n} from \"@skaile/workspaces/discovery\";\n\n/** One non-fatal note returned to the caller for surfacing in the UI. */\nexport interface FeedNote {\n feed: \"sources\" | \"libraries\" | \"store\" | \"factory\";\n message: string;\n}\n\n/**\n * Which feed an asset came from. This is the top-level grouping axis in the\n * manage TUI (\"where is this skill coming from?\") and is intentionally distinct\n * from `publisher` (asset-intrinsic identity): the same publisher can legitimately\n * appear under more than one origin (e.g. a github repo you added as a `source`\n * that is also listed in the remote `store`).\n */\nexport type AssetOrigin = \"source\" | \"library\" | \"factory\" | \"store\";\n\n/** A catalog entry tagged with the feed it was gathered from. */\nexport interface OriginEntry {\n origin: AssetOrigin;\n entry: CatalogEntry;\n /**\n * Second-level grouping label for the manage tree. Defaults (when unset) to the\n * entry's `publisher`; the source feed sets it to the source's short repo name\n * so the Sources band groups by source — \"which repo did this come from\" — not\n * by the asset-intrinsic publisher.\n */\n group?: string;\n}\n\nexport interface AssetFeedResult {\n entries: OriginEntry[];\n /**\n * Display-only domain metadata, keyed (by the consumer) on `slug`. Populated\n * from the store's `catalog.listDomains` when the connected catalog source\n * exposes it; empty otherwise. The manage TUI joins `entry.domainSlug` → a\n * domain for grouping headers, descriptions, and relation badges.\n */\n domains: CatalogDomain[];\n notes: FeedNote[];\n}\n\n// ── Domain stamping (on-disk feeds) ──────────────────────────────────────────\n//\n// `scanRepo` (the source / library / factory scan path) does not read the\n// `DOMAIN.md` sidecars, so its entries arrive with no `domainSlug` and render\n// flat under their publisher band. We re-derive the grouping here from the\n// root's `DOMAIN.md` files (the same sidecars the remote store computes\n// server-side), and fall back to the repo/source name for any entry no domain\n// claims so it still groups instead of landing in the synthetic \"no domain\" pile.\n\n/** Repo-relative POSIX path (DOMAIN.md slugs are matched whole-segment, `/`-joined). */\nfunction toPosix(p: string): string {\n return p.split(path.sep).join(\"/\");\n}\n\n/** Parse every `DOMAIN.md` sidecar under `root` into a {@link DomainMd}. */\nasync function loadDomainMds(root: string): Promise<DomainMd[]> {\n const out: DomainMd[] = [];\n for await (const { path: filePath, name } of walkDir(root)) {\n if (name !== DOMAIN_MD_FILENAME) continue;\n try {\n const { data } = parseFrontmatter(readFileSync(filePath, \"utf8\"));\n out.push(parseDomainMd(toPosix(path.relative(root, filePath)), data));\n } catch {}\n }\n return out;\n}\n\n/**\n * Stamp `domainSlug` onto every entry scanned from `root` using the root's\n * `DOMAIN.md` sidecars. Returns the `CatalogDomain` registry for the real\n * domains, plus a synthetic fallback domain when any entry used the fallback.\n * Mutates `entries` in place (sets `entry.domainSlug`).\n *\n * `synthesizeFallback` (default true): entries no domain claims fall back to\n * `fallbackSlug` (the repo/source name). Pass `false` when the caller already\n * groups these entries one level up (e.g. the source feed groups by source\n * node) — unmatched entries then get no domain (`\"\"`) and hang directly under\n * that parent, instead of nesting under a same-named fallback domain.\n */\nasync function stampDomains(\n root: string,\n fallbackSlug: string,\n entries: CatalogEntry[],\n synthesizeFallback = true,\n): Promise<CatalogDomain[]> {\n const domainMds = await loadDomainMds(root);\n const assignables = entries.map((entry) => ({\n entry,\n ref: `${entry.kind}:${entry.name}`,\n relativePath: toPosix(path.relative(root, entry.source)),\n }));\n\n let usedFallback = false;\n for (const a of assignables) {\n const slug = assignDomain(a.relativePath, domainMds);\n if (slug) a.entry.domainSlug = slug;\n else if (synthesizeFallback) {\n a.entry.domainSlug = fallbackSlug;\n usedFallback = true;\n } else a.entry.domainSlug = \"\";\n }\n\n const { index } = buildDomainIndex(assignables, domainMds);\n const catalog: CatalogDomain[] = Object.values(index).map((d) => ({\n slug: d.slug,\n title: d.title,\n ...(d.description ? { description: d.description } : {}),\n ...(d.relation ? { relation: d.relation } : {}),\n ...(d.order !== undefined ? { order: d.order } : {}),\n ...(d.recommended[0] ? { recommendedRef: d.recommended[0] } : {}),\n }));\n if (usedFallback) catalog.push({ slug: fallbackSlug, title: fallbackSlug });\n return catalog;\n}\n\n/**\n * Pull assets from local libraries (the authoring places under\n * `~/.skaile/libraries/<name>/`, not the `~/.skaile/cache/sources/` cache). Each\n * library's `path` is scanned with `scanRepo` and grouped under the bare library\n * name (`group: <name>`) so the manage TUI shows a clean `<name>` band — no\n * `library:` prefix and no redundant same-named fallback domain (mirrors the\n * source feed).\n */\nasync function libraryAssets(): Promise<{\n entries: OriginEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const [{ openLibraryManager }, { skaileHomeDir }, { getGlobalCacheDir }] = await Promise.all([\n import(\"./open-library.ts\"),\n import(\"@skaile/workspaces/library\"),\n import(\"@skaile/workspaces/core\"),\n ]);\n const { manager, close } = await openLibraryManager();\n try {\n // Authoring libraries are the rows that are NOT source clones. index.db\n // rows predating the cache migration still carry the legacy\n // ~/.skaile/sources path, so exclude both roots.\n const newRoot = getGlobalCacheDir();\n const legacyRoot = path.join(skaileHomeDir(), \"sources\");\n const libs = (await manager.listLibraries()).filter(\n (l) => !l.path.startsWith(newRoot) && !l.path.startsWith(legacyRoot),\n );\n const entries: OriginEntry[] = [];\n for (const lib of libs) {\n if (!existsSync(lib.path)) continue;\n // Scan + group under the bare library name. `synthesizeFallback: false`\n // (as the source feed does) keeps domainless assets directly under the\n // library band instead of a redundant same-named fallback domain.\n const libEntries = [...(await scanRepo(lib.path, lib.name))];\n domains.push(...(await stampDomains(lib.path, lib.name, libEntries, false)));\n for (const entry of libEntries) entries.push({ origin: \"library\", entry, group: lib.name });\n }\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n notes.push({\n feed: \"libraries\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/**\n * Pull a (best-effort) snapshot of the connected remote store catalog.\n *\n * - Returns no entries when the resolved catalog is local-only or the host\n * is unreachable (the failure becomes a note, not an exception).\n * - Maps `CatalogAsset` → `CatalogEntry`, carrying `publisher`, `repo`, and\n * `category` straight through and using `source = \"store://<id>\"`; per-file\n * `source` paths are not available for remote assets so the canonical ref is\n * a stable placeholder.\n */\nasync function storeAssets(projectDir: string): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n try {\n const { resolveCatalogSource } = await import(\"./open-library.ts\");\n const { source, close } = await resolveCatalogSource({ projectDir });\n try {\n const assets = await source.listAssets();\n const entries: CatalogEntry[] = assets.map((a) => ({\n name: a.name,\n kind: (a.kind as AssetKind) ?? \"skill\",\n description: a.description ?? \"\",\n source: `store://${a.id}`,\n publisher: a.publisher,\n repo: a.repo,\n domainSlug: a.domainSlug,\n category: a.category,\n version: a.version,\n requires: [],\n dependencies: [],\n }));\n // Domains are optional on the catalog source — only the store backend\n // (post Part 4 `catalog.listDomains`) exposes them. Duck-type the call so\n // older sources and the local catalog degrade to no domains, not an error.\n const domains = await loadDomains(source);\n return { entries, domains, notes };\n } finally {\n close();\n }\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n // Local-only catalog throws a recognizable message — silence it; absence\n // is the user's choice, not an error.\n if (!/local/i.test(msg)) {\n notes.push({ feed: \"store\", message: msg });\n }\n return { entries: [], domains: [], notes };\n }\n}\n\n/** Optional `listDomains` capability — returns `[]` when the source lacks it. */\nasync function loadDomains(source: unknown): Promise<CatalogDomain[]> {\n const fn = (source as { listDomains?: () => Promise<CatalogDomain[]> }).listDomains;\n if (typeof fn !== \"function\") return [];\n const domains = await fn.call(source);\n return Array.isArray(domains) ? domains : [];\n}\n\n/** The slice of `AssetManager` the source feed needs (kept narrow for tests). */\ninterface AssetManagerLike {\n search: () => Promise<CatalogEntry[]>;\n searchRepoRoots: () => { repoName: string; root: string }[];\n}\n\n/**\n * Project-source feed: leans on `AssetManager.search()`, which now merges\n * `skaile.yaml`'s `sources:` field into the repositories map, then stamps\n * `DOMAIN.md`-derived domain grouping onto the scanned entries via\n * `searchRepoRoots()`.\n */\nasync function sourceAssets(\n am: AssetManagerLike,\n): Promise<{ entries: OriginEntry[]; domains: CatalogDomain[] }> {\n // The bundled factory tree is injected into the repositories map as an implicit\n // source, so `am.search()` returns it too. It has its own `factory` origin band,\n // so drop it here — otherwise the same first-party skills list twice under Sources.\n const factoryPrefixes = factoryAssetRoots().map((r) => (r.endsWith(path.sep) ? r : r + path.sep));\n const isFactoryPath = (p: string) =>\n factoryPrefixes.some((fp) => p === fp.slice(0, -1) || p.startsWith(fp));\n\n const all = (await am.search()).filter((e) => !isFactoryPath(e.source));\n const entries: OriginEntry[] = [];\n const domains: CatalogDomain[] = [];\n // Group each source's entries under the source node (its short repo name), not\n // the publisher: under Sources the navigational question is \"which repo did\n // this come from\". Each entry's `source` (absolute manifest path) lives under\n // exactly one declared root, so group + stamp by prefix. The cache slug is\n // URL-keyed `github.com/<org>/<repo>` — take the trailing segment.\n for (const { repoName, root } of am.searchRepoRoots()) {\n if (isFactoryPath(root)) continue;\n const prefix = root.endsWith(path.sep) ? root : root + path.sep;\n const owned = all.filter((e) => e.source === root || e.source.startsWith(prefix));\n if (owned.length === 0) continue;\n const sourceName = repoName.split(\"/\").pop() || repoName;\n // No synthetic fallback domain — the source node already groups these, so a\n // same-named fallback would just nest the source under itself.\n domains.push(...(await stampDomains(root, sourceName, owned, false)));\n for (const entry of owned) entries.push({ origin: \"source\", entry, group: sourceName });\n }\n return { entries, domains };\n}\n\n/**\n * Factory-asset feed: scan the bundled first-party `factory-assets` tree(s)\n * resolved by `factoryAssetRoots()` (the `SKAILE_FACTORY_ASSETS_DIR` override\n * layer ahead of the bundled tree). Local disk only, no network — the entries are\n * the built-in skills / connectors / bundles that ship with `@skaile/workspaces`.\n * `factoryAssetRoots()` already dedupes, so the env override pointing at the\n * bundled dir collapses to one root.\n */\nasync function factoryAssets(): Promise<{\n entries: CatalogEntry[];\n domains: CatalogDomain[];\n notes: FeedNote[];\n}> {\n const notes: FeedNote[] = [];\n const domains: CatalogDomain[] = [];\n try {\n const entries: CatalogEntry[] = [];\n for (const root of factoryAssetRoots()) {\n const rootEntries = [...(await scanRepo(root, \"skaile-factory\"))];\n domains.push(...(await stampDomains(root, \"skaile-factory\", rootEntries)));\n entries.push(...rootEntries);\n }\n return { entries, domains, notes };\n } catch (err) {\n notes.push({\n feed: \"factory\",\n message: err instanceof Error ? err.message : String(err),\n });\n return { entries: [], domains, notes };\n }\n}\n\n/** Tag a flat entry list with a single origin. */\nfunction tag(origin: AssetOrigin, entries: CatalogEntry[]): OriginEntry[] {\n return entries.map((entry) => ({ origin, entry }));\n}\n\n/**\n * Run all feeds and return a single normalized result.\n *\n * Feeds run in parallel where possible — the store fetch is the slow one\n * (one HTTP call) and is fired alongside the local library walk. The factory\n * scan is local disk only, so it runs inline.\n */\nexport async function gatherAssetFeeds(\n am: AssetManagerLike,\n projectDir: string,\n): Promise<AssetFeedResult> {\n const [libs, store] = await Promise.all([libraryAssets(), storeAssets(projectDir)]);\n const factory = await factoryAssets();\n const src = await sourceAssets(am);\n return {\n entries: [\n ...src.entries,\n ...libs.entries,\n ...tag(\"factory\", factory.entries),\n ...tag(\"store\", store.entries),\n ],\n // Store domains first (server-curated), then the on-disk feeds; dedupe by\n // slug, first-wins, so a store-served domain shadows a same-slug local one.\n domains: dedupeBySlug([...store.domains, ...src.domains, ...libs.domains, ...factory.domains]),\n notes: [...libs.notes, ...store.notes, ...factory.notes],\n };\n}\n\n/** First-wins dedupe of a domain list by `slug`. */\nfunction dedupeBySlug(domains: CatalogDomain[]): CatalogDomain[] {\n const seen = new Set<string>();\n const out: CatalogDomain[] = [];\n for (const d of domains) {\n if (seen.has(d.slug)) continue;\n seen.add(d.slug);\n out.push(d);\n }\n return out;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { AssetManager, FACTORY_SOURCE_URL, MissingManifestError, NON_DEPLOYABLE_KINDS, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName } from '../chunk-
|
|
1
|
+
export { AssetManager, FACTORY_SOURCE_URL, MissingManifestError, NON_DEPLOYABLE_KINDS, normalizeRepoUrl, parseCanonicalLockKey, parseRefTolerant, renderMcpServerConfig, softDepName } from '../chunk-TVYXXUEN.js';
|
|
2
2
|
import '../chunk-OCLCDB7G.js';
|
|
3
3
|
import '../chunk-PW4SJE3X.js';
|
|
4
4
|
import '../chunk-UKJMQZV6.js';
|
|
@@ -2,7 +2,7 @@ import { scrubPoisonedTranscript, resolveClaudeConfigDir, SUPPORTED_CLOUDS, buil
|
|
|
2
2
|
import { dispatchCapability } from '../../chunk-RRVQAE5D.js';
|
|
3
3
|
import { fetchProviderModels } from '../../chunk-V34ZEXSE.js';
|
|
4
4
|
import { AuthError, classifyClaudeSdkError, reclassifyAuthAsAccountUnavailable, AccountUnavailableError } from '../../chunk-GAH4YRCI.js';
|
|
5
|
-
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-
|
|
5
|
+
import { BUILTIN_DRIVER_CATALOG, AgentDriver, getBridgeLogger } from '../../chunk-BIA7TJWH.js';
|
|
6
6
|
import '../../chunk-6E6PKKAD.js';
|
|
7
7
|
import '../../chunk-AZPXCHDK.js';
|
|
8
8
|
import '../../chunk-F3HNW354.js';
|
|
@@ -183,6 +183,23 @@ function firstMessageStallMs() {
|
|
|
183
183
|
const value = Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_FIRST_MESSAGE_STALL_MS;
|
|
184
184
|
return Math.min(value, turnStallMs());
|
|
185
185
|
}
|
|
186
|
+
var CLI_AUTH_RENDER_MARKERS = [
|
|
187
|
+
"failed to authenticate",
|
|
188
|
+
"api error:",
|
|
189
|
+
"authentication_error",
|
|
190
|
+
"not logged in",
|
|
191
|
+
"please run /login",
|
|
192
|
+
"oauth authentication is currently not supported"
|
|
193
|
+
];
|
|
194
|
+
function isCliAuthRenderText(text) {
|
|
195
|
+
const lower = text.toLowerCase();
|
|
196
|
+
return CLI_AUTH_RENDER_MARKERS.some((marker) => lower.includes(marker));
|
|
197
|
+
}
|
|
198
|
+
var DEFAULT_ERROR_RESULT_GRACE_MS = 5e3;
|
|
199
|
+
function errorResultGraceMs() {
|
|
200
|
+
const raw = Number(process.env.SKAILE_ERROR_RESULT_GRACE_MS);
|
|
201
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_ERROR_RESULT_GRACE_MS;
|
|
202
|
+
}
|
|
186
203
|
var RECYCLE_TURNS_FRACTION = 0.8;
|
|
187
204
|
var MIN_RECYCLE_HEADROOM = 5;
|
|
188
205
|
var PushableInputStream = class {
|
|
@@ -246,6 +263,16 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
246
263
|
emittedSanitizedLen = 0;
|
|
247
264
|
/** Set once the streaming text of the current message classifies as a self-healable auth render. */
|
|
248
265
|
suppressingAuthStream = false;
|
|
266
|
+
/**
|
|
267
|
+
* The auth render this attempt dropped on the promise that a self-heal would
|
|
268
|
+
* retry — the evidence {@link handleResultErrorPayload} otherwise lacks when
|
|
269
|
+
* the SDK reports the same 401 as an `is_error` result with an empty
|
|
270
|
+
* `errors[]`. Written only through {@link stashSuppressedAuthRender}, which
|
|
271
|
+
* applies a stricter test than suppression does; see its docblock. Cleared at
|
|
272
|
+
* both ends of {@link runPrompt}, so it can only ever describe the turn that
|
|
273
|
+
* is failing.
|
|
274
|
+
*/
|
|
275
|
+
suppressedAuthRenderText = null;
|
|
249
276
|
/** Active query — stays alive between turns (streaming input mode). */
|
|
250
277
|
query = null;
|
|
251
278
|
/**
|
|
@@ -287,6 +314,12 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
287
314
|
lastSdkMessageAt = 0;
|
|
288
315
|
/** Armed while a turn is in flight; trips when the SDK goes silent for too long. */
|
|
289
316
|
turnStallTimer = null;
|
|
317
|
+
/**
|
|
318
|
+
* Armed only while an `is_error` result waits for its follow-up iterator
|
|
319
|
+
* throw; fails the turn if that throw never lands. Cleared at every turn
|
|
320
|
+
* start and settle site so it can never fire into a later turn.
|
|
321
|
+
*/
|
|
322
|
+
errorResultGraceTimer = null;
|
|
290
323
|
/**
|
|
291
324
|
* Mirror of the in-flight {@link prompt} call's auth retry budget: `false`
|
|
292
325
|
* while the `onAuthError` self-heal is still available, `true` once it has
|
|
@@ -489,7 +522,9 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
489
522
|
this.prevText = "";
|
|
490
523
|
this.emittedSanitizedLen = 0;
|
|
491
524
|
this.suppressingAuthStream = false;
|
|
525
|
+
this.suppressedAuthRenderText = null;
|
|
492
526
|
this.turnCompleted = false;
|
|
527
|
+
this.disarmErrorResultGrace();
|
|
493
528
|
if (this.query && (this.inputStream == null || this.inputStream.ended || this.shouldRecycleQuery())) {
|
|
494
529
|
this.recycleQueryForNextTurn();
|
|
495
530
|
}
|
|
@@ -504,6 +539,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
504
539
|
await this.startQuery(message);
|
|
505
540
|
}
|
|
506
541
|
} catch (err) {
|
|
542
|
+
this.disarmErrorResultGrace();
|
|
507
543
|
this.turnResolve = null;
|
|
508
544
|
this.turnReject = null;
|
|
509
545
|
this.running = false;
|
|
@@ -558,6 +594,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
558
594
|
} finally {
|
|
559
595
|
if (!retrying) {
|
|
560
596
|
this.disarmTurnStallWatchdog();
|
|
597
|
+
this.disarmErrorResultGrace();
|
|
561
598
|
this.turnResolve = null;
|
|
562
599
|
this.turnReject = null;
|
|
563
600
|
this.running = false;
|
|
@@ -565,6 +602,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
565
602
|
this.limitTurnRejectedForFailover = false;
|
|
566
603
|
this.lastLimitInfo = null;
|
|
567
604
|
this.deferredTurnError = null;
|
|
605
|
+
this.suppressedAuthRenderText = null;
|
|
568
606
|
}
|
|
569
607
|
}
|
|
570
608
|
}
|
|
@@ -611,6 +649,42 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
611
649
|
this.turnStallTimer = null;
|
|
612
650
|
}
|
|
613
651
|
}
|
|
652
|
+
/**
|
|
653
|
+
* Bound the wait for the follow-up throw an `is_error` result implies. The
|
|
654
|
+
* throw carries the real, richly-classified error, so the deferral stays —
|
|
655
|
+
* this only caps it, turning a permanently pending turn into a bounded
|
|
656
|
+
* failure. Never arms onto an already-settled turn: an auth / account / limit
|
|
657
|
+
* payload has run `failTurn` by the time this is reached, and the same guard
|
|
658
|
+
* is re-checked inside the callback because the throw normally lands in
|
|
659
|
+
* between. Note 2026-06-08-claude-sdk-error-result-handling.
|
|
660
|
+
*/
|
|
661
|
+
armErrorResultGrace(detail, context) {
|
|
662
|
+
this.disarmErrorResultGrace();
|
|
663
|
+
if (this.turnCompleted || this.turnReject == null) return;
|
|
664
|
+
this.errorResultGraceTimer = setTimeout(() => {
|
|
665
|
+
this.errorResultGraceTimer = null;
|
|
666
|
+
if (this.turnCompleted || this.turnReject == null) return;
|
|
667
|
+
this.log.error("is_error result never produced its follow-up throw; failing the turn", {
|
|
668
|
+
subtype: context.subtype,
|
|
669
|
+
terminalReason: context.terminalReason,
|
|
670
|
+
category: detail.category
|
|
671
|
+
});
|
|
672
|
+
const message = "The model reported an error but sent no details. The conversation is intact \u2014 send your message again.";
|
|
673
|
+
this.failTurn(new Error(message), { ...detail, message });
|
|
674
|
+
}, errorResultGraceMs());
|
|
675
|
+
this.errorResultGraceTimer.unref?.();
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* The retry helpers that null `turnResolve`/`turnReject` before recursing
|
|
679
|
+
* need no call: they run only from `runPrompt`'s catch, which is reached only
|
|
680
|
+
* because `failTurn` or `rejectTurnSilently` rejected — and both disarm first.
|
|
681
|
+
*/
|
|
682
|
+
disarmErrorResultGrace() {
|
|
683
|
+
if (this.errorResultGraceTimer) {
|
|
684
|
+
clearTimeout(this.errorResultGraceTimer);
|
|
685
|
+
this.errorResultGraceTimer = null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
614
688
|
/**
|
|
615
689
|
* The SDK went silent mid-turn. Tear the query down so the *next* prompt
|
|
616
690
|
* starts a fresh one (a live-but-deaf query would otherwise swallow every
|
|
@@ -1399,6 +1473,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1399
1473
|
if (this.turnCompleted) return;
|
|
1400
1474
|
this.turnCompleted = true;
|
|
1401
1475
|
this.disarmTurnStallWatchdog();
|
|
1476
|
+
this.disarmErrorResultGrace();
|
|
1402
1477
|
if (this.turnResolve) {
|
|
1403
1478
|
this.turnResolve();
|
|
1404
1479
|
this.turnResolve = null;
|
|
@@ -1429,6 +1504,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1429
1504
|
*/
|
|
1430
1505
|
rejectTurnSilently(err) {
|
|
1431
1506
|
this.disarmTurnStallWatchdog();
|
|
1507
|
+
this.disarmErrorResultGrace();
|
|
1432
1508
|
this.turnCompleted = true;
|
|
1433
1509
|
if (this.turnReject) {
|
|
1434
1510
|
const reject = this.turnReject;
|
|
@@ -1471,6 +1547,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1471
1547
|
*/
|
|
1472
1548
|
failTurn(err, detail) {
|
|
1473
1549
|
this.disarmTurnStallWatchdog();
|
|
1550
|
+
this.disarmErrorResultGrace();
|
|
1474
1551
|
const hadPendingTurn = this.turnReject != null;
|
|
1475
1552
|
if (this.turnReject) {
|
|
1476
1553
|
this.turnReject(err);
|
|
@@ -1603,6 +1680,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1603
1680
|
const agentMsg = this.mapAssistantMessage(msg);
|
|
1604
1681
|
if (this.isSuppressibleAuthRender(agentMsg)) {
|
|
1605
1682
|
this.log.info("suppressing SDK auth-error assistant text; self-heal will retry");
|
|
1683
|
+
this.stashSuppressedAuthRender(this.assistantTextOf(agentMsg));
|
|
1606
1684
|
this.prevText = "";
|
|
1607
1685
|
this.emittedSanitizedLen = 0;
|
|
1608
1686
|
this.suppressingAuthStream = false;
|
|
@@ -1622,14 +1700,45 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1622
1700
|
this.prevText = "";
|
|
1623
1701
|
this.emittedSanitizedLen = 0;
|
|
1624
1702
|
this.suppressingAuthStream = false;
|
|
1703
|
+
this.suppressedAuthRenderText = null;
|
|
1625
1704
|
this.emit("agent-event", { type: "message_end", message: agentMsg });
|
|
1626
1705
|
}
|
|
1627
1706
|
// True when this assistant text is the SDK's render of a 401 about to be self-healed.
|
|
1628
1707
|
isSuppressibleAuthRender(msg) {
|
|
1629
|
-
if (this.authSelfHealUsed || this.config.onAuthError == null) return false;
|
|
1630
1708
|
if (msg.toolCalls && msg.toolCalls.length > 0) return false;
|
|
1631
|
-
|
|
1632
|
-
|
|
1709
|
+
return this.isSuppressibleAuthRenderText(this.assistantTextOf(msg));
|
|
1710
|
+
}
|
|
1711
|
+
/**
|
|
1712
|
+
* True when this text is the CLI's own render of a 401 a pending self-heal
|
|
1713
|
+
* will retry. Shared by both suppression sites so they cannot disagree —
|
|
1714
|
+
* why that is forced: `_devlog/notes/2026-06-08-auth-error-text-suppress.md`.
|
|
1715
|
+
*/
|
|
1716
|
+
isSuppressibleAuthRenderText(text) {
|
|
1717
|
+
if (!this.authSelfHealAvailable()) return false;
|
|
1718
|
+
return this.textIsAuthRender(text) && isCliAuthRenderText(text);
|
|
1719
|
+
}
|
|
1720
|
+
/** Whether a 401 on this attempt can still be self-healed and retried. */
|
|
1721
|
+
authSelfHealAvailable() {
|
|
1722
|
+
return !this.authSelfHealUsed && this.config.onAuthError != null;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* Stash a dropped auth render as evidence for {@link handleResultErrorPayload}.
|
|
1726
|
+
*
|
|
1727
|
+
* Deliberately stricter than the suppression predicate. Suppression only hides
|
|
1728
|
+
* a message, but this text can reach the result path and construct an
|
|
1729
|
+
* `AuthError` — which refreshes a credential, bumps that seat's health
|
|
1730
|
+
* counters, and replays the turn. `classifyClaudeSdkError` buckets a bare
|
|
1731
|
+
* "401" or "unauthorized" as auth, so ordinary prose about authentication
|
|
1732
|
+
* qualifies; requiring a marker only the CLI itself emits keeps that prose out.
|
|
1733
|
+
*/
|
|
1734
|
+
stashSuppressedAuthRender(text) {
|
|
1735
|
+
if (!this.authSelfHealAvailable()) return;
|
|
1736
|
+
if (!isCliAuthRenderText(text)) return;
|
|
1737
|
+
this.suppressedAuthRenderText = text;
|
|
1738
|
+
}
|
|
1739
|
+
/** Flatten an assistant message's text blocks; non-text blocks contribute nothing. */
|
|
1740
|
+
assistantTextOf(msg) {
|
|
1741
|
+
return typeof msg.content === "string" ? msg.content : msg.content.filter((b) => b.type === "text" && typeof b.text === "string").map((b) => b.text).join("");
|
|
1633
1742
|
}
|
|
1634
1743
|
// Cheap substring pre-filter keeps the full classifier off the hot path of
|
|
1635
1744
|
// every assistant message / stream delta; only auth-shaped text pays for it.
|
|
@@ -1646,11 +1755,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1646
1755
|
if (event?.type === "content_block_delta" && event.delta?.type === "text_delta" && event.delta.text) {
|
|
1647
1756
|
const delta = event.delta.text;
|
|
1648
1757
|
this.prevText += delta;
|
|
1649
|
-
if (!this.suppressingAuthStream && this.
|
|
1758
|
+
if (!this.suppressingAuthStream && this.isSuppressibleAuthRenderText(this.prevText)) {
|
|
1650
1759
|
this.suppressingAuthStream = true;
|
|
1651
1760
|
this.log.info("suppressing SDK auth-error stream; self-heal will retry");
|
|
1652
1761
|
}
|
|
1653
|
-
if (this.suppressingAuthStream)
|
|
1762
|
+
if (this.suppressingAuthStream) {
|
|
1763
|
+
this.stashSuppressedAuthRender(this.prevText);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1654
1766
|
const safe = streamingSafeSanitize(this.prevText);
|
|
1655
1767
|
const sanitizedDelta = safe.slice(this.emittedSanitizedLen);
|
|
1656
1768
|
this.emittedSanitizedLen = safe.length;
|
|
@@ -1774,11 +1886,16 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1774
1886
|
/**
|
|
1775
1887
|
* Classify + route an `is_error` result payload: auth-shaped errors throw
|
|
1776
1888
|
* via `failTurn`; populated non-auth errors surface a normal `error` event;
|
|
1777
|
-
* empty errors defer to the consumer-error catch.
|
|
1889
|
+
* empty errors defer to the consumer-error catch under a bounded grace timer.
|
|
1890
|
+
*
|
|
1891
|
+
* An empty `errors[]` falls back to the auth render this attempt suppressed:
|
|
1892
|
+
* handed nothing, the classifier can only answer `unknown`, stranding a 401
|
|
1893
|
+
* the render path already recognised on the same text.
|
|
1778
1894
|
*/
|
|
1779
1895
|
handleResultErrorPayload(msg, errors) {
|
|
1896
|
+
const effectiveErrors = errors.length === 0 && this.suppressedAuthRenderText != null ? [this.suppressedAuthRenderText] : errors;
|
|
1780
1897
|
const rawDetail = classifyClaudeSdkError({
|
|
1781
|
-
errors,
|
|
1898
|
+
errors: effectiveErrors,
|
|
1782
1899
|
terminalReason: msg.terminal_reason,
|
|
1783
1900
|
subtype: msg.subtype,
|
|
1784
1901
|
oauthCredential: this.usingOauthCredential
|
|
@@ -1788,6 +1905,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1788
1905
|
subtype: msg.subtype,
|
|
1789
1906
|
terminalReason: msg.terminal_reason,
|
|
1790
1907
|
errorsCount: errors.length,
|
|
1908
|
+
fromSuppressedAuthRender: effectiveErrors !== errors,
|
|
1791
1909
|
category: detail.category,
|
|
1792
1910
|
retryable: detail.retryable
|
|
1793
1911
|
});
|
|
@@ -1797,7 +1915,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1797
1915
|
this.limitRejectionSeen = true;
|
|
1798
1916
|
this.reportProviderResponse({ status: 429, ...this.lastLimitInfo ?? {} });
|
|
1799
1917
|
}
|
|
1800
|
-
const errorText =
|
|
1918
|
+
const errorText = effectiveErrors.join("; ") || `Agent stopped: ${msg.subtype}`;
|
|
1801
1919
|
if (detail.category === "auth") {
|
|
1802
1920
|
this.failTurn(new AuthError({ ...detail, message: errorText }), detail);
|
|
1803
1921
|
} else if (detail.category === "account") {
|
|
@@ -1811,6 +1929,11 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1811
1929
|
error: errorText,
|
|
1812
1930
|
detail
|
|
1813
1931
|
});
|
|
1932
|
+
} else {
|
|
1933
|
+
this.armErrorResultGrace(detail, {
|
|
1934
|
+
subtype: msg.subtype,
|
|
1935
|
+
terminalReason: msg.terminal_reason
|
|
1936
|
+
});
|
|
1814
1937
|
}
|
|
1815
1938
|
}
|
|
1816
1939
|
// ---------------------------------------------------------------------------
|