pi-code 1.0.47 → 1.0.49

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.
@@ -24,6 +24,8 @@ export interface BackgroundRun {
24
24
  stderr?: string
25
25
  /** Claude's partial marker: the run stopped at its maxTurns limit. */
26
26
  partial?: boolean
27
+ /** Temp dir holding a prompt file rebuilt for a resume; removed with the run. */
28
+ rebuiltPromptDir?: string
27
29
  /** Set while running so the run can be cancelled; cleared on completion. */
28
30
  kill?: () => void
29
31
  /** True until the child process actually closes: a cancelled child that ignores
@@ -65,7 +67,7 @@ export const MAX_FINISHED_RUNS = 20
65
67
  /** Grace between the cancel SIGTERM and the SIGKILL that ends a child ignoring it. */
66
68
  const CANCEL_KILL_GRACE_MS = 5000
67
69
 
68
- /** Bytes of stderr kept per run, enough for the boot error without buffering logs. */
70
+ /** Characters of stderr kept per run, enough for the boot error without buffering logs. */
69
71
  const STDERR_TAIL_CHARS = 2048
70
72
 
71
73
  export function activeBackgroundRuns(): number {
@@ -79,6 +81,7 @@ let finishSequence = 0
79
81
  /** Test seam: the registry is module state, so tests reset it between cases to
80
82
  * stay order-independent. */
81
83
  export function resetBackgroundRuns(): void {
84
+ for (const run of runs.values()) removeRebuiltPrompt(run)
82
85
  runs.clear()
83
86
  finishSequence = 0
84
87
  }
@@ -86,7 +89,10 @@ export function resetBackgroundRuns(): void {
86
89
  function evictFinishedRuns(): void {
87
90
  const finished = [...runs.values()].filter((run) => !run.live && run.state !== 'running')
88
91
  finished.sort((a, b) => (a.finishedAt ?? 0) - (b.finishedAt ?? 0))
89
- for (const stale of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_RUNS))) runs.delete(stale.id)
92
+ for (const stale of finished.slice(0, Math.max(0, finished.length - MAX_FINISHED_RUNS))) {
93
+ removeRebuiltPrompt(stale)
94
+ runs.delete(stale.id)
95
+ }
90
96
  }
91
97
 
92
98
  /** Line-by-line parser keeping only the last assistant text and a turn count, so a
@@ -162,6 +168,8 @@ export function cancelAllBackgroundRuns(): number {
162
168
  for (const id of Array.from(runs.keys())) {
163
169
  if (cancelBackgroundRun(id) === 'cancelled') count++
164
170
  }
171
+ // Called at quit: nothing in this registry is resumable once pi exits.
172
+ for (const run of runs.values()) removeRebuiltPrompt(run)
165
173
  return count
166
174
  }
167
175
 
@@ -190,38 +198,53 @@ export function resumeBackgroundRun(id: string, task: string, onComplete: (run:
190
198
  // Persisted so the rebuild happens once: rebuilding per resume leaked one temp
191
199
  // prompt dir every follow-up.
192
200
  const rebuilt = withRebuiltPrompt(run.spawn)
193
- run.spawn = { ...run.spawn, args: rebuilt }
194
- const args = rebuilt.map((arg) => (arg.startsWith('Task: ') ? `Task: ${task}` : arg))
201
+ run.spawn = { ...run.spawn, args: rebuilt.args }
202
+ if (rebuilt.dir) run.rebuiltPromptDir = rebuilt.dir
203
+ const args = rebuilt.args.map((arg) => (arg.startsWith('Task: ') ? `Task: ${task}` : arg))
195
204
  run.state = 'running'
196
205
  run.task = task
197
206
  run.output = undefined
198
207
  run.exitCode = undefined
199
208
  run.stderr = undefined
200
209
  run.finishedAt = undefined
210
+ // Both belong to the child that ran: a resume that dies before its first turn would
211
+ // otherwise report the previous count, and a clean follow-up to a maxTurns-capped run
212
+ // would still be offered as partial.
213
+ run.turns = 0
214
+ run.partial = undefined
201
215
  driveRun(run, { ...run.spawn, args }, onComplete)
202
216
  return 'resumed'
203
217
  }
204
218
 
205
- /** Re-point --system-prompt at a fresh file when the original is gone. */
206
- function withRebuiltPrompt(spawnSpec: BackgroundSpawn): string[] {
219
+ /** Re-point --system-prompt at a fresh file when the original is gone; `dir` is the
220
+ * temp dir created for it, which the run then owns. */
221
+ function withRebuiltPrompt(spawnSpec: BackgroundSpawn): { args: string[]; dir?: string } {
207
222
  const flag = spawnSpec.args.indexOf('--system-prompt')
208
- if (flag === -1 || !spawnSpec.promptBody) return spawnSpec.args
223
+ if (flag === -1 || !spawnSpec.promptBody) return { args: spawnSpec.args }
209
224
  const current = spawnSpec.args[flag + 1]
210
- if (current && fs.existsSync(current)) return spawnSpec.args
225
+ if (current && fs.existsSync(current)) return { args: spawnSpec.args }
211
226
  try {
212
227
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'pi-subagent-'))
213
228
  const file = path.join(dir, 'prompt.md')
214
229
  fs.writeFileSync(file, spawnSpec.promptBody, { mode: 0o600 })
215
230
  const rebuilt = [...spawnSpec.args]
216
231
  rebuilt[flag + 1] = file
217
- return rebuilt
232
+ return { args: rebuilt, dir }
218
233
  } catch {
219
234
  // Cannot rewrite it: drop the pair rather than hand pi a path it will treat as
220
235
  // prompt text, which would replace the agent persona with a temp path.
221
- return spawnSpec.args.filter((_arg, i) => i !== flag && i !== flag + 1)
236
+ return { args: spawnSpec.args.filter((_arg, i) => i !== flag && i !== flag + 1) }
222
237
  }
223
238
  }
