pi-shadow-mind 0.1.18 → 0.1.20

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/dist/index.js CHANGED
@@ -6,11 +6,8 @@ var __export = (target, all) => {
6
6
 
7
7
  // src/runtime.ts
8
8
  import { randomUUID as randomUUID3 } from "node:crypto";
9
- import {
10
- buildSessionContext
11
- } from "@earendil-works/pi-coding-agent";
12
9
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
13
- import { Text } from "@earendil-works/pi-tui";
10
+ import { Text as Text2 } from "@earendil-works/pi-tui";
14
11
 
15
12
  // src/final-response-queue.ts
16
13
  var FinalResponseQueue = class {
@@ -145,6 +142,12 @@ function isNonEmptyString(value) {
145
142
  function isStringArray(value) {
146
143
  return Array.isArray(value) && value.every((item) => isNonEmptyString(item));
147
144
  }
145
+ function cleanStringArray(value, fallback, name) {
146
+ if (value === void 0) return [...fallback];
147
+ if (!isStringArray(value))
148
+ throw new Error(`${name} must be an array of non-empty strings`);
149
+ return [...new Set(value.map((item) => item.trim()))];
150
+ }
148
151
  function isThinkingLevel(value) {
149
152
  return typeof value === "string" && THINKING_LEVELS.has(value);
150
153
  }
@@ -152,6 +155,7 @@ function isThinkingLevel(value) {
152
155
  // src/config.ts
153
156
  var DEFAULT_CONFIG = {
154
157
  heartbeatProbability: 1 / 3,
158
+ heartbeatTools: [],
155
159
  maxParallelShadows: 2,
156
160
  defaultShadowTimeoutSeconds: 300,
157
161
  headlessDrainTimeoutSeconds: 120,
@@ -164,6 +168,7 @@ function parseConfig(input) {
164
168
  }
165
169
  const value = input;
166
170
  const probability = numberInRange(value.heartbeat_probability, 0, 1, DEFAULT_CONFIG.heartbeatProbability, "heartbeat_probability");
171
+ const heartbeatTools = cleanStringArray(value.heartbeat_tools, DEFAULT_CONFIG.heartbeatTools, "heartbeat_tools");
167
172
  const parallel = positiveInteger(value.max_parallel_shadows, DEFAULT_CONFIG.maxParallelShadows, "max_parallel_shadows");
168
173
  const timeout = positiveNumber(value.default_shadow_timeout_seconds, DEFAULT_CONFIG.defaultShadowTimeoutSeconds, "default_shadow_timeout_seconds");
169
174
  const drainTimeout = positiveNumber(value.headless_drain_timeout_seconds, DEFAULT_CONFIG.headlessDrainTimeoutSeconds, "headless_drain_timeout_seconds");
@@ -176,6 +181,7 @@ function parseConfig(input) {
176
181
  }
177
182
  return {
178
183
  heartbeatProbability: probability,
184
+ heartbeatTools,
179
185
  maxParallelShadows: parallel,
180
186
  defaultShadowTimeoutSeconds: timeout,
181
187
  headlessDrainTimeoutSeconds: drainTimeout,
@@ -188,6 +194,7 @@ function parseConfig(input) {
188
194
  function serializeConfig(config) {
189
195
  return `${JSON.stringify({
190
196
  heartbeat_probability: config.heartbeatProbability,
197
+ ...config.heartbeatTools && config.heartbeatTools.length ? { heartbeat_tools: config.heartbeatTools } : {},
191
198
  max_parallel_shadows: config.maxParallelShadows,
192
199
  default_shadow_timeout_seconds: config.defaultShadowTimeoutSeconds,
193
200
  headless_drain_timeout_seconds: config.headlessDrainTimeoutSeconds,
@@ -6968,7 +6975,7 @@ function parseShadowMarkdown(source, filePath) {
6968
6975
  "activation_probability"
6969
6976
  ),
6970
6977
  trigger: triggerArray(value.trigger, ["heartbeat"], "trigger"),
6971
- activeForModels: stringArray(
6978
+ activeForModels: cleanStringArray(
6972
6979
  value.active_for_models,
6973
6980
  ["*"],
6974
6981
  "active_for_models"
@@ -6979,7 +6986,12 @@ function parseShadowMarkdown(source, filePath) {
6979
6986
  value.timeout_seconds,
6980
6987
  "timeout_seconds"
6981
6988
  ),
6982
- tools: stringArray(value.tools, [], "tools"),
6989
+ activationTools: cleanStringArray(
6990
+ value.activation_tools,
6991
+ [],
6992
+ "activation_tools"
6993
+ ),
6994
+ tools: cleanStringArray(value.tools, [], "tools"),
6983
6995
  prompt,
6984
6996
  filePath
6985
6997
  };
@@ -7010,15 +7022,9 @@ function optionalPositiveNumber(value, name) {
7010
7022
  throw new Error(`${name} must be positive`);
7011
7023
  return value;
7012
7024
  }
7013
- function stringArray(value, fallback, name) {
7014
- if (value === void 0) return [...fallback];
7015
- if (!isStringArray(value))
7016
- throw new Error(`${name} must be an array of non-empty strings`);
7017
- return [...new Set(value.map((item) => item.trim()))];
7018
- }
7019
7025
  function triggerArray(value, fallback, name) {
7020
7026
  const values = typeof value === "string" ? [value] : value;
7021
- const items = stringArray(values, fallback, name);
7027
+ const items = cleanStringArray(values, fallback, name);
7022
7028
  const allowed = new Set(SHADOW_TRIGGERS);
7023
7029
  const invalid = items.filter((item) => !allowed.has(item));
7024
7030
  if (invalid.length)
@@ -7104,6 +7110,7 @@ function serializeShadow(shadow) {
7104
7110
  ...shadow.runWithModel ? { run_with_model: shadow.runWithModel } : {},
7105
7111
  ...shadow.thinkingLevel ? { thinking_level: shadow.thinkingLevel } : {},
7106
7112
  ...shadow.timeoutSeconds !== void 0 ? { timeout_seconds: shadow.timeoutSeconds } : {},
7113
+ ...shadow.activationTools && shadow.activationTools.length ? { activation_tools: shadow.activationTools } : {},
7107
7114
  tools: shadow.tools ?? []
7108
7115
  };
7109
7116
  return `---
@@ -7114,7 +7121,8 @@ ${(shadow.prompt ?? "").trim()}
7114
7121
  `;
7115
7122
  }
7116
7123
  function describeShadow(shadow) {
7117
- return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
7124
+ const actTools = shadow.activationTools.length ? ` act_tools=${shadow.activationTools.join(",")}` : "";
7125
+ return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")}${actTools} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
7118
7126
  }
7119
7127
  function definedOnly(value) {
7120
7128
  return Object.fromEntries(
@@ -7149,6 +7157,7 @@ var SHADOW_FIELDS = {
7149
7157
  run_with_model: Type.Optional(Type.String()),
7150
7158
  thinking_level: Type.Optional(THINKING),
7151
7159
  timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
7160
+ activation_tools: Type.Optional(Type.Array(Type.String())),
7152
7161
  tools: Type.Optional(Type.Array(Type.String())),
7153
7162
  prompt: Type.Optional(Type.String())
7154
7163
  };
@@ -7270,6 +7279,7 @@ function configWriteTool(store, getConfig) {
7270
7279
  heartbeat_probability: Type.Optional(
7271
7280
  Type.Number({ minimum: 0, maximum: 1 })
7272
7281
  ),
7282
+ heartbeat_tools: Type.Optional(Type.Array(Type.String())),
7273
7283
  max_parallel_shadows: Type.Optional(Type.Integer({ minimum: 1 })),
7274
7284
  default_shadow_timeout_seconds: Type.Optional(
7275
7285
  Type.Number({ exclusiveMinimum: 0 })
@@ -7289,6 +7299,7 @@ function configWriteTool(store, getConfig) {
7289
7299
  const raw = params;
7290
7300
  const next = parseConfig({
7291
7301
  heartbeat_probability: raw.heartbeat_probability ?? current.heartbeatProbability,
7302
+ heartbeat_tools: raw.heartbeat_tools ?? current.heartbeatTools,
7292
7303
  max_parallel_shadows: raw.max_parallel_shadows ?? current.maxParallelShadows,
7293
7304
  default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
7294
7305
  headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
@@ -7336,6 +7347,7 @@ function toPatch(raw) {
7336
7347
  runWithModel: raw.run_with_model,
7337
7348
  thinkingLevel: raw.thinking_level,
7338
7349
  timeoutSeconds: raw.timeout_seconds,
7350
+ activationTools: raw.activation_tools,
7339
7351
  tools: raw.tools,
7340
7352
  prompt: raw.prompt
7341
7353
  };
@@ -7385,6 +7397,147 @@ function formatReportBatch(reports) {
7385
7397
  ${report.content}`).join("\n\n");
7386
7398
  }
7387
7399
 
7400
+ // src/report-history.ts
7401
+ var ReportHistory = class {
7402
+ constructor(limit = 5) {
7403
+ this.limit = limit;
7404
+ if (!Number.isInteger(limit) || limit < 0) throw new Error("Report limit must be a non-negative integer");
7405
+ }
7406
+ reports = [];
7407
+ add(reports, deliveredAt = (/* @__PURE__ */ new Date()).toISOString()) {
7408
+ for (const report of reports) {
7409
+ this.reports = this.reports.filter(({ runId }) => runId !== report.runId);
7410
+ this.reports.push({ ...report, deliveredAt });
7411
+ if (this.reports.length > this.limit) this.reports.shift();
7412
+ }
7413
+ }
7414
+ /** Newest delivery first; callers receive an isolated snapshot. */
7415
+ list() {
7416
+ return this.reports.map((report) => ({ ...report })).reverse();
7417
+ }
7418
+ clear() {
7419
+ this.reports = [];
7420
+ }
7421
+ };
7422
+
7423
+ // src/report-viewer.ts
7424
+ import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
7425
+ var ReportViewer = class {
7426
+ constructor(reports, theme, host) {
7427
+ this.theme = theme;
7428
+ this.host = host;
7429
+ this.body = new Text(reports.map(
7430
+ (report) => `${report.shadowName} \xB7 ${report.deliveredAt}
7431
+ run ${report.runId}
7432
+ ${report.content.replace(/\r\n?/g, "\n")}`
7433
+ ).join("\n\n"), 0, 0);
7434
+ this.actions = [
7435
+ ["escape", () => host.close()],
7436
+ ["up", () => this.scroll(-1)],
7437
+ ["down", () => this.scroll(1)],
7438
+ ["pageUp", () => this.scroll(-this.pageSize)],
7439
+ ["pageDown", () => this.scroll(this.pageSize)],
7440
+ ["home", () => {
7441
+ this.offset = 0;
7442
+ }],
7443
+ ["end", () => {
7444
+ this.offset = this.maxOffset();
7445
+ }]
7446
+ ];
7447
+ }
7448
+ body;
7449
+ offset = 0;
7450
+ contentHeight = 0;
7451
+ pageSize = 1;
7452
+ actions;
7453
+ render(width) {
7454
+ const height = Math.max(1, Math.min(Math.floor(this.host.rows() * 0.8), this.host.rows() - 2));
7455
+ this.pageSize = Math.max(1, height - 2);
7456
+ const lines = this.body.render(Math.max(1, width));
7457
+ this.contentHeight = lines.length;
7458
+ this.offset = Math.min(this.offset, this.maxOffset());
7459
+ const page = lines.slice(this.offset, this.offset + this.pageSize);
7460
+ if (height < 3) return page.slice(0, height).map((line) => truncateToWidth(line, width));
7461
+ return [
7462
+ this.theme.fg("accent", truncateToWidth("Recent Shadow reports \xB7 newest first", width)),
7463
+ ...page,
7464
+ this.theme.fg("dim", truncateToWidth(
7465
+ `Esc close \xB7 \u2191\u2193 PgUp/PgDn Home/End \xB7 ${this.offset + 1}\u2013${Math.min(this.offset + this.pageSize, lines.length)}/${lines.length}`,
7466
+ width
7467
+ ))
7468
+ ];
7469
+ }
7470
+ handleInput(data) {
7471
+ const action = this.actions.find(([key]) => matchesKey(data, key));
7472
+ if (!action) return;
7473
+ action[1]();
7474
+ this.host.requestRender();
7475
+ }
7476
+ invalidate() {
7477
+ this.body.invalidate();
7478
+ }
7479
+ maxOffset() {
7480
+ return Math.max(0, this.contentHeight - this.pageSize);
7481
+ }
7482
+ scroll(lines) {
7483
+ this.offset = Math.max(0, Math.min(this.maxOffset(), this.offset + lines));
7484
+ }
7485
+ };
7486
+
7487
+ // src/report-browser.ts
7488
+ var ReportBrowser = class {
7489
+ history = new ReportHistory();
7490
+ closeViewer;
7491
+ add(reports) {
7492
+ this.history.add(reports);
7493
+ }
7494
+ reset() {
7495
+ this.closeViewer?.();
7496
+ this.history.clear();
7497
+ }
7498
+ async handleCommand(command, ctx) {
7499
+ const [name, ...args] = command.split(/\s+/);
7500
+ if (name !== "reports") return false;
7501
+ if (args.length && args.join(" ") !== "hide") {
7502
+ ctx.ui.notify("Usage: /shadow reports [hide]", "warning");
7503
+ return true;
7504
+ }
7505
+ if (args[0] === "hide" || this.closeViewer) {
7506
+ this.closeViewer?.();
7507
+ return true;
7508
+ }
7509
+ if (ctx.mode !== "tui") {
7510
+ if (ctx.hasUI) ctx.ui.notify("The report viewer requires TUI mode. Delivered reports remain in the conversation.", "warning");
7511
+ return true;
7512
+ }
7513
+ const reports = this.history.list();
7514
+ if (!reports.length) {
7515
+ ctx.ui.notify("No reports delivered in this session yet.", "info");
7516
+ return true;
7517
+ }
7518
+ const view = { close: () => {
7519
+ } };
7520
+ try {
7521
+ await ctx.ui.custom((tui, theme, _keys, done) => {
7522
+ view.close = () => {
7523
+ if (this.closeViewer !== view.close) return;
7524
+ this.closeViewer = void 0;
7525
+ done();
7526
+ };
7527
+ this.closeViewer = view.close;
7528
+ return new ReportViewer(reports, theme, {
7529
+ rows: () => tui.terminal.rows,
7530
+ requestRender: () => tui.requestRender(),
7531
+ close: view.close
7532
+ });
7533
+ }, { overlay: true, overlayOptions: { width: "90%", maxHeight: "80%", margin: 1 } });
7534
+ } finally {
7535
+ if (this.closeViewer === view.close) this.closeViewer = void 0;
7536
+ }
7537
+ return true;
7538
+ }
7539
+ };
7540
+
7388
7541
  // src/random.ts
7389
7542
  function createRandom(seed) {
7390
7543
  if (seed === void 0) return Math.random;
@@ -7398,9 +7551,143 @@ function createRandom(seed) {
7398
7551
  };
7399
7552
  }
7400
7553
 
7554
+ // src/acp-projection.ts
7555
+ import {
7556
+ buildContextEntries,
7557
+ buildSessionContext,
7558
+ sessionEntryToContextMessages
7559
+ } from "@earendil-works/pi-coding-agent";
7560
+
7561
+ // src/acp-state.ts
7562
+ import { readFileSync } from "node:fs";
7563
+ function isRecord(value) {
7564
+ return value !== null && typeof value === "object" && !Array.isArray(value);
7565
+ }
7566
+ function readAcpBlocks(sessionFile) {
7567
+ if (!sessionFile) return [];
7568
+ try {
7569
+ const state = JSON.parse(readFileSync(`${sessionFile}.acp.json`, "utf8"));
7570
+ if (!isRecord(state) || !Array.isArray(state.blocks)) return [];
7571
+ const blocks = [];
7572
+ const seen = /* @__PURE__ */ new Set();
7573
+ for (const block of state.blocks) {
7574
+ if (!isRecord(block)) return [];
7575
+ if (block.active === false) continue;
7576
+ if (block.active !== true || typeof block.blockId !== "string" || !block.blockId || typeof block.summary !== "string" || !block.summary.trim() || !Array.isArray(block.effectiveMessageIds) || !block.effectiveMessageIds.every((id) => typeof id === "string" && id.length > 0) || seen.has(block.blockId)) return [];
7577
+ seen.add(block.blockId);
7578
+ blocks.push({
7579
+ blockId: block.blockId,
7580
+ summary: block.summary,
7581
+ effectiveMessageIds: [...block.effectiveMessageIds]
7582
+ });
7583
+ }
7584
+ return blocks;
7585
+ } catch {
7586
+ return [];
7587
+ }
7588
+ }
7589
+
7590
+ // src/acp-projection.ts
7591
+ function buildShadowSessionContext(entries, leafId, sessionFile) {
7592
+ const context = buildSessionContext(entries, leafId);
7593
+ const blocks = readAcpBlocks(sessionFile);
7594
+ if (!blocks.length) return context;
7595
+ const visible = buildContextEntries(entries, leafId).flatMap(
7596
+ (entry) => sessionEntryToContextMessages(entry).map((message) => ({ id: entry.id, message }))
7597
+ );
7598
+ return { ...context, messages: projectMessages(visible, blocks) };
7599
+ }
7600
+ function toolCalls(message) {
7601
+ return message.role === "assistant" ? message.content.filter((part) => part.type === "toolCall") : [];
7602
+ }
7603
+ function coverageIds({ id, message }) {
7604
+ const calls = toolCalls(message);
7605
+ return calls.length > 1 ? calls.map((call) => `${id}#${call.id}`) : [id];
7606
+ }
7607
+ function summaryMessage(block) {
7608
+ return {
7609
+ role: "compactionSummary",
7610
+ summary: `[ACP block ${block.blockId} \u2014 ${block.effectiveMessageIds.length} folded messages]
7611
+ ${block.summary}`,
7612
+ tokensBefore: 0,
7613
+ timestamp: 0
7614
+ };
7615
+ }
7616
+ function projectMessages(visible, blocks) {
7617
+ const indexById = /* @__PURE__ */ new Map();
7618
+ visible.forEach((item, index) => {
7619
+ for (const id of coverageIds(item)) indexById.set(id, index);
7620
+ });
7621
+ const covered = /* @__PURE__ */ new Set();
7622
+ const anchors = /* @__PURE__ */ new Map();
7623
+ for (const block of blocks) {
7624
+ const positions = block.effectiveMessageIds.flatMap((id) => {
7625
+ const index = indexById.get(id);
7626
+ return index === void 0 ? [] : [index];
7627
+ });
7628
+ if (!positions.length) continue;
7629
+ const first = positions.reduce((left, right) => Math.min(left, right));
7630
+ anchors.set(first, [...anchors.get(first) ?? [], block]);
7631
+ for (const id of block.effectiveMessageIds) covered.add(id);
7632
+ }
7633
+ if (!covered.size) return visible.map(({ message }) => message);
7634
+ const firstUser = visible.findIndex(
7635
+ ({ message }) => message.role === "user" || message.role === "custom"
7636
+ );
7637
+ const messages = [];
7638
+ visible.forEach((item, index) => {
7639
+ for (const block of anchors.get(index) ?? []) messages.push(summaryMessage(block));
7640
+ const retained = index === firstUser ? item.message : retainUncovered(item, covered);
7641
+ if (retained) messages.push(retained);
7642
+ });
7643
+ return removeOrphanedTools(messages);
7644
+ }
7645
+ function retainUncovered(item, covered) {
7646
+ const ids = coverageIds(item);
7647
+ if (!ids.some((id) => covered.has(id))) return item.message;
7648
+ if (ids.every((id) => covered.has(id))) return void 0;
7649
+ const message = item.message;
7650
+ if (message.role !== "assistant") return message;
7651
+ return {
7652
+ ...message,
7653
+ content: message.content.filter(
7654
+ (part) => part.type !== "toolCall" || !covered.has(`${item.id}#${part.id}`)
7655
+ )
7656
+ };
7657
+ }
7658
+ function removeOrphanedTools(messages) {
7659
+ const resultIds = new Set(messages.flatMap(
7660
+ (message) => message.role === "toolResult" ? [message.toolCallId] : []
7661
+ ));
7662
+ const paired = messages.flatMap((message) => {
7663
+ const calls = toolCalls(message);
7664
+ if (!calls.length || message.role !== "assistant") return [message];
7665
+ const retained = calls.filter((call) => call.name === "compress" || resultIds.has(call.id));
7666
+ if (!retained.length) return [];
7667
+ const ids = new Set(retained.map((call) => call.id));
7668
+ return [{ ...message, content: message.content.filter((part) => part.type !== "toolCall" || ids.has(part.id)) }];
7669
+ });
7670
+ const callIds = new Set(paired.flatMap((message) => toolCalls(message).map((call) => call.id)));
7671
+ return paired.filter((message) => message.role !== "toolResult" || callIds.has(message.toolCallId));
7672
+ }
7673
+
7401
7674
  // src/scheduler.ts
7402
- function shouldEvaluateHeartbeat(toolResults) {
7403
- return toolResults.length > 0;
7675
+ function extractToolNames(toolResults) {
7676
+ const names = /* @__PURE__ */ new Set();
7677
+ for (const item of toolResults) {
7678
+ if (!item || typeof item !== "object") continue;
7679
+ const name = item.toolName;
7680
+ if (typeof name === "string" && name.trim().length > 0) {
7681
+ names.add(name.trim());
7682
+ }
7683
+ }
7684
+ return names;
7685
+ }
7686
+ function shouldEvaluateHeartbeat(toolsOrResults, heartbeatTools) {
7687
+ const toolNames = toolsOrResults instanceof Set ? toolsOrResults : extractToolNames(toolsOrResults);
7688
+ if (toolNames.size === 0) return false;
7689
+ if (!heartbeatTools || heartbeatTools.length === 0) return true;
7690
+ return heartbeatTools.some((tool2) => toolNames.has(tool2));
7404
7691
  }
7405
7692
  function shouldEvaluateFinalResponse(messages) {
7406
7693
  const message = messages.at(-1);
@@ -7423,11 +7710,13 @@ function decideHeartbeat(options) {
7423
7710
  activated: [],
7424
7711
  candidates: [],
7425
7712
  modelFiltered: [],
7426
- runningExcluded: []
7713
+ runningExcluded: [],
7714
+ toolFiltered: []
7427
7715
  };
7428
7716
  }
7429
7717
  const modelFiltered = [];
7430
7718
  const runningExcluded = [];
7719
+ const toolFiltered = [];
7431
7720
  const rolls = options.shadows.filter((shadow) => {
7432
7721
  if (!shadow.enabled || !hasTrigger(shadow, "heartbeat")) return false;
7433
7722
  if (options.activeShadowIds.has(shadow.id)) {
@@ -7438,6 +7727,10 @@ function decideHeartbeat(options) {
7438
7727
  modelFiltered.push(shadow.id);
7439
7728
  return false;
7440
7729
  }
7730
+ if (!matchesActivationTools(shadow, options.executedTools)) {
7731
+ toolFiltered.push(shadow.id);
7732
+ return false;
7733
+ }
7441
7734
  return true;
7442
7735
  }).map((shadow) => ({ shadow, roll: random() }));
7443
7736
  const hits = rolls.filter(
@@ -7458,7 +7751,8 @@ function decideHeartbeat(options) {
7458
7751
  selected: selectedIds.has(shadow.id)
7459
7752
  })),
7460
7753
  modelFiltered,
7461
- runningExcluded
7754
+ runningExcluded,
7755
+ toolFiltered
7462
7756
  };
7463
7757
  }
7464
7758
  function decideFinalResponse(options) {
@@ -7486,6 +7780,14 @@ function decideFinalResponse(options) {
7486
7780
  function matchesModel(shadow, fullModelId) {
7487
7781
  return shadow.activeForModels.includes("*") || shadow.activeForModels.includes(fullModelId);
7488
7782
  }
7783
+ function matchesActivationTools(shadow, executedTools) {
7784
+ if (shadow.activationTools.length === 0) {
7785
+ return true;
7786
+ }
7787
+ if (!executedTools) return false;
7788
+ const executedSet = executedTools instanceof Set ? executedTools : new Set(executedTools);
7789
+ return shadow.activationTools.some((tool2) => executedSet.has(tool2));
7790
+ }
7489
7791
  function hasTrigger(shadow, trigger) {
7490
7792
  return shadow.trigger.includes(trigger);
7491
7793
  }
@@ -7716,7 +8018,7 @@ function serializeTrajectory(messages) {
7716
8018
  continue;
7717
8019
  }
7718
8020
  if (message.role === "compactionSummary" || message.role === "branchSummary") {
7719
- appendText(lines, "SUMMARY", message.content);
8021
+ appendText(lines, "SUMMARY", message.summary ?? message.content);
7720
8022
  }
7721
8023
  }
7722
8024
  return `<main-agent-trajectory>
@@ -8390,6 +8692,7 @@ var ShadowMindRuntime = class {
8390
8692
  });
8391
8693
  recentEvents = [];
8392
8694
  recentRuns = [];
8695
+ reports = new ReportBrowser();
8393
8696
  batcher;
8394
8697
  sessionLifetime = new SessionLifetime();
8395
8698
  epoch = 0;
@@ -8419,6 +8722,7 @@ var ShadowMindRuntime = class {
8419
8722
  this.completedWithFinalText = false;
8420
8723
  this.sessionUsage = zeroUsage();
8421
8724
  this.recentRuns.length = 0;
8725
+ this.reports.reset();
8422
8726
  await this.configStore.initialize();
8423
8727
  await this.usageStore.initialize();
8424
8728
  this.random = createRandom(this.configStore.current.randomSeed);
@@ -8442,14 +8746,16 @@ var ShadowMindRuntime = class {
8442
8746
  });
8443
8747
  this.pi.on("turn_end", async (event, ctx) => {
8444
8748
  this.latestContext = ctx;
8445
- if (!shouldEvaluateHeartbeat(event.toolResults)) {
8749
+ const toolResults = event.toolResults ?? [];
8750
+ const executedTools = extractToolNames(toolResults);
8751
+ if (executedTools.size === 0) {
8446
8752
  this.record("heartbeat-skipped", {
8447
8753
  reason: "no-tool-activity",
8448
8754
  modelCalls: this.modelCalls
8449
8755
  });
8450
8756
  return;
8451
8757
  }
8452
- await this.onHeartbeat(ctx);
8758
+ await this.onHeartbeat(ctx, executedTools);
8453
8759
  });
8454
8760
  this.pi.on("agent_end", (event, ctx) => {
8455
8761
  this.latestContext = ctx;
@@ -8481,6 +8787,7 @@ var ShadowMindRuntime = class {
8481
8787
  if (!result.settled) this.active.clear();
8482
8788
  }
8483
8789
  await this.usageStore.flush();
8790
+ this.reports.reset();
8484
8791
  ctx.ui.setStatus("shadow-mind", void 0);
8485
8792
  ctx.ui.setWidget("shadow-mind-panel", void 0);
8486
8793
  this.sessionLifetime.deactivate();
@@ -8504,6 +8811,7 @@ var ShadowMindRuntime = class {
8504
8811
  this.setPaused(!this.paused, ctx);
8505
8812
  return;
8506
8813
  }
8814
+ if (await this.reports.handleCommand(command, ctx)) return;
8507
8815
  if (command === "status") {
8508
8816
  await this.refresh(ctx);
8509
8817
  ctx.ui.notify(
@@ -8520,24 +8828,34 @@ var ShadowMindRuntime = class {
8520
8828
  this.updateStatus(ctx);
8521
8829
  }
8522
8830
  });
8523
- this.pi.registerShortcut("alt+s", {
8524
- description: "Pause or resume Shadow Mind",
8525
- handler: (ctx) => {
8526
- this.latestContext = ctx;
8527
- this.setPaused(!this.paused, ctx);
8528
- }
8529
- });
8831
+ for (const shortcut of ["f6", "alt+s"]) {
8832
+ this.pi.registerShortcut(shortcut, {
8833
+ description: "Pause or resume Shadow Mind",
8834
+ handler: (ctx) => {
8835
+ this.latestContext = ctx;
8836
+ this.setPaused(!this.paused, ctx);
8837
+ }
8838
+ });
8839
+ }
8530
8840
  this.pi.registerMessageRenderer(
8531
8841
  "shadow-report",
8532
8842
  (message, _options, theme) => {
8533
8843
  const content = typeof message.content === "string" ? message.content : "Shadow report";
8534
8844
  const prefix = theme.fg("accent", "\u{1F419} shadow \xB7 ");
8535
- return new Text(`${prefix}${content}`, 0, 0);
8845
+ return new Text2(`${prefix}${content}`, 0, 0);
8536
8846
  }
8537
8847
  );
8538
8848
  }
8539
- async onHeartbeat(ctx) {
8849
+ async onHeartbeat(ctx, executedTools) {
8540
8850
  const snapshot = await this.refresh(ctx);
8851
+ const config = this.configStore.current;
8852
+ if (!shouldEvaluateHeartbeat(executedTools, config.heartbeatTools)) {
8853
+ this.record("heartbeat-skipped", {
8854
+ reason: "tool-filtered",
8855
+ modelCalls: this.modelCalls
8856
+ });
8857
+ return;
8858
+ }
8541
8859
  if (this.paused || !ctx.model) {
8542
8860
  this.record("heartbeat-skipped", {
8543
8861
  reason: this.paused ? "paused" : "no-model",
@@ -8547,17 +8865,18 @@ var ShadowMindRuntime = class {
8547
8865
  }
8548
8866
  const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
8549
8867
  const decision = decideHeartbeat({
8550
- heartbeatProbability: this.configStore.current.heartbeatProbability,
8868
+ heartbeatProbability: config.heartbeatProbability,
8551
8869
  availableSlots: Math.max(
8552
8870
  0,
8553
- this.configStore.current.maxParallelShadows - this.active.size
8871
+ config.maxParallelShadows - this.active.size
8554
8872
  ),
8555
8873
  shadows: snapshot.shadows,
8556
8874
  activeShadowIds: new Set(
8557
8875
  [...this.active.values()].map(({ shadow }) => shadow.id)
8558
8876
  ),
8559
8877
  mainModelId: fullModelId,
8560
- random: this.random
8878
+ random: this.random,
8879
+ executedTools
8561
8880
  });
8562
8881
  this.record("heartbeat", {
8563
8882
  modelCalls: this.modelCalls,
@@ -8568,12 +8887,14 @@ var ShadowMindRuntime = class {
8568
8887
  roll
8569
8888
  })),
8570
8889
  ...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
8571
- ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
8890
+ ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {},
8891
+ ...decision.toolFiltered.length ? { toolFiltered: decision.toolFiltered } : {}
8572
8892
  });
8573
8893
  if (!decision.activated.length) return;
8574
- const context = buildSessionContext(
8894
+ const context = buildShadowSessionContext(
8575
8895
  ctx.sessionManager.getEntries(),
8576
- ctx.sessionManager.getLeafId()
8896
+ ctx.sessionManager.getLeafId(),
8897
+ ctx.sessionManager.getSessionFile()
8577
8898
  );
8578
8899
  const availableTools = new Set(
8579
8900
  this.pi.getAllTools().map((tool2) => tool2.name)
@@ -8619,9 +8940,10 @@ var ShadowMindRuntime = class {
8619
8940
  this.completionReview.schedule(request, []);
8620
8941
  return;
8621
8942
  }
8622
- const context = buildSessionContext(
8943
+ const context = buildShadowSessionContext(
8623
8944
  ctx.sessionManager.getEntries(),
8624
- ctx.sessionManager.getLeafId()
8945
+ ctx.sessionManager.getLeafId(),
8946
+ ctx.sessionManager.getSessionFile()
8625
8947
  );
8626
8948
  const availableTools = new Set(
8627
8949
  this.pi.getAllTools().map((tool2) => tool2.name)
@@ -8733,7 +9055,7 @@ var ShadowMindRuntime = class {
8733
9055
  count: current.length
8734
9056
  });
8735
9057
  const idle = this.latestContext?.isIdle() ?? true;
8736
- this.sessionLifetime.run(() => {
9058
+ const delivered = this.sessionLifetime.run(() => {
8737
9059
  this.pi.sendMessage(
8738
9060
  {
8739
9061
  customType: "shadow-report",
@@ -8749,6 +9071,7 @@ var ShadowMindRuntime = class {
8749
9071
  { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" }
8750
9072
  );
8751
9073
  });
9074
+ if (delivered) this.reports.add(current);
8752
9075
  }
8753
9076
  async refresh(ctx) {
8754
9077
  const config = await this.configStore.reload();
@@ -8850,7 +9173,7 @@ var ShadowMindRuntime = class {
8850
9173
  const detail = failed ? ` ${event.data?.error ?? event.data?.reason}` : "";
8851
9174
  return `${new Date(event.at).toLocaleTimeString("en-GB", { hour12: false })} ${event.kind}${detail}`;
8852
9175
  }),
8853
- "Shortcut: Alt+S toggle \xB7 Commands: /shadow toggle | pause | resume | status | hide"
9176
+ "Shortcut: Alt+S toggle \xB7 Commands: /shadow toggle | pause | resume | status | hide | reports"
8854
9177
  ];
8855
9178
  }
8856
9179
  };