feat: add economy/maximum presets, tool-lab and subagent-cursor extensions
Some checks failed
CI / windows node 24 / wine blocking (push) Has been skipped
CI / node 22.19 (push) Has been skipped
CI / node 26 (push) Has been skipped
CI / python 3.10 / keyless SDK (push) Has been skipped
CI / python runtime / release-shaped Linux x64 (push) Has been skipped
CI / wine apt cache (push) Successful in 7s
CI / serial / linux (push) Has been skipped
Deploy documentation / build (push) Failing after 1m25s
Deploy documentation / deploy (push) Has been skipped
Landlock Run / Matrix (push) Successful in 5s
Release (vendor) / Pack npm tarballs (push) Failing after 2m47s
Release (dsh) / Pack npm tarballs (push) Failing after 1m56s
Sandbox / sandbox e2e (landlock, ubuntu-24.04) (push) Failing after 1m57s
Sandbox / sandbox e2e (bwrap, ubuntu-latest) (push) Failing after 1m19s
Release (vendor) / Publish to npm (push) Has been skipped
Release (dsh) / Publish to npm (push) Has been skipped
CI / serial / windows (self-hosted standby) (push) Has been cancelled
CI / larger-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, typecheck) (push) Has been cancelled
Landlock Run / darwin (no platform package — degradation proof) (push) Has been cancelled
Landlock Run / ${{ matrix.platform }} (push) Has been cancelled
CI / node 24 / static (push) Has been cancelled
CI / node 24 / coverage (push) Has been cancelled
CI / node 24 / snapshots and artifacts (push) Has been cancelled
CI / windows node 24 / native complete (push) Has been cancelled
CI / serial / linux (self-hosted standby) (push) Has been cancelled
CI / serial / macos (push) Has been cancelled
CI / larger-runner-benchmark (16, windows, dsh-windows-2025-16core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (32, windows, dsh-windows-2025-32core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (4, windows, dsh-windows-2025-4core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (64, windows, dsh-windows-2025-64core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (8, windows, dsh-windows-2025-8core, production-site) (push) Has been cancelled
CI / larger-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, typecheck) (push) Has been cancelled
CI / larger-runner-benchmark (96, windows, dsh-windows-2025-96core, production-site) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, linux, dsh-ubuntu-24-04-16core, 16) (push) Has been cancelled
CI / consolidated-runner-benchmark (16, windows, dsh-windows-2025-16core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, linux, dsh-ubuntu-24-04-32core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (32, windows, dsh-windows-2025-32core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, linux, dsh-ubuntu-24-04-4core, 4) (push) Has been cancelled
CI / consolidated-runner-benchmark (4, windows, dsh-windows-2025-4core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, linux, dsh-ubuntu-24-04-64core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (64, windows, dsh-windows-2025-64core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, 8) (push) Has been cancelled
CI / consolidated-runner-benchmark (8, windows, dsh-windows-2025-8core, 2) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, linux, dsh-ubuntu-24-04-96core, 32) (push) Has been cancelled
CI / consolidated-runner-benchmark (96, windows, dsh-windows-2025-96core, 2) (push) Has been cancelled
CI / all checks passed (push) Has been cancelled
Sandbox / sandbox e2e (seatbelt, macos-latest) (push) Has been cancelled
CI / larger-runner-benchmark (8, linux, dsh-ubuntu-24-04-8core, typecheck) (push) Has been cancelled
Sandbox / sandbox e2e (landlock, ubuntu-24.04-arm) (push) Has been cancelled
E2E (real DeepSeek API) / e2e (push) Failing after 1m24s

- new economy and maximum agent presets with three-role pipeline skill
- new packages/extensions/tool-lab (home-lab ComfyUI/Docling/Whishper tools)
- new packages/subagent/subagent-cursor provider
- openrouter balance UI with on-demand refresh
- session projection context-seed boundary fold
- regenerate docs catalogs; keep local searxng benchmark scripts
This commit is contained in:
2026-08-23 22:12:30 +07:00
parent e818e37f0a
commit 8719fa84f0
112 changed files with 5686 additions and 169 deletions

View File

@@ -0,0 +1,52 @@
# Agent Note: Projection units fold against a header-derived context
Status: implemented
## Problem
A forked subagent's session log opens with a verbatim copy of its parent's log. `SessionHeader.seedLength` records that inherited prefix, and consumers that must attribute work to the session that actually did it already slice past it — `dsh-agent`'s inbox and `dsh-schedule`'s domain both do.
Projection units could not. `ProjectionDefinition.apply(state, event)` received only the event stream, so every unit folded the inherited prefix as if the child had produced it. For `openRouterCost` that made a child's value report its parent's spend: measured on a real fork with `seedLength` 109342, the child folded to $2.0401 over 575 priced steps while its own work was $0.1073 over 25. Summing a parent with its subagents — what the composer dock does to show delegated spend — therefore counted the parent once per forked child, inflating an $8.59 session to $12.79.
The log alone cannot supply the boundary. `session/end-seed` marks a constructor seed, but a resumed ordinary session appends one too, so the marker cannot distinguish a fork boundary from a resume boundary: the parent session in the measurement above carries four of them. Only the durable header separates the two.
## Decision
`ProjectionDefinition.apply` takes a third parameter, `ProjectionFoldContext` — per-session facts derived from the session's durable header, currently just `seedLength`. The registry supplies it on every call, so a unit reads session facts without ever touching a `Session`:
- `snapshot`, the eager drive, and the lazy cell build derive it from `session.header` through the exported `foldContextOf`.
- `restore` takes it as a required fourth parameter, because a detached fold has no Session to derive it from. Its four callers each already hold the stored header: the projection cache's cold read (`tail.meta`, `whole.meta`), api-proxy's detached history baseline, and the subagent catalog's identity probe.
The parameter is required in the interface and unused by 13 of the 14 shipped units: a two-parameter implementation satisfies a three-parameter signature, so no other unit changed.
`openRouterCost` skips events below the boundary and bumps `stateVersion` to 4. It still reads `request/context` from the inherited prefix — those records carry no cost, and the route they establish is what attributes the child's first chunk-only step, which would otherwise fold unpriced.
`CostDock` sums the session plus its subagent subtree over the session list's `parentId`/`origin` rows. That sum is only sound because each value now describes its own session's work.
## What the context does not decide
`ProjectionFoldContext` is not a general escape hatch to session state. It carries header facts a pure fold legitimately needs and nothing derived from mutable session state, which would break the synchronous-fold and plain-JSON-state guarantees the persisted cache depends on. A unit describing the whole conversation rather than the session's own work — context pressure, the visible transcript — correctly ignores `seedLength` and folds the inherited prefix like any other event.
## Alternatives considered
**Reset the accumulated state on `session/end-seed`.** No framework change, and the marker is already durable. Rejected because a resumed ordinary session appends the same marker: resetting on it would zero a session's spend on every reopen. The marker cannot name which boundary it is.
**Track a second accumulator for "spend since the last marker" and let the client pick per `origin`.** Contained entirely in this plugin. Rejected because it is wrong for a resumed continuable subagent, whose own earlier segments fall before the last marker and would silently vanish from its total — trading a 20× over-count for a quiet under-count.
**Deduplicate by the `steps` map's `${turn}:${step}` keys across the lineage.** The inherited steps carry the parent's own keys, so a union would drop them. Rejected because sibling forks both continue from the same fork point and mint colliding keys for their own first steps, and a child seeded with nothing numbers from 0 exactly like its parent.
**Have the client stop adding forked children entirely.** Never inflated. Rejected because delegated spend is the figure the dock exists to show; hiding it answers the wrong question.
**Give `apply` the whole `Session`.** Simpler signature change. Rejected because it hands every unit a mutable, non-JSON object and an appendable log, inviting folds that read state outside the event they were given — the exact discipline the unit contract exists to enforce.
## Consequences
Every unit's value now means "this session's own work" or "the whole conversation" by explicit choice rather than by accident. The cost is one more parameter on the seam's central function and a required argument on `restore`, which is what makes the omission impossible to reintroduce silently: a caller with no header cannot compile.
The `stateVersion` bump discards persisted `openRouterCost` rows. Sessions whose rows are dropped and which are never reopened read as absent rather than refolded, because the cold read serves the cache and never folds a cold log — so historical subagent spend stays missing until something opens those sessions. `dsh-client-ui-openrouter-usage` records that gap under its known limitations; a cold-fold aggregate is a separate decision.
`tokenUsage` and `sessionStats` still fold a forked child's inherited prefix. That is now a deliberate, visible choice rather than an invisible one: whether those figures should describe own work or inherited history is a question for their owners, and the context they need is already in hand.
## Testing
`packages/session/session-projection/tests/registry.spec.ts` proves the context reaches `apply` on all three drive paths — eager drive, lazy cell build after events flowed, and `restore` with a caller-supplied context — through a unit that counts only its own events. `packages/llm/openrouter-usage/tests/projection.spec.ts` builds a real fork through `ctx.sessions.create(undefined, { seed, meta: { seedLength } })` and asserts the child excludes the inherited prefix while the parent's figure is untouched, plus that a chunk-only child step still prices from a route recorded in that prefix. `packages/client/ui-openrouter-usage/tests/cost-dock.client.spec.tsx` covers the subtree walk: nested chains, ordinary forks excluded, a descendant with no projection value skipped, and `parentId` cycle termination.