@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.
@@ -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
- 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
- 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
- `/cadence` started in such a directory attaches to the same owner, and real project directories are never redirected.
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
- Each project has its own browser entry and owner; this is not a same-page cross-project browser router.
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` 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`.
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.
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` stops the owner orderly and keeps the registration for the next boot or login.
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
- `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`.
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, `auth.json` and both SDK packages.
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
- Without `auth.json` the agent is written but not loaded.
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
- Without `auth.json` the entry is registered but nothing starts.
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
- It displays the epoch, exact coordination/history digests, owner PID/host observation and independent process/credential/storage uncertainty.
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.13",
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
+ }
@@ -2,5 +2,5 @@
2
2
  export const nativeBuild = {
3
3
  kind: "native",
4
4
  contractHash:
5
- "e9b1ea4bb8d1c275a0cb0cf70947f9ac58e5de0cc438d551dc96809f3e82dd7c",
5
+ "944f3b53bd5b7f1d3f30868e0947d900e356ae9a2bcfb9f591edd988b3d5eb3a",
6
6
  } as const;
@@ -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
- // Pi started in the home directory attaches to the ~/cadence landing owner,
196
- // the same one the management client starts there. Observation only.
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();
@@ -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
- var LANDING_WORKSPACE = "cadence";
292
- function effectiveWorkspace(requested, stateHome, home = homedir()) {
293
- if (privateWorkspace(requested, stateHome))
294
- throw new Error("Private state cannot be a workspace");
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 {
@@ -1,4 +1,6 @@
1
- // Ordinary SDK child, not Pi CLI startup. No discovery, credential files or fallback executor.
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
- parseWireJson(
190
- new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)),
191
- LIMITS.workerRequestBytes,
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
- sessionManager: SessionManager.inMemory(cwd),
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 {