@abelxiaoxing/cadence 2.0.13 → 2.0.15
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 +163 -70
- package/THIRD_PARTY_NOTICES.md +1 -0
- package/docs/remote-access.md +13 -0
- package/docs/reuse-and-distribution.md +13 -2
- package/docs/service-management.md +72 -15
- package/licenses/pi-web-ui-reuse.md +2 -0
- package/package.json +5 -2
- package/src/conversation-context.mjs +147 -0
- package/src/conversation-resources.mjs +47 -0
- package/src/native-build.ts +1 -1
- package/src/native-client.ts +2 -2
- package/src/operator-cli.mjs +4 -7
- package/src/sdk-worker.mjs +41 -8
- package/src/service/contracts/workbench.ts +303 -0
- package/src/service/coordination.ts +276 -44
- package/src/service-cli.mjs +1437 -568
- package/src/service-client.ts +49 -0
- package/src/service-host.mjs +15969 -13027
- package/src/service-http-client.ts +50 -24
- package/src/service-wire.ts +26 -4
- package/src/task-store.ts +12 -11
- package/src/worker-public.mjs +1 -0
- package/web/dist/assets/cadence.css +1 -1
- package/web/dist/assets/cadence.js +13 -7
- package/web/dist/manifest.json +7 -7
|
@@ -20,9 +20,12 @@ cadence stop --workspace /absolute/project
|
|
|
20
20
|
Defaults are loopback `127.0.0.1:17423`, normal Pi agent configuration, and detached service lifetime.
|
|
21
21
|
An explicit port0 selects and persists a positive port once; restart uses the selected configuration, not a fallback endpoint.
|
|
22
22
|
Missing/corrupt existing setup is preserved, not filled in or reset.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
Commands without `--workspace` address the unified workbench, which has no execution cwd and never creates `~/cadence`.
|
|
24
|
+
Use `cadence workbench start|status|diagnose|stop|token-link|token-rotate` to name that target explicitly.
|
|
25
|
+
Its start accepts `--foreground`, `--state-home`, `--agent-dir`, `--server-config` and a private `--config` containing an `HttpPolicy` object (the schema used below for project HTTP policy).
|
|
26
|
+
The configuration's port/token come from the `.env`; a selected HTTP policy is retained across orderly restarts.
|
|
27
|
+
A project root containing the private state home is refused, with no alternate directory creation; native `/cadence` also requires a real project selection.
|
|
28
|
+
Existing `~/cadence` content is never removed, relocated or automatically adopted.
|
|
26
29
|
A directory inside `cadence-assistant-v1` is never an engineering workspace, even for root or `project open --create`.
|
|
27
30
|
Apart from the background service below, there is no supervisor installation, provider login or credential migration.
|
|
28
31
|
|
|
@@ -43,7 +46,16 @@ The JSON result reports the canonical workspace, actual task cwd and endpoint; t
|
|
|
43
46
|
Keep that link private.
|
|
44
47
|
Open Pi in the same project and use `/cadence` for the native client; explicit `cadence stop --workspace <path>` stops only that project.
|
|
45
48
|
Opening another project never changes the cwd or cancels work in the former one.
|
|
46
|
-
|
|
49
|
+
The default workbench routes all associated projects through one origin, including a configured HTTPS proxy.
|
|
50
|
+
Its project picker browses directory names readonly; only **Open this project** starts or reuses an owner, without admitting a task or model call.
|
|
51
|
+
Project navigation remounts only the browser client; the original owner's work continues.
|
|
52
|
+
Browser proxy traffic keeps browser authority and the owner's own attachments, CSRF/build checks, grants and history proofs; native management routes and local-capability headers are not forwarded.
|
|
53
|
+
The gateway also refuses the legacy project-level Open command, so every opening is associated through the workbench instead of returning an independent backend token link.
|
|
54
|
+
The gateway's token, backend cookies and native capabilities are never returned in project DTOs or stored in browser localStorage.
|
|
55
|
+
Token rotation retires and detaches its exact browser attachments without cancelling admitted work.
|
|
56
|
+
The private account state stores bounded project references and opening receipts, not copied worktrees, Pi profiles or imported transcripts.
|
|
57
|
+
Private coordination/Pi credential directories and foreign-owned project selections are refused.
|
|
58
|
+
Direct project entry links remain available as a compatibility route; that route alone still uses separate loopback project entries.
|
|
47
59
|
Git subdirectories and aliases resolve to the same checkout, while overlapping parent/child owners still refuse.
|
|
48
60
|
An unknown, quarantined or inaccessible selection is never treated as a new project or recovered by choosing another state home.
|
|
49
61
|
|
|
@@ -72,12 +84,18 @@ Linux also accepts `--name <name>` on every server command to select `cadence-<n
|
|
|
72
84
|
Names must start with a letter or digit and contain only letters, digits, `_` and `-` (at most48characters).
|
|
73
85
|
|
|
74
86
|
`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.
|
|
75
|
-
`install`
|
|
76
|
-
The
|
|
87
|
+
`install` without `--workspace` runs `workbench start --foreground --server-config <file>` (Linux, macOS), or without `--foreground` on Windows.
|
|
88
|
+
The service process may use its package directory as a technical launch cwd; it is never an agent workspace and no project scan or model runtime is initialized there.
|
|
89
|
+
An explicit `--workspace` retains single-project service compatibility and must name an existing suitable directory.
|
|
90
|
+
A reinstall without `--workspace` explicitly selects the unified entry after orderly Stop of the old generated definition; it neither deletes the former workspace nor imports its data.
|
|
91
|
+
A plain `restart` retains the definition's existing target, including an older explicit project service, rather than silently migrating it.
|
|
77
92
|
The `.env` [port and token](#port-and-token) select the endpoint (17423 by default) and the token; an occupied endpoint fails instead of falling back.
|
|
78
93
|
Install and restart print the private entry link once the owner answers; the JSON result on stdout never carries the token.
|
|
79
94
|
`restart` rewrites an existing definition with the current Node and package paths, keeping its workspace and the user's disable choices, and starts it; run it after an npm upgrade, a Node change or a `models.json` edit.
|
|
80
|
-
`stop`
|
|
95
|
+
`stop` joins the workbench's accepted openings and associated exact-epoch project owners, then closes the entry; unrelated project owners stay independent.
|
|
96
|
+
If one associated owner refuses or has a changed epoch, Stop still reaches the other known owners serially, while the workbench remains fenced rather than reporting a successful Stop.
|
|
97
|
+
Project-specific `stop --workspace <path>` remains scoped to that owner.
|
|
98
|
+
The generated registration stays for the next boot or login.
|
|
81
99
|
`uninstall` stops it orderly and deletes only what Cadence generated; workspace, state home, history, linger and Pi sources stay.
|
|
82
100
|
Because npm runs no uninstall script, uninstall the service before `npm uninstall -g`; a left-behind definition fails to start once its CLI path disappears.
|
|
83
101
|
A definition Cadence did not generate is left unchanged (`foreign`), and `status` reports `absent` when none is registered.
|
|
@@ -85,7 +103,8 @@ A definition Cadence did not generate is left unchanged (`foreign`), and `status
|
|
|
85
103
|
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.
|
|
86
104
|
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.
|
|
87
105
|
Missing or unusable Pi sources refuse before an owner registers; a new owner without the SDK quarantines.
|
|
88
|
-
|
|
106
|
+
The no-project workbench starts without either an SDK runtime or Pi authentication; missing sources are reported only when a project is explicitly opened.
|
|
107
|
+
For an explicit single-project definition, the older guards remain: no SDK means no definition, and missing `auth.json` means registered but waiting for Pi login.
|
|
89
108
|
|
|
90
109
|
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.
|
|
91
110
|
It is a warning rather than part of the info card because Pi's TUI replaces one info status with the next.
|
|
@@ -175,7 +194,7 @@ Only where polkit requires authorization for `set-self-linger` (older systemd de
|
|
|
175
194
|
`--no-linger` or `CADENCE_LINGER=0` skips linger; `restart` never changes it, so `loginctl disable-linger` is kept.
|
|
176
195
|
The unit bakes the installer's PATH (minus npm lifecycle entries), LANG/LC_ALL, an absolute XDG_STATE_HOME and a custom PI_CODING_AGENT_DIR, so shell management sees the same state home and Pi sources.
|
|
177
196
|
Proxy variables are not baked; add a validated pair with `systemctl --user edit cadence`.
|
|
178
|
-
It carries `ConditionPathExists=` for the CLI
|
|
197
|
+
It carries `ConditionPathExists=` for the CLI; explicit single-project definitions also require `auth.json` and both SDK packages, while the project-less workbench does not.
|
|
179
198
|
|
|
180
199
|
`restart` stops an active unit with its loaded definition, rewrites the file and starts it, without enabling a disabled unit; a masked unit is never enabled or started.
|
|
181
200
|
`ExecStop` is the authenticated orderly Stop, so admitted work is stopped, not detached; `stop` and `uninstall` (`disable --now`) use it too.
|
|
@@ -191,7 +210,7 @@ To keep the service off while Cadence stays installed, use `systemctl --user dis
|
|
|
191
210
|
|
|
192
211
|
`install` writes `~/Library/LaunchAgents/com.abelxiaoxing.cadence.plist` with `RunAtLoad`, runs `launchctl enable` (undoing a `launchctl disable`) and loads it into `gui/<uid>` with `launchctl bootstrap`, which starts it; launchd loads it again at every login.
|
|
193
212
|
It bakes the same PATH, LANG/LC_ALL, XDG_STATE_HOME and PI_CODING_AGENT_DIR as the Linux unit and logs to `~/Library/Logs/cadence.log`.
|
|
194
|
-
|
|
213
|
+
For an explicit single-project service, missing `auth.json` leaves the agent written but not loaded; the project-less workbench can start without Pi login.
|
|
195
214
|
Because launchd stops a job only by signaling it, `restart`, `stop`, `uninstall` and a reinstall first stop the owner through the authenticated orderly Stop, and change nothing when that Stop is `unconfirmed`, `quarantined` or `unavailable`.
|
|
196
215
|
Only then does Cadence unload (`bootout`) or replace the agent; `stop` leaves it loaded, and the finished job starts again at the next login.
|
|
197
216
|
There is no `KeepAlive`: launchd has no start limit, and a restart never clears fences, so a failed owner stays down until `cadence server restart`.
|
|
@@ -201,10 +220,10 @@ At logout or shutdown launchd signals the job, which is not orderly Stop and can
|
|
|
201
220
|
### Windows login Run entry
|
|
202
221
|
|
|
203
222
|
`install` writes `%LOCALAPPDATA%\cadence-server\cadence.vbs`, beside rather than inside the private state home, and the per-user value `HKCU\Software\Microsoft\Windows\CurrentVersion\Run\Cadence`, which runs it with `wscript.exe` at login; no administrator rights are needed.
|
|
204
|
-
The launcher is UTF-16 with a byte-order mark, so non-ASCII paths work, and runs `start --workspace` with window style 0, so no console window appears; the detached owner keeps running after the launcher exits.
|
|
223
|
+
The launcher is UTF-16 with a byte-order mark, so non-ASCII paths work, and runs `workbench start` (or `start --workspace` for an explicit project) with window style 0, so no console window appears; the detached owner keeps running after the launcher exits.
|
|
205
224
|
It bakes only a custom PI_CODING_AGENT_DIR, because the login environment already carries PATH and `%LOCALAPPDATA%`.
|
|
206
225
|
`install` also removes Task Manager's `StartupApproved` switch for the entry (undoing a disable there); `restart` keeps both the entry and that switch, and `status` reports a disabled entry.
|
|
207
|
-
|
|
226
|
+
For an explicit single-project service, missing `auth.json` leaves the entry registered but starts nothing; the project-less workbench can start without Pi login.
|
|
208
227
|
Nothing supervises the detached owner, so a failed owner is not restarted, and every `restart`, `stop`, `uninstall` and reinstall first runs the authenticated orderly Stop and changes nothing unless it is confirmed; Cadence never kills a process.
|
|
209
228
|
A `Cadence` Run value without Cadence's launcher is `foreign` and left unchanged; registry values are only tested for presence, because `reg.exe` prints them in the console code page.
|
|
210
229
|
At sign-out or shutdown Windows ends the owner without orderly Stop, which can leave recovery fences; run `cadence server stop` first while work is running.
|
|
@@ -239,7 +258,7 @@ At sign-out or shutdown Windows ends the owner without orderly Stop, which can l
|
|
|
239
258
|
Ctrl-C, SIGTERM, machine crash or terminal destruction is **not** the authenticated orderly Stop route and can leave recovery fences.
|
|
240
259
|
A recovered `stopped` registration means acknowledged fence release, not retroactive native closure certification.
|
|
241
260
|
|
|
242
|
-
`diagnose` reports fixed pending/recording facts and `proxyAuth: direct|validated-proxy
|
|
261
|
+
`diagnose` reports fixed pending/recording facts and `proxyAuth: direct|validated-proxy` when the owner is reachable; a blocked global registry instead reports its fixed lock reason and local recovery hint.
|
|
243
262
|
No normal status/diagnosis prints access secrets, provider credentials or outbound proxy URLs.
|
|
244
263
|
Keep the explicitly revealed token link private.
|
|
245
264
|
Browser entry redirects to a token-free URL and uses an HttpOnly cookie; that does not retract a link already copied into history, a clipboard or external logs.
|
|
@@ -281,8 +300,46 @@ cadence recover inspect --workspace /absolute/project
|
|
|
281
300
|
cadence recover resolve --workspace /absolute/project
|
|
282
301
|
```
|
|
283
302
|
|
|
284
|
-
Inspection is readonly.
|
|
285
|
-
|
|
303
|
+
Inspection is readonly and handles the global registry lock before project-owner recovery.
|
|
304
|
+
|
|
305
|
+
### Residual global registry lock
|
|
306
|
+
|
|
307
|
+
`status` and `diagnose` report `reason: "registry-locked"` and the lock holder observation (`live`, `absent`, `ambiguous` or `unreadable`).
|
|
308
|
+
Project Open reports `registry-locked` instead of an opaque `unavailable`; the folder picker explains the local recovery command.
|
|
309
|
+
This lock is shared by every project in the selected state home, so trying a different project does not bypass it.
|
|
310
|
+
Use the same account and `--state-home` as the owner; omit that flag only when it uses the default state home:
|
|
311
|
+
|
|
312
|
+
```sh
|
|
313
|
+
cadence recover inspect --workspace /absolute/project --state-home /absolute/state-home
|
|
314
|
+
cadence recover resolve --workspace /absolute/project --state-home /absolute/state-home
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
With a registry lock present, inspection reports `scope: "global-registry"`, the exact lock-incarnation digest, holder PID/host, reservation count and any previous resolver's liveness.
|
|
318
|
+
A project with no selected owner can be used to address this global operation; it still must be an existing suitable directory.
|
|
319
|
+
Resolve requires the same four fresh local-terminal acknowledgments described below, with the displayed lock-incarnation digest instead of a service epoch.
|
|
320
|
+
The holder must be on this host and its PID probe must specifically report absence; age, a remote host, permission denial or a reused live PID never authorizes release.
|
|
321
|
+
|
|
322
|
+
Recovery validates the registry and checks that unreserved project selections have no remaining owner/checkout fences.
|
|
323
|
+
A normally stopped project whose working directory was later removed is checked through its retained private metadata; recovery neither requires nor reconstructs that directory.
|
|
324
|
+
An interrupted reservation remains reserved even when its project initialization is incomplete: unrelated projects can reopen, but that project's uncertain resources are not repaired.
|
|
325
|
+
An older release interrupted after removing a reservation but before releasing its project locks is unsafe and remains blocked, as do corrupt/partial registry bytes, linked files and changed resources.
|
|
326
|
+
Inspection reports `blocked-preserve-state` when it cannot establish those conditions; preserve that state for manual diagnosis rather than deleting locks.
|
|
327
|
+
|
|
328
|
+
Before releasing only `registry.lock`, the resolver exclusively creates, flushes and closes a private receipt under `service-registry-v1/recovery-v1/<lock-incarnation>/<attempt>.json`, retaining the original lock bytes and acknowledged digest.
|
|
329
|
+
It then rechecks the lock incarnation, registry, unreserved project selections, receipt and terminal authority.
|
|
330
|
+
Concurrent resolvers cannot release a successor lock.
|
|
331
|
+
If a resolver dies after the receipt but before release, a new inspection and fresh terminal confirmation may append the next attempt after that resolver's same-host PID is absent; neither receipt nor lock is silently reclaimed.
|
|
332
|
+
Torn or unsafe receipts remain preserved and refused.
|
|
333
|
+
Inspection is bounded to 1024 namespace entries and 128 attempts for one lock incarnation; it never evicts evidence to make space.
|
|
334
|
+
|
|
335
|
+
All project reservations, owner locks, checkout locks, capabilities, history and unknown outcomes remain unchanged.
|
|
336
|
+
No Open/Start is replayed, no process is signaled, and no service is installed or started by recovery.
|
|
337
|
+
After success, explicitly open the desired project again.
|
|
338
|
+
If its own owner is still fenced, run `recover inspect` again and follow project-owner recovery separately; restoring global registry access does not certify or release that owner's work.
|
|
339
|
+
|
|
340
|
+
### Project-owner recovery
|
|
341
|
+
|
|
342
|
+
With no registry lock present, inspection displays the epoch, exact coordination/history digests, owner PID/host observation and independent process/credential/storage uncertainty.
|
|
286
343
|
Those digests identify preserved resources, not successful work or credential validity.
|
|
287
344
|
A journal with no unmatched intent does not prove every escaped descendant or external credential writer stopped.
|
|
288
345
|
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# pi-web-ui presentation reuse
|
|
2
2
|
|
|
3
3
|
Cadence adapts source from [xing-shuyin/pi-web-ui](https://github.com/xing-shuyin/pi-web-ui), revision `e8bab487af208bc9a1ab60a821652323f598127f`.
|
|
4
|
+
The ordinary-conversation composer, folder picker, file browser and agent-dock presentation additionally reference revision `944f556463d6693a3f0cfe375c22680090e85bbe`, with separate file hashes recorded in the source inventory.
|
|
4
5
|
Copyright (c) 2026 xing-shuyin.
|
|
5
6
|
The full existing [MIT license](pi-web-ui-MIT.txt) is retained unchanged.
|
|
6
7
|
This adaptation is not endorsed by the upstream author.
|
|
7
8
|
|
|
8
9
|
Adapted families include the three-pane workspace and drawers, nested navigation and plan editing, lazy transcript/tool cards, Markdown/stream/code/math/copy, readonly preview and diff, search/deep links, reconciliation, and the standalone run-trace client.
|
|
9
10
|
Cadence replaces upstream execution, arbitrary messaging, history repair, mutation, plugin and credential-storage boundaries with its finite typed shared service client.
|
|
11
|
+
Ordinary chat has its own Cadence service implementation; the upstream subagent/goal/delegate runtimes, prompt composer, PTY and plugin system are not included.
|
|
10
12
|
Timeline geometry is explicitly ordinal; recorded worker-local clocks and native check duration remain independently labeled, never aligned into inferred overlap.
|
|
11
13
|
The source-level file/hash/modification inventory is `provenance/webui-reuse.json`.
|
|
12
14
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abelxiaoxing/cadence",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.15",
|
|
4
4
|
"description": "Shared Pi task workspace: native terminal and WebUI, persistent service, checks and human review.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,7 +60,10 @@
|
|
|
60
60
|
"README.md",
|
|
61
61
|
"LICENSE",
|
|
62
62
|
"THIRD_PARTY_NOTICES.md",
|
|
63
|
-
"licenses"
|
|
63
|
+
"licenses",
|
|
64
|
+
"src/conversation-context.mjs",
|
|
65
|
+
"src/conversation-resources.mjs",
|
|
66
|
+
"src/service/contracts/workbench.ts"
|
|
64
67
|
],
|
|
65
68
|
"pi": {
|
|
66
69
|
"extensions": [
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Private ordinary-chat context. No Pi session import, hidden thinking or provider envelopes. */
|
|
2
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
canonicalJson,
|
|
5
|
+
closed,
|
|
6
|
+
integer,
|
|
7
|
+
list,
|
|
8
|
+
oneOf,
|
|
9
|
+
parseWireJson,
|
|
10
|
+
text,
|
|
11
|
+
} from "./worker-public.mjs";
|
|
12
|
+
import { DetailSanitizer, sanitizeText } from "./worker-redaction.mjs";
|
|
13
|
+
|
|
14
|
+
export const CONTEXT_BYTES = 2 * 1024 * 1024;
|
|
15
|
+
const clean = (s, secrets, max = CONTEXT_BYTES) =>
|
|
16
|
+
sanitizeText(text(s, max), secrets);
|
|
17
|
+
function argumentsData(value, secrets) {
|
|
18
|
+
const sanitizer = new DetailSanitizer("canonical-json", secrets);
|
|
19
|
+
const source =
|
|
20
|
+
sanitizer.write(canonicalJson(value, 64 * 1024)) + sanitizer.end();
|
|
21
|
+
if (sanitizer.limited) throw new Error("Unsupported context arguments");
|
|
22
|
+
return parseWireJson(source, 64 * 1024);
|
|
23
|
+
}
|
|
24
|
+
/** Deliberately retain text and tool exchanges, not just last assistant prose. */
|
|
25
|
+
export function conversationMessages(messages, secrets = []) {
|
|
26
|
+
return list(messages, 4096, (message) => {
|
|
27
|
+
if (!message || typeof message !== "object")
|
|
28
|
+
throw new Error("Invalid context");
|
|
29
|
+
const role = oneOf(message.role, ["user", "assistant", "toolResult"]);
|
|
30
|
+
const timestamp = integer(message.timestamp, 0);
|
|
31
|
+
const content =
|
|
32
|
+
typeof message.content === "string"
|
|
33
|
+
? [{ type: "text", text: clean(message.content, secrets) }]
|
|
34
|
+
: list(message.content, 256, (block) => {
|
|
35
|
+
if (block.type === "text")
|
|
36
|
+
return { type: "text", text: clean(block.text, secrets) };
|
|
37
|
+
if (role === "assistant" && block.type === "toolCall")
|
|
38
|
+
return {
|
|
39
|
+
type: "toolCall",
|
|
40
|
+
id: clean(block.id, secrets, 512),
|
|
41
|
+
name: clean(block.name, secrets, 128),
|
|
42
|
+
arguments: argumentsData(block.arguments, secrets),
|
|
43
|
+
};
|
|
44
|
+
// Images, hidden thinking and opaque provider signatures never become retained context.
|
|
45
|
+
return null;
|
|
46
|
+
}).filter(Boolean);
|
|
47
|
+
if (role === "user") return { role, timestamp, content };
|
|
48
|
+
if (role === "toolResult")
|
|
49
|
+
return {
|
|
50
|
+
role,
|
|
51
|
+
timestamp,
|
|
52
|
+
content,
|
|
53
|
+
toolCallId: clean(message.toolCallId, secrets, 512),
|
|
54
|
+
toolName: clean(message.toolName, secrets, 128),
|
|
55
|
+
isError: message.isError === true,
|
|
56
|
+
};
|
|
57
|
+
const usage = closed(message.usage, [
|
|
58
|
+
"input",
|
|
59
|
+
"output",
|
|
60
|
+
"cacheRead",
|
|
61
|
+
"cacheWrite",
|
|
62
|
+
"totalTokens",
|
|
63
|
+
"cost",
|
|
64
|
+
]);
|
|
65
|
+
const cost = closed(usage.cost, [
|
|
66
|
+
"input",
|
|
67
|
+
"output",
|
|
68
|
+
"cacheRead",
|
|
69
|
+
"cacheWrite",
|
|
70
|
+
"total",
|
|
71
|
+
]);
|
|
72
|
+
for (const value of [
|
|
73
|
+
...Object.values(usage).filter((v) => typeof v !== "object"),
|
|
74
|
+
...Object.values(cost),
|
|
75
|
+
])
|
|
76
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0)
|
|
77
|
+
throw new Error("Invalid usage");
|
|
78
|
+
return {
|
|
79
|
+
role,
|
|
80
|
+
timestamp,
|
|
81
|
+
content,
|
|
82
|
+
api: clean(message.api, secrets, 128),
|
|
83
|
+
provider: clean(message.provider, secrets, 128),
|
|
84
|
+
model: clean(message.model, secrets, 256),
|
|
85
|
+
usage,
|
|
86
|
+
stopReason: oneOf(message.stopReason, [
|
|
87
|
+
"stop",
|
|
88
|
+
"length",
|
|
89
|
+
"toolUse",
|
|
90
|
+
"error",
|
|
91
|
+
"aborted",
|
|
92
|
+
]),
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
export function readConversationContext(path) {
|
|
97
|
+
if (path === null) return [];
|
|
98
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
99
|
+
try {
|
|
100
|
+
const st = fstatSync(fd);
|
|
101
|
+
if (
|
|
102
|
+
!st.isFile() ||
|
|
103
|
+
st.nlink !== 1 ||
|
|
104
|
+
st.size > CONTEXT_BYTES ||
|
|
105
|
+
(process.platform !== "win32" &&
|
|
106
|
+
(st.mode & 0o077 || st.uid !== process.getuid()))
|
|
107
|
+
)
|
|
108
|
+
throw new Error("Invalid context file");
|
|
109
|
+
const buffer = Buffer.alloc(st.size + 1);
|
|
110
|
+
let size = 0;
|
|
111
|
+
while (size < buffer.length) {
|
|
112
|
+
const n = readSync(fd, buffer, size, buffer.length - size, null);
|
|
113
|
+
if (!n) break;
|
|
114
|
+
size += n;
|
|
115
|
+
}
|
|
116
|
+
const after = fstatSync(fd);
|
|
117
|
+
if (
|
|
118
|
+
size !== st.size ||
|
|
119
|
+
after.size !== st.size ||
|
|
120
|
+
after.mtimeMs !== st.mtimeMs
|
|
121
|
+
)
|
|
122
|
+
throw new Error("Changed context file");
|
|
123
|
+
const o = closed(
|
|
124
|
+
parseWireJson(
|
|
125
|
+
new TextDecoder("utf-8", { fatal: true }).decode(
|
|
126
|
+
buffer.subarray(0, size),
|
|
127
|
+
),
|
|
128
|
+
CONTEXT_BYTES,
|
|
129
|
+
),
|
|
130
|
+
["schema", "messages"],
|
|
131
|
+
);
|
|
132
|
+
if (o.schema !== "cadence-conversation-context/1")
|
|
133
|
+
throw new Error("Invalid context format");
|
|
134
|
+
return conversationMessages(o.messages);
|
|
135
|
+
} finally {
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
export function encodeConversationContext(messages, secrets) {
|
|
140
|
+
return canonicalJson(
|
|
141
|
+
{
|
|
142
|
+
schema: "cadence-conversation-context/1",
|
|
143
|
+
messages: conversationMessages(messages, secrets),
|
|
144
|
+
},
|
|
145
|
+
CONTEXT_BYTES,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** Ordinary Pi context only: no extension/plugin executor, persona or prompt composer. */
|
|
2
|
+
import { readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
loadProjectContextFiles,
|
|
6
|
+
loadSkills,
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
function optional(path) {
|
|
10
|
+
try {
|
|
11
|
+
if (!statSync(path).isFile() || statSync(path).size > 64 * 1024)
|
|
12
|
+
throw new Error("Resource too large");
|
|
13
|
+
return readFileSync(path, "utf8");
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error?.code === "ENOENT") return undefined;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function conversationResources(base, cwd, agentDir) {
|
|
20
|
+
const project = join(cwd, ".pi");
|
|
21
|
+
const system =
|
|
22
|
+
optional(join(project, "SYSTEM.md")) ??
|
|
23
|
+
optional(join(agentDir, "SYSTEM.md"));
|
|
24
|
+
const append = [
|
|
25
|
+
optional(join(agentDir, "APPEND_SYSTEM.md")),
|
|
26
|
+
optional(join(project, "APPEND_SYSTEM.md")),
|
|
27
|
+
].filter((v) => v !== undefined);
|
|
28
|
+
const agentsFiles = loadProjectContextFiles({ cwd, agentDir });
|
|
29
|
+
const skills = loadSkills({
|
|
30
|
+
cwd,
|
|
31
|
+
agentDir,
|
|
32
|
+
skillPaths: [],
|
|
33
|
+
includeDefaults: true,
|
|
34
|
+
});
|
|
35
|
+
if (
|
|
36
|
+
Buffer.byteLength(JSON.stringify({ system, append, agentsFiles, skills })) >
|
|
37
|
+
256 * 1024
|
|
38
|
+
)
|
|
39
|
+
throw new Error("Resources too large");
|
|
40
|
+
return {
|
|
41
|
+
...base,
|
|
42
|
+
getSystemPrompt: () => system,
|
|
43
|
+
getAppendSystemPrompt: () => append,
|
|
44
|
+
getAgentsFiles: () => ({ agentsFiles }),
|
|
45
|
+
getSkills: () => skills,
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/native-build.ts
CHANGED
package/src/native-client.ts
CHANGED
|
@@ -192,8 +192,8 @@ export class NativeConnection {
|
|
|
192
192
|
// Failed detach keeps its exact transport/slot. Explicit re-entry may retry
|
|
193
193
|
// detach, but never forget it and allocate a replacement on uncertainty.
|
|
194
194
|
await this.detach();
|
|
195
|
-
//
|
|
196
|
-
//
|
|
195
|
+
// Native clients require an explicit project; never create or redirect to
|
|
196
|
+
// a default workspace. Observation only.
|
|
197
197
|
const access = new ServiceCoordination(
|
|
198
198
|
effectiveWorkspace(cwd).workspace,
|
|
199
199
|
).localAccess();
|
package/src/operator-cli.mjs
CHANGED
|
@@ -288,13 +288,10 @@ function stateHomeInside(root, stateHome = defaultStateHome()) {
|
|
|
288
288
|
function privateWorkspace(requested, stateHome = defaultStateHome()) {
|
|
289
289
|
return contains(join(canonicalAmbientRoot(stateHome), "cadence-assistant-v1"), canonicalAmbientRoot(requested));
|
|
290
290
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (!stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
296
|
-
return { workspace: requested, redirected: false };
|
|
297
|
-
return { workspace: join(home, LANDING_WORKSPACE), redirected: true };
|
|
291
|
+
function effectiveWorkspace(requested, stateHome, _home) {
|
|
292
|
+
if (privateWorkspace(requested, stateHome) || stateHomeInside(resolveWorkspace(requested).root, stateHome))
|
|
293
|
+
throw new Error("Choose a project outside Cadence private state; no default workspace was created");
|
|
294
|
+
return { workspace: requested, redirected: false };
|
|
298
295
|
}
|
|
299
296
|
|
|
300
297
|
class TaskStore {
|
package/src/sdk-worker.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Isolated SDK child, not Pi CLI startup. Task discovery stays disabled;
|
|
2
|
+
// ordinary conversations explicitly load only context/skills, never extensions.
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
2
4
|
import {
|
|
3
5
|
createProvider,
|
|
4
6
|
InMemoryCredentialStore,
|
|
@@ -12,6 +14,11 @@ import {
|
|
|
12
14
|
SessionManager,
|
|
13
15
|
SettingsManager,
|
|
14
16
|
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import {
|
|
18
|
+
encodeConversationContext,
|
|
19
|
+
readConversationContext,
|
|
20
|
+
} from "./conversation-context.mjs";
|
|
21
|
+
import { conversationResources } from "./conversation-resources.mjs";
|
|
15
22
|
import { CaptureEmitter, SdkCapture } from "./worker-capture.mjs";
|
|
16
23
|
import {
|
|
17
24
|
closed,
|
|
@@ -185,13 +192,27 @@ try {
|
|
|
185
192
|
if (inputBytes > 128 * 1024) refuse("parse", "input-bound");
|
|
186
193
|
chunks.push(chunk);
|
|
187
194
|
}
|
|
195
|
+
const parsed = parseWireJson(
|
|
196
|
+
new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)),
|
|
197
|
+
LIMITS.workerRequestBytes,
|
|
198
|
+
);
|
|
188
199
|
const envelope = closed(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
["request", "cwd", "model", "auth"],
|
|
200
|
+
parsed,
|
|
201
|
+
Object.hasOwn(parsed ?? {}, "conversation")
|
|
202
|
+
? ["request", "cwd", "model", "auth", "conversation"]
|
|
203
|
+
: ["request", "cwd", "model", "auth"],
|
|
194
204
|
);
|
|
205
|
+
const conversation =
|
|
206
|
+
envelope.conversation === undefined
|
|
207
|
+
? null
|
|
208
|
+
: closed(envelope.conversation, ["previous", "agentDir"]);
|
|
209
|
+
if (
|
|
210
|
+
conversation &&
|
|
211
|
+
((conversation.previous !== null &&
|
|
212
|
+
typeof conversation.previous !== "string") ||
|
|
213
|
+
typeof conversation.agentDir !== "string")
|
|
214
|
+
)
|
|
215
|
+
refuse("parse", "invalid-conversation");
|
|
195
216
|
const publicRequest = decodeWorkerPublicRequest(envelope.request);
|
|
196
217
|
const request = {
|
|
197
218
|
...envelope,
|
|
@@ -329,6 +350,10 @@ try {
|
|
|
329
350
|
process.stdout.on("error", stdoutFailureListener);
|
|
330
351
|
stage = "session";
|
|
331
352
|
const requestedThinkingLevel = thinkingLevel ?? "off";
|
|
353
|
+
const manager = SessionManager.inMemory(cwd);
|
|
354
|
+
if (conversation)
|
|
355
|
+
for (const message of readConversationContext(conversation.previous))
|
|
356
|
+
manager.appendMessage(message);
|
|
332
357
|
const created = await createAgentSession({
|
|
333
358
|
cwd,
|
|
334
359
|
agentDir: cwd,
|
|
@@ -351,8 +376,10 @@ try {
|
|
|
351
376
|
),
|
|
352
377
|
]
|
|
353
378
|
: [],
|
|
354
|
-
resourceLoader
|
|
355
|
-
|
|
379
|
+
resourceLoader: conversation
|
|
380
|
+
? conversationResources(resourceLoader, cwd, conversation.agentDir)
|
|
381
|
+
: resourceLoader,
|
|
382
|
+
sessionManager: manager,
|
|
356
383
|
settingsManager: SettingsManager.inMemory({
|
|
357
384
|
compaction: { enabled: false },
|
|
358
385
|
retry: { enabled: false },
|
|
@@ -483,6 +510,12 @@ try {
|
|
|
483
510
|
stage = "assistant-final";
|
|
484
511
|
if (failure || terminalStopReason !== "stop" || ingress.frames !== 0)
|
|
485
512
|
refuse("assistant-final", "terminal-incomplete");
|
|
513
|
+
// Anonymous pipe only. The owner applies its access-secret sanitizer before
|
|
514
|
+
// any checkpoint reaches disk; worker output cannot contain owner credentials.
|
|
515
|
+
if (conversation) {
|
|
516
|
+
stage = "conversation-context";
|
|
517
|
+
writeFileSync(3, encodeConversationContext(session.messages, secrets));
|
|
518
|
+
}
|
|
486
519
|
await capture.final();
|
|
487
520
|
await emitter.drain();
|
|
488
521
|
} catch {
|