@abelxiaoxing/cadence 2.0.9 → 2.0.11
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/README.md +92 -25
- package/docs/reuse-and-distribution.md +3 -3
- package/docs/service-management.md +84 -3
- package/package.json +1 -1
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +15 -9
- package/src/operator-cli.mjs +13 -2
- package/src/sdk-worker.mjs +5 -1
- package/src/service/contracts/models.ts +14 -2
- package/src/service-cli.mjs +465 -75
- package/src/service-host.mjs +69 -17
- package/src/task-shell.ts +1 -1
- package/src/task-store.ts +44 -10
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +7 -7
- package/web/dist/manifest.json +7 -7
package/README.md
CHANGED
|
@@ -36,13 +36,15 @@ The token admits anyone who has it to the workspace, so keep the file and the li
|
|
|
36
36
|
To change either, edit the line and run `cadence server restart`, which prints the new link; old links and browser cookies stop working.
|
|
37
37
|
A workspace that an earlier version started with a generated token switches to the file at that restart and keeps its history.
|
|
38
38
|
|
|
39
|
-
| Platform
|
|
40
|
-
|
|
|
41
|
-
| Linux
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
| Platform | Service | Starts |
|
|
40
|
+
| ------------ | ----------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
41
|
+
| Linux (root) | System unit `/etc/systemd/system/cadence.service` | At boot, no linger |
|
|
42
|
+
| Linux (user) | User unit `~/.local/share/systemd/user/cadence.service` | At boot through linger, else at login |
|
|
43
|
+
| macOS | LaunchAgent `~/Library/LaunchAgents/com.abelxiaoxing.cadence.plist` | At login |
|
|
44
|
+
| Windows | `HKCU\…\CurrentVersion\Run\Cadence`, starting `%LOCALAPPDATA%\cadence-server\cadence.vbs` | At login, without a console window |
|
|
45
|
+
|
|
46
|
+
On Linux, root defaults to `--system` and never uses linger; ordinary accounts default to `--user`.
|
|
47
|
+
In user scope, install enables linger without a prompt when logind allows it, and otherwise asks `loginctl enable-linger`, which may want your password; `--no-linger` (or `CADENCE_LINGER=0`) keeps the service to login.
|
|
46
48
|
Open the printed private link once; the browser then keeps an HttpOnly cookie, and `http://127.0.0.1:17423/` alone only says how to get the link.
|
|
47
49
|
For native `/cadence`, let Pi load the same copy: `pi install "$(npm root -g)/@abelxiaoxing/cadence"` records that path and loads it in place, so an npm upgrade updates both.
|
|
48
50
|
|
|
@@ -54,7 +56,7 @@ cadence server uninstall # before npm uninstall -g, which runs no Cadence scri
|
|
|
54
56
|
```
|
|
55
57
|
|
|
56
58
|
Upgrade with `npm install -g @abelxiaoxing/cadence@latest`, then `cadence server restart`, which rewrites the service definition with the current Node and package paths.
|
|
57
|
-
Restart, stop and uninstall go through the authenticated orderly Stop, so running work is stopped, not detached;
|
|
59
|
+
Restart, stop and uninstall go through the authenticated orderly Stop, so running work is stopped, not detached; system/root, macOS and Windows management refuse to signal or replace a service when its Stop is not confirmed.
|
|
58
60
|
`stop` keeps the registration, so the service starts again at the next boot or login.
|
|
59
61
|
Uninstall removes only the generated definition; workspace, history, linger and Pi sources stay.
|
|
60
62
|
|
|
@@ -138,6 +140,32 @@ Its chrome follows pi-web-ui: a compact topbar with view tabs and icon actions,
|
|
|
138
140
|
The interface is in English or Simplified Chinese: it follows the browser's first preferred language, and the topbar's globe chip (`中文` / `EN`) switches it, remembered only in that browser.
|
|
139
141
|
Error codes, identifiers, commands and recorded content stay untranslated.
|
|
140
142
|
|
|
143
|
+
### Root services and independent projects
|
|
144
|
+
|
|
145
|
+
Linux can run Cadence as a system-level root service, without a user bus or linger:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
sudo -H cadence server install --system --workspace /srv/project
|
|
149
|
+
sudo -H cadence server status --system
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Root defaults to `--system`; an ordinary account still defaults to `--user`, which retains the existing linger behavior.
|
|
153
|
+
There is no automatic sudo or credential/profile copying: the root owner uses root's own Pi sources and private state, and has full host permissions, not a sandbox.
|
|
154
|
+
Stop old owners before changing accounts or state homes; a different account's private namespace cannot coordinate the former owner's work.
|
|
155
|
+
|
|
156
|
+
Open another existing folder explicitly, or create a new project directory:
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
cadence project open /absolute/project
|
|
160
|
+
cadence project open /absolute/new-project --create
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
A new project selects an available loopback port once; reopening keeps its exact owner, port, cwd and history.
|
|
164
|
+
The command prints its private WebUI entry link; it does not create a task, choose a model or Start execution, and existing projects' admitted work continues.
|
|
165
|
+
Each project has its own WebUI entry and native `/cadence` attachment, not a global mutable cwd or a same-page cross-project router.
|
|
166
|
+
For independent boot services, use `cadence server install --system --name api --workspace /srv/api --port 17424` as root, and manage it with the same `--system --name api` flags.
|
|
167
|
+
See [service management](docs/service-management.md) for migration, named port/token files and limits.
|
|
168
|
+
|
|
141
169
|
## Execution, checks and review
|
|
142
170
|
|
|
143
171
|
- Graphs contain 1–16 required assignments with explicit purposes/dependencies and exact model choice; no recursive spawning or autonomous retry.
|
|
@@ -217,22 +245,22 @@ Historical logs are not runtime dependencies, and removing development tooling m
|
|
|
217
245
|
|
|
218
246
|
### Source ownership and API boundaries
|
|
219
247
|
|
|
220
|
-
| Owner
|
|
221
|
-
|
|
|
222
|
-
| `src/index.ts`, `src/task-shell.ts`, `src/native-*.ts`
|
|
223
|
-
| `src/agent-panel.ts`, `src/agent-panel-model.ts`, `src/agent-panel-cadence.ts`
|
|
224
|
-
| `src/service/host.ts`, `src/service/cli.ts`, `src/service/{autostart,launchd,login-run}.ts`
|
|
225
|
-
| `src/service/{coordination,private-files,owner,recovery}.ts`
|
|
226
|
-
| `src/service/{admission,task-actions,graph,scheduler}.ts`
|
|
227
|
-
| `src/service/{model-config,model-context,model-choice,auth-network}.ts`, `src/sdk-network.ts`
|
|
228
|
-
| `src/worker-public.mjs` and `.d.mts`, `src/service-contract.ts`
|
|
229
|
-
| `src/service/contracts/`, `src/service-client.ts`, `src/service-wire.ts`, `src/service-http-client.ts`
|
|
230
|
-
| `src/subagent-process.ts`, `src/sdk-worker.mjs`, `src/worker-{capture,redaction,tools}.mjs`
|
|
231
|
-
| `src/history/{writer,run-state,publication}.ts`
|
|
232
|
-
| `src/history/` readers/proof/search/export/live modules
|
|
233
|
-
| `src/task-store.ts`, `src/task-types.ts`, `src/task-diff.ts`
|
|
234
|
-
| `web/src/`
|
|
235
|
-
| `scripts/`, `provenance/`, `licenses/`
|
|
248
|
+
| Owner | Responsibility |
|
|
249
|
+
| ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
250
|
+
| `src/index.ts`, `src/task-shell.ts`, `src/native-*.ts` | Nonactivating Pi entry, raw native consent, typed service attachment, paging and verified export; no embedded executor |
|
|
251
|
+
| `src/agent-panel.ts`, `src/agent-panel-model.ts`, `src/agent-panel-cadence.ts` | Readonly agent panel over Pi's `subagent:*` bus, fed with a running owner's children; never starts or cancels work |
|
|
252
|
+
| `src/service/host.ts`, `src/service/cli.ts`, `src/service/{autostart,system-service,launchd,login-run}.ts` | Single-owner composition, explicit project launch and generated system/user service definitions; management stays SDK-free |
|
|
253
|
+
| `src/service/{coordination,private-files,owner,recovery}.ts` | Canonical overlap exclusion, exact locks, original-operation lifetime and informed local recovery |
|
|
254
|
+
| `src/service/{admission,task-actions,graph,scheduler}.ts` | Context-bound actions, actual current views, finite graph scheduling, check/review barriers |
|
|
255
|
+
| `src/service/{model-config,model-context,model-choice,auth-network}.ts`, `src/sdk-network.ts` | Existing Pi sources, explicit exact model/thinking, original OAuth writeback and strict shared network policy |
|
|
256
|
+
| `src/worker-public.mjs` and `.d.mts`, `src/service-contract.ts` | One portable core/domain/capture codec with matching declarations and facade; TS core/domain/capture modules re-export it |
|
|
257
|
+
| `src/service/contracts/`, `src/service-client.ts`, `src/service-wire.ts`, `src/service-http-client.ts` | Closed DTOs, typed named operations, reply correlation, transport bounds and one projection tracker |
|
|
258
|
+
| `src/subagent-process.ts`, `src/sdk-worker.mjs`, `src/worker-{capture,redaction,tools}.mjs` | Same native worker/check launcher, genuine SDK capture, incremental sanitizer and bounded no-raw-spill bash adapter |
|
|
259
|
+
| `src/history/{writer,run-state,publication}.ts` | One committed sequence authority, shared semantic reducers and expected-byte strict-v1 summaries |
|
|
260
|
+
| `src/history/` readers/proof/search/export/live modules | Original-source integrity, bounded retained navigation, exact search and complete nonblocking exports |
|
|
261
|
+
| `src/task-store.ts`, `src/task-types.ts`, `src/task-diff.ts` | Literal workspace identity, bounded plain summaries/configuration and honest change observation |
|
|
262
|
+
| `web/src/` | Reused presentation and thin shared-client adapters, not SDK/session/execution ownership |
|
|
263
|
+
| `scripts/`, `provenance/`, `licenses/` | Reproducible builds, exact package/body/reference/notice audits and pinned reuse |
|
|
236
264
|
|
|
237
265
|
The native path is `index → shell → native client → service → scheduler/process/history`.
|
|
238
266
|
`src/task-controller.ts` remains a legacy regression subject, not a reachable or shipped production executor.
|
|
@@ -245,6 +273,11 @@ Use their current declarations rather than copying DTO parsing, reducers, a seco
|
|
|
245
273
|
### Admission, execution and original lifetimes
|
|
246
274
|
|
|
247
275
|
- One canonical checkout/state-home domain has one persistent owner, including while idle.
|
|
276
|
+
Explicit project launch uses a new port only for a new checkout selection; reopened projects retain exact cwd/port/history, and no project navigation changes another owner's execution.
|
|
277
|
+
Root Git observations require Git's own ownership check: reset `safe.directory` exceptions per command, drop inherited `SUDO_UID` and disable fsmonitor, without configuration writes or ownership changes.
|
|
278
|
+
Merely selecting a foreign-owned repository cannot grant permission to run its clean/process filters; use its owning account instead.
|
|
279
|
+
Ordinary-user Git trust behavior remains unchanged.
|
|
280
|
+
System services require uid0, bypass user managers/logind entirely and require confirmed native Stop before manager stop or definition replacement; changed state-home inputs, old user units and detached running owners refuse implicit adoption.
|
|
248
281
|
Registry reservation atomically rejects canonical ancestor/descendant overlaps, including nested Git roots and aliases; a different state-home does not coordinate arbitrary other owners.
|
|
249
282
|
- Native entry requires fresh raw Enter within1500ms, trusted/idle TUI, no pending modal/messages and no child guard.
|
|
250
283
|
Browser authentication grants access, not task consent; normal mutation ingress also requires independently built compatible client/service/assets identities.
|
|
@@ -302,6 +335,8 @@ Never await a run's own settlement from inside its awaited subscriber.
|
|
|
302
335
|
Sanitize known literals, structured sensitive values and terminal controls before stdout and again before owner persistence.
|
|
303
336
|
Preserve split UTF-8, escaped literals, surrogate pairs and possible secret prefixes across chunks; independent per-chunk string replacement is insufficient.
|
|
304
337
|
Tool-start is an SDK observation, not proof of native execution; tool error is not a parent check result.
|
|
338
|
+
A prompt/preflight/non-success terminal response is labeled `worker-failure: prompt`, not `capture`; the recorded message stop reason, actual exit and cleanup facts stay independent.
|
|
339
|
+
A browser settlement timeout must retain its last receipt/active-roster facts before fixture teardown: a later Stop and its uncertain publication are not evidence that capture caused the original wait failure.
|
|
305
340
|
Hidden thinking, signatures, provider envelopes and binary/media bodies remain explicitly omitted.
|
|
306
341
|
Known-secret sanitation is not universal business-secret detection or an OS privacy sandbox.
|
|
307
342
|
|
|
@@ -309,6 +344,9 @@ Only the serialized history writer assigns committed sequence and persisted cont
|
|
|
309
344
|
Commit actual admission/auth/launch/result facts in order; an index, observer callback or candidate reducer cannot authorize effects.
|
|
310
345
|
Quota/exhaustion/refusal stops unsafe work without eviction; torn tails retain a verified prefix and visible gaps, while interior corruption refuses affected ranges.
|
|
311
346
|
Derived indexes are optional accelerators: readers validate original batches/artifacts, use bounded fallback and never repair on view.
|
|
347
|
+
Live task/run/child views share an already observed containing snapshot only within one notification drain, retaining its exact context and committed watermark.
|
|
348
|
+
Narrowing requires the requested parent/child to be present; a historical run outside the task horizon still takes its independent reader, and no observation is cached into the next drain or used as renewed action consent.
|
|
349
|
+
Transient reuse keeps at most one page-sized source per viewer, rather than retaining every unobserved run ring.
|
|
312
350
|
Current/plan/review reads replay a task's batches, so per-file cost bounds them: on a slow host (Windows Node 22) that replay once exceeded the 100ms read quantum after review, leaving the task `unavailable`.
|
|
313
351
|
`PrivateFiles` reuses a directory walk only within one synchronous `DirectoryWalks` scope (a read budget or lease check), after re-observing that directory's identity; an await or later call walks again, and the quantum is never raised instead.
|
|
314
352
|
When a batch settles a task's publication, the owner writer remembers in process memory that task's fold state (check facts, latest run, plan/lineage progress with original admission locators) under its chain head's sequence and hash, a bounded number of tasks and states.
|
|
@@ -379,6 +417,7 @@ Byte values are UTF-8/encoded bounds where specified; they are not targets to si
|
|
|
379
417
|
| Token source the user edits (`CADENCE_TOKEN` in `.env`) | Read once before binding, never bound for the run: an edit cannot fence Stop |
|
|
380
418
|
| Saved port/token source differs from `.env` | Only when stopped, reselect just those under the reservation; running ones refuse |
|
|
381
419
|
| User-made `.env` (0755/0644, umask 002, dotfiles links) | Like Pi's sources: ownership only, links followed; each refusal names its reason |
|
|
420
|
+
| Credential value inside a provider/model ID (Ollama `ollama`) | Withhold and count only that catalog entry; choosing it refuses; others selectable |
|
|
382
421
|
|
|
383
422
|
Good: current context → explicit grant → durable acceptance → genuine original native/storage settlement → exact retained dependency → real checks → human review.
|
|
384
423
|
Base: a client leaves and another reattaches to the same accepted run without launching it again.
|
|
@@ -395,6 +434,8 @@ Read affected source and adjacent tests, identify the actual owner of a contract
|
|
|
395
434
|
Use ordinary source editing and directly affected checks while developing; independently review complete capabilities and run full `verify` at stable integration boundaries.
|
|
396
435
|
Do not run the entire suite after every local edit, but do not replace final integration with unit mocks or citation counts.
|
|
397
436
|
`bun run test` runs unit files in parallel, then `serialProcessFiles` (real PTY/SDK process flows) and `serialScanFiles` (>8MiB cold scans) from `vitest.config.ts` one at a time; a file belongs there only when it measurably exceeds its own waits under shared CPU.
|
|
437
|
+
`vitest run --config test/fast.config.ts` selects only its reviewed short-test allowlist, without inheriting root projects; `test/integration.config.ts` selects the exact complement and retains the serial projects.
|
|
438
|
+
`test/test-selection.test.ts` compares the runner's actual file discovery across all three configurations, rejecting duplicates, overlap, omissions and moved serial files; new tests belong to integration until explicitly promoted.
|
|
398
439
|
Create a fresh 0700 root on the checkout's disk filesystem and explicitly set TMPDIR/TEMP/TMP to it; a nearly full tmpfs `/tmp` makes owners refuse at their free-space floor (`receipt-inconclusive`) and `web/dist` publication fail with EXDEV.
|
|
399
440
|
A test waits for the event it needs (both providers contacted, a row settled), not a wall-clock guess at how long process starts take; fixed waits stay only where time is the contract.
|
|
400
441
|
Use fabricated credentials, isolated browser/profile/state roots and local fixtures; never personal credentials, automatic downloads or unknown process cleanup.
|
|
@@ -412,18 +453,44 @@ Regression owners:
|
|
|
412
453
|
- `agent-panel`, `agent-panel-model`, `agent-panel-cadence`: panel keys/rendering and readonly owner observation that never starts, cancels or replays work.
|
|
413
454
|
- `test/web/`: adapted pure/SSR tests separately from actual built-browser contexts, HTTPS, mixed clients, focus/viewports and complete downloads.
|
|
414
455
|
- `package-*`, `distribution`, `service-package`, `independence`: exact tar/member/body/import/notice closure and nonactivating discovery.
|
|
415
|
-
- `service-autostart`, `install-config`: explicit `cadence server` registration on systemd, launchd and the Windows Run key, definition escaping, restart/disable/mask/foreign handling, orderly Stop before every change, the `.env` port and token lines and a package without install scripts.
|
|
456
|
+
- `service-autostart`, `project-start`, `install-config`: explicit `cadence server` registration on systemd (root system scope without linger, legacy user scope), launchd and the Windows Run key, separate named service definitions and directory owners, definition escaping, restart/disable/mask/foreign handling, orderly Stop before every change, the `.env` port and token lines and a package without install scripts.
|
|
416
457
|
- `service-cli`: real owners started from the `.env` port and token, reselection of a stopped workspace, edits while running and rotation of the token line.
|
|
417
458
|
- `workspace-landing`: home-directory redirection to `~/cadence` across management, native attach and observation.
|
|
418
459
|
|
|
419
460
|
Regenerate operator/service/native/browser outputs only through their canonical builders; `--check` variants compare new private output without overwriting canonical bytes.
|
|
420
461
|
WebUI text is keyed by its English source: fixed text goes through `useT().t`, whose type requires a `web/src/i18n/zh.ts` entry with the same `{name}` marks, and run-time status words or kept notices through `tx`, which passes unknown text and codes through.
|
|
421
462
|
The timeline client takes the chosen language from its bridge and is remounted on a switch; browser scenarios pin `locale: "en-US"` so English assertions do not follow the host's `LANG`.
|
|
463
|
+
The browser is conversation-first: the left panel groups the current service project, the actual active roster and retained task conversations, with run/subagent children under their task.
|
|
464
|
+
One service still exposes one checkout; the project section is not a cross-project registry, Pi session importer or cwd switcher.
|
|
465
|
+
The center shows retained message text as Markdown by default; tools/context/timeline/run facts and export/link actions live under Details, while the collapsible right inspector remains bound to the task's current plan/check/change/review context even when viewing a child.
|
|
466
|
+
Historical child links bind exact scope and watermark.
|
|
467
|
+
Bounded history and Back never silently rebase; running conversations offer an explicit new-activity update, and source limitations/progress stay visible even when complete-record metadata is folded.
|
|
468
|
+
Cancel uses the independent active roster (including the fallback strip when workspace discovery fails); navigation, project selection and hiding panels never cancel or authorize execution.
|
|
469
|
+
|
|
422
470
|
Publish `web/dist` from Node: Vite run under Bun lays out its preload helper differently, so a Bun build fails a Node `build:web:check`.
|
|
423
471
|
Review the independently enumerated package member list and every emitted body, not a count or an expected list generated from the tar under test.
|
|
424
472
|
Keep public Pi peers external for the extension, the exact Undici/ws runtime edges, the exact optional SDK pins for the standalone owner and all upstream/font/vendor notices.
|
|
425
473
|
Preserve original license bytes through their precise Git attributes; line-ending normalization must not invalidate audited source/notice hashes.
|
|
426
474
|
|
|
475
|
+
### Release 2.0.11 acceptance snapshot
|
|
476
|
+
|
|
477
|
+
Full `verify` passed on the 2.0.11 release candidate before this evidence-only paragraph: 1976 root tests in 80 files (27 skipped on Linux), 122 web tests, 5 real-browser scenarios and the 132-member pack audit.
|
|
478
|
+
Observed environment: Linux x86_64, Node26.7.0, Bun1.3.14, Pi SDK0.84.3 and existing Chrome for Testing151.0.7922.34.
|
|
479
|
+
The root Git regressions use real Git with simulated ownership mismatch, and system service management uses isolated filesystem/manager adapters; no real root/systemd installation, new CI matrix, macOS or Windows qualification is claimed.
|
|
480
|
+
|
|
481
|
+
This release adds explicit directory project launch and independent named Linux system/root services, and a conversation-first WebUI with retained task/run/subagent navigation.
|
|
482
|
+
Root observation no longer automatically trusts foreign Git repositories; inactive service definitions cannot stop or adopt detached owners, including through restart or uninstall.
|
|
483
|
+
Fast/integration test selection is checked against actual Vitest discovery, and provider prompt failures remain separate from capture failures.
|
|
484
|
+
Live task/run/child views reuse only an already observed containing snapshot within one notification drain, never a previous round's action context.
|
|
485
|
+
|
|
486
|
+
### Release 2.0.10 acceptance snapshot
|
|
487
|
+
|
|
488
|
+
Full `verify` passed on the 2.0.10 release tree (`4843334` plus the version bump), in the 2.0.3 environment below.
|
|
489
|
+
It ran 1949 root tests in 78 files (27 Windows-only controls skipped on Linux), 116 web tests, 5 real-browser scenarios and the 132-member pack audit, and CI 40278386880 passed 12/12 on the candidate `3cd94f4` (this commit before its snapshot).
|
|
490
|
+
That CI run's first Windows Node 22 `parallel` attempt failed only `workspace-landing`'s cleanup (EBUSY removing the just-stopped landing workspace), as in 2.0.6's first run; the job passed when rerun.
|
|
491
|
+
The fix answers a 2.0.9 report: an Ollama `"apiKey": "ollama"` under a provider named `ollama` made the whole model catalog refuse `unsupported-context`.
|
|
492
|
+
An isolated owner with that `models.json` beside a second provider reproduced the refusal on 2.0.9; on 2.0.10 Chromium listed the second provider's models and explained the one withheld entry in English and Chinese.
|
|
493
|
+
|
|
427
494
|
### Release 2.0.9 acceptance snapshot
|
|
428
495
|
|
|
429
496
|
Full `verify` passed on the 2.0.9 release tree (`0e720a1` plus the version bump), in the 2.0.3 environment below.
|
|
@@ -8,9 +8,9 @@ Repository `provenance/webui-reuse.json` records source/destination hashes and e
|
|
|
8
8
|
|
|
9
9
|
| Source family | Package adaptation |
|
|
10
10
|
| ------------------------------------ | ------------------------------------------------------------------------ |
|
|
11
|
-
| App, TopBar, panels, layout, drawers |
|
|
12
|
-
| conv-groups, PlanBoard |
|
|
13
|
-
| MessageList, Message, ToolCallBlock |
|
|
11
|
+
| App, TopBar, panels, layout, drawers | Conversation-first workspace, project/session sidebar and phone drawers |
|
|
12
|
+
| conv-groups, PlanBoard | Active/history task and subagent tree, finite graph and explicit consent |
|
|
13
|
+
| MessageList, Message, ToolCallBlock | Default Markdown transcript, folded metadata and bounded retained detail |
|
|
14
14
|
| Markdown, StreamMarkdown, copy | Sanitized Markdown/math/code, no script/external-image execution |
|
|
15
15
|
| FilePreview, SCM presentation | Readonly observed changes, no Git/file mutation API |
|
|
16
16
|
| GlobalSearchModal, search, deep link | Literal durable search, exact versioned hits and scoped export |
|
|
@@ -23,12 +23,41 @@ Missing/corrupt existing setup is preserved, not filled in or reset.
|
|
|
23
23
|
A workspace that contains the private state home (the home directory and its ancestors, such as `C:\Users\<you>` with `%LOCALAPPDATA%` or `~` with `~/.local/state`) would expose private history and locks to change observation, so every command there uses `~/cadence` (`%USERPROFILE%\cadence`) instead of refusing.
|
|
24
24
|
Only `start` creates it; `status` reports `absent` without creating it, an explicit `--workspace` adds one informational line, and JSON output names the effective `workspace` only when it differs.
|
|
25
25
|
`/cadence` started in such a directory attaches to the same owner, and real project directories are never redirected.
|
|
26
|
+
A directory inside `cadence-assistant-v1` is never an engineering workspace, even for root or `project open --create`.
|
|
26
27
|
Apart from the background service below, there is no supervisor installation, provider login or credential migration.
|
|
27
28
|
|
|
29
|
+
## Open a project directory
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
cadence project open /absolute/existing-project
|
|
33
|
+
cadence project open /absolute/new-project --create
|
|
34
|
+
# Optional explicit sources, without copying them:
|
|
35
|
+
cadence project open /srv/project --agent-dir /root/.pi/agent --state-home /root/.local/state
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Without a path, `project open` uses the current directory; a missing directory refuses unless `--create` was supplied.
|
|
39
|
+
The project may be a Git checkout or an ordinary directory, including spaces and Unicode names; it need not be inside `~/cadence` or the package directory.
|
|
40
|
+
The command explicitly starts a detached owner, or observes the already running exact owner; it never creates a task, chooses a model, renews a grant or starts a worker.
|
|
41
|
+
New projects bind port0 and retain the resulting positive port, avoiding the default17423 collision; existing selections keep their exact port, cwd, agent sources and history.
|
|
42
|
+
The JSON result reports the canonical workspace, actual task cwd and endpoint; the private entry link is printed separately to stderr, like `server install`.
|
|
43
|
+
Keep that link private.
|
|
44
|
+
Open Pi in the same project and use `/cadence` for the native client; explicit `cadence stop --workspace <path>` stops only that project.
|
|
45
|
+
Opening another project never changes the cwd or cancels work in the former one.
|
|
46
|
+
Each project has its own browser entry and owner; this is not a same-page cross-project browser router.
|
|
47
|
+
Git subdirectories and aliases resolve to the same checkout, while overlapping parent/child owners still refuse.
|
|
48
|
+
An unknown, quarantined or inaccessible selection is never treated as a new project or recovered by choosing another state home.
|
|
49
|
+
|
|
50
|
+
When run as root, Git observation requires a repository Git recognizes as root-owned: Cadence clears `safe.directory` exceptions per command, removes inherited `SUDO_UID` and disables fsmonitor.
|
|
51
|
+
A foreign-owned repository refuses even if a global `safe.directory=*` would allow it; selecting a path is not consent to execute its configured clean/process filters.
|
|
52
|
+
Use the repository's owning account instead, or deliberately provision a root-owned checkout outside Cadence; no Git configuration edit, `chown`, credential import or permission reset is performed.
|
|
53
|
+
Ordinary-user Git trust behavior remains intact.
|
|
54
|
+
Trusted repositories can still carry executable Git configuration; root is not an isolation or sandbox guarantee.
|
|
55
|
+
Changing to root changes the effective account and its private namespace: stop all old Cadence owners before doing so, since different accounts/state homes do not coordinate each other's original work.
|
|
56
|
+
|
|
28
57
|
## Background service
|
|
29
58
|
|
|
30
59
|
```sh
|
|
31
|
-
cadence server install [--workspace /absolute/project] [--no-linger]
|
|
60
|
+
cadence server install [--workspace /absolute/project] [--system|--user] [--no-linger]
|
|
32
61
|
cadence server status
|
|
33
62
|
cadence server restart
|
|
34
63
|
cadence server stop
|
|
@@ -37,6 +66,11 @@ cadence server uninstall
|
|
|
37
66
|
|
|
38
67
|
Only these explicit commands register, change or remove the service; no package install, update hook, `/cadence` or browser visit does.
|
|
39
68
|
The package has no install script, so npm 12 and Bun skipping untrusted scripts by default changes nothing; the pinned Pi SDK's blocked `@google/genai` preinstall only echoes `no-op`, and its `protobufjs` postinstall only warns about version schemes.
|
|
69
|
+
Linux server commands accept `--system` or `--user` after the action; root defaults to system scope and other accounts to user scope.
|
|
70
|
+
`--system` requires root and never runs sudo itself; `--user` explicitly selects the legacy per-user manager, even when invoked by root.
|
|
71
|
+
Linux also accepts `--name <name>` on every server command to select `cadence-<name>.service`, and `install` accepts `--port <1-65535>` for a missing port setting.
|
|
72
|
+
Names must start with a letter or digit and contain only letters, digits, `_` and `-` (at most48characters).
|
|
73
|
+
|
|
40
74
|
`install` also creates the research template `~/.pi/agent/cadence/.env` when it is missing, never overwriting it, whatever the service result, and adds its [port and token](#port-and-token) lines when they are missing.
|
|
41
75
|
`install` works from any package location and runs `start --foreground --workspace <workspace> --server-config <file>` (Linux, macOS) or the same without `--foreground` (Windows) from that copy with the installing Node, without `--port`.
|
|
42
76
|
The workspace is `~/cadence` (created if missing), an existing absolute `--workspace`, or on later installs the one the definition already runs; a workspace containing the service's state home lands on `~/cadence` with a note.
|
|
@@ -51,7 +85,7 @@ A definition Cadence did not generate is left unchanged (`foreign`), and `status
|
|
|
51
85
|
Pi's installer omits peers, so the standalone owner and workers load the Pi SDK (`pi-coding-agent`, `pi-ai`) from exact 0.84.3 optional dependencies.
|
|
52
86
|
Package files may be hard links into the package manager's store (Bun's default install); Cadence binds their exact content, not their link count.
|
|
53
87
|
Missing or unusable Pi sources refuse before an owner registers; a new owner without the SDK quarantines.
|
|
54
|
-
`install` therefore writes
|
|
88
|
+
`install` therefore writes no service definition while the SDK is unresolvable (for example, optional dependencies omitted), and registers without starting while Pi's `auth.json` is missing (`waiting-for-pi-login`); log in to Pi, then run `cadence server restart`.
|
|
55
89
|
|
|
56
90
|
While no service is registered, Pi's session-start card shows a separate warning naming `cadence server install` for an installed package copy (`node <copy>/src/service-cli.mjs server install` for Pi's managed copy, which puts no bin on `PATH`); a checkout loaded with `pi -e` stays quiet.
|
|
57
91
|
It is a warning rather than part of the info card because Pi's TUI replaces one info status with the next.
|
|
@@ -88,9 +122,53 @@ Under the new owner reservation Cadence retires the previous token with a new ac
|
|
|
88
122
|
A different agent directory, host or proxy still conflicts, and a running owner refuses until it has stopped.
|
|
89
123
|
An interrupted replacement keeps the reservation as a fence and is never repaired automatically, like any interrupted start.
|
|
90
124
|
|
|
125
|
+
### Linux systemd system/root unit (no linger)
|
|
126
|
+
|
|
127
|
+
```sh
|
|
128
|
+
# Root must already have its own usable Pi credentials and SDK installation.
|
|
129
|
+
sudo -H cadence server install --system --workspace /srv/project
|
|
130
|
+
sudo -H cadence server status --system
|
|
131
|
+
sudo -H cadence server restart --system
|
|
132
|
+
sudo -H cadence server stop --system
|
|
133
|
+
sudo -H cadence server uninstall --system
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Use an absolute Node and package CLI path with `sudo -H` if `cadence` is not on root's PATH.
|
|
137
|
+
These commands generate `/etc/systemd/system/cadence.service` with `User=root` and `WantedBy=multi-user.target`, and use `systemctl --system`.
|
|
138
|
+
They never consult or change linger, require no logged-in user manager and start at boot independently of root login.
|
|
139
|
+
The definition freezes HOME, PATH, LANG/LC_ALL, an explicit XDG_STATE_HOME and custom PI_CODING_AGENT_DIR; root must own its selected Pi sources, and no normal-user profile or credentials are copied.
|
|
140
|
+
Root runs the agent and approved checks with full root privileges: browser authentication remains access, not permission to Start, and this is not a sandbox.
|
|
141
|
+
Status and install output identify system scope; Pi's root startup hint recommends `--system`, never `loginctl enable-linger`.
|
|
142
|
+
|
|
143
|
+
A prior user-scope definition of the same name causes `scope-conflict`, never automatic migration.
|
|
144
|
+
For a root account's older user unit, explicitly run `cadence server uninstall --user` first, then install `--system`; this preserves its workspace, private history, credentials and linger setting.
|
|
145
|
+
For another account's service, first stop/uninstall it as that account; root must not silently import its private state or assume its owners are absent.
|
|
146
|
+
A detached owner already running in the target directory causes `project-running`; stop it explicitly before installing a different supervisor.
|
|
147
|
+
An old but inactive unit does not own a subsequently detached owner: install, restart, stop and uninstall also refuse in that case, without sending Stop to it, and changing the requested target does not bypass the old directory's check.
|
|
148
|
+
An unconfirmed or quarantined Stop, changed generated file, changed HOME/XDG state selection, a mask or a foreign unit prevents replacement/adoption.
|
|
149
|
+
`restart`, `stop` and `uninstall` await the authenticated original owner Stop before invoking systemctl, so manager timeouts are never used as proof of settlement.
|
|
150
|
+
Restart keeps disablement and does not unmask anything; uninstall removes only the generated definition, never data or linger.
|
|
151
|
+
Direct systemctl shutdown still has the unit's ExecStop and timeout behavior described below; it is not an alternative recovery mechanism.
|
|
152
|
+
|
|
153
|
+
### Independent named Linux services
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
sudo -H cadence server install --system --name api --workspace /srv/api --port 17424
|
|
157
|
+
sudo -H cadence server install --system --name web --workspace /srv/web --port 17425
|
|
158
|
+
sudo -H cadence server status --system --name api
|
|
159
|
+
sudo -H cadence server restart --system --name web
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The units, ports, access tokens, working directories and owner lifetimes are separate; restarting or stopping one does not touch another.
|
|
163
|
+
A named service uses `~/.pi/agent/cadence/servers/<name>.env` under its effective account, never a copy of the default token or research/provider configuration.
|
|
164
|
+
A new named configuration requires `--port`; existing port/token lines are preserved, and a conflicting `--port` refuses instead of rewriting them.
|
|
165
|
+
Edit the selected named file explicitly and restart that same `--name` to change its port.
|
|
166
|
+
An occupied port still refuses rather than silently falling back; use `project open` when an automatically selected detached port is preferable.
|
|
167
|
+
All projects still share one coordination state home for their account, retaining canonical overlap checks and uncertain fences.
|
|
168
|
+
|
|
91
169
|
### Linux systemd user unit
|
|
92
170
|
|
|
93
|
-
`install` writes `~/.local/share/systemd/user/cadence.service`, enables it for `default.target` (also undoing a `disable`, never a `mask`) and starts it.
|
|
171
|
+
`install --user` writes `~/.local/share/systemd/user/cadence.service`, enables it for `default.target` (also undoing a `disable`, never a `mask`) and starts it.
|
|
94
172
|
A user unit runs with the user manager, which starts at login unless logind lingers the user, so install also enables linger when it is off.
|
|
95
173
|
It first calls `SetUserLinger` through `busctl` with `interactive=false` and no interactive authorization (bounded to 15s), because `loginctl enable-linger` always requests an interactive check that a desktop polkit agent can turn into a password dialog even with `--no-ask-password`.
|
|
96
174
|
Only where polkit requires authorization for `set-self-linger` (older systemd defaults) does it then run `loginctl enable-linger`, which may ask for your password; that comes after the start, so an interrupted prompt never leaves the owner stopped, and a refusal reports `"linger":"refused"` with a login-only unit.
|
|
@@ -152,6 +230,9 @@ At sign-out or shutdown Windows ends the owner without orderly Stop, which can l
|
|
|
152
230
|
`models.json` may use only fields the pinned SDK supports.
|
|
153
231
|
`supportsMidConvoSystemMessages` from newer Pi is accepted because the pinned SDK has no system-role conversation message; other unknown fields refuse rather than being silently ignored.
|
|
154
232
|
The owner model choice offers every thinking level the pinned SDK supports for a model, `off` through `xhigh` and `max`; a model whose `thinkingLevelMap` maps only `max` (as some DeepSeek configurations do) is chosen at `max`, and a child runs only at exactly that level.
|
|
233
|
+
A model whose provider or model ID contains a known credential value is withheld from the catalog, since showing the ID would reveal that value; the catalog says how many models are withheld, and the rest stay selectable.
|
|
234
|
+
Credential values are `auth.json` keys, `models.json` `apiKey` and header values (and the variables they name) and service-environment variables whose names contain `KEY`, `TOKEN`, `SECRET`, `PASS`, `AUTH` or `COOKIE`.
|
|
235
|
+
The common Ollama placeholder `"apiKey": "ollama"` under a provider named `ollama` is such a case: give the placeholder a value that matches no provider or model ID (for example `"local-no-auth"`), then restart the service.
|
|
155
236
|
The owner loads `models.json` once at start: after it changes, the model catalog, the chosen model and the workspace view refuse as `stale` (credential changes in `auth.json` are read fresh).
|
|
156
237
|
Restart the service to reload it (`cadence server restart`, or `stop` then `start`), then choose the owner model again.
|
|
157
238
|
- A foreground owner can be managed from another terminal.
|
package/package.json
CHANGED
package/src/native-build.ts
CHANGED
package/src/native-client.ts
CHANGED
|
@@ -32,12 +32,14 @@ function definition(o: AutostartObservation): string | undefined {
|
|
|
32
32
|
switch (o.platform) {
|
|
33
33
|
case "linux":
|
|
34
34
|
return existsSync(o.systemd)
|
|
35
|
-
?
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
? o.uid === 0
|
|
36
|
+
? join(o.systemUnitDirectory ?? "/etc/systemd/system", UNIT)
|
|
37
|
+
: join(
|
|
38
|
+
xdg(o.env.XDG_DATA_HOME, join(o.home, ".local", "share")),
|
|
39
|
+
"systemd",
|
|
40
|
+
"user",
|
|
41
|
+
UNIT,
|
|
42
|
+
)
|
|
41
43
|
: undefined;
|
|
42
44
|
case "darwin":
|
|
43
45
|
return join(o.home, "Library", "LaunchAgents", `${LABEL}.plist`);
|
|
@@ -52,11 +54,11 @@ function definition(o: AutostartObservation): string | undefined {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
const bootStart = (o: AutostartObservation) =>
|
|
55
|
-
o.platform === "linux" && o.env.CADENCE_LINGER !== "0";
|
|
57
|
+
o.platform === "linux" && (o.uid === 0 || o.env.CADENCE_LINGER !== "0");
|
|
56
58
|
/** Pi's managed copy puts no `cadence` bin on PATH; CADENCE_LINGER=0 keeps
|
|
57
59
|
* the suggested command login-only, too. */
|
|
58
60
|
const installCommand = (o: AutostartObservation) =>
|
|
59
|
-
`${piUserCopy(o) ? `node ${JSON.stringify(join(o.root, "src", "service-cli.mjs"))}` : "cadence"} server install${o.platform === "linux" && o.env.CADENCE_LINGER === "0" ? " --no-linger" : ""}`;
|
|
61
|
+
`${piUserCopy(o) ? `node ${JSON.stringify(join(o.root, "src", "service-cli.mjs"))}` : "cadence"} server install${o.platform === "linux" && o.uid === 0 ? " --system" : o.platform === "linux" && o.env.CADENCE_LINGER === "0" ? " --no-linger" : ""}`;
|
|
60
62
|
|
|
61
63
|
/** Name the explicit command while no background service is registered.
|
|
62
64
|
* Reads paths only. */
|
|
@@ -78,6 +80,9 @@ export interface AutostartObservation {
|
|
|
78
80
|
lingerFile: string;
|
|
79
81
|
/** Exists only on a systemd-booted machine. */
|
|
80
82
|
systemd: string;
|
|
83
|
+
uid?: number;
|
|
84
|
+
/** Only supplied by readonly observation tests. */
|
|
85
|
+
systemUnitDirectory?: string;
|
|
81
86
|
}
|
|
82
87
|
function hostObservation(): AutostartObservation {
|
|
83
88
|
let user = "";
|
|
@@ -93,6 +98,7 @@ function hostObservation(): AutostartObservation {
|
|
|
93
98
|
root: fileURLToPath(new URL("..", import.meta.url)),
|
|
94
99
|
lingerFile: user ? join("/var/lib/systemd/linger", user) : "",
|
|
95
100
|
systemd: SYSTEMD_BOOTED,
|
|
101
|
+
uid: process.getuid?.(),
|
|
96
102
|
};
|
|
97
103
|
}
|
|
98
104
|
/** Same selection as Pi's getAgentDir: PI_CODING_AGENT_DIR or ~/.pi/agent. */
|
|
@@ -139,7 +145,7 @@ export function autostartNotice(
|
|
|
139
145
|
return installedCopy(o)
|
|
140
146
|
? `Cadence服务:后台服务未安装,WebUI(默认 http://127.0.0.1:17423)不会运行。安装并${bootStart(o) ? "开机" : "登录后"}自启:${installCommand(o)}`
|
|
141
147
|
: undefined;
|
|
142
|
-
if (o.platform !== "linux") return;
|
|
148
|
+
if (o.platform !== "linux" || o.uid === 0) return;
|
|
143
149
|
const config = xdg(o.env.XDG_CONFIG_HOME, join(o.home, ".config"));
|
|
144
150
|
const user = join(config, "systemd", "user");
|
|
145
151
|
const runtime = o.env.XDG_RUNTIME_DIR;
|
package/src/operator-cli.mjs
CHANGED
|
@@ -210,15 +210,19 @@ function readBounded(path, max, packaged = false) {
|
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
212
|
function gitEnvironment() {
|
|
213
|
+
const root = process.getuid?.() === 0;
|
|
213
214
|
return {
|
|
214
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_"))),
|
|
215
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("GIT_") && !(root && name === "SUDO_UID"))),
|
|
215
216
|
GIT_OPTIONAL_LOCKS: "0",
|
|
216
217
|
LC_ALL: "C"
|
|
217
218
|
};
|
|
218
219
|
}
|
|
220
|
+
function gitArguments(args) {
|
|
221
|
+
return process.platform !== "win32" && process.getuid?.() === 0 ? ["-c", "safe.directory=", "-c", "core.fsmonitor=false", ...args] : args;
|
|
222
|
+
}
|
|
219
223
|
function resolveWorkspace(cwd) {
|
|
220
224
|
const actual = realpathSync(cwd);
|
|
221
|
-
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
225
|
+
const result = spawnSync("git", gitArguments(["rev-parse", "--show-toplevel"]), {
|
|
222
226
|
cwd: actual,
|
|
223
227
|
env: gitEnvironment(),
|
|
224
228
|
encoding: "utf8",
|
|
@@ -281,8 +285,13 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
281
285
|
const canonical = realpathSync(root);
|
|
282
286
|
return contains(canonical, privateRootFor(canonical, stateHome));
|
|
283
287
|
}
|
|
288
|
+
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
289
|
+
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
290
|
+
}
|
|
284
291
|
var LANDING_WORKSPACE = "cadence";
|
|
285
292
|
function effectiveWorkspace(requested, stateHome, home = homedir()) {
|
|
293
|
+
if (privateWorkspace(requested, stateHome))
|
|
294
|
+
throw new Error("Private state cannot be a workspace");
|
|
286
295
|
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
287
296
|
return { workspace: requested, redirected: false };
|
|
288
297
|
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
@@ -294,6 +303,8 @@ class TaskStore {
|
|
|
294
303
|
privateRoot;
|
|
295
304
|
constructor(root, stateHome = defaultStateHome()) {
|
|
296
305
|
this.root = realpathSync(root);
|
|
306
|
+
if (privateWorkspace(this.root, stateHome))
|
|
307
|
+
throw new Error("Private state cannot be a workspace");
|
|
297
308
|
this.tasks = join(this.root, ".cadence", "assistant", "tasks");
|
|
298
309
|
this.privateRoot = privateRootFor(this.root, stateHome);
|
|
299
310
|
if (contains(this.root, this.privateRoot))
|
package/src/sdk-worker.mjs
CHANGED
|
@@ -63,6 +63,8 @@ const fail = (where, category) => {
|
|
|
63
63
|
failure ??= { stage: where, class: category };
|
|
64
64
|
process.exitCode = 1;
|
|
65
65
|
if (first) {
|
|
66
|
+
// Prompt/preflight/terminal failure is distinct from a capture failure.
|
|
67
|
+
// A normal provider error can have a completely retained error message.
|
|
66
68
|
if (
|
|
67
69
|
emitter?.state.position.workerSeq &&
|
|
68
70
|
!emitter.state.position.final &&
|
|
@@ -77,7 +79,9 @@ const fail = (where, category) => {
|
|
|
77
79
|
? "cleanup"
|
|
78
80
|
: where === "network"
|
|
79
81
|
? "network"
|
|
80
|
-
: "
|
|
82
|
+
: ["prompt", "preflight", "assistant-final"].includes(where)
|
|
83
|
+
? "prompt"
|
|
84
|
+
: "capture",
|
|
81
85
|
})
|
|
82
86
|
.catch(() => {});
|
|
83
87
|
} catch {
|
|
@@ -43,6 +43,9 @@ export interface ModelCandidate {
|
|
|
43
43
|
export interface ModelCatalog extends ModelChoiceState {
|
|
44
44
|
status: "complete" | "capacity";
|
|
45
45
|
entries: ModelCandidate[];
|
|
46
|
+
/** Models left out because a provider/model ID contains a known credential
|
|
47
|
+
* literal or control characters; never their identifiers. */
|
|
48
|
+
withheld: number;
|
|
46
49
|
}
|
|
47
50
|
export interface ChooseModelRequest {
|
|
48
51
|
expected: ChoiceVersion;
|
|
@@ -119,11 +122,19 @@ export function decodeModelCandidate(value: unknown): ModelCandidate {
|
|
|
119
122
|
}
|
|
120
123
|
export function decodeModelCatalog(value: unknown): ModelCatalog {
|
|
121
124
|
canonicalJson({ kind: "ok", value }, LIMITS.pageBytes);
|
|
122
|
-
const o = closed(value, [
|
|
125
|
+
const o = closed(value, [
|
|
126
|
+
"version",
|
|
127
|
+
"selected",
|
|
128
|
+
"status",
|
|
129
|
+
"entries",
|
|
130
|
+
"withheld",
|
|
131
|
+
]);
|
|
123
132
|
const entries = list(o.entries, LIMITS.maxPageEntries, decodeModelCandidate);
|
|
124
133
|
const status = oneOf(o.status, ["complete", "capacity"]);
|
|
134
|
+
const withheld = integer(o.withheld, 0, LIMITS.maxPageEntries);
|
|
125
135
|
if (
|
|
126
|
-
(status === "capacity" && entries.length) ||
|
|
136
|
+
(status === "capacity" && (entries.length || withheld)) ||
|
|
137
|
+
entries.length + withheld > LIMITS.maxPageEntries ||
|
|
127
138
|
new Set(entries.map((e) => canonicalJson([e.provider, e.model]))).size !==
|
|
128
139
|
entries.length
|
|
129
140
|
)
|
|
@@ -133,5 +144,6 @@ export function decodeModelCatalog(value: unknown): ModelCatalog {
|
|
|
133
144
|
selected: nullable(o.selected, decodeModelSelection),
|
|
134
145
|
status,
|
|
135
146
|
entries,
|
|
147
|
+
withheld,
|
|
136
148
|
};
|
|
137
149
|
}
|