pi-invisible-continue 0.2.0 → 0.2.2

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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/continue.ts +95 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -60,7 +60,7 @@ User types "/continue"
60
60
 
61
61
  ### Trade-off: bypasses AgentSession
62
62
 
63
- `agent.prompt([])` is called on the `Agent` directly, bypassing `AgentSession._runAgentPrompt()`. This means auto-retry on errors and auto-compaction are not triggered after a `/continue`. For a manual command where the user explicitly said "keep going," this is acceptable they can always `/continue` again.
63
+ `agent.prompt([])` is called on the `Agent` directly, bypassing `AgentSession._runAgentPrompt()`. This means auto-compaction is not triggered after a `/continue`. Auto-retry is not a concern [pi-retry](https://github.com/monotykamary/pi-retry) covers that gap by listening for `agent_end` events and re-triggering the loop on errors. Since the agent still emits events through `AgentSession`'s subscriber, pi-retry's handlers fire normally even after an `agent.prompt([])` continuation.
64
64
 
65
65
  ---
66
66
 
package/continue.ts CHANGED
@@ -9,19 +9,24 @@ import {
9
9
  * pi-invisible-continue — resume the agentic loop without the LLM seeing any new prompt.
10
10
  *
11
11
  * Strategy:
12
- * - Monkey-patch Agent.prototype.prompt to capture the Agent instance
12
+ * - Monkey-patch Agent.prototype.subscribe to capture the Agent instance
13
13
  * - /continue calls agent.prompt([]) directly, starting a fresh agent loop
14
14
  * with an empty prompt — no message is injected into context at all
15
15
  * - The LLM receives the exact same message list it had before
16
16
  * - No session JSONL artifact, no convertToLlm involvement, no filter needed
17
17
  *
18
- * This bypasses AgentSession._runAgentPrompt, so auto-retry and auto-compaction
19
- * are not triggered after a manual /continue. The user can always /continue again.
18
+ * This bypasses AgentSession._runAgentPrompt, so auto-compaction is not
19
+ * triggered after a manual /continue. Auto-retry is not a concern — pi-retry
20
+ * covers that gap via its agent_end handler, which still fires because the
21
+ * agent's processEvents propagates to AgentSession's subscriber normally.
20
22
  */
21
23
 
22
24
  // Capture the live Agent instance when AgentSession subscribes to it.
23
25
  // subscribe() is called during AgentSession construction — fires on both
24
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.
25
30
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
31
  let _agent: Agent | null = null;
27
32
  const _origSubscribe = Agent.prototype.subscribe as (this: Agent, ...args: any[]) => any;
@@ -30,6 +35,68 @@ Agent.prototype.subscribe = function (this: Agent, ...args: any[]) {
30
35
  return _origSubscribe.apply(this, args);
31
36
  };
32
37
 
38
+ // 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
+ let _continueInProgress = false;
43
+
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
+
33
100
  export default function (pi: ExtensionAPI) {
34
101
  pi.registerCommand("continue", {
35
102
  description: CONTINUE_COMMAND_DESCRIPTION,
@@ -37,6 +104,10 @@ export default function (pi: ExtensionAPI) {
37
104
  await runContinueCommand(ctx, args);
38
105
  },
39
106
  });
107
+
108
+ pi.on("session_start", () => {
109
+ _continueInProgress = false;
110
+ });
40
111
  }
41
112
 
42
113
  async function runContinueCommand(
@@ -82,5 +153,25 @@ async function runContinueCommand(
82
153
  await ctx.waitForIdle();
83
154
  }
84
155
 
85
- await _agent.prompt([]);
156
+ // Guard: if pi-retry or pi-vcc already has an invisible continue in-flight,
157
+ // skip — their prompt([]) will resume the loop.
158
+ if (_continueInProgress) {
159
+ ctx.ui.notify(
160
+ "pi-invisible-continue: Another invisible continue is already in progress.",
161
+ "info",
162
+ );
163
+ return;
164
+ }
165
+ _continueInProgress = true;
166
+
167
+ try {
168
+ await _agent.waitForIdle();
169
+ try {
170
+ await _agent.prompt([]);
171
+ } catch {
172
+ // Agent is already processing — something else is driving.
173
+ }
174
+ } finally {
175
+ _continueInProgress = false;
176
+ }
86
177
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-invisible-continue",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",