@oh-my-pi/pi-coding-agent 15.12.0 → 15.12.1

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/src/lsp/index.ts CHANGED
@@ -2132,6 +2132,11 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2132
2132
  const position = { line: resolvedLine - 1, character: resolvedCharacter };
2133
2133
 
2134
2134
  let output: string;
2135
+ // Set on bare empty-lookup outcomes (no definition/references/…): the
2136
+ // result carries no information once consumed, so compaction may elide
2137
+ // it. Clean diagnostics runs are NOT useless — they are verification
2138
+ // evidence.
2139
+ let useless = false;
2135
2140
 
2136
2141
  if (needsProjectIndex && !isRustAnalyzerServer) {
2137
2142
  await waitForProjectLoaded(client, signal);
@@ -2157,6 +2162,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2157
2162
 
2158
2163
  if (locations.length === 0) {
2159
2164
  output = "No definition found";
2165
+ useless = true;
2160
2166
  } else {
2161
2167
  const lines = await Promise.all(
2162
2168
  locations.map(location => formatLocationWithContext(location, this.session.cwd)),
@@ -2181,6 +2187,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2181
2187
 
2182
2188
  if (locations.length === 0) {
2183
2189
  output = "No type definition found";
2190
+ useless = true;
2184
2191
  } else {
2185
2192
  const lines = await Promise.all(
2186
2193
  locations.map(location => formatLocationWithContext(location, this.session.cwd)),
@@ -2205,6 +2212,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2205
2212
 
2206
2213
  if (locations.length === 0) {
2207
2214
  output = "No implementation found";
2215
+ useless = true;
2208
2216
  } else {
2209
2217
  const lines = await Promise.all(
2210
2218
  locations.map(location => formatLocationWithContext(location, this.session.cwd)),
@@ -2242,6 +2250,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2242
2250
 
2243
2251
  if (!result || result.length === 0) {
2244
2252
  output = "No references found";
2253
+ useless = true;
2245
2254
  } else {
2246
2255
  const contextualReferences = result.slice(0, REFERENCE_CONTEXT_LIMIT);
2247
2256
  const plainReferences = result.slice(REFERENCE_CONTEXT_LIMIT);
@@ -2381,6 +2390,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2381
2390
 
2382
2391
  if (!result || result.length === 0) {
2383
2392
  output = "No symbols found";
2393
+ useless = true;
2384
2394
  } else {
2385
2395
  const relPath = formatPathRelativeToCwd(targetFile, this.session.cwd);
2386
2396
  if ("selectionRange" in result[0]) {
@@ -2444,6 +2454,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
2444
2454
  return {
2445
2455
  content: [{ type: "text", text: output }],
2446
2456
  details: { serverName, action, success: true, request: params },
2457
+ ...(useless ? { useless: true } : {}),
2447
2458
  };
2448
2459
  } catch (err) {
2449
2460
  if (err instanceof ToolError) throw err;
@@ -6155,7 +6155,13 @@ export class AgentSession {
6155
6155
 
6156
6156
  async #pruneToolOutputs(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
6157
6157
  const branchEntries = this.sessionManager.getBranch();
6158
- const result = pruneToolOutputs(branchEntries, this.#withPlanProtection(DEFAULT_PRUNE_CONFIG));
6158
+ const result = pruneToolOutputs(
6159
+ branchEntries,
6160
+ this.#withPlanProtection({
6161
+ ...DEFAULT_PRUNE_CONFIG,
6162
+ pruneUseless: this.settings.getGroup("compaction").dropUseless,
6163
+ }),
6164
+ );
6159
6165
  if (result.prunedCount === 0) {
6160
6166
  return undefined;
6161
6167
  }
@@ -6169,19 +6175,22 @@ export class AgentSession {
6169
6175
  }
6170
6176
 
6171
6177
  /**
6172
- * Per-turn supersede pass: prune older `read` results that a newer read of
6173
- * the same file has made stale. Cache-aware (only fires when the suffix
6174
- * after a candidate is small or the session has been idle long enough that
6175
- * the provider prompt cache is cold), so it is cheap to run every turn.
6176
- * Gated on the `compaction.supersedeReads` setting.
6178
+ * Per-turn stale-result pass: prune older `read` results that a newer read
6179
+ * of the same file has made stale, plus results their tool flagged
6180
+ * contextually useless. Cache-aware (only fires when the suffix after a
6181
+ * candidate is small or the session has been idle long enough that the
6182
+ * provider prompt cache is cold), so it is cheap to run every turn. Gated
6183
+ * on the `compaction.supersedeReads` and `compaction.dropUseless` settings.
6177
6184
  */
6178
- async #pruneSupersededReads(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
6179
- if (!this.settings.getGroup("compaction").supersedeReads) return undefined;
6185
+ async #pruneStaleToolResults(): Promise<{ prunedCount: number; tokensSaved: number } | undefined> {
6186
+ const { supersedeReads, dropUseless } = this.settings.getGroup("compaction");
6187
+ if (!supersedeReads && !dropUseless) return undefined;
6180
6188
  const branchEntries = this.sessionManager.getBranch();
6181
6189
  const result = pruneSupersededToolResults(
6182
6190
  branchEntries,
6183
6191
  this.#withPlanProtection({
6184
- supersedeKey: readToolSupersedeKey,
6192
+ supersedeKey: supersedeReads ? readToolSupersedeKey : undefined,
6193
+ pruneUseless: dropUseless,
6185
6194
  protectedTools: [...DEFAULT_PRUNE_CONFIG.protectedTools],
6186
6195
  }),
6187
6196
  );
@@ -6861,9 +6870,10 @@ export class AgentSession {
6861
6870
  return false;
6862
6871
  }
6863
6872
 
6864
- // Supersede pass runs every turn, before any threshold gating: it is cheap
6865
- // (bails when no candidate) and independent of the compaction setting.
6866
- const supersedeResult = await this.#pruneSupersededReads();
6873
+ // Stale-result pass runs every turn, before any threshold gating: it is
6874
+ // cheap (bails when no candidate) and independent of the compaction
6875
+ // setting.
6876
+ const supersedeResult = await this.#pruneStaleToolResults();
6867
6877
 
6868
6878
  const compactionSettings = this.settings.getGroup("compaction");
6869
6879
  if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
@@ -221,7 +221,9 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
221
221
  const parseMessage = cappedParseErrors.length
222
222
  ? `\n${formatParseErrors(cappedParseErrors, parseErrorsTotal).join("\n")}`
223
223
  : "";
224
- return toolResult(baseDetails).text(`${noMatchMessage}${parseMessage}`).done();
224
+ // Zero matches is useless even with parse issues: the follow-up
225
+ // call has already corrected course by the time compaction runs.
226
+ return toolResult(baseDetails).text(`${noMatchMessage}${parseMessage}`).useless().done();
225
227
  }
226
228
 
227
229
  const useHashLines = resolveFileDisplayMode(this.session).hashLines;
package/src/tools/find.ts CHANGED
@@ -239,7 +239,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
239
239
  const parts = ["No files found matching pattern"];
240
240
  if (notice) parts.push(notice);
241
241
  if (missingPathsNote) parts.push(missingPathsNote);
242
- return toolResult(details).text(parts.join("\n")).done();
242
+ // Zero results is useless regardless of notices: the follow-up
243
+ // call has already corrected course by the time compaction runs.
244
+ return toolResult(details).text(parts.join("\n")).useless().done();
243
245
  }
244
246
 
245
247
  const listLimit = applyListLimit(files, { limit: effectiveLimit });
package/src/tools/gh.ts CHANGED
@@ -2452,7 +2452,7 @@ function buildTextResult(
2452
2452
  text: string,
2453
2453
  sourceUrl?: string,
2454
2454
  details?: GhToolDetails,
2455
- options?: { artifactId?: string; artifactLabel?: string },
2455
+ options?: { artifactId?: string; artifactLabel?: string; useless?: boolean },
2456
2456
  ): AgentToolResult<GhToolDetails> {
2457
2457
  const builder = toolResult<GhToolDetails>(details).text(
2458
2458
  appendArtifactReference(text, options?.artifactId, options?.artifactLabel ?? "Saved artifact"),
@@ -2460,6 +2460,9 @@ function buildTextResult(
2460
2460
  if (sourceUrl) {
2461
2461
  builder.sourceUrl(sourceUrl);
2462
2462
  }
2463
+ if (options?.useless) {
2464
+ builder.useless();
2465
+ }
2463
2466
  return builder.done();
2464
2467
  }
2465
2468
 
@@ -3405,7 +3408,9 @@ async function executeSearchIssues(
3405
3408
 
3406
3409
  const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
3407
3410
  const items = (response.items ?? []).map(apiIssueToSearchResult);
3408
- return buildTextResult(formatSearchResults("issues", displayQuery, repo, items));
3411
+ return buildTextResult(formatSearchResults("issues", displayQuery, repo, items), undefined, undefined, {
3412
+ useless: items.length === 0,
3413
+ });
3409
3414
  }
3410
3415
 
3411
3416
  async function executeSearchPrs(
@@ -3423,7 +3428,9 @@ async function executeSearchPrs(
3423
3428
 
3424
3429
  const response = await git.github.json<GhApiSearchResponse<GhApiSearchIssueItem>>(session.cwd, args, signal);
3425
3430
  const items = (response.items ?? []).map(apiIssueToSearchResult);
3426
- return buildTextResult(formatSearchResults("pull requests", displayQuery, repo, items));
3431
+ return buildTextResult(formatSearchResults("pull requests", displayQuery, repo, items), undefined, undefined, {
3432
+ useless: items.length === 0,
3433
+ });
3427
3434
  }
3428
3435
 
3429
3436
  async function executeSearchCode(
@@ -3442,7 +3449,9 @@ async function executeSearchCode(
3442
3449
 
3443
3450
  const response = await git.github.json<GhApiSearchResponse<GhApiSearchCodeItem>>(session.cwd, args, signal);
3444
3451
  const items = (response.items ?? []).map(apiCodeToSearchResult);
3445
- return buildTextResult(formatSearchCodeResults(query, repo, items));
3452
+ return buildTextResult(formatSearchCodeResults(query, repo, items), undefined, undefined, {
3453
+ useless: items.length === 0,
3454
+ });
3446
3455
  }
3447
3456
 
3448
3457
  async function executeSearchCommits(
@@ -3460,7 +3469,9 @@ async function executeSearchCommits(
3460
3469
 
3461
3470
  const response = await git.github.json<GhApiSearchResponse<GhApiSearchCommitItem>>(session.cwd, args, signal);
3462
3471
  const items = (response.items ?? []).map(apiCommitToSearchResult);
3463
- return buildTextResult(formatSearchCommitsResults(displayQuery, repo, items));
3472
+ return buildTextResult(formatSearchCommitsResults(displayQuery, repo, items), undefined, undefined, {
3473
+ useless: items.length === 0,
3474
+ });
3464
3475
  }
3465
3476
 
3466
3477
  async function executeSearchRepos(
@@ -3476,7 +3487,9 @@ async function executeSearchRepos(
3476
3487
 
3477
3488
  const response = await git.github.json<GhApiSearchResponse<GhApiSearchRepoItem>>(session.cwd, args, signal);
3478
3489
  const items = (response.items ?? []).map(apiRepoToSearchResult);
3479
- return buildTextResult(formatSearchReposResults(query, items));
3490
+ return buildTextResult(formatSearchReposResults(query, items), undefined, undefined, {
3491
+ useless: items.length === 0,
3492
+ });
3480
3493
  }
3481
3494
 
3482
3495
  async function executeRunWatch(
@@ -3751,6 +3764,7 @@ async function executeRunWatch(
3751
3764
  `No workflow runs found for ${repo}@${formatShortSha(headSha) ?? headSha} after ${elapsedSec}s (${pollCount} polls). The commit may not trigger any GitHub Actions workflows, or Actions may be disabled for this repository. Pass \`run\` to watch a specific run.`,
3752
3765
  undefined,
3753
3766
  buildCommitRunWatchDetails(repo, headSha, branch, runs, { state: "completed", pollCount }),
3767
+ { useless: true },
3754
3768
  );
3755
3769
  }
3756
3770
  await scheduler.wait(currentIntervalSeconds() * 1000, { signal });
package/src/tools/irc.ts CHANGED
@@ -310,6 +310,8 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
310
310
  return {
311
311
  content: [{ type: "text", text: `No message${filterNote} within ${formatDuration(timeoutMs)}.` }],
312
312
  details: { op: "wait", from: senderId, waited: null },
313
+ // A clean wait timeout carries no information once consumed.
314
+ useless: true,
313
315
  };
314
316
  }
315
317
  return {
@@ -324,6 +326,8 @@ export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
324
326
  return {
325
327
  content: [{ type: "text", text: "Inbox empty." }],
326
328
  details: { op: "inbox", from: senderId, inbox: [] },
329
+ // An empty inbox drain carries no information once consumed.
330
+ useless: true,
327
331
  };
328
332
  }
329
333
  const header = params.peek ? `${messages.length} unread message(s):` : `${messages.length} message(s):`;
package/src/tools/job.ts CHANGED
@@ -171,6 +171,9 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
171
171
  return {
172
172
  content: [{ type: "text", text: message }],
173
173
  details: { jobs: [] },
174
+ // Nothing found / nothing to wait for is noise once consumed —
175
+ // the follow-up call has already corrected course.
176
+ useless: true,
174
177
  };
175
178
  }
176
179
 
@@ -334,12 +337,17 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
334
337
  }
335
338
  }
336
339
 
340
+ const details: JobToolDetails = {
341
+ jobs: jobResults,
342
+ ...(cancelOutcomes.length ? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) } : {}),
343
+ };
337
344
  return {
338
345
  content: [{ type: "text", text: lines.join("\n").trimEnd() }],
339
- details: {
340
- jobs: jobResults,
341
- ...(cancelOutcomes.length ? { cancelled: cancelOutcomes.map(({ id, status }) => ({ id, status })) } : {}),
342
- },
346
+ details,
347
+ // A poll where everything is still running carries no new information
348
+ // once a later poll exists same predicate the TUI uses to displace
349
+ // stale waiting frames.
350
+ ...(isWaitingPollDetails(details) ? { useless: true } : {}),
343
351
  };
344
352
  }
345
353
  }
@@ -43,6 +43,7 @@ export class MemoryRecallTool implements AgentTool<typeof memoryRecallSchema> {
43
43
  return {
44
44
  content: [{ type: "text", text: "No relevant memories found." }],
45
45
  details: {},
46
+ useless: true,
46
47
  };
47
48
  }
48
49
  const formatted = state.formatScopedRecallWithIds(results);
@@ -79,6 +80,7 @@ export class MemoryRecallTool implements AgentTool<typeof memoryRecallSchema> {
79
80
  return {
80
81
  content: [{ type: "text", text: "No relevant memories found." }],
81
82
  details: {},
83
+ useless: true,
82
84
  };
83
85
  }
84
86
  const formatted = formatMemories(results);
@@ -1124,7 +1124,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
1124
1124
  ? `No more results (${totalFilesLabel} files total; skip=${normalizedSkip} is past the end)`
1125
1125
  : "No matches found";
1126
1126
  const text = warningNote ? `${noMatchText}\n${warningNote}` : noMatchText;
1127
- return toolResult(details).text(text).done();
1127
+ // Zero matches is useless regardless of warnings: by the time
1128
+ // compaction runs, the follow-up call has already corrected course.
1129
+ return toolResult(details).text(text).useless().done();
1128
1130
  }
1129
1131
  const outputLines: string[] = [];
1130
1132
  let linesTruncated = false;
@@ -13,6 +13,7 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
13
13
  #meta = outputMeta();
14
14
  #content: ToolContent = [];
15
15
  #isError = false;
16
+ #useless = false;
16
17
 
17
18
  constructor(details?: TDetails) {
18
19
  this.#details = details ?? ({} as TDetails);
@@ -74,6 +75,12 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
74
75
  return this;
75
76
  }
76
77
 
78
+ /** Marks the result contextually useless — compaction may elide it once consumed. */
79
+ useless(value = true): this {
80
+ this.#useless = value;
81
+ return this;
82
+ }
83
+
77
84
  done(): AgentToolResult<TDetails> {
78
85
  const meta = this.#meta.get();
79
86
  if (meta) {
@@ -85,6 +92,7 @@ export class ToolResultBuilder<TDetails extends DetailsWithMeta> {
85
92
  content: this.#content,
86
93
  details: hasDetails ? this.#details : undefined,
87
94
  ...(this.#isError ? { isError: true } : {}),
95
+ ...(this.#useless && !this.#isError ? { useless: true } : {}),
88
96
  };
89
97
  }
90
98
  }