pi-squire 0.20.0 → 0.20.1
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 +12 -1
- package/README.md +4 -1
- package/package.json +1 -1
- package/src/fork.ts +18 -1
- package/src/result-sessions.ts +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.20.1] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- `delegation_fork` warns when its effective model differs from the source's
|
|
14
|
+
last model: copied context will be written to a new cache, with an approximate
|
|
15
|
+
token count when source usage is available. The model parameter and README
|
|
16
|
+
explain the cost; same-model forks retain their cache behavior. New fork tests
|
|
17
|
+
cover changed/same models, effective model locks and missing or invalid usage.
|
|
18
|
+
|
|
9
19
|
## [0.20.0] - 2026-11-02
|
|
10
20
|
|
|
11
21
|
### Added
|
|
@@ -1111,7 +1121,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
1111
1121
|
|
|
1112
1122
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
1113
1123
|
|
|
1114
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.20.
|
|
1124
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.20.1...HEAD
|
|
1125
|
+
[0.20.1]: https://github.com/gvanderclay/pi-squire/compare/v0.20.0...v0.20.1
|
|
1115
1126
|
[0.20.0]: https://github.com/gvanderclay/pi-squire/compare/v0.19.1...v0.20.0
|
|
1116
1127
|
[0.19.1]: https://github.com/gvanderclay/pi-squire/compare/v0.19.0...v0.19.1
|
|
1117
1128
|
[0.19.0]: https://github.com/gvanderclay/pi-squire/compare/v0.18.4...v0.19.0
|
package/README.md
CHANGED
|
@@ -1670,7 +1670,10 @@ folder must exist, its agent must still be enabled, and current nesting grants,
|
|
|
1670
1670
|
depth, capacity and trust checks apply. Copied history does not transfer
|
|
1671
1671
|
ownership of the original child's descendants. Model, thinking and auto-exit
|
|
1672
1672
|
default to the source's last settings with the same locks and fallbacks as a
|
|
1673
|
-
resume; the current roster supplies the prompt and tools.
|
|
1673
|
+
resume; the current roster supplies the prompt and tools. A different effective
|
|
1674
|
+
model re-reads the copied context without the source's cache; the result warns
|
|
1675
|
+
about the new cache write, estimating copied tokens when source usage is
|
|
1676
|
+
available. Keep the source's model unless the task needs another role.
|
|
1674
1677
|
`session:launch` is emitted with the `delegation_fork` tool call id.
|
|
1675
1678
|
|
|
1676
1679
|
## Hooks
|
package/package.json
CHANGED
package/src/fork.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { Type } from "typebox";
|
|
|
3
3
|
|
|
4
4
|
import { disabledProblem, THINKING_LEVELS } from "./agents.ts";
|
|
5
5
|
import { dirtyNotice } from "./render.ts";
|
|
6
|
+
import { finishedModel, sessionInputTokens } from "./result-sessions.ts";
|
|
6
7
|
import { deriveState, isStarted, type StartedRecorded } from "./results.ts";
|
|
7
8
|
import type { ResumeDeps, Resumed } from "./resume.ts";
|
|
8
9
|
import { sessionFor, settleModel } from "./resume-checks.ts";
|
|
@@ -88,6 +89,14 @@ async function sharedFolderNotice(deps: ForkDeps, record: StartedRecorded, workt
|
|
|
88
89
|
return [`shares a working folder with running delegation ${record.id}; pass worktree to isolate`];
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
function modelNotice(record: StartedRecorded, sessionPath: string, model: string): string[] {
|
|
93
|
+
const sourceModel = finishedModel(sessionPath) ?? record.result?.finishedModel ?? record.model;
|
|
94
|
+
if (model === sourceModel) return [];
|
|
95
|
+
const tokens = sessionInputTokens(sessionPath);
|
|
96
|
+
const estimate = tokens === undefined ? "copied tokens" : `about ${Math.round(tokens / 1000)}k copied tokens`;
|
|
97
|
+
return [`${model} differs from the source's ${sourceModel}: ${estimate} will be written to a new cache.`];
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
function launchText(id: string, details: Forked, chosen: Awaited<ReturnType<ResumeDeps["chooseModel"]>>): string {
|
|
92
101
|
const workspace = details.workspace;
|
|
93
102
|
const managed =
|
|
@@ -97,6 +106,13 @@ function launchText(id: string, details: Forked, chosen: Awaited<ReturnType<Resu
|
|
|
97
106
|
return `Forked delegation ${id} as ${details.id} in window ${details.windowName} in ${details.cwd}.${managed} The original session is untouched. Its result arrives as a message that starts your next turn on its own; delegation_close ends it.${fallbackNote(chosen)}${details.notices.map((notice) => ` ${notice}`).join("")}`;
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
const modelParameter = Type.Optional(
|
|
110
|
+
Type.String({
|
|
111
|
+
description:
|
|
112
|
+
"provider/id; defaults to the source's last model. A different model re-reads the copied context without its cache; keep the source's model unless the task needs another role.",
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
100
116
|
export function forkTool(deps: ForkDeps): ToolDef {
|
|
101
117
|
return {
|
|
102
118
|
name: "delegation_fork",
|
|
@@ -106,7 +122,7 @@ export function forkTool(deps: ForkDeps): ToolDef {
|
|
|
106
122
|
parameters: Type.Object({
|
|
107
123
|
id: Type.String({ description: "The direct child's delegation id whose context to copy." }),
|
|
108
124
|
task: Type.String(),
|
|
109
|
-
model:
|
|
125
|
+
model: modelParameter,
|
|
110
126
|
thinking: Type.Optional(
|
|
111
127
|
Type.String({ description: `${THINKING_LEVELS.join(", ")}; defaults to the source's last level.` }),
|
|
112
128
|
),
|
|
@@ -141,6 +157,7 @@ export function forkTool(deps: ForkDeps): ToolDef {
|
|
|
141
157
|
ctx,
|
|
142
158
|
);
|
|
143
159
|
const source = forkSource(record, session);
|
|
160
|
+
chosen.notices.push(...modelNotice(record, session.path, chosen.model));
|
|
144
161
|
chosen.notices.push(...(await sharedFolderNotice(deps, record, given(params.worktree))));
|
|
145
162
|
const details = await deps.launch(
|
|
146
163
|
{
|
package/src/result-sessions.ts
CHANGED
|
@@ -78,6 +78,32 @@ export function finishedModel(sessionPath: string | undefined): string | undefin
|
|
|
78
78
|
return undefined;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Approximate copied context from the last assistant request, excluding its output. */
|
|
82
|
+
export function sessionInputTokens(sessionPath: string): number | undefined {
|
|
83
|
+
let lines: string[];
|
|
84
|
+
try {
|
|
85
|
+
lines = readFileSync(sessionPath, "utf8").split("\n");
|
|
86
|
+
} catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
90
|
+
let entry: { type?: unknown; message?: { role?: unknown; usage?: Record<string, unknown> } };
|
|
91
|
+
try {
|
|
92
|
+
entry = JSON.parse(lines[i]);
|
|
93
|
+
} catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (entry?.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
97
|
+
const usage = entry.message.usage;
|
|
98
|
+
const counts = [usage?.input, usage?.cacheRead, usage?.cacheWrite];
|
|
99
|
+
if (!counts.every((count): count is number => typeof count === "number" && Number.isFinite(count) && count >= 0))
|
|
100
|
+
return undefined;
|
|
101
|
+
const total = counts.reduce((sum, count) => sum + count, 0);
|
|
102
|
+
return Number.isFinite(total) && total > 0 ? total : undefined;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
81
107
|
/** One session-file line, as much of it as the branch walk reads. */
|
|
82
108
|
type BranchEntry = { type?: unknown; id?: unknown; parentId?: unknown; thinkingLevel?: unknown };
|
|
83
109
|
|