@skaile/workspaces 3.6.1 → 3.7.1
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 +56 -0
- package/dist/{asset-feeds-YK6VBW4Y.js → asset-feeds-RJEO72GF.js} +4 -4
- package/dist/{asset-feeds-YK6VBW4Y.js.map → asset-feeds-RJEO72GF.js.map} +1 -1
- package/dist/asset-manager/index.js +1 -1
- package/dist/bridge/drivers/claude-sdk.js +217 -22
- 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 +68 -6
- package/dist/bridge/src/drivers/claude-sdk.d.ts.map +1 -1
- package/dist/{chunk-YYITPROS.js → chunk-5UHEJNBO.js} +3 -3
- package/dist/{chunk-YYITPROS.js.map → chunk-5UHEJNBO.js.map} +1 -1
- package/dist/{chunk-E554W7ZH.js → chunk-6K2B2VVH.js} +2 -2
- package/dist/{chunk-E554W7ZH.js.map → chunk-6K2B2VVH.js.map} +1 -1
- package/dist/{chunk-ZNDPABMQ.js → chunk-6N2YOG7K.js} +41 -20
- package/dist/chunk-6N2YOG7K.js.map +1 -0
- package/dist/chunk-AXEOG4B5.js +193 -0
- package/dist/chunk-AXEOG4B5.js.map +1 -0
- package/dist/{chunk-QJQL2DGN.js → chunk-CA26655X.js} +4 -4
- package/dist/{chunk-QJQL2DGN.js.map → chunk-CA26655X.js.map} +1 -1
- package/dist/{chunk-GVXR32DT.js → chunk-DITWUCNL.js} +3 -3
- package/dist/{chunk-GVXR32DT.js.map → chunk-DITWUCNL.js.map} +1 -1
- package/dist/{chunk-BGVTNQ23.js → chunk-FFDFRQHB.js} +6 -6
- package/dist/{chunk-BGVTNQ23.js.map → chunk-FFDFRQHB.js.map} +1 -1
- package/dist/{chunk-4Q76UBTV.js → chunk-GRMQBBN4.js} +2 -2
- package/dist/{chunk-4Q76UBTV.js.map → chunk-GRMQBBN4.js.map} +1 -1
- package/dist/{chunk-VC2I2U6Q.js → chunk-HY6CIKFH.js} +3 -3
- package/dist/chunk-HY6CIKFH.js.map +1 -0
- package/dist/{chunk-GOCFHDGC.js → chunk-JIQIWH6F.js} +2 -2
- package/dist/{chunk-GOCFHDGC.js.map → chunk-JIQIWH6F.js.map} +1 -1
- package/dist/{chunk-CL75GNIW.js → chunk-MDDE36QI.js} +4 -4
- package/dist/{chunk-CL75GNIW.js.map → chunk-MDDE36QI.js.map} +1 -1
- package/dist/{chunk-ZZ6LN5NO.js → chunk-NC5YXVZF.js} +3 -3
- package/dist/{chunk-ZZ6LN5NO.js.map → chunk-NC5YXVZF.js.map} +1 -1
- package/dist/{chunk-M3CKNK33.js → chunk-OJ3SCEVN.js} +6 -6
- package/dist/{chunk-M3CKNK33.js.map → chunk-OJ3SCEVN.js.map} +1 -1
- package/dist/{chunk-L3KQJ2Y7.js → chunk-ONLLHZ4O.js} +15 -20
- package/dist/chunk-ONLLHZ4O.js.map +1 -0
- package/dist/{chunk-FYJY4UIR.js → chunk-OYZIURPH.js} +3 -3
- package/dist/{chunk-FYJY4UIR.js.map → chunk-OYZIURPH.js.map} +1 -1
- package/dist/{chunk-7T3O4KFG.js → chunk-TNEGQCTP.js} +4 -4
- package/dist/{chunk-7T3O4KFG.js.map → chunk-TNEGQCTP.js.map} +1 -1
- package/dist/{chunk-BC5MXJZA.js → chunk-U5NPUWBU.js} +3 -3
- package/dist/{chunk-BC5MXJZA.js.map → chunk-U5NPUWBU.js.map} +1 -1
- package/dist/{chunk-UTMZU56B.js → chunk-UXLT4YQU.js} +3 -3
- package/dist/{chunk-UTMZU56B.js.map → chunk-UXLT4YQU.js.map} +1 -1
- package/dist/{chunk-AFA7NYN2.js → chunk-YRLSGA6X.js} +2 -2
- package/dist/{chunk-AFA7NYN2.js.map → chunk-YRLSGA6X.js.map} +1 -1
- package/dist/cli/index.js +33 -32
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/index.js +6 -5
- package/dist/{ensure-sources-2RW742IO.js → ensure-sources-LIEDRFLL.js} +4 -4
- package/dist/{ensure-sources-2RW742IO.js.map → ensure-sources-LIEDRFLL.js.map} +1 -1
- package/dist/factory-assets/connectors/box.js +6 -5
- package/dist/factory-assets/connectors/deploy.js +6 -5
- package/dist/factory-assets/connectors/devserver.js +6 -5
- package/dist/factory-assets/connectors/flow/CONNECTOR.md +1 -1
- package/dist/factory-assets/connectors/flow/adapter.d.ts.map +1 -1
- package/dist/factory-assets/connectors/flow/adapter.js +6 -5
- package/dist/factory-assets/connectors/flow/contract/index.d.ts +1 -0
- package/dist/factory-assets/connectors/flow/contract/index.d.ts.map +1 -1
- package/dist/factory-assets/connectors/flow/contract/input-schema.d.ts +49 -0
- package/dist/factory-assets/connectors/flow/contract/input-schema.d.ts.map +1 -0
- package/dist/factory-assets/connectors/flow/contract/schema.d.ts +39 -0
- package/dist/factory-assets/connectors/flow/contract/schema.d.ts.map +1 -1
- package/dist/factory-assets/connectors/flow/contract.js +3 -2
- package/dist/factory-assets/connectors/flow/engine.js +2 -2
- package/dist/factory-assets/connectors/flow/run-flow.js +7 -6
- package/dist/factory-assets/connectors/flow.js +6 -5
- package/dist/factory-assets/connectors/git.js +6 -5
- package/dist/factory-assets/connectors/gmail.js +6 -5
- package/dist/factory-assets/connectors/googledrive.js +6 -5
- package/dist/factory-assets/connectors/local.js +6 -5
- package/dist/factory-assets/connectors/mattermost.js +6 -5
- package/dist/factory-assets/connectors/memory.js +6 -5
- package/dist/factory-assets/connectors/minio.js +6 -5
- package/dist/factory-assets/connectors/postgres.js +6 -5
- package/dist/factory-assets/connectors/s3.js +6 -5
- package/dist/factory-assets/connectors/sharepoint.js +6 -5
- package/dist/factory-assets/connectors/sqlite.js +6 -5
- package/dist/factory-assets/connectors/static-server.js +6 -5
- package/dist/factory-assets/connectors/tunnel.js +6 -5
- package/dist/factory-assets/connectors/webdav.js +6 -5
- package/dist/factory-assets/connectors/xstate-store.js +6 -5
- package/dist/factory-assets/connectors/xstate.js +6 -5
- package/dist/{flows-W7EYXP6R.js → flows-4R4VJ5VM.js} +3 -3
- package/dist/{flows-W7EYXP6R.js.map → flows-4R4VJ5VM.js.map} +1 -1
- package/dist/{helpers-D7W7C5NL.js → helpers-3LCAPFGM.js} +4 -4
- package/dist/{helpers-D7W7C5NL.js.map → helpers-3LCAPFGM.js.map} +1 -1
- package/dist/{library-publish-X4ZR2PG6.js → library-publish-JD2PJOZO.js} +8 -8
- package/dist/{library-publish-X4ZR2PG6.js.map → library-publish-JD2PJOZO.js.map} +1 -1
- package/dist/{open-library-6VUMR6I6.js → open-library-T4H6CZKH.js} +7 -7
- package/dist/{open-library-6VUMR6I6.js.map → open-library-T4H6CZKH.js.map} +1 -1
- package/dist/{project-RS6ANRJT.js → project-VYCQHKJ5.js} +5 -5
- package/dist/{project-RS6ANRJT.js.map → project-VYCQHKJ5.js.map} +1 -1
- package/dist/runner/index.js +10 -9
- package/dist/runner/src/compaction/orchestrator.d.ts +4 -0
- package/dist/runner/src/compaction/orchestrator.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/flow.js +2 -2
- package/dist/sdk/index.js +10 -9
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runner.js +10 -9
- package/dist/{setup-4TE4XIDD.js → setup-JDAYBHHG.js} +4 -4
- package/dist/{setup-4TE4XIDD.js.map → setup-JDAYBHHG.js.map} +1 -1
- package/dist/{source-DF3YRE6B.js → source-RJ7JM6FD.js} +8 -8
- package/dist/{source-DF3YRE6B.js.map → source-RJ7JM6FD.js.map} +1 -1
- package/dist/{source-manifest-publish-L5JH2WD3.js → source-manifest-publish-QTHBFG3X.js} +9 -9
- package/dist/{source-manifest-publish-L5JH2WD3.js.map → source-manifest-publish-QTHBFG3X.js.map} +1 -1
- package/dist/tui/index.js +10 -9
- package/dist/tui/index.js.map +1 -1
- package/dist/types/src/events.d.ts +9 -0
- package/dist/types/src/events.d.ts.map +1 -1
- package/dist/workspace-plugin/adapters/mcp.js +1 -1
- package/dist/workspace-plugin/adapters/omp.js +3 -3
- package/dist/workspace-plugin/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-L3KQJ2Y7.js.map +0 -1
- package/dist/chunk-VC2I2U6Q.js.map +0 -1
- package/dist/chunk-ZNDPABMQ.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#569](https://github.com/skaile-ai/workspaces/pull/569) [`cd1942d`](https://github.com/skaile-ai/workspaces/commit/cd1942dc3236715e91ed3fac415a0ce87bf08bc7) Thanks [@Frozen666](https://github.com/Frozen666)! - claude-sdk driver: migrate to streaming input mode and add a first-SDK-message stall tier ([#566](https://github.com/skaile-ai/workspaces/issues/566))
|
|
8
|
+
|
|
9
|
+
The query is now constructed with a persistent pushable `AsyncIterable` prompt instead of a plain string. A string prompt made the SDK mark the query single-turn and close the CLI's stdin at the first result; a CLI kept alive past that close by live `run_in_background` tasks then silently dropped the next prompt, wedging the session for the full 30-minute stall budget. With streaming input mode, stdin stays open across turns and every prompt is pushed into the same live stream.
|
|
10
|
+
|
|
11
|
+
The turn-stall watchdog now has two tiers: until the first SDK message of a turn arrives, the new `SKAILE_FIRST_MESSAGE_STALL_MS` budget (default 5 minutes, clamped to the mid-turn budget) applies — total silence after dispatch means the query is deaf, not busy. The existing `SKAILE_TURN_STALL_MS` 30-minute ceiling still governs mid-turn silence (long tool calls).
|
|
12
|
+
|
|
13
|
+
Because the CLI's `--max-turns` accounting may accumulate across a resident streaming session, the driver recycles the resident query at a turn boundary once the last result's reported `num_turns` reaches 80% of `maxTurns`; the next prompt respawns with `--resume`.
|
|
14
|
+
|
|
15
|
+
- [#571](https://github.com/skaile-ai/workspaces/pull/571) [`1634aaa`](https://github.com/skaile-ai/workspaces/commit/1634aaa9868f71796de1ad93bf8bd2038c4cb6ca) Thanks [@peteralbert](https://github.com/peteralbert)! - claude-sdk driver: never settle a turn on a query that did not run it ([#568](https://github.com/skaile-ai/workspaces/issues/568))
|
|
16
|
+
|
|
17
|
+
Threshold compaction fired at the right context fill but resolved in the same second with an empty summary in ~85% of attempts. The trigger runs inside the driver's own `result` emit, so the compaction prompt was pushed into a resident query whose generator was already ending; that ending consumer loop's terminal `completeTurn()` then settled the _compaction_ turn instead of the turn it served, resolving it with zero captured text. `validateCompactionOutput` recorded `empty`, which read as a bad model response rather than the infrastructure failure it was.
|
|
18
|
+
|
|
19
|
+
A pending turn that has not seen a single SDK message when its serving generator ends is now rejected as retryable and replayed once on a fresh query (`--resume`, so the conversation is preserved) -- unless the caller aborted it, which looks identical from the consumer loop but must never be replayed. The rejection emits no `agent-event: error` — the host must not see a failure the driver is recovering from. Manual compaction, the auth self-heal, the stale-resume retry and the poisoned-transcript retry are unchanged.
|
|
20
|
+
|
|
21
|
+
The runner also stops calling `compact("threshold")` from inside the driver's event emit: it is deferred to a macrotask and re-checked at fire time.
|
|
22
|
+
|
|
23
|
+
`CompactionAttemptEvent` gains an optional `elapsedMs` (wall-clock ms spent in the compaction `prompt()` call), so an instant `empty` is distinguishable from a genuine empty model response.
|
|
24
|
+
|
|
25
|
+
Compaction capture no longer accumulates the same assistant text more than once. The Claude SDK driver reports one message as streamed deltas _and_ `message_start` _and_ `message_end`, so every stored summary was duplicated up to three times.
|
|
26
|
+
|
|
27
|
+
## 3.7.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- [#564](https://github.com/skaile-ai/workspaces/pull/564) [`51b3594`](https://github.com/skaile-ai/workspaces/commit/51b3594d40307c85ee3f0d87e80898e84c5efb19) Thanks [@henkbla](https://github.com/henkbla)! - flow: never persist a `request_input` schema no host can render
|
|
32
|
+
|
|
33
|
+
`request_input` cast the agent's `schema` argument straight into `InputSchema`
|
|
34
|
+
without checking it, so the shapes an agent naturally reaches for — `options:
|
|
35
|
+
["Approve", "Reject"]`, or options carrying a label and no `id` — were stored as
|
|
36
|
+
choice gates with undefined option ids. Hosts rendered them as unselectable
|
|
37
|
+
controls and the run parked on a question no human could answer.
|
|
38
|
+
|
|
39
|
+
The argument now goes through `coerceInputSchema`, which brings the tolerated
|
|
40
|
+
shapes onto the contract (a bare string becomes `{ id, label }`; a missing id is
|
|
41
|
+
derived from the label, disambiguated by the option index when two labels match)
|
|
42
|
+
and validates the result against the same `InputSchema` object a flow definition
|
|
43
|
+
is validated against. Anything left unanswerable — an unknown `kind`, an option
|
|
44
|
+
or field with no usable label, a duplicate explicit id, an id or label of the
|
|
45
|
+
wrong type, an empty option or field list, a `choice` field with nothing to
|
|
46
|
+
select — is refused at the call site with an issue naming the path and what was
|
|
47
|
+
wrong, and nothing is stored, so the agent can correct the schema and call again.
|
|
48
|
+
A form gate is held to the same bar as a choice gate: its fields are deduped and
|
|
49
|
+
must be answerable. Nothing is ever silently dropped or substituted: each
|
|
50
|
+
unusable entry produces its own issue, and an id the agent wrote is never
|
|
51
|
+
replaced by one derived from the label. Each refusal's issues close with the
|
|
52
|
+
accepted shapes, so any caller's message is actionable without appending them.
|
|
53
|
+
|
|
54
|
+
`coerceInputSchema`, `InputSchemaCoercion`, and `INPUT_SCHEMA_SHAPES_HINT` are
|
|
55
|
+
exported from `@skaile/workspaces/factory-assets/connectors/flow/contract` so a
|
|
56
|
+
host normalizing a stored gate on read applies the identical rule rather than a
|
|
57
|
+
second one that can drift.
|
|
58
|
+
|
|
3
59
|
## 3.6.1
|
|
4
60
|
|
|
5
61
|
### Patch Changes
|
|
@@ -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-T4H6CZKH.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-T4H6CZKH.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-RJEO72GF.js.map
|
|
206
|
+
//# sourceMappingURL=asset-feeds-RJEO72GF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../cli/src/asset-feeds.ts"],"names":[],"mappings":"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","file":"asset-feeds-YK6VBW4Y.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-RJEO72GF.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-JIQIWH6F.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-YRLSGA6X.js';
|
|
6
6
|
import '../../chunk-6E6PKKAD.js';
|
|
7
7
|
import '../../chunk-AZPXCHDK.js';
|
|
8
8
|
import '../../chunk-F3HNW354.js';
|
|
@@ -159,13 +159,62 @@ if (typeof globalThis.require === "undefined") {
|
|
|
159
159
|
}
|
|
160
160
|
}
|
|
161
161
|
function freshRetryBudget() {
|
|
162
|
-
return { staleResume: false, poison: false, auth: false };
|
|
162
|
+
return { staleResume: false, poison: false, auth: false, queryEnded: false };
|
|
163
163
|
}
|
|
164
|
+
var QueryEndedBeforeTurnError = class extends Error {
|
|
165
|
+
constructor(message) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.name = "QueryEndedBeforeTurnError";
|
|
168
|
+
}
|
|
169
|
+
};
|
|
164
170
|
var DEFAULT_TURN_STALL_MS = 30 * 6e4;
|
|
165
171
|
function turnStallMs() {
|
|
166
172
|
const raw = Number(process.env.SKAILE_TURN_STALL_MS);
|
|
167
173
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TURN_STALL_MS;
|
|
168
174
|
}
|
|
175
|
+
var DEFAULT_FIRST_MESSAGE_STALL_MS = 5 * 6e4;
|
|
176
|
+
function firstMessageStallMs() {
|
|
177
|
+
const raw = Number(process.env.SKAILE_FIRST_MESSAGE_STALL_MS);
|
|
178
|
+
const value = Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_FIRST_MESSAGE_STALL_MS;
|
|
179
|
+
return Math.min(value, turnStallMs());
|
|
180
|
+
}
|
|
181
|
+
var RECYCLE_TURNS_FRACTION = 0.8;
|
|
182
|
+
var MIN_RECYCLE_HEADROOM = 5;
|
|
183
|
+
var PushableInputStream = class {
|
|
184
|
+
queue = [];
|
|
185
|
+
wake = null;
|
|
186
|
+
done = false;
|
|
187
|
+
consumed = false;
|
|
188
|
+
/** `true` once {@link end} ran — pushes are no longer possible. */
|
|
189
|
+
get ended() {
|
|
190
|
+
return this.done;
|
|
191
|
+
}
|
|
192
|
+
push(message) {
|
|
193
|
+
if (this.done) throw new Error("input stream already ended");
|
|
194
|
+
this.queue.push(message);
|
|
195
|
+
this.wake?.();
|
|
196
|
+
}
|
|
197
|
+
/** Finish the iterable; the SDK responds by closing the CLI's stdin. Idempotent. */
|
|
198
|
+
end() {
|
|
199
|
+
if (this.done) return;
|
|
200
|
+
this.done = true;
|
|
201
|
+
this.wake?.();
|
|
202
|
+
}
|
|
203
|
+
async *[Symbol.asyncIterator]() {
|
|
204
|
+
if (this.consumed) throw new Error("input stream already has a consumer");
|
|
205
|
+
this.consumed = true;
|
|
206
|
+
while (true) {
|
|
207
|
+
while (this.queue.length > 0) {
|
|
208
|
+
yield this.queue.shift();
|
|
209
|
+
}
|
|
210
|
+
if (this.done) return;
|
|
211
|
+
await new Promise((resolve) => {
|
|
212
|
+
this.wake = resolve;
|
|
213
|
+
});
|
|
214
|
+
this.wake = null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
};
|
|
169
218
|
var ABORT_GRACE_MS = 3e3;
|
|
170
219
|
var claudeBinaryCache;
|
|
171
220
|
var claudeBinaryResolved = false;
|
|
@@ -191,8 +240,26 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
191
240
|
emittedSanitizedLen = 0;
|
|
192
241
|
/** Set once the streaming text of the current message classifies as a self-healable auth render. */
|
|
193
242
|
suppressingAuthStream = false;
|
|
194
|
-
/** Active query —
|
|
243
|
+
/** Active query — stays alive between turns (streaming input mode). */
|
|
195
244
|
query = null;
|
|
245
|
+
/**
|
|
246
|
+
* The live query's prompt stream. Created in {@link startQuery}, fed one user
|
|
247
|
+
* message per turn, and ended wherever the query is dropped — an ended stream
|
|
248
|
+
* with a live query means the CLI's stdin is closed and the query can never
|
|
249
|
+
* accept another turn.
|
|
250
|
+
*/
|
|
251
|
+
inputStream = null;
|
|
252
|
+
/**
|
|
253
|
+
* `true` once any SDK message arrived after the current turn's watchdog was
|
|
254
|
+
* armed — flips the stall budget from the first-message tier to the mid-turn
|
|
255
|
+
* ceiling. See {@link armTurnStallWatchdog}.
|
|
256
|
+
*/
|
|
257
|
+
firstSdkMessageSeen = false;
|
|
258
|
+
/**
|
|
259
|
+
* `num_turns` reported by the most recent result message. Feeds
|
|
260
|
+
* {@link shouldRecycleQuery}; reset on every fresh spawn.
|
|
261
|
+
*/
|
|
262
|
+
lastNumTurns = null;
|
|
196
263
|
/**
|
|
197
264
|
* OS pid of the CLI subprocess backing `query`, captured via
|
|
198
265
|
* `spawnClaudeCodeProcess` in {@link startQuery}. Feeds
|
|
@@ -309,8 +376,8 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
309
376
|
* Sends a user message to the Claude Agent SDK and resolves when the turn completes.
|
|
310
377
|
*
|
|
311
378
|
* If a live SDK query already exists (from a prior turn in this session) the message
|
|
312
|
-
* is
|
|
313
|
-
* is started applying the resume chain described in the class-level remarks.
|
|
379
|
+
* is pushed into its persistent prompt stream (streaming input mode). Otherwise a new
|
|
380
|
+
* query is started applying the resume chain described in the class-level remarks.
|
|
314
381
|
*
|
|
315
382
|
* @param message - Plain-text user prompt.
|
|
316
383
|
* @throws {Error} When the SDK reports a fatal error that cannot be auto-recovered.
|
|
@@ -339,7 +406,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
339
406
|
if (!this.sdk) await this.start();
|
|
340
407
|
this.authSelfHealUsed = _budget.auth;
|
|
341
408
|
let retrying = false;
|
|
342
|
-
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth;
|
|
409
|
+
const isFirstAttempt = !_budget.staleResume && !_budget.poison && !_budget.auth && !_budget.queryEnded;
|
|
343
410
|
if (isFirstAttempt) {
|
|
344
411
|
this._lastTokens = null;
|
|
345
412
|
this._lastContextTokens = null;
|
|
@@ -349,14 +416,16 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
349
416
|
this.emittedSanitizedLen = 0;
|
|
350
417
|
this.suppressingAuthStream = false;
|
|
351
418
|
this.turnCompleted = false;
|
|
419
|
+
if (this.query && (this.inputStream == null || this.inputStream.ended || this.shouldRecycleQuery())) {
|
|
420
|
+
this.recycleQueryForNextTurn();
|
|
421
|
+
}
|
|
352
422
|
const turnPromise = new Promise((resolve, reject) => {
|
|
353
423
|
this.turnResolve = resolve;
|
|
354
424
|
this.turnReject = reject;
|
|
355
425
|
});
|
|
356
426
|
try {
|
|
357
|
-
if (this.query) {
|
|
358
|
-
|
|
359
|
-
this.query.streamInput(this.singleItemIterable(userMsg));
|
|
427
|
+
if (this.query && this.inputStream) {
|
|
428
|
+
this.inputStream.push(this.buildUserMessage(message));
|
|
360
429
|
} else {
|
|
361
430
|
await this.startQuery(message);
|
|
362
431
|
}
|
|
@@ -370,6 +439,11 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
370
439
|
try {
|
|
371
440
|
await turnPromise;
|
|
372
441
|
} catch (err) {
|
|
442
|
+
if (!_budget.queryEnded && err instanceof QueryEndedBeforeTurnError) {
|
|
443
|
+
this.log.warn("turn was enqueued into an ending query; replaying on a fresh one");
|
|
444
|
+
retrying = true;
|
|
445
|
+
return this.runPrompt(message, { ..._budget, queryEnded: true });
|
|
446
|
+
}
|
|
373
447
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
374
448
|
const isStaleResume = !_budget.staleResume && /No conversation found with session ID/i.test(errMsg);
|
|
375
449
|
if (isStaleResume) {
|
|
@@ -406,13 +480,22 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
406
480
|
* Arm the per-turn inactivity watchdog. Self-rescheduling rather than a single
|
|
407
481
|
* absolute timer so any mid-turn SDK message pushes the deadline out; the
|
|
408
482
|
* timer is `unref`'d so an armed watchdog never holds the process open.
|
|
483
|
+
*
|
|
484
|
+
* Two budget tiers: until the first SDK message of the turn arrives the
|
|
485
|
+
* (small) {@link firstMessageStallMs} budget applies — silence there means
|
|
486
|
+
* the query is deaf, not busy. Once anything arrives, the (large)
|
|
487
|
+
* {@link turnStallMs} ceiling takes over, because a long-running tool call
|
|
488
|
+
* legitimately produces no SDK messages. The budget is re-read every tick, so
|
|
489
|
+
* the first message flips tiers without re-arming.
|
|
409
490
|
*/
|
|
410
491
|
armTurnStallWatchdog() {
|
|
411
492
|
this.disarmTurnStallWatchdog();
|
|
412
493
|
if (this.turnCompleted || this.turnReject == null) return;
|
|
413
|
-
|
|
494
|
+
this.firstSdkMessageSeen = false;
|
|
495
|
+
const currentBudget = () => this.firstSdkMessageSeen ? turnStallMs() : firstMessageStallMs();
|
|
414
496
|
const tick = () => {
|
|
415
497
|
if (this.turnCompleted || this.turnReject == null) return;
|
|
498
|
+
const budget = currentBudget();
|
|
416
499
|
if (this.hasPendingQuestion()) {
|
|
417
500
|
this.turnStallTimer = setTimeout(tick, budget);
|
|
418
501
|
this.turnStallTimer.unref?.();
|
|
@@ -427,7 +510,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
427
510
|
this.onTurnStalled(idleMs);
|
|
428
511
|
};
|
|
429
512
|
this.lastSdkMessageAt = Date.now();
|
|
430
|
-
this.turnStallTimer = setTimeout(tick,
|
|
513
|
+
this.turnStallTimer = setTimeout(tick, currentBudget());
|
|
431
514
|
this.turnStallTimer.unref?.();
|
|
432
515
|
}
|
|
433
516
|
disarmTurnStallWatchdog() {
|
|
@@ -443,7 +526,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
443
526
|
*/
|
|
444
527
|
onTurnStalled(idleMs) {
|
|
445
528
|
const idle = idleMs < 6e4 ? `${Math.round(idleMs / 1e3)}s` : `${Math.round(idleMs / 6e4)}m`;
|
|
446
|
-
this.log.error("turn stalled \u2014 no SDK message received; resetting the query", {
|
|
529
|
+
this.log.error("turn stalled \u2014 no SDK message received; resetting the query", {
|
|
530
|
+
idleMs,
|
|
531
|
+
firstMessageOfTurnSeen: this.firstSdkMessageSeen
|
|
532
|
+
});
|
|
447
533
|
const descendants = this.snapshotDescendantPids();
|
|
448
534
|
this.hardResetQuery();
|
|
449
535
|
this.killPids(descendants);
|
|
@@ -465,6 +551,62 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
465
551
|
* abort signal) runs, then {@link killPids} after.
|
|
466
552
|
*/
|
|
467
553
|
hardResetQuery() {
|
|
554
|
+
this.endInputStream();
|
|
555
|
+
if (this.query) {
|
|
556
|
+
try {
|
|
557
|
+
this.query.close();
|
|
558
|
+
} catch {
|
|
559
|
+
}
|
|
560
|
+
this.query = null;
|
|
561
|
+
}
|
|
562
|
+
this.abortController?.abort();
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* End the persistent prompt stream (the SDK responds by closing the CLI's
|
|
566
|
+
* stdin) and forget it. Called wherever the query is dropped so a stale
|
|
567
|
+
* stream can never be pushed into. Safe to call with no stream.
|
|
568
|
+
*/
|
|
569
|
+
endInputStream() {
|
|
570
|
+
if (this.inputStream) {
|
|
571
|
+
this.inputStream.end();
|
|
572
|
+
this.inputStream = null;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* `maxTurns` counts agentic turns, but whether the CLI resets that count per
|
|
577
|
+
* user turn or accumulates it across a resident streaming session is
|
|
578
|
+
* undocumented. This guard is correct under either semantics: once the last
|
|
579
|
+
* result's reported `num_turns` is within {@link RECYCLE_TURNS_FRACTION}
|
|
580
|
+
* headroom of the ceiling, the resident query is dropped at the next turn
|
|
581
|
+
* boundary and the prompt respawns with `--resume` (which restarts the
|
|
582
|
+
* count). Under per-turn semantics this fires only when a single prompt
|
|
583
|
+
* nearly exhausted the budget by itself — where a recycle is harmless.
|
|
584
|
+
*/
|
|
585
|
+
shouldRecycleQuery() {
|
|
586
|
+
if (this.lastNumTurns == null) return false;
|
|
587
|
+
const max = this.config.maxTurns ?? 15;
|
|
588
|
+
const threshold = Math.min(
|
|
589
|
+
Math.max(1, Math.floor(max * RECYCLE_TURNS_FRACTION)),
|
|
590
|
+
Math.max(1, max - MIN_RECYCLE_HEADROOM)
|
|
591
|
+
);
|
|
592
|
+
return this.lastNumTurns >= threshold;
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Gracefully drop the resident query between turns so the next prompt starts
|
|
596
|
+
* a fresh CLI resuming the same session. Deliberately does NOT sweep the
|
|
597
|
+
* CLI's descendant processes: a between-turns recycle mirrors the legacy
|
|
598
|
+
* spawn-per-turn behavior, where the CLI's background tasks (`run_in_background`
|
|
599
|
+
* dev servers) outlive the turn boundary. Note they become orphans no later
|
|
600
|
+
* sweep can reach — snapshotDescendantPids walks from `cliProcessPid`, which
|
|
601
|
+
* the next spawn overwrites — exactly as in legacy spawn-per-turn.
|
|
602
|
+
*/
|
|
603
|
+
recycleQueryForNextTurn() {
|
|
604
|
+
this.log.info("recycling resident query before the next turn", {
|
|
605
|
+
reportedTurns: this.lastNumTurns,
|
|
606
|
+
maxTurns: this.config.maxTurns ?? 15,
|
|
607
|
+
inputStreamEnded: this.inputStream?.ended ?? null
|
|
608
|
+
});
|
|
609
|
+
this.endInputStream();
|
|
468
610
|
if (this.query) {
|
|
469
611
|
try {
|
|
470
612
|
this.query.close();
|
|
@@ -550,6 +692,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
550
692
|
this.sessionId = "";
|
|
551
693
|
this.hasSession = false;
|
|
552
694
|
this.query = null;
|
|
695
|
+
this.endInputStream();
|
|
553
696
|
this.config.resumeSessionId = void 0;
|
|
554
697
|
this.turnResolve = null;
|
|
555
698
|
this.turnReject = null;
|
|
@@ -596,6 +739,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
596
739
|
fatal: false
|
|
597
740
|
});
|
|
598
741
|
this.query = null;
|
|
742
|
+
this.endInputStream();
|
|
599
743
|
this.turnResolve = null;
|
|
600
744
|
this.turnReject = null;
|
|
601
745
|
this.running = false;
|
|
@@ -636,6 +780,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
636
780
|
if (refreshed) {
|
|
637
781
|
this.log.info("credentials refreshed; retrying prompt");
|
|
638
782
|
this.query = null;
|
|
783
|
+
this.endInputStream();
|
|
639
784
|
this.turnResolve = null;
|
|
640
785
|
this.turnReject = null;
|
|
641
786
|
this.running = false;
|
|
@@ -838,8 +983,11 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
838
983
|
options.env = { ...baseEnv, ...this.config.env ?? {}, ANTHROPIC_API_KEY: apiKey };
|
|
839
984
|
}
|
|
840
985
|
const { query } = this.sdk;
|
|
986
|
+
this.inputStream = new PushableInputStream();
|
|
987
|
+
this.inputStream.push(this.buildUserMessage(message));
|
|
988
|
+
this.lastNumTurns = null;
|
|
841
989
|
const queryArgs = {
|
|
842
|
-
prompt:
|
|
990
|
+
prompt: this.inputStream,
|
|
843
991
|
options
|
|
844
992
|
};
|
|
845
993
|
if (apiKey) queryArgs.apiKey = apiKey;
|
|
@@ -1023,11 +1171,14 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1023
1171
|
* If it ends unexpectedly, query is nulled so the next prompt() restarts.
|
|
1024
1172
|
*/
|
|
1025
1173
|
async consumeMessages() {
|
|
1174
|
+
const servedQuery = this.query;
|
|
1026
1175
|
void this.fetchSlashCommands();
|
|
1027
1176
|
try {
|
|
1028
|
-
for await (const msg of
|
|
1177
|
+
for await (const msg of servedQuery) {
|
|
1178
|
+
if (this.query !== servedQuery) break;
|
|
1029
1179
|
if (this.abortController?.signal.aborted) break;
|
|
1030
1180
|
this.lastSdkMessageAt = Date.now();
|
|
1181
|
+
this.firstSdkMessageSeen = true;
|
|
1031
1182
|
this.captureSessionId(msg);
|
|
1032
1183
|
this.handleSdkMessage(msg);
|
|
1033
1184
|
if (msg.type === "result" && !msg.is_error) {
|
|
@@ -1035,11 +1186,29 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1035
1186
|
}
|
|
1036
1187
|
}
|
|
1037
1188
|
} catch (err) {
|
|
1038
|
-
this.
|
|
1189
|
+
if (this.query === servedQuery) {
|
|
1190
|
+
this.handleConsumerError(err);
|
|
1191
|
+
} else {
|
|
1192
|
+
this.log.debug("stale consumer error after query replacement; ignoring", {
|
|
1193
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1039
1196
|
} finally {
|
|
1040
1197
|
this.log.debug("query generator ended");
|
|
1041
|
-
this.
|
|
1042
|
-
|
|
1198
|
+
if (this.query === servedQuery) {
|
|
1199
|
+
this.query = null;
|
|
1200
|
+
this.endInputStream();
|
|
1201
|
+
const neverDispatched = !this.turnCompleted && this.turnReject != null && !this.firstSdkMessageSeen && !this.abortController?.signal.aborted;
|
|
1202
|
+
if (neverDispatched) {
|
|
1203
|
+
this.rejectTurnSilently(
|
|
1204
|
+
new QueryEndedBeforeTurnError(
|
|
1205
|
+
"The model process ended before this turn was dispatched."
|
|
1206
|
+
)
|
|
1207
|
+
);
|
|
1208
|
+
} else {
|
|
1209
|
+
this.completeTurn();
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1043
1212
|
}
|
|
1044
1213
|
}
|
|
1045
1214
|
/** Capture the SDK session id from a message (first one assigns hasSession). */
|
|
@@ -1102,6 +1271,27 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1102
1271
|
});
|
|
1103
1272
|
}
|
|
1104
1273
|
}
|
|
1274
|
+
/**
|
|
1275
|
+
* Reject the current turn WITHOUT emitting an `agent-event: error`.
|
|
1276
|
+
*
|
|
1277
|
+
* The one legitimate use is a turn that is about to be replayed: the host
|
|
1278
|
+
* must not see an error for a failure the driver is itself recovering from
|
|
1279
|
+
* (the same reasoning as `failTurn`'s auth self-heal deferral). Ordering
|
|
1280
|
+
* mirrors {@link failTurn} — settle the promise before touching anything
|
|
1281
|
+
* else — so a turn can never be left pending.
|
|
1282
|
+
*
|
|
1283
|
+
* Anything a caller should actually learn about goes through `failTurn`.
|
|
1284
|
+
*/
|
|
1285
|
+
rejectTurnSilently(err) {
|
|
1286
|
+
this.disarmTurnStallWatchdog();
|
|
1287
|
+
this.turnCompleted = true;
|
|
1288
|
+
if (this.turnReject) {
|
|
1289
|
+
const reject = this.turnReject;
|
|
1290
|
+
this.turnResolve = null;
|
|
1291
|
+
this.turnReject = null;
|
|
1292
|
+
reject(err);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1105
1295
|
/**
|
|
1106
1296
|
* Signal the current turn as failed. When a structured `detail` is
|
|
1107
1297
|
* supplied, the AgentEvent carries the category + hint so downstream
|
|
@@ -1302,6 +1492,8 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1302
1492
|
* tokens — surface all four to consumers that want a complete picture.
|
|
1303
1493
|
*/
|
|
1304
1494
|
captureResultUsage(msg) {
|
|
1495
|
+
const numTurns = msg.num_turns;
|
|
1496
|
+
if (typeof numTurns === "number") this.lastNumTurns = numTurns;
|
|
1305
1497
|
const usage = msg.usage;
|
|
1306
1498
|
if (usage) {
|
|
1307
1499
|
const tokens = {};
|
|
@@ -1354,6 +1546,10 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1354
1546
|
subtype: msg.subtype,
|
|
1355
1547
|
errors
|
|
1356
1548
|
});
|
|
1549
|
+
const awaitingFollowUpThrow = looseMsg.is_error === true && errors.length === 0;
|
|
1550
|
+
if (!this.turnCompleted && !awaitingFollowUpThrow) {
|
|
1551
|
+
this.completeTurn();
|
|
1552
|
+
}
|
|
1357
1553
|
}
|
|
1358
1554
|
/**
|
|
1359
1555
|
* Classify + route an `is_error` result payload: auth-shaped errors throw
|
|
@@ -1391,7 +1587,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1391
1587
|
// ---------------------------------------------------------------------------
|
|
1392
1588
|
// Helpers
|
|
1393
1589
|
// ---------------------------------------------------------------------------
|
|
1394
|
-
/** Build an SDKUserMessage for
|
|
1590
|
+
/** Build an SDKUserMessage for the persistent prompt stream. */
|
|
1395
1591
|
buildUserMessage(text) {
|
|
1396
1592
|
return {
|
|
1397
1593
|
type: "user",
|
|
@@ -1403,10 +1599,6 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1403
1599
|
session_id: this.sessionId
|
|
1404
1600
|
};
|
|
1405
1601
|
}
|
|
1406
|
-
/** Wrap a single value as an AsyncIterable that yields once. */
|
|
1407
|
-
async *singleItemIterable(item) {
|
|
1408
|
-
yield item;
|
|
1409
|
-
}
|
|
1410
1602
|
mapAssistantMessage(msg) {
|
|
1411
1603
|
const contentBlocks = [];
|
|
1412
1604
|
const toolCalls = [];
|
|
@@ -1467,6 +1659,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1467
1659
|
*/
|
|
1468
1660
|
async abort() {
|
|
1469
1661
|
this.denyPendingQuestion("turn aborted before the question was answered");
|
|
1662
|
+
if (!this.running && this.query) return;
|
|
1470
1663
|
const abortedQuery = this.query;
|
|
1471
1664
|
const abortedController = this.abortController;
|
|
1472
1665
|
const descendants = this.snapshotDescendantPids();
|
|
@@ -1507,6 +1700,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1507
1700
|
kill() {
|
|
1508
1701
|
this.denyPendingQuestion("session terminated before the question was answered");
|
|
1509
1702
|
const descendants = this.snapshotDescendantPids();
|
|
1703
|
+
this.endInputStream();
|
|
1510
1704
|
if (this.query) {
|
|
1511
1705
|
try {
|
|
1512
1706
|
this.query.close();
|
|
@@ -1539,6 +1733,7 @@ var ClaudeSdkDriver = class extends AgentDriver {
|
|
|
1539
1733
|
async resetSession() {
|
|
1540
1734
|
this.denyPendingQuestion("session reset before the question was answered");
|
|
1541
1735
|
const descendants = this.snapshotDescendantPids();
|
|
1736
|
+
this.endInputStream();
|
|
1542
1737
|
if (this.query) {
|
|
1543
1738
|
try {
|
|
1544
1739
|
this.query.close();
|