pi-invisible-continue 0.3.8 → 0.3.10
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 +1 -0
- package/continue.ts +4 -1
- package/package.json +10 -10
- package/src/index.ts +48 -0
package/README.md
CHANGED
|
@@ -35,6 +35,7 @@ The continuation deliberately goes through `AgentSession`:
|
|
|
35
35
|
- A concurrent prompt is queued through the native follow-up path rather than racing it.
|
|
36
36
|
- Auto-retry, compaction, abort, and `agent_settled` behavior remain intact.
|
|
37
37
|
- The marker is hidden from the TUI and filtered from every LLM request.
|
|
38
|
+
- When a continuation starts after pi's auto-retry is exhausted, the trailing failed assistant attempts (`stopReason: "error"`/`"aborted"`, no tool calls) are also dropped from the request — the LLM never sees the failure either.
|
|
38
39
|
|
|
39
40
|
## How It Works
|
|
40
41
|
|
package/continue.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
INVISIBLE_CONTINUE_CUSTOM_TYPE,
|
|
5
5
|
getLastAssistantMessageText,
|
|
6
6
|
isInvisibleContinueMarker,
|
|
7
|
+
stripTrailingIncompleteAssistants,
|
|
7
8
|
} from "./src/index.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -13,8 +14,10 @@ import {
|
|
|
13
14
|
*/
|
|
14
15
|
export default function invisibleContinueExtension(pi: ExtensionAPI) {
|
|
15
16
|
pi.on("context", (event) => {
|
|
17
|
+
const hasMarker = event.messages.some((message) => isInvisibleContinueMarker(message));
|
|
16
18
|
const messages = event.messages.filter((message) => !isInvisibleContinueMarker(message));
|
|
17
|
-
|
|
19
|
+
const next = hasMarker ? stripTrailingIncompleteAssistants(messages) : messages;
|
|
20
|
+
if (next.length !== event.messages.length) return { messages: next };
|
|
18
21
|
});
|
|
19
22
|
|
|
20
23
|
pi.registerCommand("continue", {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-invisible-continue",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"description": "Invisible session continuation for pi — resume the agentic loop without sending ANY prompt the LLM can see",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Tom X Nguyen",
|
|
@@ -30,8 +30,15 @@
|
|
|
30
30
|
"src/",
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"test:watch": "vitest",
|
|
36
|
+
"test:coverage": "vitest run --coverage",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"lint:dead": "knip --no-gitignore"
|
|
39
|
+
},
|
|
33
40
|
"devDependencies": {
|
|
34
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
41
|
+
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
35
42
|
"@types/node": "25.9.1",
|
|
36
43
|
"@vitest/coverage-v8": "4.1.7",
|
|
37
44
|
"knip": "6.14.1",
|
|
@@ -47,12 +54,5 @@
|
|
|
47
54
|
"brace-expansion": "5.0.6",
|
|
48
55
|
"protobufjs": "8.4.0",
|
|
49
56
|
"ws": "8.20.1"
|
|
50
|
-
},
|
|
51
|
-
"scripts": {
|
|
52
|
-
"test": "vitest run",
|
|
53
|
-
"test:watch": "vitest",
|
|
54
|
-
"test:coverage": "vitest run --coverage",
|
|
55
|
-
"typecheck": "tsc --noEmit",
|
|
56
|
-
"lint:dead": "knip --no-gitignore"
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,54 @@ export function isInvisibleContinueMarker(message: unknown): boolean {
|
|
|
14
14
|
);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const INCOMPLETE_ASSISTANT_STOP_REASONS = new Set(["error", "aborted"]);
|
|
18
|
+
|
|
19
|
+
export function isIncompleteAssistantMessage(message: unknown): boolean {
|
|
20
|
+
if (!message || typeof message !== "object") return false;
|
|
21
|
+
const candidate = message as { role?: unknown; stopReason?: unknown };
|
|
22
|
+
return (
|
|
23
|
+
candidate.role === "assistant" &&
|
|
24
|
+
typeof candidate.stopReason === "string" &&
|
|
25
|
+
INCOMPLETE_ASSISTANT_STOP_REASONS.has(candidate.stopReason)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function hasToolCallBlocks(message: unknown): boolean {
|
|
30
|
+
if (!message || typeof message !== "object") return false;
|
|
31
|
+
const content = (message as { content?: unknown }).content;
|
|
32
|
+
return (
|
|
33
|
+
Array.isArray(content) &&
|
|
34
|
+
content.some(
|
|
35
|
+
(block) =>
|
|
36
|
+
!!block &&
|
|
37
|
+
typeof block === "object" &&
|
|
38
|
+
(block as { type?: unknown }).type === "toolCall",
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Pop trailing incomplete assistant attempts (retry-exhausted errors, aborts)
|
|
45
|
+
* so a continuation does not re-serve the failed run to the LLM.
|
|
46
|
+
*
|
|
47
|
+
* Guards: stops at the first message that is not an incomplete assistant,
|
|
48
|
+
* never pops an assistant carrying toolCall blocks (their tool results must
|
|
49
|
+
* stay paired), and never empties the context entirely.
|
|
50
|
+
*/
|
|
51
|
+
export function stripTrailingIncompleteAssistants<T>(
|
|
52
|
+
messages: ReadonlyArray<T>,
|
|
53
|
+
): T[] {
|
|
54
|
+
const stripped = [...messages];
|
|
55
|
+
while (
|
|
56
|
+
stripped.length > 1 &&
|
|
57
|
+
isIncompleteAssistantMessage(stripped[stripped.length - 1]) &&
|
|
58
|
+
!hasToolCallBlocks(stripped[stripped.length - 1])
|
|
59
|
+
) {
|
|
60
|
+
stripped.pop();
|
|
61
|
+
}
|
|
62
|
+
return stripped;
|
|
63
|
+
}
|
|
64
|
+
|
|
17
65
|
export function getLastAssistantMessageText(
|
|
18
66
|
entries: ReadonlyArray<{ type: string; message?: { role?: string; content?: unknown } }>,
|
|
19
67
|
): string | undefined {
|