224
239
 
240
+ /** The rebuilt prompt lives as long as its run can be resumed, so it goes when the run
241
+ * leaves the registry: eviction, quit, or the test reset. */
242
+ function removeRebuiltPrompt(run: BackgroundRun): void {
243
+ if (!run.rebuiltPromptDir) return
244
+ fs.rmSync(run.rebuiltPromptDir, { recursive: true, force: true })
245
+ run.rebuiltPromptDir = undefined
246
+ }
247
+
225
248
  export function startBackgroundRun(agent: string, task: string, invocation: BackgroundSpawn, onComplete: (run: BackgroundRun) => void, presetId?: string): string | null {
226
249
  // Checked here, synchronously with registration: callers await temp-file writes
227
250
  // between any check of their own and this call, so a parallel tool-call batch
@@ -387,8 +387,12 @@ export default function todoExtension(pi: ExtensionAPI) {
387
387
  content: [{ type: 'text' as const, text }],
388
388
  details,
389
389
  })
390
- const ok = (action: TodoAction, text: string) => toolMessage(text, { action, todos: [...todos], nextId })
391
- const fail = (action: TodoAction, error: string) => toolMessage(`Error: ${error}`, { action, todos: [...todos], nextId, error })
390
+ // pi keeps a result's details by reference in the live session, and start/complete
391
+ // mutate the todo objects in place, so each result carries its own copies: without
392
+ // them a rewind replays, and the TUI re-renders, today's statuses on earlier calls.
393
+ const snapshot = () => todos.map((t) => ({ ...t }))
394
+ const ok = (action: TodoAction, text: string) => toolMessage(text, { action, todos: snapshot(), nextId })
395
+ const fail = (action: TodoAction, error: string) => toolMessage(`Error: ${error}`, { action, todos: snapshot(), nextId, error })
392
396
 
393
397
  const handleAdd = (params: TodoParamsType) => {
394
398
  if (!params.text) return fail('add', 'text required for add')
package/extensions/web.ts CHANGED
@@ -13,7 +13,7 @@ import type { Usage } from '@earendil-works/pi-ai'
13
13
  import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
14
14
  import { Type } from 'typebox'
15
15
 
16
- import { htmlToMarkdown } from './internal/html-markdown.js'
16
+ import { htmlToMarkdown, removeTags } from './internal/html-markdown.js'
17
17
  import { completeText } from './internal/model-complete.js'
18
18
  import { capForContext } from './internal/output-guard.js'
19
19
  import { httpFetch } from './internal/web-transport.js'
@@ -40,11 +40,7 @@ export function decodeEntities(text: string): string {
40
40
  }
41
41
 
42
42
  export function stripTags(html: string): string {
43
- // [^<>] rather than [^>]: excluding `<` bounds a failed match at the next tag start
44
- // instead of rescanning to end of input, which is what makes the strip linear.
45
- return decodeEntities(html.replace(/<[^<>]*>/g, ''))
46
- .replace(/\s+/g, ' ')
47
- .trim()
43
+ return decodeEntities(removeTags(html)).replace(/\s+/g, ' ').trim()
48
44
  }
49
45
 
50
46
  /** Resolve DuckDuckGo's redirect links (/l/?uddg=<encoded>) to the target URL. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-code",
3
- "version": "1.0.47",
4
- "description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, and subagents",
3
+ "version": "1.0.49",
4
+ "description": "Claude Code experience for the pi coding agent: reads your .claude config (rules, commands, skills, hooks, output styles, MCP servers, agents) and adds todo, checkpoints, memory, web, subagents, and goals",
5
5
  "keywords": [
6
6
  "pi",
7
7
  "pi-package",