@andares/pi-toolkits 0.9.0 → 0.9.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/README.md
CHANGED
|
@@ -311,11 +311,14 @@ The global list runs first, so a project can override an earlier setting (for ex
|
|
|
311
311
|
|
|
312
312
|
| session_start reason | Replayed | Why |
|
|
313
313
|
| --- | --- | --- |
|
|
314
|
-
| `startup` / `new
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
314
|
+
| `startup` / `new`, empty session | ✅ | a new session, replay as declared |
|
|
315
|
+
| `startup` on a session that already has history | ❌ | that is a process started on an existing session (`pi -c` / `-r` / `--session` / `--fork`), and its branch already carries the state a replay would rebuild |
|
|
316
|
+
| `fork` (in-process) | ✅ | the branch point may predate the config's appearance |
|
|
317
|
+
| `resume` (in-process `/resume`) | ❌ | the restored branch already carries the mount state from its time, replaying would conflict |
|
|
317
318
|
| `reload` | ❌ | extensions are rebuilt, replaying would duplicate side effects |
|
|
318
319
|
|
|
320
|
+
> The `startup` split is not cosmetic: pi reports `startup` for **every** process start, including one that opens an existing session. `resume` is emitted only for in-process session switches, never for `pi -c`. Lines 1–2 of the table are therefore decided by the session's content (does it already hold a message?), not by the reason alone.
|
|
321
|
+
|
|
319
322
|
**Safety gates** (in order):
|
|
320
323
|
|
|
321
324
|
1. **project trust** — no command runs automatically in an untrusted directory (a warning is shown); a config parse failure likewise prevents execution (an error is shown)
|
package/README.zh-CN.md
CHANGED
|
@@ -311,11 +311,14 @@ run = [
|
|
|
311
311
|
|
|
312
312
|
| session_start reason | 是否重放 | 原因 |
|
|
313
313
|
| --- | --- | --- |
|
|
314
|
-
| `startup` / `new
|
|
315
|
-
| `
|
|
316
|
-
| `
|
|
314
|
+
| `startup` / `new`,空会话 | ✅ | 新会话,按声明重放 |
|
|
315
|
+
| `startup`,但会话已有历史 | ❌ | 这是「进程启动即带着旧会话」(`pi -c` / `-r` / `--session` / `--fork`),其分支已携带重放本该重建的状态 |
|
|
316
|
+
| `fork`(进程内) | ✅ | 分支点可能早于配置出现 |
|
|
317
|
+
| `resume`(进程内 `/resume`) | ❌ | 恢复的分支已携带当时的挂载状态,重放反而会冲突 |
|
|
317
318
|
| `reload` | ❌ | 扩展重建,重放会重复副作用 |
|
|
318
319
|
|
|
320
|
+
> `startup` 这一行为什么要拆开:pi 对**每一次进程启动**都报 `startup`,包括「启动即打开旧会话」的情形;`resume` 只在进程内切换会话时发出,`pi -c` 永远不会报它。所以前两行的判定依据是**会话内容**(是否已有消息),不能只看 reason。
|
|
321
|
+
|
|
319
322
|
**安全门**(按序):
|
|
320
323
|
|
|
321
324
|
1. **project trust** —— 未信任目录不自动执行任何命令(warning 提示);配置解析失败同样不执行(error 提示)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andares/pi-toolkits",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Integrated toolkit extension for pi — ask mode (read-only Q&A), session-scoped permission checks, model favorites, prompt stash, third-party code review, session autostart, and a dedicated compaction model",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
GLOBAL_AUTOSTART_FILENAME,
|
|
20
20
|
parseAutostartConfig,
|
|
21
21
|
RUNNABLE_REASONS,
|
|
22
|
+
shouldReplay,
|
|
22
23
|
} from "./autostart.js";
|
|
23
24
|
import { registerAutostart } from "./index.js";
|
|
24
25
|
|
|
@@ -112,6 +113,28 @@ describe("RUNNABLE_REASONS", () => {
|
|
|
112
113
|
});
|
|
113
114
|
});
|
|
114
115
|
|
|
116
|
+
describe("shouldReplay", () => {
|
|
117
|
+
it("replays a genuinely new session", () => {
|
|
118
|
+
expect(shouldReplay("startup", false)).toBe(true);
|
|
119
|
+
expect(shouldReplay("new", false)).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("skips a startup that already has history (a resumed process)", () => {
|
|
123
|
+
// `pi -c`, `-r`, `--session` and `--fork` all report `startup` for a
|
|
124
|
+
// session that already carries history.
|
|
125
|
+
expect(shouldReplay("startup", true)).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("replays a fork even though the branch has history", () => {
|
|
129
|
+
expect(shouldReplay("fork", true)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("skips in-process resume and reload", () => {
|
|
133
|
+
expect(shouldReplay("resume", true)).toBe(false);
|
|
134
|
+
expect(shouldReplay("reload", true)).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
115
138
|
/* ------------------------------------------------------------------ */
|
|
116
139
|
/* registerAutostart integration (mocked pi surface) */
|
|
117
140
|
/* ------------------------------------------------------------------ */
|
|
@@ -122,6 +145,7 @@ type SessionStartHandler = (
|
|
|
122
145
|
cwd: string;
|
|
123
146
|
isProjectTrusted: () => boolean;
|
|
124
147
|
ui: { notify: (message: string, type?: string) => void };
|
|
148
|
+
sessionManager: { buildContextEntries: () => Array<{ type: string }> };
|
|
125
149
|
},
|
|
126
150
|
) => Promise<void>;
|
|
127
151
|
|
|
@@ -133,6 +157,8 @@ interface Harness {
|
|
|
133
157
|
commands: Array<{ name: string; source: string }>;
|
|
134
158
|
failCommands: Set<string>;
|
|
135
159
|
setTrusted(trusted: boolean): void;
|
|
160
|
+
/** Simulate a session that already had a conversation (resumed process). */
|
|
161
|
+
setHistory(entries: Array<{ type: string }>): void;
|
|
136
162
|
start(reason: string): Promise<void>;
|
|
137
163
|
}
|
|
138
164
|
|
|
@@ -158,6 +184,12 @@ async function makeHarness(
|
|
|
158
184
|
await writeFile(join(cwd, AUTOSTART_CONFIG_PATH), config, "utf8");
|
|
159
185
|
}
|
|
160
186
|
let trusted = true;
|
|
187
|
+
// Boot-level entries every session has; a resumed session adds messages.
|
|
188
|
+
let history: Array<{ type: string }> = [
|
|
189
|
+
{ type: "session" },
|
|
190
|
+
{ type: "model_change" },
|
|
191
|
+
{ type: "thinking_level_change" },
|
|
192
|
+
];
|
|
161
193
|
let handler: SessionStartHandler = async () => {};
|
|
162
194
|
const sent: Harness["sent"] = [];
|
|
163
195
|
const notifications: Harness["notifications"] = [];
|
|
@@ -191,6 +223,9 @@ async function makeHarness(
|
|
|
191
223
|
setTrusted(value: boolean) {
|
|
192
224
|
trusted = value;
|
|
193
225
|
},
|
|
226
|
+
setHistory(entries) {
|
|
227
|
+
history = entries;
|
|
228
|
+
},
|
|
194
229
|
async start(reason: string) {
|
|
195
230
|
await handler({ reason }, {
|
|
196
231
|
cwd,
|
|
@@ -200,6 +235,7 @@ async function makeHarness(
|
|
|
200
235
|
notifications.push({ message, type });
|
|
201
236
|
},
|
|
202
237
|
},
|
|
238
|
+
sessionManager: { buildContextEntries: () => history },
|
|
203
239
|
});
|
|
204
240
|
},
|
|
205
241
|
cwd,
|
|
@@ -266,6 +302,38 @@ describe("registerAutostart", () => {
|
|
|
266
302
|
expect(h.notifications).toEqual([]);
|
|
267
303
|
});
|
|
268
304
|
|
|
305
|
+
it("skips a process started on an existing session (pi -c reports startup)", async () => {
|
|
306
|
+
const h = await harness(fullConfig);
|
|
307
|
+
h.setHistory([
|
|
308
|
+
{ type: "session" },
|
|
309
|
+
{ type: "model_change" },
|
|
310
|
+
{ type: "message" },
|
|
311
|
+
]);
|
|
312
|
+
await h.start("startup");
|
|
313
|
+
expect(h.sent).toEqual([]);
|
|
314
|
+
expect(h.notifications).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("still replays when the session holds only boot-level entries", async () => {
|
|
318
|
+
const h = await harness(fullConfig);
|
|
319
|
+
h.setHistory([
|
|
320
|
+
{ type: "session" },
|
|
321
|
+
{ type: "model_change" },
|
|
322
|
+
{ type: "thinking_level_change" },
|
|
323
|
+
{ type: "session_info" },
|
|
324
|
+
{ type: "custom" },
|
|
325
|
+
]);
|
|
326
|
+
await h.start("startup");
|
|
327
|
+
expect(h.sent).toHaveLength(2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("replays a fork of a session that has history", async () => {
|
|
331
|
+
const h = await harness(fullConfig);
|
|
332
|
+
h.setHistory([{ type: "message" }]);
|
|
333
|
+
await h.start("fork");
|
|
334
|
+
expect(h.sent).toHaveLength(2);
|
|
335
|
+
});
|
|
336
|
+
|
|
269
337
|
it("silently skips when no config file exists", async () => {
|
|
270
338
|
const h = await harness(null);
|
|
271
339
|
await h.start("startup");
|
|
@@ -41,6 +41,30 @@ export const RUNNABLE_REASONS: ReadonlySet<string> = new Set([
|
|
|
41
41
|
"fork",
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Whether this session start should replay the command lists.
|
|
46
|
+
*
|
|
47
|
+
* The reason alone is not enough at process start. `pi -c` (`--continue`),
|
|
48
|
+
* `-r` (`--resume`), `--session <path>` and `--fork <path>` all open an
|
|
49
|
+
* existing session *before* the first runtime exists, and that runtime is
|
|
50
|
+
* created without an explicit session_start event, so extensions see pi's
|
|
51
|
+
* default reason — `startup` — not `resume` (verified on pi 1.0.0–1.3.0:
|
|
52
|
+
* `main.js` builds the initial session manager from those flags and calls
|
|
53
|
+
* `createAgentSessionRuntime` with no `sessionStartEvent`;
|
|
54
|
+
* `agent-session.js` defaults it to `{ reason: "startup" }`). `resume` is only
|
|
55
|
+
* emitted for in-process session switches, never for a resumed process.
|
|
56
|
+
*
|
|
57
|
+
* So the session content decides: a `startup` on a session that already has
|
|
58
|
+
* conversation history is a resumed (or forked) process, and its branch already
|
|
59
|
+
* carries the state a replay would rebuild. `hasHistory` is the same signal pi
|
|
60
|
+
* itself uses to recognise an existing session
|
|
61
|
+
* (`buildSessionContext().messages.length > 0`).
|
|
62
|
+
*/
|
|
63
|
+
export function shouldReplay(reason: string, hasHistory: boolean): boolean {
|
|
64
|
+
if (reason === "startup") return !hasHistory;
|
|
65
|
+
return RUNNABLE_REASONS.has(reason);
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
/** Parse/validation failure with a user-facing message. */
|
|
45
69
|
export class AutostartParseError extends Error {}
|
|
46
70
|
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Execution gates, in order:
|
|
14
14
|
*
|
|
15
|
-
* - reason filter —
|
|
15
|
+
* - reason filter — see `shouldReplay`: startup/new/fork replay, but a
|
|
16
|
+
* `startup` that already carries conversation history is a process started
|
|
17
|
+
* on an existing session (`pi -c`/`-r`/`--session`/`--fork`) and does not
|
|
16
18
|
* - config file exists — a missing file is a silent opt-in skip
|
|
17
19
|
* - project trust (project source only)
|
|
18
20
|
* - per-command pre-validation against `pi.getCommands()`: only commands
|
|
@@ -44,7 +46,7 @@ import {
|
|
|
44
46
|
commandNameOf,
|
|
45
47
|
GLOBAL_AUTOSTART_FILENAME,
|
|
46
48
|
parseAutostartConfig,
|
|
47
|
-
|
|
49
|
+
shouldReplay,
|
|
48
50
|
} from "./autostart.js";
|
|
49
51
|
|
|
50
52
|
export interface AutostartOptions {
|
|
@@ -63,6 +65,38 @@ function messageOf(error: unknown): string {
|
|
|
63
65
|
return error instanceof Error ? error.message : String(error);
|
|
64
66
|
}
|
|
65
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Entry types that prove the session already held a conversation.
|
|
70
|
+
*
|
|
71
|
+
* `buildContextEntries()` returns the active branch's entries including the
|
|
72
|
+
* boot-level ones (`session`, `model_change`, `thinking_level_change`), so
|
|
73
|
+
* presence alone is not enough — a brand-new session has those too.
|
|
74
|
+
*/
|
|
75
|
+
const HISTORY_ENTRY_TYPES: ReadonlySet<string> = new Set([
|
|
76
|
+
"message",
|
|
77
|
+
"custom_message",
|
|
78
|
+
"compaction",
|
|
79
|
+
"branch_summary",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Whether the session already carries conversation history.
|
|
84
|
+
*
|
|
85
|
+
* This is how a `startup` on an existing session (a resumed process) is told
|
|
86
|
+
* apart from a genuinely new one — see {@link shouldReplay}. An unreadable
|
|
87
|
+
* session manager counts as new, which keeps the feature working in hosts where
|
|
88
|
+
* the session surface is unavailable.
|
|
89
|
+
*/
|
|
90
|
+
function hasConversationHistory(ctx: ExtensionContext): boolean {
|
|
91
|
+
try {
|
|
92
|
+
return ctx.sessionManager
|
|
93
|
+
.buildContextEntries()
|
|
94
|
+
.some((entry) => HISTORY_ENTRY_TYPES.has(entry.type));
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
export function registerAutostart(
|
|
67
101
|
pi: ExtensionAPI,
|
|
68
102
|
options: AutostartOptions = {},
|
|
@@ -101,7 +135,10 @@ export function registerAutostart(
|
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
pi.on("session_start", async (event, ctx) => {
|
|
104
|
-
|
|
138
|
+
// `pi -c` / `-r` / `--session` / `--fork` report `startup` for a session
|
|
139
|
+
// that already has history; the content is what tells them apart. See
|
|
140
|
+
// shouldReplay in autostart.ts for the host-side evidence.
|
|
141
|
+
if (!shouldReplay(event.reason, hasConversationHistory(ctx))) return;
|
|
105
142
|
|
|
106
143
|
const sources: Source[] = [
|
|
107
144
|
{
|