pi-invisible-continue 0.2.2 → 0.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/continue.ts +34 -100
- package/package.json +1 -3
package/continue.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Agent } from "@earendil-works/pi-agent-core";
|
|
3
2
|
import {
|
|
4
3
|
CONTINUE_COMMAND_DESCRIPTION,
|
|
5
4
|
getLastAssistantMessageText,
|
|
@@ -9,99 +8,35 @@ import {
|
|
|
9
8
|
* pi-invisible-continue — resume the agentic loop without the LLM seeing any new prompt.
|
|
10
9
|
*
|
|
11
10
|
* Strategy:
|
|
12
|
-
* -
|
|
13
|
-
* -
|
|
14
|
-
*
|
|
15
|
-
* -
|
|
16
|
-
*
|
|
11
|
+
* - Use pi.sendMessage() with triggerTurn: true to start a new agent turn
|
|
12
|
+
* - The message uses role: "custom" which is filtered by convertToLlm(),
|
|
13
|
+
* so the LLM receives the exact same message list it had before
|
|
14
|
+
* - This goes through AgentSession._runAgentPrompt, so auto-compaction,
|
|
15
|
+
* auto-retry, and other session lifecycle features work correctly
|
|
16
|
+
* - No prototype monkey-patching needed — no fragile Agent import, no
|
|
17
|
+
* module duplication issues
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* Why not agent.prompt([])?
|
|
20
|
+
* The previous approach monkey-patched Agent.prototype.subscribe to capture
|
|
21
|
+
* the Agent instance, then called agent.prompt([]) directly. This broke when
|
|
22
|
+
* the extension's import of Agent resolved to a different class (from the
|
|
23
|
+
* package's own node_modules/@earendil-works/pi-agent-core@0.75.4) than the
|
|
24
|
+
* one AgentSession uses (from pi's bundled 0.79.1). The subscribe patch was
|
|
25
|
+
* applied to the wrong prototype, so the agent was never captured.
|
|
26
|
+
*
|
|
27
|
+
* The new approach avoids this entirely by using the extension API's
|
|
28
|
+
* sendMessage() with triggerTurn: true, which is resolved at runtime by the
|
|
29
|
+
* AgentSession and doesn't depend on importing Agent at all.
|
|
22
30
|
*/
|
|
23
31
|
|
|
24
|
-
// Capture the live Agent instance when AgentSession subscribes to it.
|
|
25
|
-
// subscribe() is called during AgentSession construction — fires on both
|
|
26
|
-
// fresh sessions and session resumes, unlike prompt().
|
|
27
|
-
//
|
|
28
|
-
// Chain the previous patch (if pi-retry or pi-vcc already patched it)
|
|
29
|
-
// so all extensions can coexist.
|
|
30
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
31
|
-
let _agent: Agent | null = null;
|
|
32
|
-
const _origSubscribe = Agent.prototype.subscribe as (this: Agent, ...args: any[]) => any;
|
|
33
|
-
Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
|
|
34
|
-
_agent = this;
|
|
35
|
-
return _origSubscribe.apply(this, args);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
32
|
// Mutex: only one invisible continue may be in-flight at a time.
|
|
39
|
-
// Without this, concurrent /continue (or /continue during pi-retry/pi-vcc
|
|
40
|
-
// auto-continuation) race through waitForIdle() and both call prompt([]),
|
|
41
|
-
// producing "Agent is already processing".
|
|
42
33
|
let _continueInProgress = false;
|
|
43
34
|
|
|
44
|
-
// Monkey-patch continue() so the session's built-in loop cooperates with
|
|
45
|
-
// our mutex AND can convert the "Cannot continue from assistant" error
|
|
46
|
-
// into a prompt([]) call when the agent was mid-task.
|
|
47
|
-
// Without this, the session's continue() would throw when the last message
|
|
48
|
-
// is an assistant (common after compaction), and the agent loop would die
|
|
49
|
-
// leaving mid-task work unfinished.
|
|
50
|
-
//
|
|
51
|
-
// When continue() throws "Cannot continue from message role: assistant":
|
|
52
|
-
// - stopReason "stop" → agent finished cleanly, don't continue
|
|
53
|
-
// - stopReason "aborted" → user cancelled, don't continue
|
|
54
|
-
// - stopReason "error" → pi-retry handles errors, don't race it
|
|
55
|
-
// - stopReason "toolUse" or "length" → mid-task, fall back to prompt([])
|
|
56
|
-
//
|
|
57
|
-
// Chains the previous patch (pi-retry, pi-vcc) so all mutexes are respected.
|
|
58
|
-
const _origContinue = Agent.prototype.continue;
|
|
59
|
-
Agent.prototype.continue = function (this: Agent) {
|
|
60
|
-
const self = this;
|
|
61
|
-
return (async (): Promise<void> => {
|
|
62
|
-
while (_continueInProgress) {
|
|
63
|
-
await new Promise(r => setTimeout(r, 10));
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
await _origContinue.call(self);
|
|
67
|
-
} catch (e: any) {
|
|
68
|
-
const msg = e?.message ?? '';
|
|
69
|
-
if (msg.includes('Cannot continue from message role') ||
|
|
70
|
-
msg.includes('Cannot continue from an assistant message')) {
|
|
71
|
-
// Check stopReason — only continue if the agent was mid-task
|
|
72
|
-
const lastMsg = self.state.messages[self.state.messages.length - 1];
|
|
73
|
-
if (lastMsg?.role === 'assistant' &&
|
|
74
|
-
lastMsg.stopReason !== 'stop' &&
|
|
75
|
-
lastMsg.stopReason !== 'aborted' &&
|
|
76
|
-
lastMsg.stopReason !== 'error') {
|
|
77
|
-
// Agent was mid-task — fall back to prompt([])
|
|
78
|
-
if (!_continueInProgress) {
|
|
79
|
-
_continueInProgress = true;
|
|
80
|
-
try {
|
|
81
|
-
await self.prompt([]);
|
|
82
|
-
} catch {
|
|
83
|
-
// Agent already processing or other transient error
|
|
84
|
-
} finally {
|
|
85
|
-
_continueInProgress = false;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// For stop/aborted/error: return void, the session loop exits naturally
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (msg.includes('Agent is already processing')) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
throw e;
|
|
96
|
-
}
|
|
97
|
-
})();
|
|
98
|
-
};
|
|
99
|
-
|
|
100
35
|
export default function (pi: ExtensionAPI) {
|
|
101
36
|
pi.registerCommand("continue", {
|
|
102
37
|
description: CONTINUE_COMMAND_DESCRIPTION,
|
|
103
38
|
handler: async (args, ctx) => {
|
|
104
|
-
await runContinueCommand(ctx, args);
|
|
39
|
+
await runContinueCommand(pi, ctx, args);
|
|
105
40
|
},
|
|
106
41
|
});
|
|
107
42
|
|
|
@@ -111,6 +46,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
111
46
|
}
|
|
112
47
|
|
|
113
48
|
async function runContinueCommand(
|
|
49
|
+
pi: ExtensionAPI,
|
|
114
50
|
ctx: ExtensionCommandContext,
|
|
115
51
|
args: string,
|
|
116
52
|
): Promise<void> {
|
|
@@ -121,7 +57,6 @@ async function runContinueCommand(
|
|
|
121
57
|
[
|
|
122
58
|
"pi-invisible-continue status:",
|
|
123
59
|
` Agent idle: ${idle ? "yes" : "no"}`,
|
|
124
|
-
` Captured agent: ${_agent ? "yes" : "no"}`,
|
|
125
60
|
` Last assistant: ${last ?? "(none)"}`.slice(0, 120),
|
|
126
61
|
].join("\n"),
|
|
127
62
|
"info",
|
|
@@ -141,20 +76,12 @@ async function runContinueCommand(
|
|
|
141
76
|
return;
|
|
142
77
|
}
|
|
143
78
|
|
|
144
|
-
if (!_agent) {
|
|
145
|
-
ctx.ui.notify(
|
|
146
|
-
"pi-invisible-continue: Agent instance not captured. Internal error?",
|
|
147
|
-
"warning",
|
|
148
|
-
);
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
79
|
if (!ctx.isIdle()) {
|
|
153
80
|
await ctx.waitForIdle();
|
|
154
81
|
}
|
|
155
82
|
|
|
156
83
|
// Guard: if pi-retry or pi-vcc already has an invisible continue in-flight,
|
|
157
|
-
// skip — their
|
|
84
|
+
// skip — their triggerTurn will resume the loop.
|
|
158
85
|
if (_continueInProgress) {
|
|
159
86
|
ctx.ui.notify(
|
|
160
87
|
"pi-invisible-continue: Another invisible continue is already in progress.",
|
|
@@ -165,12 +92,19 @@ async function runContinueCommand(
|
|
|
165
92
|
_continueInProgress = true;
|
|
166
93
|
|
|
167
94
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
95
|
+
// Send an invisible custom message that triggers a new agent turn.
|
|
96
|
+
// convertToLlm() filters role:"custom" messages, so the LLM sees
|
|
97
|
+
// the same context as before — no injected prompt.
|
|
98
|
+
pi.sendMessage(
|
|
99
|
+
{ customType: "pi-invisible-continue", content: "", display: false, details: undefined },
|
|
100
|
+
{ triggerTurn: true },
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
// Wait for the triggered turn (and any post-run continuations like
|
|
104
|
+
// auto-compaction and auto-retry) to complete.
|
|
105
|
+
await ctx.waitForIdle();
|
|
106
|
+
} catch {
|
|
107
|
+
// Agent is already processing — something else is driving.
|
|
174
108
|
} finally {
|
|
175
109
|
_continueInProgress = false;
|
|
176
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-invisible-continue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -38,8 +38,6 @@
|
|
|
38
38
|
"lint:dead": "knip --no-gitignore"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@earendil-works/pi-agent-core": "0.75.4",
|
|
42
|
-
"@earendil-works/pi-ai": "0.75.4",
|
|
43
41
|
"@earendil-works/pi-coding-agent": "0.75.4",
|
|
44
42
|
"@types/node": "25.9.1",
|
|
45
43
|
"@vitest/coverage-v8": "4.1.7",
|