@evandu/super-pi 0.1.70 → 0.1.72
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 +12 -0
- package/dist/client/assets/index-Bi2QvT9i.js +63 -0
- package/dist/client/assets/index-CKdlMXJt.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.js +83 -31
- package/dist/server/http-server.js +74 -1
- package/dist/server/index.js +3 -0
- package/dist/server/rpc-channel.d.ts +109 -0
- package/dist/server/rpc-channel.js +117 -0
- package/dist/server/runtime.d.ts +6 -227
- package/dist/server/runtime.js +66 -358
- package/dist/server/server-context.d.ts +2 -0
- package/dist/server/session-repository.d.ts +99 -0
- package/dist/server/session-repository.js +245 -0
- package/dist/server/store.d.ts +12 -0
- package/dist/server/store.js +45 -5
- package/dist/server/subagents.js +3 -2
- package/dist/server/tool-registry.d.ts +35 -0
- package/dist/server/tool-registry.js +74 -0
- package/dist/server/types.d.ts +20 -1
- package/dist/server/ws-server.d.ts +5 -0
- package/dist/server/ws-server.js +49 -3
- package/package.json +5 -2
- package/dist/client/assets/index-B0eJbg4U.js +0 -54
- package/dist/client/assets/index-CbHwRb22.css +0 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
/** Open-ended Pi RPC record; consumers validate fields before use. */
|
|
3
|
+
export type RpcRecord = {
|
|
4
|
+
type?: string;
|
|
5
|
+
id?: string;
|
|
6
|
+
success?: boolean;
|
|
7
|
+
error?: string;
|
|
8
|
+
/** Every `response` record's payload. Kept opaque: only the catalog reader knows these shapes. */
|
|
9
|
+
data?: unknown;
|
|
10
|
+
/**
|
|
11
|
+
* `tool_execution_end` carries the tool's OUTPUT. Declared so it can be REDUCED by
|
|
12
|
+
* extractToolResultText and then dropped: the raw object must never travel past this record, and that
|
|
13
|
+
* reduction is the whole reason the result is safe to persist. Kept opaque for the same reason `data` is.
|
|
14
|
+
*/
|
|
15
|
+
result?: unknown;
|
|
16
|
+
/** The tool_execution_update record's running output. Reduced by the same function as `result`, never carried raw. */
|
|
17
|
+
partialResult?: unknown;
|
|
18
|
+
/**
|
|
19
|
+
* The inner event of a `message_update`. `message` rides on `done` - the authoritative final
|
|
20
|
+
* assistant message, with its text content, its stop reason and its model (pi-ai AssistantMessageEvent).
|
|
21
|
+
*/
|
|
22
|
+
assistantMessageEvent?: {
|
|
23
|
+
type?: string;
|
|
24
|
+
delta?: string;
|
|
25
|
+
contentIndex?: number;
|
|
26
|
+
partial?: unknown;
|
|
27
|
+
reason?: string;
|
|
28
|
+
message?: {
|
|
29
|
+
role?: string;
|
|
30
|
+
content?: unknown;
|
|
31
|
+
stopReason?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Cumulative usage of the CURRENT assistant message, repeated on every one of that message's updates
|
|
37
|
+
* (modes/json-event.ts:56-59). Reading it anywhere but on `done` counts the same tokens per token.
|
|
38
|
+
*/
|
|
39
|
+
usage?: {
|
|
40
|
+
input?: number;
|
|
41
|
+
output?: number;
|
|
42
|
+
cacheRead?: number;
|
|
43
|
+
cacheWrite?: number;
|
|
44
|
+
cost?: {
|
|
45
|
+
total?: number;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
toolCallId?: string;
|
|
49
|
+
toolName?: string;
|
|
50
|
+
args?: unknown;
|
|
51
|
+
/**
|
|
52
|
+
* tool_execution_end payload. `result` is NOT declared: it is deliberately never read, because for a
|
|
53
|
+
* `read` it is the file's contents and this type is the one place a reader would reach for it. The two
|
|
54
|
+
* numbers below are the whole of what a human-readable tool row can honestly report.
|
|
55
|
+
*/
|
|
56
|
+
isError?: boolean;
|
|
57
|
+
durationMs?: number;
|
|
58
|
+
method?: string;
|
|
59
|
+
title?: string;
|
|
60
|
+
options?: unknown;
|
|
61
|
+
message?: string;
|
|
62
|
+
placeholder?: string;
|
|
63
|
+
prefill?: string;
|
|
64
|
+
timeout?: number;
|
|
65
|
+
notifyType?: string;
|
|
66
|
+
statusKey?: string;
|
|
67
|
+
statusText?: string;
|
|
68
|
+
widgetKey?: string;
|
|
69
|
+
widgetLines?: unknown;
|
|
70
|
+
widgetPlacement?: string;
|
|
71
|
+
text?: string;
|
|
72
|
+
extensionPath?: string;
|
|
73
|
+
event?: string;
|
|
74
|
+
steering?: unknown;
|
|
75
|
+
followUp?: unknown;
|
|
76
|
+
};
|
|
77
|
+
/** One Pi child, its stdout reader, and the single writer to its stdin. */
|
|
78
|
+
export declare class RpcChannel {
|
|
79
|
+
private readonly child;
|
|
80
|
+
private readonly records;
|
|
81
|
+
/** See `stash`: a drain loop must not discard another command's response. */
|
|
82
|
+
private readonly stashed;
|
|
83
|
+
private readonly waiters;
|
|
84
|
+
private buffer;
|
|
85
|
+
private exited;
|
|
86
|
+
private exitReason;
|
|
87
|
+
/** stderr is kept only to explain an early exit; a well-behaved Pi writes nothing here. */
|
|
88
|
+
stderr: string;
|
|
89
|
+
constructor(child: ChildProcessWithoutNullStreams);
|
|
90
|
+
get exitCode(): number | null;
|
|
91
|
+
get closed(): boolean;
|
|
92
|
+
private wake;
|
|
93
|
+
/** Waits for stdout or process exit without adding a fixed polling delay to every record. */
|
|
94
|
+
private changed;
|
|
95
|
+
/** Writes one JSONL command. Throws rather than silently dropping: a command Pi never sees is a lie the caller would act on. */
|
|
96
|
+
write(command: object): void;
|
|
97
|
+
/** Returns a response to its waiter after the drain loop encountered it. */
|
|
98
|
+
stash(record: RpcRecord): void;
|
|
99
|
+
/** Removes and returns the next record, or undefined after `timeoutMs` with nothing to read. */
|
|
100
|
+
next(timeoutMs?: number): Promise<RpcRecord | undefined>;
|
|
101
|
+
/** Sends a command and waits for its response. For one command; use write()+waitFor() to overlap several. */
|
|
102
|
+
request(command: {
|
|
103
|
+
id: string;
|
|
104
|
+
type: string;
|
|
105
|
+
} & Record<string, unknown>, timeoutMs: number): Promise<RpcRecord>;
|
|
106
|
+
private take;
|
|
107
|
+
/** Waits for one correlated response while leaving every unrelated record in order. */
|
|
108
|
+
waitFor(id: string, timeoutMs: number, what?: string): Promise<RpcRecord>;
|
|
109
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** One Pi child, its stdout reader, and the single writer to its stdin. */
|
|
2
|
+
export class RpcChannel {
|
|
3
|
+
child;
|
|
4
|
+
records = [];
|
|
5
|
+
/** See `stash`: a drain loop must not discard another command's response. */
|
|
6
|
+
stashed = new Map();
|
|
7
|
+
waiters = new Set();
|
|
8
|
+
buffer = "";
|
|
9
|
+
exited = false;
|
|
10
|
+
exitReason = "";
|
|
11
|
+
/** stderr is kept only to explain an early exit; a well-behaved Pi writes nothing here. */
|
|
12
|
+
stderr = "";
|
|
13
|
+
constructor(child) {
|
|
14
|
+
this.child = child;
|
|
15
|
+
child.stdout.setEncoding("utf8");
|
|
16
|
+
child.stderr.setEncoding("utf8");
|
|
17
|
+
child.stderr.on("data", (chunk) => { this.stderr += chunk.toString(); });
|
|
18
|
+
child.stdout.on("data", (chunk) => {
|
|
19
|
+
this.buffer += chunk.toString("utf8");
|
|
20
|
+
let changed = false;
|
|
21
|
+
let newline;
|
|
22
|
+
while ((newline = this.buffer.indexOf("\n")) >= 0) {
|
|
23
|
+
const line = this.buffer.slice(0, newline).replace(/\r$/, "");
|
|
24
|
+
this.buffer = this.buffer.slice(newline + 1);
|
|
25
|
+
if (!line)
|
|
26
|
+
continue;
|
|
27
|
+
try {
|
|
28
|
+
this.records.push(JSON.parse(line));
|
|
29
|
+
changed = true;
|
|
30
|
+
}
|
|
31
|
+
catch { /* Pi stdout must be JSONL */ }
|
|
32
|
+
}
|
|
33
|
+
if (changed)
|
|
34
|
+
this.wake();
|
|
35
|
+
});
|
|
36
|
+
child.once("exit", (code, signal) => {
|
|
37
|
+
this.exited = true;
|
|
38
|
+
this.exitReason = signal ? `signal ${signal}` : `code ${code}`;
|
|
39
|
+
this.wake();
|
|
40
|
+
});
|
|
41
|
+
child.once("error", (error) => {
|
|
42
|
+
this.exited = true;
|
|
43
|
+
this.exitReason = error.message;
|
|
44
|
+
this.wake();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
get exitCode() { return this.child.exitCode; }
|
|
48
|
+
get closed() { return this.exited || this.child.exitCode !== null; }
|
|
49
|
+
wake() {
|
|
50
|
+
for (const resolve of this.waiters)
|
|
51
|
+
resolve();
|
|
52
|
+
this.waiters.clear();
|
|
53
|
+
}
|
|
54
|
+
/** Waits for stdout or process exit without adding a fixed polling delay to every record. */
|
|
55
|
+
changed(timeoutMs) {
|
|
56
|
+
if (timeoutMs <= 0)
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const finish = () => { clearTimeout(timer); this.waiters.delete(finish); resolve(); };
|
|
60
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
61
|
+
this.waiters.add(finish);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Writes one JSONL command. Throws rather than silently dropping: a command Pi never sees is a lie the caller would act on. */
|
|
65
|
+
write(command) {
|
|
66
|
+
if (this.child.killed || this.child.stdin.destroyed)
|
|
67
|
+
throw new Error(`Pi RPC stdin is closed, so ${JSON.stringify(command)} was not sent`);
|
|
68
|
+
this.child.stdin.write(`${JSON.stringify(command)}\n`);
|
|
69
|
+
}
|
|
70
|
+
/** Returns a response to its waiter after the drain loop encountered it. */
|
|
71
|
+
stash(record) {
|
|
72
|
+
if (record.type === "response" && typeof record.id === "string") {
|
|
73
|
+
this.stashed.set(record.id, record);
|
|
74
|
+
this.wake();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Removes and returns the next record, or undefined after `timeoutMs` with nothing to read. */
|
|
78
|
+
async next(timeoutMs = 100) {
|
|
79
|
+
const deadline = performance.now() + timeoutMs;
|
|
80
|
+
for (;;) {
|
|
81
|
+
if (this.records.length)
|
|
82
|
+
return this.records.shift();
|
|
83
|
+
if (this.exited || this.child.exitCode !== null || performance.now() >= deadline)
|
|
84
|
+
return undefined;
|
|
85
|
+
await this.changed(deadline - performance.now());
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** Sends a command and waits for its response. For one command; use write()+waitFor() to overlap several. */
|
|
89
|
+
async request(command, timeoutMs) {
|
|
90
|
+
this.write(command);
|
|
91
|
+
return this.waitFor(command.id, timeoutMs, command.type);
|
|
92
|
+
}
|
|
93
|
+
take(id) {
|
|
94
|
+
const index = this.records.findIndex((record) => record.type === "response" && record.id === id);
|
|
95
|
+
if (index >= 0)
|
|
96
|
+
return this.records.splice(index, 1)[0];
|
|
97
|
+
const stashed = this.stashed.get(id);
|
|
98
|
+
if (!stashed)
|
|
99
|
+
return undefined;
|
|
100
|
+
this.stashed.delete(id);
|
|
101
|
+
return stashed;
|
|
102
|
+
}
|
|
103
|
+
/** Waits for one correlated response while leaving every unrelated record in order. */
|
|
104
|
+
async waitFor(id, timeoutMs, what = id) {
|
|
105
|
+
const deadline = performance.now() + timeoutMs;
|
|
106
|
+
for (;;) {
|
|
107
|
+
const record = this.take(id);
|
|
108
|
+
if (record)
|
|
109
|
+
return record;
|
|
110
|
+
if (this.exited || this.child.exitCode !== null)
|
|
111
|
+
throw new Error(`Pi RPC exited (${this.exitReason || `code ${this.child.exitCode}`}) while waiting for ${what}. ${this.stderr.trim()}`);
|
|
112
|
+
if (performance.now() >= deadline)
|
|
113
|
+
throw new Error(`Pi did not answer ${what} within ${timeoutMs}ms`);
|
|
114
|
+
await this.changed(deadline - performance.now());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
package/dist/server/runtime.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type RpcRecord } from "./rpc-channel.js";
|
|
2
|
+
export { RpcChannel, type RpcRecord } from "./rpc-channel.js";
|
|
2
3
|
import type { ActiveMemory } from "./active-memory.js";
|
|
4
|
+
export { forkSessionFile, truncateSessionFile } from "./session-repository.js";
|
|
3
5
|
import { type AgentDefinition, type Conversation, type ExtensionUiAnswer, type ExtensionUiRequest, type MessageImage, type ModelCatalog, type ModelOption, type ModelSelection, type PiQueueSnapshot, type ToolCallNotice, type ToolCallPreparing } from "./types.js";
|
|
4
6
|
/**
|
|
5
7
|
* Who answers Pi's Extension UI questions. The runtime never invents an answer: a blocking method
|
|
@@ -56,6 +58,8 @@ export interface AnswerInput {
|
|
|
56
58
|
* revert boundary, which the revert handler reports in words.
|
|
57
59
|
*/
|
|
58
60
|
onUserEntry?: (entryId: string) => void | Promise<void>;
|
|
61
|
+
/** Session file Pi resolved for the live worker. A host may cache it as a locator for a later revert. */
|
|
62
|
+
onSessionFile?: (path: string) => void | Promise<void>;
|
|
59
63
|
/**
|
|
60
64
|
* Called once per `tool_execution_start`, with the tool's name and the bounded label naming what it was
|
|
61
65
|
* aimed at.
|
|
@@ -263,80 +267,6 @@ export declare class WelcomeRuntime implements AgentRuntime {
|
|
|
263
267
|
* through the same channel instead of writing a second JSONL reader. Every field below is optional, so
|
|
264
268
|
* a reader must check before using one.
|
|
265
269
|
*/
|
|
266
|
-
export type RpcRecord = {
|
|
267
|
-
type?: string;
|
|
268
|
-
id?: string;
|
|
269
|
-
success?: boolean;
|
|
270
|
-
error?: string;
|
|
271
|
-
/** Every `response` record's payload. Kept opaque: only the catalog reader knows these shapes. */
|
|
272
|
-
data?: unknown;
|
|
273
|
-
/**
|
|
274
|
-
* `tool_execution_end` carries the tool's OUTPUT. Declared so it can be REDUCED by
|
|
275
|
-
* extractToolResultText and then dropped: the raw object must never travel past this record, and that
|
|
276
|
-
* reduction is the whole reason the result is safe to persist. Kept opaque for the same reason `data` is.
|
|
277
|
-
*/
|
|
278
|
-
result?: unknown;
|
|
279
|
-
/** The tool_execution_update record's running output. Reduced by the same function as `result`, never carried raw. */
|
|
280
|
-
partialResult?: unknown;
|
|
281
|
-
/**
|
|
282
|
-
* The inner event of a `message_update`. `message` rides on `done` - the authoritative final
|
|
283
|
-
* assistant message, with its text content, its stop reason and its model (pi-ai AssistantMessageEvent).
|
|
284
|
-
*/
|
|
285
|
-
assistantMessageEvent?: {
|
|
286
|
-
type?: string;
|
|
287
|
-
delta?: string;
|
|
288
|
-
contentIndex?: number;
|
|
289
|
-
partial?: unknown;
|
|
290
|
-
reason?: string;
|
|
291
|
-
message?: {
|
|
292
|
-
role?: string;
|
|
293
|
-
content?: unknown;
|
|
294
|
-
stopReason?: string;
|
|
295
|
-
model?: string;
|
|
296
|
-
};
|
|
297
|
-
};
|
|
298
|
-
/**
|
|
299
|
-
* Cumulative usage of the CURRENT assistant message, repeated on every one of that message's updates
|
|
300
|
-
* (modes/json-event.ts:56-59). Reading it anywhere but on `done` counts the same tokens per token.
|
|
301
|
-
*/
|
|
302
|
-
usage?: {
|
|
303
|
-
input?: number;
|
|
304
|
-
output?: number;
|
|
305
|
-
cacheRead?: number;
|
|
306
|
-
cacheWrite?: number;
|
|
307
|
-
cost?: {
|
|
308
|
-
total?: number;
|
|
309
|
-
};
|
|
310
|
-
};
|
|
311
|
-
toolCallId?: string;
|
|
312
|
-
toolName?: string;
|
|
313
|
-
args?: unknown;
|
|
314
|
-
/**
|
|
315
|
-
* tool_execution_end payload. `result` is NOT declared: it is deliberately never read, because for a
|
|
316
|
-
* `read` it is the file's contents and this type is the one place a reader would reach for it. The two
|
|
317
|
-
* numbers below are the whole of what a human-readable tool row can honestly report.
|
|
318
|
-
*/
|
|
319
|
-
isError?: boolean;
|
|
320
|
-
durationMs?: number;
|
|
321
|
-
method?: string;
|
|
322
|
-
title?: string;
|
|
323
|
-
options?: unknown;
|
|
324
|
-
message?: string;
|
|
325
|
-
placeholder?: string;
|
|
326
|
-
prefill?: string;
|
|
327
|
-
timeout?: number;
|
|
328
|
-
notifyType?: string;
|
|
329
|
-
statusKey?: string;
|
|
330
|
-
statusText?: string;
|
|
331
|
-
widgetKey?: string;
|
|
332
|
-
widgetLines?: unknown;
|
|
333
|
-
widgetPlacement?: string;
|
|
334
|
-
text?: string;
|
|
335
|
-
extensionPath?: string;
|
|
336
|
-
event?: string;
|
|
337
|
-
steering?: unknown;
|
|
338
|
-
followUp?: unknown;
|
|
339
|
-
};
|
|
340
270
|
/**
|
|
341
271
|
* Reads Pi's `queue_update` payload, or returns undefined when the record is not one.
|
|
342
272
|
*
|
|
@@ -366,58 +296,6 @@ export declare function describeExtensionUiRequest(record: RpcRecord): Extension
|
|
|
366
296
|
* cannot produce a response Pi would read as a missing field.
|
|
367
297
|
*/
|
|
368
298
|
export declare function buildExtensionUiResponse(request: ExtensionUiRequest, answer: ExtensionUiAnswer): ExtensionUiResponse;
|
|
369
|
-
/** One Pi child, its stdout reader, and the single writer to its stdin. */
|
|
370
|
-
export declare class RpcChannel {
|
|
371
|
-
private readonly child;
|
|
372
|
-
private readonly records;
|
|
373
|
-
/**
|
|
374
|
-
* Responses a drain loop pulled off the queue before the waiter that wanted them looked.
|
|
375
|
-
*
|
|
376
|
-
* THE RACE THIS EXISTS TO SETTLE, and it is not a rare one - it cost this file an intermittent test
|
|
377
|
-
* failure. One queue serves two kinds of reader: `waitFor`, which is after a specific command's answer by
|
|
378
|
-
* id, and `next`, which is a turn's drain loop reading whatever arrives first. The loop does not know
|
|
379
|
-
* which ids someone is waiting for, so it used to take the first record of any kind - including the answer
|
|
380
|
-
* to a `get_entries` written moments earlier - and the waiter then waited out its whole timeout for a
|
|
381
|
-
* response that had already been consumed and discarded.
|
|
382
|
-
*
|
|
383
|
-
* So the drain loop no longer decides an unmatched response is garbage: it HANDS IT BACK here, and the
|
|
384
|
-
* waiter finds it whether it got there first or second. Nothing is dropped, and nothing is waited for
|
|
385
|
-
* twice.
|
|
386
|
-
*/
|
|
387
|
-
private readonly stashed;
|
|
388
|
-
private buffer;
|
|
389
|
-
/** stderr is kept only to explain an early exit; a well-behaved Pi writes nothing here. */
|
|
390
|
-
stderr: string;
|
|
391
|
-
constructor(child: ChildProcessWithoutNullStreams);
|
|
392
|
-
get exitCode(): number | null;
|
|
393
|
-
/** Writes one JSONL command. Throws rather than silently dropping: a command Pi never sees is a lie the caller would act on. */
|
|
394
|
-
write(command: object): void;
|
|
395
|
-
/**
|
|
396
|
-
* Returns one record to whoever is waiting for it, after a drain loop read it.
|
|
397
|
-
*
|
|
398
|
-
* Called for every response the loop does not consume itself. See `RpcChannel`'s note on `stashed` for why
|
|
399
|
-
* this is not the same as discarding it - discarding it silently cost a turn its revert boundary.
|
|
400
|
-
*/
|
|
401
|
-
stash(record: RpcRecord): void;
|
|
402
|
-
/** Removes and returns the next record, or undefined after `timeoutMs` with nothing to read. */
|
|
403
|
-
next(timeoutMs?: number): Promise<RpcRecord | undefined>;
|
|
404
|
-
/** Sends a command and waits for its response. For one command; use write()+waitFor() to overlap several. */
|
|
405
|
-
request(command: {
|
|
406
|
-
id: string;
|
|
407
|
-
type: string;
|
|
408
|
-
} & Record<string, unknown>, timeoutMs: number): Promise<RpcRecord>;
|
|
409
|
-
/**
|
|
410
|
-
* Returns one response by id, from the queue if it is still there and from the stash if a drain loop got
|
|
411
|
-
* there first. Removing it from the stash is what keeps a single answer from being handed out twice.
|
|
412
|
-
*/
|
|
413
|
-
private take;
|
|
414
|
-
/**
|
|
415
|
-
* Waits for the response to one command id. Every record that is not that response stays QUEUED in
|
|
416
|
-
* order, so a caller can have three requests in flight and still read all three answers: whoever
|
|
417
|
-
* asks first takes its own record and leaves the rest for the drain loop.
|
|
418
|
-
*/
|
|
419
|
-
waitFor(id: string, timeoutMs: number, what?: string): Promise<RpcRecord>;
|
|
420
|
-
}
|
|
421
299
|
/**
|
|
422
300
|
* The commands a freshly spawned Pi must be told before it runs a conversation's turn.
|
|
423
301
|
*
|
|
@@ -446,105 +324,6 @@ export declare function sessionCommands(conversation: Pick<Conversation, "model"
|
|
|
446
324
|
* only the shim, which is the exact failure this function exists to prevent.
|
|
447
325
|
*/
|
|
448
326
|
export declare function killTree(pid: number): Promise<void>;
|
|
449
|
-
/**
|
|
450
|
-
* Cuts a Pi session file down to the header plus everything up to and including `boundaryEntryId`.
|
|
451
|
-
*
|
|
452
|
-
* ============================ WHAT THIS DESTROYS ============================
|
|
453
|
-
* **Every entry written after the boundary is gone from disk, permanently, and there is no way to
|
|
454
|
-
* reconstruct it from this file.** That includes not only the messages a person can see but also Pi's
|
|
455
|
-
* `tool_call` records, `compaction` summaries, `model_change` and `thinking_level_change` markers, usage
|
|
456
|
-
* accounting, and anything a future Pi version adds. This is the step the whole revert design is built
|
|
457
|
-
* around (research §4.2) and the step that cannot be undone by the application - Super Pi has no undo,
|
|
458
|
-
* and adding one was explicitly left out of scope.
|
|
459
|
-
*
|
|
460
|
-
* It does NOT touch the workspace. No file the agent read or wrote is read, written or reverted here.
|
|
461
|
-
* =============================================================================
|
|
462
|
-
*
|
|
463
|
-
* WHY TRUNCATE RATHER THAN ASK PI. Pi's `SessionManager` is append-only by design
|
|
464
|
-
* (`session-manager.ts:1516-1519`: "Entries cannot be modified or deleted"), and its in-session rewind -
|
|
465
|
-
* `branch()`, `navigateTree()` - is IN-MEMORY ONLY (`:1573-1584`): `_buildIndex()` re-reads the whole
|
|
466
|
-
* file on every `open()` and sets the leaf to the last line unconditionally (`:1103-1122`). Super Pi
|
|
467
|
-
* spawns one Pi per turn and kills it afterwards, so a leaf pointer set in one turn is gone before the
|
|
468
|
-
* next one starts - a "call `branch()` once" solution would look correct for a turn and then silently
|
|
469
|
-
* forget (research §5 pitfall 2). Truncating the file is the only version of this that is a durable state
|
|
470
|
-
* change rather than per-turn bookkeeping that can be skipped exactly once.
|
|
471
|
-
*
|
|
472
|
-
* WHY THE FORMAT IS ASSUMED RATHER THAN GUESSED. The shape this relies on - line 0 is the one and only
|
|
473
|
-
* `type: "session"` header, every other line is one self-contained JSON entry, every entry's `parentId`
|
|
474
|
-
* already appears earlier in the file, no blank lines, and a trailing newline - was measured against
|
|
475
|
-
* real Pi by scripts/probe-revert-session-format.mjs, which also proves the `parentId` property that
|
|
476
|
-
* makes a prefix a VALID session rather than a fragment. That probe is the reason this function may
|
|
477
|
-
* index by line at all; every one of those claims is re-checked here before anything is written, so a
|
|
478
|
-
* Pi that changed the format fails loudly instead of producing a file Pi cannot open.
|
|
479
|
-
*
|
|
480
|
-
* WHY THE WRITE IS TEMP-THEN-RENAME. Pi appends with a bare `appendFileSync`
|
|
481
|
-
* (`session-manager.ts:1187`) and makes no atomicity guarantee of its own. Writing the truncated copy
|
|
482
|
-
* under a different name and renaming it over the original means a reader never observes a half-written
|
|
483
|
-
* file, and a failure before the rename leaves the original untouched.
|
|
484
|
-
*
|
|
485
|
-
* WHY A BACKUP IS KEPT. `.super-pi-revert-backup` is written next to the session file, deliberately not
|
|
486
|
-
* named `*.jsonl` so Pi's own discovery (`readdirSync` filtered on that suffix,
|
|
487
|
-
* `session-manager.ts:1881`) never lists it, and deliberately NOT surfaced anywhere as a feature:
|
|
488
|
-
* Super Pi has no undo and does not pretend to. It exists so that an irreversible, unrecoverable-by-code
|
|
489
|
-
* operation on a person's conversation history is at least recoverable by hand.
|
|
490
|
-
*
|
|
491
|
-
* EXPORTED so it can be tested against a fabricated session file in a temp directory, without Pi and
|
|
492
|
-
* without touching anybody's real transcript.
|
|
493
|
-
*/
|
|
494
|
-
export declare function truncateSessionFile(path: string, boundaryEntryId: string, expectedSessionId: string, beforeBoundary?: boolean): Promise<{
|
|
495
|
-
linesKept: number;
|
|
496
|
-
entriesDropped: number;
|
|
497
|
-
}>;
|
|
498
|
-
/**
|
|
499
|
-
* Writes a second Pi session file that begins as an EXACT copy of one, under a different session id.
|
|
500
|
-
*
|
|
501
|
-
* ============================ WHAT THIS DOES TO THE ORIGINAL ============================
|
|
502
|
-
* **Nothing.** The source file is read and never written, never truncated, never renamed. This is the
|
|
503
|
-
* whole difference from `truncateSessionFile`, and it is the reason a fork is the safe half of this pair
|
|
504
|
-
* of operations: a revert destroys entries permanently and keeps a hand-recovery backup, while a fork
|
|
505
|
-
* leaves both files whole and keeps no backup because there is nothing to recover.
|
|
506
|
-
* ========================================================================================
|
|
507
|
-
*
|
|
508
|
-
* WHY A COPY AND NOT PI'S `fork()`. Covered at length on `ClientCommand`'s `conversation.fork`: Pi's fork
|
|
509
|
-
* always tears down the runtime and switches to the new session (`agent-session-runtime.ts:299-307`), and
|
|
510
|
-
* Super Pi has no runtime to switch - it spawns one Pi per turn and kills it. The file is the only durable
|
|
511
|
-
* thing a fork has to produce here, so the file is what gets produced.
|
|
512
|
-
*
|
|
513
|
-
* ============================ WHAT THIS DOES TO PI's ENTRY IDS ============================
|
|
514
|
-
* **Nothing either, and this is a deliberate divergence from Pi's own `createBranchedSession`, which
|
|
515
|
-
* RENUMBERS `parentId` (`:1655-1666`).** Keeping every id verbatim means every cross-reference already
|
|
516
|
-
* inside the file stays valid, and research pitfall 6 is exactly why that matters: a Pi entry may point
|
|
517
|
-
* at other entries by id in ways this code has never enumerated - a `compaction` record carries a
|
|
518
|
-
* `firstKeptEntryId`, `branch_summary` carries the branch point - and renumbering ids means rewriting every
|
|
519
|
-
* one of those references correctly, with no way to find them all. Copying the bytes cannot break a
|
|
520
|
-
* reference because it changes nothing a reference points at. The cost is that the same entry id now
|
|
521
|
-
* exists in two files; that is safe because `_buildIndex` builds its `byId` map per file (`:1103-1122`),
|
|
522
|
-
* and Super Pi only ever opens one file per process.
|
|
523
|
-
*
|
|
524
|
-
* WHAT THE HEADER CHANGES, and it is two fields. `id` becomes the new conversation's id, because that is
|
|
525
|
-
* what Pi's lookup matches (`findLocalSessionByExactId` reads the header, probe B) and therefore what makes
|
|
526
|
-
* `--session-id <forked>` open THIS file rather than silently inventing an empty session with that id
|
|
527
|
-
* (`main.ts:436-448`, measured: `Warning: No project session found with id '...'`). And `parentSession`
|
|
528
|
-
* records the file this one came from, which is the same one-line lineage pointer Pi itself writes
|
|
529
|
-
* (`:1675-1682`) and is not a link - the files share nothing. `cwd`, `version` and `timestamp` are copied
|
|
530
|
-
* verbatim: `cwd` MUST be identical, because Pi's lookup requires the recorded cwd to equal the one it is
|
|
531
|
-
* asked from, and the fork always lives in the same workspace as its source.
|
|
532
|
-
*
|
|
533
|
-
* THE SAME FORMAT CHECKS AS TRUNCATE, AND FOR THE SAME REASON. Probe A measured this file's shape against
|
|
534
|
-
* real Pi (`scripts/probe-revert-session-format.mjs`); a Pi upgrade can invalidate any of those claims, so
|
|
535
|
-
* every one is re-checked before a byte is written and a failure names the shape actually found. Here the
|
|
536
|
-
* `parentId` property is checked across the WHOLE file rather than across a kept prefix, because the whole
|
|
537
|
-
* file is what is being copied.
|
|
538
|
-
*
|
|
539
|
-
* TEMP-THEN-RENAME, because Pi appends with a bare `appendFileSync` (`:1187`) and the target must never be
|
|
540
|
-
* observed half-written - a half-written file under the forked id is precisely the "Pi opens an empty
|
|
541
|
-
* session and the history has vanished with no error anywhere" outcome probe B was about.
|
|
542
|
-
*
|
|
543
|
-
* EXPORTED so it can be tested against fabricated session files in a temp directory, without Pi.
|
|
544
|
-
*/
|
|
545
|
-
export declare function forkSessionFile(sourcePath: string, targetPath: string, expectedSessionId: string, targetSessionId: string): Promise<{
|
|
546
|
-
entriesCopied: number;
|
|
547
|
-
}>;
|
|
548
327
|
/**
|
|
549
328
|
* Reads Pi's `steer` disposition out of its response payload.
|
|
550
329
|
*
|
|
@@ -712,7 +491,7 @@ export declare class PiRpcRuntime implements AgentRuntime {
|
|
|
712
491
|
* own address, so it is the same string on every turn and is read once from the constructor's options.
|
|
713
492
|
*/
|
|
714
493
|
private taskExtensionEnv;
|
|
715
|
-
answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, images, memoryReview, memorySourceConversationId }: AnswerInput): AsyncIterable<string>;
|
|
494
|
+
answer({ conversation, prompt, signal, cwd, host, onPiQueue, onUserEntry, onSessionFile, onToolCall, onToolUpdate, onToolPreparing, onToolEnd, onThinking, images, memoryReview, memorySourceConversationId }: AnswerInput): AsyncIterable<string>;
|
|
716
495
|
/**
|
|
717
496
|
* Reads back the id of the user message this turn just created, and hands it to the caller.
|
|
718
497
|
*
|