@signalridge/pi-sessions 0.0.0-stage → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## 1.3.0
4
+ ### Minor Changes
5
+
6
+ - 0ec64d1: Introduce an opt-in experimental package for human-controlled workspace/all-workspace session navigation and bounded, editable summary handoffs into linked sessions. Approval only prefills the editor; it never submits a model turn. Native custom entries support explicit recovery after reload.
7
+
8
+ ### Patch Changes
9
+
10
+ - 74b65a5: Reject malformed assistant error text before restoration, snapshot stale in-memory handoff parents without rewriting history, revoke approval on subsequent delivered switch attempts, and cancel pending native character jumps before inserting reviewed paste.
11
+ - 0925139: Validate native system replay and discovery metadata before switching, migrate historical v2 messages without writes, revoke prefill after source retirement, and preserve review Undo, configured selection controls and foreign overlay focus. Make CLI fixture startup cleanup exception-safe and document the host's RPC observation limits.
12
+
13
+ ## 1.2.0
14
+
15
+ - Initial experimental package: workspace-scoped session navigation and explicitly reviewed, linked summary handoffs. No automatic model turn or session-file deletion.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 signalridge
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,50 @@
1
- # Temporary Holding Version
1
+ # @signalridge/pi-sessions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Experimental, opt-in human-controlled session navigation and editable summary handoffs. Tested with Pi **1.0.4**; host packages are peers, not bundled. MIT, independent implementation. This is not Plan mode's approved-plan implementation action.
4
+
5
+ Try explicitly with `pi -e ./packages/pi-sessions/src/index.ts`. This package is not enabled by default. Session mutations are human-command-only, never model-callable.
6
+
7
+ ## Commands
8
+
9
+ - `/sessions`: list sessions whose native workspace matches the active workspace. `/sessions all`: explicitly include other workspaces from Pi's default session store and the current configured session directory.
10
+ - Filter by name, stable session ID or workspace; select to inspect bounded metadata and recent user/assistant text. Confirm before native switching. There is **no delete or rename** operation. The native selector includes deletion, so this package uses a bounded independent picker.
11
+ - `/handoff <goal>`: optionally ask the **currently selected model** to synthesize a goal-oriented summary. An omitted goal opens human input. `recover` as the complete argument is reserved for recovery, not a goal; write a descriptive goal such as `recover interrupted work` instead.
12
+ - `/handoff recover`: explicitly inspect the approved summary saved on the current branch, review/edit it, then separately confirm replacing the current unsent draft. No automatic recovery overwrite, new session or model turn.
13
+
14
+ TUI and dialog-capable RPC are supported. Print/JSON commands are rejected before authentication, provider requests or session mutations. RPC clients must implement confirm/input/select/editor. Pi 1.0.4 exposes neither RPC draft epochs nor conditional editor writes: clients must reject stale consent and preserve client-local drafts. The latest destination **Yes explicitly authorizes draft replacement**; a newer client-local draft alone cannot revoke it on the server.
15
+
16
+ ## Sharing and handoff
17
+
18
+ Generation is opt-in **for every invocation**. Only at most 16 KiB UTF-8 of the most recent 12 user/assistant text messages is offered; each message is capped at 2 KiB. Tool results/arguments, images, thinking, system messages, custom entries, compaction summaries and credential objects are not copied. Common credential patterns are redacted, but prose may still contain secrets: inspect/redact the exact excerpt in the multiline editor before a second sharing confirmation. The goal (at most 2 KiB) is also sent to that selected model. No upload/share service is used.
19
+
20
+ Authentication/generation failures produce a generic message, never raw provider errors, and offer a **manual** summary editor. Declining generation also offers a manual editor, not a fake generated summary. Escape cancels live TUI loading/generation; requests time out after 60 seconds. Ownership retirement aborts our requests and closes only our overlays. RPC pending-editor results are fenced when retired (the host has no editor cancellation API).
21
+
22
+ Review/edit the summary in a multiline editor. TUI text and complete bracketed-paste payloads are sanitized **before** native insertion, including split paste markers and native-supported CSI-u pasted controls: sanitization does not move the insertion cursor or add dirty Undo snapshots. Native cursor movement, Kitty printable keys, CJK/IME and atomic paste Undo are retained. Large pastes are immediately expanded into the reviewable document, never hidden behind paste markers; Undo restores the pre-paste document and earlier editing history. Approval preserves exact leading/trailing whitespace, not Pi 1.0.4's trimmed submit callback. Choice/picker controls honor configured `tui.select.*` actions and show their bindings. Approved text must be nonempty, terminal-safe and **at most 16 KiB UTF-8**; oversized approved text is rejected, never truncated. Explicit draft-replacement consent and final creation confirmation precede native `newSession({ parentSession, setup, withSession })`.
23
+
24
+ The destination saves `pi-sessions:handoff` version 1 as a **non-model custom entry** with the approved summary, goal and parent identity. Its optional prefill intent stores the source model identity and, for TUI only, the SHA256 hash of the explicitly consented source unsent draft—**never the raw unsent draft**. RPC cannot read that draft and stores no draft hash. Older records without intent remain explicitly recoverable, but never auto-prefill.
25
+
26
+ After native setup, the **destination controller's `session_start` with reason `new`** owns prefill under the same interaction lock and lifecycle fences as commands. The source `withSession` callback only reports setup/model warnings, without asynchronous dialogs or editor writes. The destination first vets its current file: the exact custom entry ID/data and native parent link must be durably present, not just in memory after failed setup. It revalidates before writing the editor. RPC requires fresh destination consent; TUI only replaces an empty editor or the consented source draft matching its saved hash. Later TUI drafts are preserved. A missing/different destination model is reported, not submitted to. Nothing sends a message, starts a turn or invokes implementation; you decide whether and when to send the draft.
27
+
28
+ ## Native storage and recovery
29
+
30
+ The native session manager remains authoritative; no parallel session-history database is kept. Pi 1.0.4 defers custom-only file persistence until the first user/assistant message. To make the reviewed handoff durable **before** that first turn, setup writes the public native header and entries to a unique, exclusive `0600` JSONL file, then reloads it using public `setSessionFile`. It never adds a synthetic model-context message.
31
+
32
+ An existing saved source file is reused as the linked parent only when every exact active-branch record is present and reopening it selects that same leaf; a native tree-selected ancestor or sibling branch receives its own snapshot instead. If a nonempty source has no saved file (including an allocated-but-unwritten path), or its otherwise valid file is stale, its active native branch and native parent link are cloned into a unique private `0600` snapshot with a new ID in a private temporary directory. The old file is never overwritten; malformed existing storage is rejected, not silently bypassed. A cancelled/vetoed/failed **pre-replacement** creation removes this snapshot. Once native replacement begins it is retained, even if later setup/rebinding/prefill fails, so the source remains resumable by the parent path. In-memory destinations also receive a native recovery file; their later conversation remains subject to the host's no-session policy. Retained temporary parent/recovery files survive reload, but OS temporary-directory cleanup can eventually remove them; retain important source sessions in normal persistent storage.
33
+
34
+ This record/leaf comparison is a **preflight check**, not a promise that a reused native parent is immutable. Later native source appends can advance its history while replacement hooks are pending or after it is resumed. Observable source changes during preflight revoke editor-prefill authorization; native replacement and its live parent link may still proceed with recovery-only data. A private snapshot records the captured branch when created, rather than reusing a stale or differently selected source file.
35
+
36
+ Setup/prefill errors never start a model turn. Missing, unreadable or mismatched durable handoff data prevents automatic prefill. If source approval is revoked during a later asynchronous before-switch handler, native replacement may still proceed: setup retains the summary/parent for explicit recovery but strips prefill intent. Only a still-current source owner can carry approval across expected replacement shutdown. The initiated replacement's expected before-switch exemption is single-use: any subsequent delivered attempt revokes approval, including another new-session or same-target resume attempt later vetoed by another extension. Independent human attempts are not blocked by this revocation. If setup failed, resume the native parent and retry. If the summary was saved but prefill failed, `/handoff recover` can inspect it after reload. Startup, reload, resume and fork never consume prefill intent or automatically modify the editor; recovery remains explicit.
37
+
38
+ ## Workspaces and limits
39
+
40
+ **Actual Pi 1.0.4 runtime switching opens the session header workspace and recreates cwd-bound services there**; it does not change the process's global cwd. Cross-workspace selection requires a separate explicit warning/confirmation. After switching we report both history workspace and the actual fresh context's tool/resource workspace. A host retaining its execution workspace is reported as restoring history only; this package never patches private cwd state. Missing workspaces are rejected before switching rather than invoking a private fallback.
41
+
42
+ Every candidate is revalidated immediately before native switching: path, native header ID/cwd, JSONL tree and original-byte content digest must still match the preview. Preflight checks native restoration payloads, including usage/cost numbers, system sections/tool deltas and role-appropriate persisted content, then runs public nonmutating context projection, system replay and token estimation before source teardown. Supported v2 entries (including historical `hookMessage`) are migrated on an in-memory clone only. Reads stop at 32 MiB plus one oversize probe byte even if a file grows, and check cancellation between chunks. Detected unreadable/malformed data is rejected without opening or repairing its native manager. Validation accepts supported native v2/v3 payloads with entries up to 4 MiB; unsupported/older/oversized files can still be opened with Pi's own facilities. Arbitrary extension-owned custom data remains opaque; preflight is not a proof that every third-party renderer or external filesystem writer is safe. Invalid discovery metadata is skipped per row rather than hiding the valid collection. Preview text is inert and excludes non-user/assistant content. Preview is capped at 6 KiB, retained rows at 200, and visible rows adapt to the terminal while reserving controls. Undersized terminals require resizing before approval. Terminal controls/bidi instructions are stripped; CJK and narrow widths are supported in regular/fullscreen UI.
43
+
44
+ **Pi's SDK list/listAll may load full session bodies and return all results. These calls do not promise bounded memory.** Limits above bound this package's retained display data, not SDK memory or disk scan work. Abort signals are passed to SDK list operations.
45
+
46
+ Ownership captures the manager/session identity, full native active-branch fingerprint (including latest entry data), model identity and TUI draft/editor-factory identity. Observable changes retire the operation permanently; switching back does not revive approval. All await boundaries are checked, including after authentication and destination consent. While destination RPC consent is pending, `/sessions` and `/handoff` report the existing interaction warning. **Fences cover delivered lifecycle events and observable state, not every attempted host action.** Delivered native switch/tree/fork/compact attempts (including vetoes after our observer), shutdown, agent start or model selection retire that consent; a late Yes cannot revive retired ownership. Pi 1.0.4 short-circuits on an earlier extension's veto, so our observer may never receive that attempt. An unchanged branch/model and an invisible RPC draft provide no substitute signal. RPC clients must reject stale consent and preserve local drafts; the latest Yes explicitly authorizes replacement. Stronger guarantees require host-level non-short-circuiting attempt observation and draft epoch/conditional-write support. Source and destination ownership are runtime-local and do not rely on shared event buses. Async TUI generation completion also requires its owned overlay to remain focused; a newer foreign dialog retires continuation instead of losing focus. This package does not install a custom main editor/footer or restore stale editor snapshots.
47
+
48
+ ## Development
49
+
50
+ `bun run --cwd packages/pi-sessions check` runs lint, typecheck and native-SDK/unit tests. Tests do not require provider credentials and assert no destination model turn.
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@signalridge/pi-sessions",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.3.0",
4
+ "description": "Human-reviewed session navigation and summary handoffs for Pi.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "sessions",
12
+ "handoff"
13
+ ],
14
+ "files": [
15
+ "README.md",
16
+ "LICENSE",
17
+ "CHANGELOG.md",
18
+ "src"
19
+ ],
20
+ "pi": {
21
+ "extensions": [
22
+ "./src/index.ts"
23
+ ]
24
+ },
25
+ "piExtension": {
26
+ "lifecycle": "experimental"
27
+ },
28
+ "scripts": {
29
+ "check": "bun run lint && bun run typecheck && bun run test",
30
+ "format": "biome check --write .",
31
+ "lint": "biome check .",
32
+ "typecheck": "tsc --noEmit",
33
+ "test": "vitest run test --config vitest.config.ts"
34
+ },
35
+ "peerDependencies": {
36
+ "@earendil-works/pi-ai": "*",
37
+ "@earendil-works/pi-coding-agent": "*",
38
+ "@earendil-works/pi-tui": "*"
39
+ },
40
+ "devDependencies": {
41
+ "@earendil-works/pi-ai": "1.0.4",
42
+ "@earendil-works/pi-coding-agent": "1.0.4",
43
+ "@earendil-works/pi-tui": "1.0.4"
44
+ },
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/signalridge/pi-extensions",
48
+ "directory": "packages/pi-sessions"
49
+ },
50
+ "homepage": "https://github.com/signalridge/pi-extensions/tree/main/packages/pi-sessions",
51
+ "bugs": {
52
+ "url": "https://github.com/signalridge/pi-extensions/issues"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ }
57
+ }
@@ -0,0 +1,84 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { handoff, prefillHandoff } from "./handoff.js";
3
+ import { navigate } from "./navigation.js";
4
+ import { Owner, Retired } from "./ownership.js";
5
+ import { requireHuman } from "./ui.js";
6
+
7
+ export function registerSessions(pi: ExtensionAPI): void {
8
+ let active: Owner<ExtensionContext> | undefined;
9
+ let started = false;
10
+ const retire = () => active?.retire();
11
+ pi.on("session_before_switch", (event) => {
12
+ if (!active) return;
13
+ if (
14
+ active.expectedSwitch &&
15
+ active.committing === event.reason &&
16
+ (event.reason !== "resume" || event.targetSessionFile === active.target)
17
+ ) {
18
+ active.expectedSwitch = false;
19
+ if (!active.current()) return { cancel: true };
20
+ } else retire();
21
+ });
22
+ pi.on("session_shutdown", (event) => active?.shutdown(event));
23
+ pi.on("session_before_tree", retire);
24
+ pi.on("session_before_fork", retire);
25
+ pi.on("session_before_compact", retire);
26
+ pi.on("session_tree", retire);
27
+ pi.on("agent_start", retire);
28
+ pi.on("model_select", () => {
29
+ if (active?.requireModel) retire();
30
+ });
31
+
32
+ pi.on("session_start", async (event, ctx) => {
33
+ // Native setup precedes destination binding. Recovery starts never consume
34
+ // persisted intent; only this fresh runtime's first, genuinely new session can.
35
+ if (started) return;
36
+ started = true;
37
+ if (event.reason !== "new" || !ctx.hasUI || (ctx.mode !== "tui" && ctx.mode !== "rpc")) return;
38
+ await own(ctx, true, prefillHandoff);
39
+ });
40
+
41
+ async function own<Context extends ExtensionContext>(
42
+ ctx: Context,
43
+ requireModel: boolean,
44
+ work: (owner: Owner<Context>) => Promise<void>,
45
+ ): Promise<void> {
46
+ if (active) {
47
+ ctx.ui.notify("A sessions interaction is already open. Finish or cancel it first.", "warning");
48
+ return;
49
+ }
50
+ const owner = new Owner(ctx, requireModel);
51
+ active = owner;
52
+ try {
53
+ await work(owner);
54
+ } catch (error: unknown) {
55
+ if (!(error instanceof Retired) && owner.current()) {
56
+ ctx.ui.notify(
57
+ "Session operation unavailable: invalid/unreadable session, workspace or UI. No model turn was submitted. Retry from the current session.",
58
+ "warning",
59
+ );
60
+ }
61
+ } finally {
62
+ owner.finish();
63
+ if (active === owner) active = undefined;
64
+ }
65
+ }
66
+ async function run(ctx: ExtensionCommandContext, argument: string, kind: "sessions" | "handoff"): Promise<void> {
67
+ // Reject headless commands before idle/auth/session operations.
68
+ requireHuman(ctx);
69
+ await own(ctx, kind === "handoff", async (owner) => {
70
+ await owner.wait(ctx.waitForIdle());
71
+ owner.check();
72
+ if (kind === "sessions") await navigate(owner, argument);
73
+ else await handoff(owner, argument);
74
+ });
75
+ }
76
+ pi.registerCommand("sessions", {
77
+ description: "Preview/resume current-workspace sessions; 'all' includes other workspaces",
78
+ handler: (args, ctx) => run(ctx, args, "sessions"),
79
+ });
80
+ pi.registerCommand("handoff", {
81
+ description: "Review a goal-oriented summary into a linked session, or explicitly 'recover' one",
82
+ handler: (args, ctx) => run(ctx, args, "handoff"),
83
+ });
84
+ }