pi-shadow-mind 0.1.13 → 0.1.15

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
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // src/runtime.ts
8
- import { randomUUID } from "node:crypto";
8
+ import { randomUUID as randomUUID3 } from "node:crypto";
9
9
  import {
10
10
  buildSessionContext
11
11
  } from "@earendil-works/pi-coding-agent";
@@ -6760,6 +6760,12 @@ var browser_default = dist_exports;
6760
6760
  // src/registry.ts
6761
6761
  import { mkdir as mkdir2, readFile as readFile2, readdir, stat } from "node:fs/promises";
6762
6762
  import { basename, extname, join as join2 } from "node:path";
6763
+
6764
+ // src/types.ts
6765
+ var DEFAULT_READ_TOOLS = ["read", "grep", "find", "ls"];
6766
+ var SHADOW_TRIGGERS = ["heartbeat", "final_response"];
6767
+
6768
+ // src/registry.ts
6763
6769
  var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
6764
6770
  var ShadowRegistry = class {
6765
6771
  directory;
@@ -6787,8 +6793,14 @@ var ShadowRegistry = class {
6787
6793
  continue;
6788
6794
  }
6789
6795
  try {
6790
- const definition = parseShadowMarkdown(await readFile2(filePath, "utf8"), filePath);
6791
- this.cache.set(filePath, { mtimeMs: fileStat.mtimeMs, shadow: definition });
6796
+ const definition = parseShadowMarkdown(
6797
+ await readFile2(filePath, "utf8"),
6798
+ filePath
6799
+ );
6800
+ this.cache.set(filePath, {
6801
+ mtimeMs: fileStat.mtimeMs,
6802
+ shadow: definition
6803
+ });
6792
6804
  shadows.push(definition);
6793
6805
  } catch (error) {
6794
6806
  const message = error instanceof Error ? error.message : String(error);
@@ -6803,7 +6815,10 @@ var ShadowRegistry = class {
6803
6815
  const ids = /* @__PURE__ */ new Set();
6804
6816
  for (const shadow of shadows) {
6805
6817
  if (ids.has(shadow.id)) {
6806
- diagnostics.push({ filePath: shadow.filePath, message: `duplicate shadow id: ${shadow.id}` });
6818
+ diagnostics.push({
6819
+ filePath: shadow.filePath,
6820
+ message: `duplicate shadow id: ${shadow.id}`
6821
+ });
6807
6822
  } else {
6808
6823
  ids.add(shadow.id);
6809
6824
  unique.push(shadow);
@@ -6813,14 +6828,18 @@ var ShadowRegistry = class {
6813
6828
  }
6814
6829
  };
6815
6830
  function parseShadowMarkdown(source, filePath) {
6816
- const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(source);
6831
+ const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(
6832
+ source
6833
+ );
6817
6834
  if (!match) throw new Error("missing YAML frontmatter");
6818
6835
  const meta = browser_default.parse(match[1]);
6819
- if (!meta || typeof meta !== "object" || Array.isArray(meta)) throw new Error("frontmatter must be an object");
6836
+ if (!meta || typeof meta !== "object" || Array.isArray(meta))
6837
+ throw new Error("frontmatter must be an object");
6820
6838
  const value = meta;
6821
6839
  const fallbackId = basename(filePath, extname(filePath));
6822
6840
  const id = stringValue(value.id, fallbackId, "id");
6823
- if (!ID_PATTERN.test(id)) throw new Error("id must match [a-z0-9][a-z0-9_-]*");
6841
+ if (!ID_PATTERN.test(id))
6842
+ throw new Error("id must match [a-z0-9][a-z0-9_-]*");
6824
6843
  const prompt = match[2].trim();
6825
6844
  if (!prompt) throw new Error("shadow prompt body is empty");
6826
6845
  return {
@@ -6828,11 +6847,23 @@ function parseShadowMarkdown(source, filePath) {
6828
6847
  name: stringValue(value.name, id, "name"),
6829
6848
  enabled: booleanValue(value.enabled, true, "enabled"),
6830
6849
  debug: booleanValue(value.debug, false, "debug"),
6831
- activationProbability: probabilityValue(value.activation_probability, 0.3, "activation_probability"),
6832
- activeForModels: stringArray(value.active_for_models, ["*"], "active_for_models"),
6850
+ activationProbability: probabilityValue(
6851
+ value.activation_probability,
6852
+ 0.3,
6853
+ "activation_probability"
6854
+ ),
6855
+ trigger: triggerArray(value.trigger, ["heartbeat"], "trigger"),
6856
+ activeForModels: stringArray(
6857
+ value.active_for_models,
6858
+ ["*"],
6859
+ "active_for_models"
6860
+ ),
6833
6861
  runWithModel: optionalString(value.run_with_model, "run_with_model"),
6834
6862
  thinkingLevel: optionalThinking(value.thinking_level),
6835
- timeoutSeconds: optionalPositiveNumber(value.timeout_seconds, "timeout_seconds"),
6863
+ timeoutSeconds: optionalPositiveNumber(
6864
+ value.timeout_seconds,
6865
+ "timeout_seconds"
6866
+ ),
6836
6867
  tools: stringArray(value.tools, [], "tools"),
6837
6868
  prompt,
6838
6869
  filePath
@@ -6840,7 +6871,8 @@ function parseShadowMarkdown(source, filePath) {
6840
6871
  }
6841
6872
  function stringValue(value, fallback, name) {
6842
6873
  if (value === void 0) return fallback;
6843
- if (!isNonEmptyString(value)) throw new Error(`${name} must be a non-empty string`);
6874
+ if (!isNonEmptyString(value))
6875
+ throw new Error(`${name} must be a non-empty string`);
6844
6876
  return value.trim();
6845
6877
  }
6846
6878
  function optionalString(value, name) {
@@ -6853,19 +6885,31 @@ function booleanValue(value, fallback, name) {
6853
6885
  }
6854
6886
  function probabilityValue(value, fallback, name) {
6855
6887
  if (value === void 0) return fallback;
6856
- if (!isFiniteNumber(value) || !inRange(value, 0, 1)) throw new Error(`${name} must be between 0 and 1`);
6888
+ if (!isFiniteNumber(value) || !inRange(value, 0, 1))
6889
+ throw new Error(`${name} must be between 0 and 1`);
6857
6890
  return value;
6858
6891
  }
6859
6892
  function optionalPositiveNumber(value, name) {
6860
6893
  if (value === void 0) return void 0;
6861
- if (!isFiniteNumber(value) || value <= 0) throw new Error(`${name} must be positive`);
6894
+ if (!isFiniteNumber(value) || value <= 0)
6895
+ throw new Error(`${name} must be positive`);
6862
6896
  return value;
6863
6897
  }
6864
6898
  function stringArray(value, fallback, name) {
6865
6899
  if (value === void 0) return [...fallback];
6866
- if (!isStringArray(value)) throw new Error(`${name} must be an array of non-empty strings`);
6900
+ if (!isStringArray(value))
6901
+ throw new Error(`${name} must be an array of non-empty strings`);
6867
6902
  return [...new Set(value.map((item) => item.trim()))];
6868
6903
  }
6904
+ function triggerArray(value, fallback, name) {
6905
+ const values = typeof value === "string" ? [value] : value;
6906
+ const items = stringArray(values, fallback, name);
6907
+ const allowed = new Set(SHADOW_TRIGGERS);
6908
+ const invalid = items.filter((item) => !allowed.has(item));
6909
+ if (invalid.length)
6910
+ throw new Error(`${name} contains invalid trigger: ${invalid.join(", ")}`);
6911
+ return items;
6912
+ }
6869
6913
  function optionalThinking(value) {
6870
6914
  if (value === void 0) return void 0;
6871
6915
  if (!isThinkingLevel(value)) throw new Error("thinking_level is invalid");
@@ -6887,17 +6931,37 @@ var EntityStore = class {
6887
6931
  return shadow;
6888
6932
  }
6889
6933
  async create(draft) {
6890
- if ((await this.list()).some((shadow) => shadow.id === draft.id)) throw new Error(`shadow already exists: ${draft.id}`);
6891
- return this.writeParsed(join3(this.registry.directory, `${draft.id}.md`), draft, { overwrite: false });
6934
+ if ((await this.list()).some((shadow) => shadow.id === draft.id))
6935
+ throw new Error(`shadow already exists: ${draft.id}`);
6936
+ return this.writeParsed(
6937
+ join3(this.registry.directory, `${draft.id}.md`),
6938
+ draft,
6939
+ { overwrite: false }
6940
+ );
6892
6941
  }
6893
6942
  async update(id, patch) {
6894
6943
  const current = await this.get(id);
6895
- return this.writeParsed(current.filePath, { ...current, ...definedOnly(patch), id, prompt: patch.prompt ?? current.prompt }, { overwrite: true });
6944
+ return this.writeParsed(
6945
+ current.filePath,
6946
+ {
6947
+ ...current,
6948
+ ...definedOnly(patch),
6949
+ id,
6950
+ prompt: patch.prompt ?? current.prompt
6951
+ },
6952
+ { overwrite: true }
6953
+ );
6896
6954
  }
6897
6955
  async writeParsed(filePath, draft, options) {
6898
- const source = serializeShadow({ ...draft, prompt: draft.prompt ?? "Describe this Shadow Mind's responsibility." });
6956
+ const source = serializeShadow({
6957
+ ...draft,
6958
+ prompt: draft.prompt ?? "Describe this Shadow Mind's responsibility."
6959
+ });
6899
6960
  const parsed = parseShadowMarkdown(source, filePath);
6900
- await writeFile2(filePath, source, { encoding: "utf8", ...options.overwrite ? {} : { flag: "wx" } });
6961
+ await writeFile2(filePath, source, {
6962
+ encoding: "utf8",
6963
+ ...options.overwrite ? {} : { flag: "wx" }
6964
+ });
6901
6965
  return parsed;
6902
6966
  }
6903
6967
  async setEnabled(id, enabled) {
@@ -6920,6 +6984,7 @@ function serializeShadow(shadow) {
6920
6984
  enabled: shadow.enabled ?? true,
6921
6985
  debug: shadow.debug ?? false,
6922
6986
  activation_probability: shadow.activationProbability ?? 0.3,
6987
+ trigger: shadow.trigger ?? ["heartbeat"],
6923
6988
  active_for_models: shadow.activeForModels ?? ["*"],
6924
6989
  ...shadow.runWithModel ? { run_with_model: shadow.runWithModel } : {},
6925
6990
  ...shadow.thinkingLevel ? { thinking_level: shadow.thinkingLevel } : {},
@@ -6934,21 +6999,68 @@ ${(shadow.prompt ?? "").trim()}
6934
6999
  `;
6935
7000
  }
6936
7001
  function describeShadow(shadow) {
6937
- return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} models=${shadow.activeForModels.join(",")} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
7002
+ 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)}`;
6938
7003
  }
6939
7004
  function definedOnly(value) {
6940
- return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== void 0));
7005
+ return Object.fromEntries(
7006
+ Object.entries(value).filter(([, item]) => item !== void 0)
7007
+ );
6941
7008
  }
6942
7009
 
7010
+ // src/final-response-queue.ts
7011
+ var FinalResponseQueue = class {
7012
+ constructor(options) {
7013
+ this.options = options;
7014
+ }
7015
+ pending = [];
7016
+ enqueue(items) {
7017
+ this.pending.push(...items);
7018
+ this.pump();
7019
+ }
7020
+ slotAvailable() {
7021
+ this.pump();
7022
+ }
7023
+ clear() {
7024
+ this.pending.length = 0;
7025
+ }
7026
+ pump() {
7027
+ while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
7028
+ const epoch = this.options.currentEpoch();
7029
+ const activeShadowIds = this.options.activeShadowIds();
7030
+ const index = this.pending.findIndex(
7031
+ (item2) => item2.epoch !== epoch || !activeShadowIds.has(item2.shadowId)
7032
+ );
7033
+ if (index < 0) return;
7034
+ const [item] = this.pending.splice(index, 1);
7035
+ if (!item || item.epoch !== epoch) continue;
7036
+ this.options.launch(item);
7037
+ }
7038
+ }
7039
+ };
7040
+
6943
7041
  // src/management-tools.ts
6944
7042
  import { Type } from "typebox";
6945
7043
  var ID = Type.String({ pattern: "^[a-z0-9][a-z0-9_-]*$" });
6946
- var THINKING = Type.Union([Type.Literal("minimal"), Type.Literal("low"), Type.Literal("medium"), Type.Literal("high"), Type.Literal("xhigh"), Type.Literal("max")]);
7044
+ var THINKING = Type.Union([
7045
+ Type.Literal("minimal"),
7046
+ Type.Literal("low"),
7047
+ Type.Literal("medium"),
7048
+ Type.Literal("high"),
7049
+ Type.Literal("xhigh"),
7050
+ Type.Literal("max")
7051
+ ]);
7052
+ var TRIGGER = Type.Union([
7053
+ Type.Literal("heartbeat"),
7054
+ Type.Literal("final_response")
7055
+ ]);
6947
7056
  var SHADOW_FIELDS = {
6948
7057
  name: Type.Optional(Type.String()),
6949
7058
  enabled: Type.Optional(Type.Boolean()),
6950
7059
  debug: Type.Optional(Type.Boolean()),
6951
- activation_probability: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
7060
+ activation_probability: Type.Optional(
7061
+ Type.Number({ minimum: 0, maximum: 1 })
7062
+ ),
7063
+ trigger: Type.Optional(Type.Array(TRIGGER)),
6952
7064
  active_for_models: Type.Optional(Type.Array(Type.String())),
6953
7065
  run_with_model: Type.Optional(Type.String()),
6954
7066
  thinking_level: Type.Optional(THINKING),
@@ -6967,80 +7079,160 @@ function registerManagementTools(pi, store, getConfig) {
6967
7079
  pi.registerTool(configWriteTool(store, getConfig));
6968
7080
  }
6969
7081
  function listTool(store) {
6970
- return tool("list_shadows", "List Shadows", "List all valid globally configured Shadow Minds.", Type.Object({}), async () => {
6971
- const shadows = await store.list();
6972
- return textResult(shadows.length ? shadows.map(describeShadow).join("\n") : "No Shadow Minds configured.");
6973
- });
7082
+ return tool(
7083
+ "list_shadows",
7084
+ "List Shadows",
7085
+ "List all valid globally configured Shadow Minds.",
7086
+ Type.Object({}),
7087
+ async () => {
7088
+ const shadows = await store.list();
7089
+ return textResult(
7090
+ shadows.length ? shadows.map(describeShadow).join("\n") : "No Shadow Minds configured."
7091
+ );
7092
+ }
7093
+ );
6974
7094
  }
6975
7095
  function createTool(store) {
6976
- return tool("create_shadow", "Create Shadow", "Create a global Shadow Mind definition after user confirmation.", Type.Object({ id: ID, ...SHADOW_FIELDS, prompt: Type.String() }), async (_id, params, _signal, _update, ctx) => {
6977
- const raw = params;
6978
- if (!await confirm(ctx, "Create Shadow Mind", `Create ${String(raw.id)} in the global registry?`)) return textResult("Cancelled.");
6979
- const shadow = await store.create(toDraft(raw));
6980
- return textResult(`Created ${describeShadow(shadow)}`);
6981
- });
7096
+ return tool(
7097
+ "create_shadow",
7098
+ "Create Shadow",
7099
+ "Create a global Shadow Mind definition after user confirmation.",
7100
+ Type.Object({ id: ID, ...SHADOW_FIELDS, prompt: Type.String() }),
7101
+ async (_id, params, _signal, _update, ctx) => {
7102
+ const raw = params;
7103
+ if (!await confirm(
7104
+ ctx,
7105
+ "Create Shadow Mind",
7106
+ `Create ${String(raw.id)} in the global registry?`
7107
+ ))
7108
+ return textResult("Cancelled.");
7109
+ const shadow = await store.create(toDraft(raw));
7110
+ return textResult(`Created ${describeShadow(shadow)}`);
7111
+ }
7112
+ );
6982
7113
  }
6983
7114
  function updateTool(store) {
6984
- return tool("update_shadow", "Update Shadow", "Update a global Shadow Mind definition after user confirmation. The id is immutable.", Type.Object({ id: ID, ...SHADOW_FIELDS }), async (_id, params, _signal, _update, ctx) => {
6985
- const raw = params;
6986
- if (!await confirm(ctx, "Update Shadow Mind", `Apply changes to ${String(raw.id)}?`)) return textResult("Cancelled.");
6987
- const shadow = await store.update(String(raw.id), toPatch(raw));
6988
- return textResult(`Updated ${describeShadow(shadow)}`);
6989
- });
7115
+ return tool(
7116
+ "update_shadow",
7117
+ "Update Shadow",
7118
+ "Update a global Shadow Mind definition after user confirmation. The id is immutable.",
7119
+ Type.Object({ id: ID, ...SHADOW_FIELDS }),
7120
+ async (_id, params, _signal, _update, ctx) => {
7121
+ const raw = params;
7122
+ if (!await confirm(
7123
+ ctx,
7124
+ "Update Shadow Mind",
7125
+ `Apply changes to ${String(raw.id)}?`
7126
+ ))
7127
+ return textResult("Cancelled.");
7128
+ const shadow = await store.update(String(raw.id), toPatch(raw));
7129
+ return textResult(`Updated ${describeShadow(shadow)}`);
7130
+ }
7131
+ );
6990
7132
  }
6991
7133
  function toggleTool(store, enabled) {
6992
7134
  const action = enabled ? "enable" : "disable";
6993
- return tool(`${action}_shadow`, `${enabled ? "Enable" : "Disable"} Shadow`, `${enabled ? "Enable" : "Disable"} a global Shadow Mind after user confirmation.`, Type.Object({ id: ID }), async (_id, params, _signal, _update, ctx) => {
6994
- const id = String(params.id);
6995
- if (!await confirm(ctx, `${enabled ? "Enable" : "Disable"} Shadow Mind`, `${action} ${id}?`)) return textResult("Cancelled.");
6996
- return textResult(describeShadow(await store.setEnabled(id, enabled)));
6997
- });
7135
+ return tool(
7136
+ `${action}_shadow`,
7137
+ `${enabled ? "Enable" : "Disable"} Shadow`,
7138
+ `${enabled ? "Enable" : "Disable"} a global Shadow Mind after user confirmation.`,
7139
+ Type.Object({ id: ID }),
7140
+ async (_id, params, _signal, _update, ctx) => {
7141
+ const id = String(params.id);
7142
+ if (!await confirm(
7143
+ ctx,
7144
+ `${enabled ? "Enable" : "Disable"} Shadow Mind`,
7145
+ `${action} ${id}?`
7146
+ ))
7147
+ return textResult("Cancelled.");
7148
+ return textResult(describeShadow(await store.setEnabled(id, enabled)));
7149
+ }
7150
+ );
6998
7151
  }
6999
7152
  function deleteTool(store) {
7000
- return tool("delete_shadow", "Delete Shadow", "Delete a Shadow Mind definition after user confirmation. Debug logs are retained.", Type.Object({ id: ID }), async (_id, params, _signal, _update, ctx) => {
7001
- const id = String(params.id);
7002
- if (!await confirm(ctx, "Delete Shadow Mind", `Delete ${id}? Its debug logs will be kept.`)) return textResult("Cancelled.");
7003
- await store.delete(id);
7004
- return textResult(`Deleted ${id}; logs were retained.`);
7005
- });
7153
+ return tool(
7154
+ "delete_shadow",
7155
+ "Delete Shadow",
7156
+ "Delete a Shadow Mind definition after user confirmation. Debug logs are retained.",
7157
+ Type.Object({ id: ID }),
7158
+ async (_id, params, _signal, _update, ctx) => {
7159
+ const id = String(params.id);
7160
+ if (!await confirm(
7161
+ ctx,
7162
+ "Delete Shadow Mind",
7163
+ `Delete ${id}? Its debug logs will be kept.`
7164
+ ))
7165
+ return textResult("Cancelled.");
7166
+ await store.delete(id);
7167
+ return textResult(`Deleted ${id}; logs were retained.`);
7168
+ }
7169
+ );
7006
7170
  }
7007
7171
  function configReadTool(store) {
7008
- return tool("get_shadow_config", "Get Shadow Config", "Read the global Shadow Mind runtime configuration.", Type.Object({}), async () => textResult(await store.readConfig()));
7172
+ return tool(
7173
+ "get_shadow_config",
7174
+ "Get Shadow Config",
7175
+ "Read the global Shadow Mind runtime configuration.",
7176
+ Type.Object({}),
7177
+ async () => textResult(await store.readConfig())
7178
+ );
7009
7179
  }
7010
7180
  function configWriteTool(store, getConfig) {
7011
- return tool("update_shadow_config", "Update Shadow Config", "Update global Shadow Mind runtime configuration after user confirmation.", Type.Object({
7012
- heartbeat_probability: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
7013
- max_parallel_shadows: Type.Optional(Type.Integer({ minimum: 1 })),
7014
- default_shadow_timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
7015
- headless_drain_timeout_seconds: Type.Optional(Type.Number({ exclusiveMinimum: 0 })),
7016
- result_batch_window_ms: Type.Optional(Type.Integer({ minimum: 0 })),
7017
- default_shadow_model: Type.Optional(Type.String()),
7018
- default_thinking_level: Type.Optional(THINKING),
7019
- random_seed: Type.Optional(Type.Integer({ minimum: 0, maximum: 4294967295 }))
7020
- }), async (_id, params, _signal, _update, ctx) => {
7021
- const current = getConfig();
7022
- const raw = params;
7023
- const next = parseConfig({
7024
- heartbeat_probability: raw.heartbeat_probability ?? current.heartbeatProbability,
7025
- max_parallel_shadows: raw.max_parallel_shadows ?? current.maxParallelShadows,
7026
- default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
7027
- headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
7028
- result_batch_window_ms: raw.result_batch_window_ms ?? current.resultBatchWindowMs,
7029
- default_shadow_model: raw.default_shadow_model ?? current.defaultShadowModel,
7030
- default_thinking_level: raw.default_thinking_level ?? current.defaultThinkingLevel,
7031
- random_seed: raw.random_seed ?? current.randomSeed
7032
- });
7033
- if (!await confirm(ctx, "Update Shadow Config", `Apply this config?
7034
- ${JSON.stringify(next, null, 2)}`)) return textResult("Cancelled.");
7035
- await store.writeConfig(next);
7036
- return textResult("Shadow Mind config updated.");
7037
- });
7181
+ return tool(
7182
+ "update_shadow_config",
7183
+ "Update Shadow Config",
7184
+ "Update global Shadow Mind runtime configuration after user confirmation.",
7185
+ Type.Object({
7186
+ heartbeat_probability: Type.Optional(
7187
+ Type.Number({ minimum: 0, maximum: 1 })
7188
+ ),
7189
+ max_parallel_shadows: Type.Optional(Type.Integer({ minimum: 1 })),
7190
+ default_shadow_timeout_seconds: Type.Optional(
7191
+ Type.Number({ exclusiveMinimum: 0 })
7192
+ ),
7193
+ headless_drain_timeout_seconds: Type.Optional(
7194
+ Type.Number({ exclusiveMinimum: 0 })
7195
+ ),
7196
+ result_batch_window_ms: Type.Optional(Type.Integer({ minimum: 0 })),
7197
+ default_shadow_model: Type.Optional(Type.String()),
7198
+ default_thinking_level: Type.Optional(THINKING),
7199
+ random_seed: Type.Optional(
7200
+ Type.Integer({ minimum: 0, maximum: 4294967295 })
7201
+ )
7202
+ }),
7203
+ async (_id, params, _signal, _update, ctx) => {
7204
+ const current = getConfig();
7205
+ const raw = params;
7206
+ const next = parseConfig({
7207
+ heartbeat_probability: raw.heartbeat_probability ?? current.heartbeatProbability,
7208
+ max_parallel_shadows: raw.max_parallel_shadows ?? current.maxParallelShadows,
7209
+ default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
7210
+ headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
7211
+ result_batch_window_ms: raw.result_batch_window_ms ?? current.resultBatchWindowMs,
7212
+ default_shadow_model: raw.default_shadow_model ?? current.defaultShadowModel,
7213
+ default_thinking_level: raw.default_thinking_level ?? current.defaultThinkingLevel,
7214
+ random_seed: raw.random_seed ?? current.randomSeed
7215
+ });
7216
+ if (!await confirm(
7217
+ ctx,
7218
+ "Update Shadow Config",
7219
+ `Apply this config?
7220
+ ${JSON.stringify(next, null, 2)}`
7221
+ ))
7222
+ return textResult("Cancelled.");
7223
+ await store.writeConfig(next);
7224
+ return textResult("Shadow Mind config updated.");
7225
+ }
7226
+ );
7038
7227
  }
7039
7228
  function tool(name, label, description, parameters, execute) {
7040
7229
  return { name, label, description, parameters, execute };
7041
7230
  }
7042
7231
  async function confirm(ctx, title, message) {
7043
- if (!ctx.hasUI) throw new Error("This write requires a UI confirmation, but no dialog-capable UI is available.");
7232
+ if (!ctx.hasUI)
7233
+ throw new Error(
7234
+ "This write requires a UI confirmation, but no dialog-capable UI is available."
7235
+ );
7044
7236
  return ctx.ui.confirm(title, message);
7045
7237
  }
7046
7238
  function textResult(text) {
@@ -7055,6 +7247,7 @@ function toPatch(raw) {
7055
7247
  enabled: raw.enabled,
7056
7248
  debug: raw.debug,
7057
7249
  activationProbability: raw.activation_probability,
7250
+ trigger: raw.trigger,
7058
7251
  activeForModels: raw.active_for_models,
7059
7252
  runWithModel: raw.run_with_model,
7060
7253
  thinkingLevel: raw.thinking_level,
@@ -7125,16 +7318,34 @@ function createRandom(seed) {
7125
7318
  function shouldEvaluateHeartbeat(toolResults) {
7126
7319
  return toolResults.length > 0;
7127
7320
  }
7321
+ function shouldEvaluateFinalResponse(messages) {
7322
+ const message = messages.at(-1);
7323
+ if (!message || typeof message !== "object" || message.role !== "assistant")
7324
+ return false;
7325
+ const content = message.content;
7326
+ if (typeof content === "string") return content.trim().length > 0;
7327
+ return Array.isArray(content) && content.some((block) => {
7328
+ if (!block || typeof block !== "object") return false;
7329
+ const value = block;
7330
+ return value.type === "text" && typeof value.text === "string" && value.text.trim().length > 0;
7331
+ });
7332
+ }
7128
7333
  function decideHeartbeat(options) {
7129
7334
  const random = options.random ?? Math.random;
7130
7335
  const heartbeatRoll = random();
7131
7336
  if (heartbeatRoll >= options.heartbeatProbability || options.availableSlots <= 0) {
7132
- return { heartbeatRoll, activated: [], candidates: [], modelFiltered: [], runningExcluded: [] };
7337
+ return {
7338
+ heartbeatRoll,
7339
+ activated: [],
7340
+ candidates: [],
7341
+ modelFiltered: [],
7342
+ runningExcluded: []
7343
+ };
7133
7344
  }
7134
7345
  const modelFiltered = [];
7135
7346
  const runningExcluded = [];
7136
7347
  const rolls = options.shadows.filter((shadow) => {
7137
- if (!shadow.enabled) return false;
7348
+ if (!shadow.enabled || !hasTrigger(shadow, "heartbeat")) return false;
7138
7349
  if (options.activeShadowIds.has(shadow.id)) {
7139
7350
  runningExcluded.push(shadow.id);
7140
7351
  return false;
@@ -7145,20 +7356,55 @@ function decideHeartbeat(options) {
7145
7356
  }
7146
7357
  return true;
7147
7358
  }).map((shadow) => ({ shadow, roll: random() }));
7148
- const hits = rolls.filter(({ shadow, roll }) => roll < shadow.activationProbability);
7149
- const selected = sample(hits, Math.min(options.availableSlots, hits.length), random);
7359
+ const hits = rolls.filter(
7360
+ ({ shadow, roll }) => roll < shadow.activationProbability
7361
+ );
7362
+ const selected = sample(
7363
+ hits,
7364
+ Math.min(options.availableSlots, hits.length),
7365
+ random
7366
+ );
7150
7367
  const selectedIds = new Set(selected.map(({ shadow }) => shadow.id));
7151
7368
  return {
7152
7369
  heartbeatRoll,
7153
7370
  activated: selected,
7154
- candidates: rolls.map(({ shadow, roll }) => ({ shadowId: shadow.id, roll, selected: selectedIds.has(shadow.id) })),
7371
+ candidates: rolls.map(({ shadow, roll }) => ({
7372
+ shadowId: shadow.id,
7373
+ roll,
7374
+ selected: selectedIds.has(shadow.id)
7375
+ })),
7155
7376
  modelFiltered,
7156
7377
  runningExcluded
7157
7378
  };
7158
7379
  }
7380
+ function decideFinalResponse(options) {
7381
+ const modelFiltered = [];
7382
+ const candidates = options.shadows.filter((shadow) => {
7383
+ if (!shadow.enabled || !hasTrigger(shadow, "final_response"))
7384
+ return false;
7385
+ if (!matchesModel(shadow, options.mainModelId)) {
7386
+ modelFiltered.push(shadow.id);
7387
+ return false;
7388
+ }
7389
+ return true;
7390
+ }).map((shadow) => ({ shadow, roll: 0 }));
7391
+ return {
7392
+ activated: candidates,
7393
+ candidates: candidates.map(({ shadow, roll }) => ({
7394
+ shadowId: shadow.id,
7395
+ roll,
7396
+ selected: true
7397
+ })),
7398
+ modelFiltered,
7399
+ runningExcluded: []
7400
+ };
7401
+ }
7159
7402
  function matchesModel(shadow, fullModelId) {
7160
7403
  return shadow.activeForModels.includes("*") || shadow.activeForModels.includes(fullModelId);
7161
7404
  }
7405
+ function hasTrigger(shadow, trigger) {
7406
+ return shadow.trigger.includes(trigger);
7407
+ }
7162
7408
  function sample(values, count, random) {
7163
7409
  const copy = [...values];
7164
7410
  for (let index = copy.length - 1; index > 0; index -= 1) {
@@ -7209,8 +7455,70 @@ import {
7209
7455
  createAgentSession
7210
7456
  } from "@earendil-works/pi-coding-agent";
7211
7457
 
7212
- // src/types.ts
7213
- var DEFAULT_READ_TOOLS = ["read", "grep", "find", "ls"];
7458
+ // src/usage.ts
7459
+ function zeroUsage() {
7460
+ return {
7461
+ requests: 0,
7462
+ input: 0,
7463
+ output: 0,
7464
+ cacheRead: 0,
7465
+ cacheWrite: 0,
7466
+ totalTokens: 0,
7467
+ cost: {
7468
+ input: 0,
7469
+ output: 0,
7470
+ cacheRead: 0,
7471
+ cacheWrite: 0,
7472
+ total: 0
7473
+ }
7474
+ };
7475
+ }
7476
+ function addUsage(left, right) {
7477
+ return {
7478
+ requests: left.requests + right.requests,
7479
+ input: left.input + right.input,
7480
+ output: left.output + right.output,
7481
+ cacheRead: left.cacheRead + right.cacheRead,
7482
+ cacheWrite: left.cacheWrite + right.cacheWrite,
7483
+ totalTokens: left.totalTokens + right.totalTokens,
7484
+ cost: {
7485
+ input: left.cost.input + right.cost.input,
7486
+ output: left.cost.output + right.cost.output,
7487
+ cacheRead: left.cost.cacheRead + right.cost.cacheRead,
7488
+ cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
7489
+ total: left.cost.total + right.cost.total
7490
+ }
7491
+ };
7492
+ }
7493
+ function formatUsageTokens(tokens) {
7494
+ if (tokens < 1e3) return tokens.toLocaleString("en-US");
7495
+ if (tokens < 999950) return `${formatCompact(tokens / 1e3)}k`;
7496
+ return `${formatCompact(tokens / 1e6)}m`;
7497
+ }
7498
+ function formatUsageCost(cost) {
7499
+ if (cost === 0) return "$0";
7500
+ if (cost >= 0.01) return `$${cost.toFixed(2)}`;
7501
+ if (cost >= 1e-3) return `$${cost.toFixed(3)}`;
7502
+ return `$${cost.toFixed(4)}`;
7503
+ }
7504
+ function formatUsageSummary(usage) {
7505
+ return `${usage.requests} requests \xB7 ${formatUsageTokens(usage.totalTokens)} tokens \xB7 API ${formatUsageCost(usage.cost.total)}`;
7506
+ }
7507
+ function formatUsageDetail(scope, usage) {
7508
+ return [
7509
+ `usage ${scope}`,
7510
+ `${usage.requests} requests`,
7511
+ `${formatUsageTokens(usage.totalTokens)} total`,
7512
+ `${formatUsageTokens(usage.input)} input`,
7513
+ `${formatUsageTokens(usage.output)} output`,
7514
+ `${formatUsageTokens(usage.cacheRead)} cache read`,
7515
+ `${formatUsageTokens(usage.cacheWrite)} cache write`,
7516
+ `API ${formatUsageCost(usage.cost.total)}`
7517
+ ].join(" \xB7 ");
7518
+ }
7519
+ function formatCompact(value) {
7520
+ return Number(value.toFixed(1)).toString();
7521
+ }
7214
7522
 
7215
7523
  // src/protocol.ts
7216
7524
  var RUNTIME_PROTOCOL = `You are a Shadow Mind running beside the main agent.
@@ -7374,6 +7682,7 @@ function buildRunResult(options) {
7374
7682
  const { reason, error, durationMs, session, baseMessageCount, missingTools, thinkingLevel } = options;
7375
7683
  const ownMessages = session ? session.messages.slice(baseMessageCount) : [];
7376
7684
  const metrics = toolMetrics(ownMessages);
7685
+ const usage = usageMetrics(ownMessages);
7377
7686
  return {
7378
7687
  reason,
7379
7688
  ...error !== void 0 ? { error } : {},
@@ -7381,6 +7690,7 @@ function buildRunResult(options) {
7381
7690
  toolNames: session?.getActiveToolNames() ?? [],
7382
7691
  missingTools,
7383
7692
  ...metrics,
7693
+ usage,
7384
7694
  ...thinkingLevel !== void 0 ? { thinkingLevel } : {},
7385
7695
  sessionFile: session?.sessionFile
7386
7696
  };
@@ -7455,6 +7765,7 @@ var ShadowRunner = class {
7455
7765
  extensions: base.extensions.filter((extension) => !isSelfExtension(extension.resolvedPath))
7456
7766
  })
7457
7767
  });
7768
+ await resourceLoader.reload();
7458
7769
  const reportTool = createReportTool((content) => {
7459
7770
  if (state.reported || controller.signal.aborted) return;
7460
7771
  state.reported = true;
@@ -7501,11 +7812,33 @@ function toolMetrics(messages) {
7501
7812
  }
7502
7813
  const toolStats = [...stats.values()].sort((a, b) => b.calls - a.calls);
7503
7814
  return {
7504
- toolCalls: toolStats.reduce((sum, stat2) => sum + stat2.calls, 0),
7505
- toolFailures: toolStats.reduce((sum, stat2) => sum + stat2.failures, 0),
7815
+ toolCalls: toolStats.reduce((sum, stat3) => sum + stat3.calls, 0),
7816
+ toolFailures: toolStats.reduce((sum, stat3) => sum + stat3.failures, 0),
7506
7817
  toolStats
7507
7818
  };
7508
7819
  }
7820
+ function usageMetrics(messages) {
7821
+ let aggregate = zeroUsage();
7822
+ for (const message of messages) {
7823
+ if (message.role !== "assistant" || !message.usage) continue;
7824
+ aggregate = addUsage(aggregate, {
7825
+ requests: 1,
7826
+ input: message.usage.input,
7827
+ output: message.usage.output,
7828
+ cacheRead: message.usage.cacheRead,
7829
+ cacheWrite: message.usage.cacheWrite,
7830
+ totalTokens: message.usage.totalTokens,
7831
+ cost: {
7832
+ input: message.usage.cost.input,
7833
+ output: message.usage.cost.output,
7834
+ cacheRead: message.usage.cost.cacheRead,
7835
+ cacheWrite: message.usage.cost.cacheWrite,
7836
+ total: message.usage.cost.total
7837
+ }
7838
+ });
7839
+ }
7840
+ return aggregate;
7841
+ }
7509
7842
  function createReportTool(onReport) {
7510
7843
  return {
7511
7844
  name: "report_to_main",
@@ -7566,7 +7899,7 @@ async function waitForSettled(options) {
7566
7899
  const pollMs = options.pollMs ?? 25;
7567
7900
  const quietMs = options.quietMs ?? 50;
7568
7901
  const now = options.now ?? Date.now;
7569
- const delay = options.delay ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
7902
+ const delay2 = options.delay ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
7570
7903
  const started = now();
7571
7904
  let settledSince;
7572
7905
  while (now() - started < options.timeoutMs) {
@@ -7577,24 +7910,407 @@ async function waitForSettled(options) {
7577
7910
  settledSince = void 0;
7578
7911
  }
7579
7912
  const remaining = options.timeoutMs - (now() - started);
7580
- await delay(Math.min(pollMs, Math.max(1, remaining)));
7913
+ await delay2(Math.min(pollMs, Math.max(1, remaining)));
7581
7914
  }
7582
7915
  return { settled: false, durationMs: now() - started };
7583
7916
  }
7584
7917
 
7918
+ // src/usage-store.ts
7919
+ import { randomUUID as randomUUID2 } from "node:crypto";
7920
+ import { mkdir as mkdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
7921
+ import { dirname as dirname3, join as join6 } from "node:path";
7922
+
7923
+ // src/usage-file-lock.ts
7924
+ import { randomUUID } from "node:crypto";
7925
+ import { mkdir as mkdir4, readFile as readFile4, rename, rm, stat as stat2, utimes, writeFile as writeFile3 } from "node:fs/promises";
7926
+ import { join as join5 } from "node:path";
7927
+ var USAGE_LOCK_OWNER_FILE = "owner";
7928
+ var USAGE_LOCK_OPERATION_DIRECTORY = ".operation";
7929
+ async function acquireUsageFileLock(usagePath, timing = {}) {
7930
+ const resolved = resolveTiming(timing);
7931
+ const lockPath = `${usagePath}.lock`;
7932
+ const owner = { token: randomUUID(), pid: process.pid };
7933
+ while (true) {
7934
+ try {
7935
+ await mkdir4(lockPath);
7936
+ } catch (error) {
7937
+ if (error.code !== "EEXIST") throw error;
7938
+ await recoverStaleUsageLock(lockPath, resolved);
7939
+ await delay(resolved.retryMs);
7940
+ continue;
7941
+ }
7942
+ try {
7943
+ await writeOwner(lockPath, owner);
7944
+ } catch (error) {
7945
+ await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
7946
+ throw error;
7947
+ }
7948
+ return startLease(lockPath, owner, resolved);
7949
+ }
7950
+ }
7951
+ function startLease(lockPath, owner, timing) {
7952
+ let stopped = false;
7953
+ let heartbeat = Promise.resolve();
7954
+ const timer = setInterval(() => {
7955
+ heartbeat = heartbeat.then(async () => {
7956
+ if (stopped) return;
7957
+ const current = await readUsageLockState(lockPath);
7958
+ if (!current || current.owner?.token !== owner.token) {
7959
+ stopped = true;
7960
+ return;
7961
+ }
7962
+ const now = /* @__PURE__ */ new Date();
7963
+ await utimes(join5(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
7964
+ }).catch(() => void 0);
7965
+ }, timing.heartbeatMs);
7966
+ timer.unref();
7967
+ return async () => {
7968
+ stopped = true;
7969
+ clearInterval(timer);
7970
+ await heartbeat;
7971
+ await releaseUsageLock(lockPath, owner.token, timing);
7972
+ };
7973
+ }
7974
+ async function recoverStaleUsageLock(lockPath, timing) {
7975
+ const observed = await readUsageLockState(lockPath);
7976
+ if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return;
7977
+ const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
7978
+ if (!await claimUsageLockOperation(operationPath, timing)) return;
7979
+ try {
7980
+ const current = await readUsageLockState(lockPath);
7981
+ if (!current || !sameUsageLock(current, observed) || !isRecoverableUsageLock(current, timing.staleMs)) return;
7982
+ const abandonedPath = `${lockPath}.${randomUUID()}.stale`;
7983
+ try {
7984
+ await rename(lockPath, abandonedPath);
7985
+ } catch (error) {
7986
+ if (error.code === "ENOENT") return;
7987
+ throw error;
7988
+ }
7989
+ await rm(abandonedPath, { recursive: true, force: true });
7990
+ } finally {
7991
+ await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
7992
+ }
7993
+ }
7994
+ async function releaseUsageLock(lockPath, token, timing) {
7995
+ const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
7996
+ while (true) {
7997
+ const observed = await readUsageLockState(lockPath);
7998
+ if (!observed || observed.owner?.token !== token) return;
7999
+ if (!await claimUsageLockOperation(operationPath, timing)) {
8000
+ await delay(timing.retryMs);
8001
+ continue;
8002
+ }
8003
+ try {
8004
+ const current = await readUsageLockState(lockPath);
8005
+ if (!current || current.owner?.token !== token) return;
8006
+ const releasedPath = `${lockPath}.${token}.released`;
8007
+ try {
8008
+ await rename(lockPath, releasedPath);
8009
+ } catch (error) {
8010
+ if (error.code === "ENOENT") continue;
8011
+ throw error;
8012
+ }
8013
+ await rm(releasedPath, { recursive: true, force: true });
8014
+ return;
8015
+ } finally {
8016
+ await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
8017
+ }
8018
+ }
8019
+ }
8020
+ async function claimUsageLockOperation(operationPath, timing) {
8021
+ const owner = { token: randomUUID(), pid: process.pid };
8022
+ try {
8023
+ await mkdir4(operationPath);
8024
+ try {
8025
+ await writeOwner(operationPath, owner);
8026
+ } catch (error) {
8027
+ await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
8028
+ throw error;
8029
+ }
8030
+ return true;
8031
+ } catch (error) {
8032
+ if (error.code !== "EEXIST") {
8033
+ if (error.code === "ENOENT") return false;
8034
+ throw error;
8035
+ }
8036
+ }
8037
+ const observed = await readUsageLockState(operationPath);
8038
+ if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return false;
8039
+ const abandonedPath = `${operationPath}.${randomUUID()}.stale`;
8040
+ try {
8041
+ await rename(operationPath, abandonedPath);
8042
+ } catch (error) {
8043
+ if (error.code === "ENOENT") return false;
8044
+ throw error;
8045
+ }
8046
+ await rm(abandonedPath, { recursive: true, force: true });
8047
+ return false;
8048
+ }
8049
+ async function writeOwner(path, owner) {
8050
+ await writeFile3(join5(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
8051
+ encoding: "utf8",
8052
+ flag: "wx"
8053
+ });
8054
+ }
8055
+ async function readUsageLockState(lockPath) {
8056
+ let lock;
8057
+ try {
8058
+ lock = await stat2(lockPath);
8059
+ } catch (error) {
8060
+ if (error.code === "ENOENT") return void 0;
8061
+ throw error;
8062
+ }
8063
+ try {
8064
+ const ownerPath = join5(lockPath, USAGE_LOCK_OWNER_FILE);
8065
+ const [raw, ownerFile] = await Promise.all([readFile4(ownerPath, "utf8"), stat2(ownerPath)]);
8066
+ return {
8067
+ owner: parseOwner(raw),
8068
+ mtimeMs: ownerFile.mtimeMs,
8069
+ dev: lock.dev,
8070
+ ino: lock.ino
8071
+ };
8072
+ } catch (error) {
8073
+ if (error.code !== "ENOENT") throw error;
8074
+ return { mtimeMs: lock.mtimeMs, dev: lock.dev, ino: lock.ino };
8075
+ }
8076
+ }
8077
+ function parseOwner(raw) {
8078
+ try {
8079
+ const value = JSON.parse(raw);
8080
+ if (typeof value.token !== "string" || !value.token || !Number.isSafeInteger(value.pid) || value.pid <= 0) return void 0;
8081
+ return { token: value.token, pid: value.pid };
8082
+ } catch {
8083
+ return void 0;
8084
+ }
8085
+ }
8086
+ function isRecoverableUsageLock(lock, staleMs) {
8087
+ return Date.now() - lock.mtimeMs >= staleMs && !isProcessAlive(lock.owner?.pid);
8088
+ }
8089
+ function isProcessAlive(pid) {
8090
+ if (pid === void 0) return false;
8091
+ try {
8092
+ process.kill(pid, 0);
8093
+ return true;
8094
+ } catch (error) {
8095
+ return error.code !== "ESRCH";
8096
+ }
8097
+ }
8098
+ function sameUsageLock(left, right) {
8099
+ return left.owner !== void 0 && right.owner !== void 0 ? left.owner.token === right.owner.token : left.owner === right.owner && left.dev === right.dev && left.ino === right.ino;
8100
+ }
8101
+ function resolveTiming(timing) {
8102
+ return {
8103
+ retryMs: timing.retryMs ?? 25,
8104
+ staleMs: timing.staleMs ?? 3e4,
8105
+ heartbeatMs: timing.heartbeatMs ?? 1e4
8106
+ };
8107
+ }
8108
+ function delay(ms) {
8109
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
8110
+ }
8111
+
8112
+ // src/usage-store.ts
8113
+ var USAGE_DOCUMENT_VERSION = 1;
8114
+ var UsageStore = class {
8115
+ usagePath;
8116
+ lifetime = zeroUsage();
8117
+ pendingUsage = zeroUsage();
8118
+ lastError;
8119
+ initialization;
8120
+ pendingWrite = Promise.resolve();
8121
+ constructor(agentDir) {
8122
+ this.usagePath = join6(agentDir, "shadow-minds", "usage.json");
8123
+ }
8124
+ initialize() {
8125
+ this.initialization ??= this.load();
8126
+ return this.initialization;
8127
+ }
8128
+ get current() {
8129
+ return {
8130
+ ...this.lifetime,
8131
+ cost: { ...this.lifetime.cost }
8132
+ };
8133
+ }
8134
+ get error() {
8135
+ return this.lastError;
8136
+ }
8137
+ add(usage) {
8138
+ this.lifetime = addUsage(this.lifetime, usage);
8139
+ this.pendingUsage = addUsage(this.pendingUsage, usage);
8140
+ return this.enqueueWrite();
8141
+ }
8142
+ async flush() {
8143
+ await this.pendingWrite;
8144
+ if (!isZeroUsage(this.pendingUsage)) await this.enqueueWrite();
8145
+ }
8146
+ enqueueWrite() {
8147
+ this.pendingWrite = this.pendingWrite.then(
8148
+ () => this.writePending(),
8149
+ () => this.writePending()
8150
+ ).then(
8151
+ () => {
8152
+ this.lastError = void 0;
8153
+ },
8154
+ (error) => {
8155
+ this.lastError = error instanceof Error ? error.message : String(error);
8156
+ }
8157
+ );
8158
+ return this.pendingWrite;
8159
+ }
8160
+ async load() {
8161
+ try {
8162
+ const raw = await readFile5(this.usagePath, "utf8");
8163
+ this.lifetime = parseUsageDocument(JSON.parse(raw)).lifetime;
8164
+ this.lastError = void 0;
8165
+ } catch (error) {
8166
+ if (error.code === "ENOENT") {
8167
+ this.lifetime = zeroUsage();
8168
+ this.lastError = void 0;
8169
+ return;
8170
+ }
8171
+ this.lifetime = zeroUsage();
8172
+ this.lastError = error instanceof Error ? error.message : String(error);
8173
+ }
8174
+ }
8175
+ async writePending() {
8176
+ const delta = this.pendingUsage;
8177
+ if (isZeroUsage(delta)) return;
8178
+ await mkdir5(dirname3(this.usagePath), { recursive: true });
8179
+ const releaseLock = await acquireUsageFileLock(this.usagePath);
8180
+ try {
8181
+ const committed = addUsage(await readUsageLifetime(this.usagePath), delta);
8182
+ await writeUsageDocument(this.usagePath, committed);
8183
+ this.pendingUsage = subtractUsage(this.pendingUsage, delta);
8184
+ this.lifetime = addUsage(committed, this.pendingUsage);
8185
+ } finally {
8186
+ await releaseLock();
8187
+ }
8188
+ }
8189
+ };
8190
+ function subtractUsage(left, right) {
8191
+ return {
8192
+ requests: left.requests - right.requests,
8193
+ input: left.input - right.input,
8194
+ output: left.output - right.output,
8195
+ cacheRead: left.cacheRead - right.cacheRead,
8196
+ cacheWrite: left.cacheWrite - right.cacheWrite,
8197
+ totalTokens: left.totalTokens - right.totalTokens,
8198
+ cost: {
8199
+ input: left.cost.input - right.cost.input,
8200
+ output: left.cost.output - right.cost.output,
8201
+ cacheRead: left.cost.cacheRead - right.cost.cacheRead,
8202
+ cacheWrite: left.cost.cacheWrite - right.cost.cacheWrite,
8203
+ total: left.cost.total - right.cost.total
8204
+ }
8205
+ };
8206
+ }
8207
+ function isZeroUsage(usage) {
8208
+ return usage.requests === 0 && usage.input === 0 && usage.output === 0 && usage.cacheRead === 0 && usage.cacheWrite === 0 && usage.totalTokens === 0 && usage.cost.input === 0 && usage.cost.output === 0 && usage.cost.cacheRead === 0 && usage.cost.cacheWrite === 0 && usage.cost.total === 0;
8209
+ }
8210
+ async function readUsageLifetime(usagePath) {
8211
+ try {
8212
+ const raw = await readFile5(usagePath, "utf8");
8213
+ return parseUsageDocument(JSON.parse(raw)).lifetime;
8214
+ } catch (error) {
8215
+ if (error.code === "ENOENT") return zeroUsage();
8216
+ throw error;
8217
+ }
8218
+ }
8219
+ async function writeUsageDocument(usagePath, lifetime) {
8220
+ const temporaryPath = `${usagePath}.${randomUUID2()}.tmp`;
8221
+ try {
8222
+ const document = { version: USAGE_DOCUMENT_VERSION, lifetime };
8223
+ await writeFile4(temporaryPath, `${JSON.stringify(document, null, 2)}
8224
+ `, "utf8");
8225
+ await rename2(temporaryPath, usagePath);
8226
+ } finally {
8227
+ await rm2(temporaryPath, { force: true }).catch(() => void 0);
8228
+ }
8229
+ }
8230
+ function parseUsageDocument(input) {
8231
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
8232
+ throw new Error("usage must be a JSON object");
8233
+ }
8234
+ const value = input;
8235
+ if (value.version !== USAGE_DOCUMENT_VERSION) throw new Error("usage version is unsupported");
8236
+ return { version: USAGE_DOCUMENT_VERSION, lifetime: parseUsage(value.lifetime, "lifetime") };
8237
+ }
8238
+ function parseUsage(input, name) {
8239
+ if (input === null || typeof input !== "object" || Array.isArray(input)) {
8240
+ throw new Error(`${name} must be an object`);
8241
+ }
8242
+ const value = input;
8243
+ const cost = value.cost;
8244
+ if (cost === null || typeof cost !== "object" || Array.isArray(cost)) {
8245
+ throw new Error(`${name}.cost must be an object`);
8246
+ }
8247
+ const costValue = cost;
8248
+ return {
8249
+ requests: nonNegativeInteger2(value.requests, `${name}.requests`),
8250
+ input: nonNegativeNumber(value.input, `${name}.input`),
8251
+ output: nonNegativeNumber(value.output, `${name}.output`),
8252
+ cacheRead: nonNegativeNumber(value.cacheRead, `${name}.cacheRead`),
8253
+ cacheWrite: nonNegativeNumber(value.cacheWrite, `${name}.cacheWrite`),
8254
+ totalTokens: nonNegativeNumber(value.totalTokens, `${name}.totalTokens`),
8255
+ cost: {
8256
+ input: nonNegativeNumber(costValue.input, `${name}.cost.input`),
8257
+ output: nonNegativeNumber(costValue.output, `${name}.cost.output`),
8258
+ cacheRead: nonNegativeNumber(costValue.cacheRead, `${name}.cost.cacheRead`),
8259
+ cacheWrite: nonNegativeNumber(costValue.cacheWrite, `${name}.cost.cacheWrite`),
8260
+ total: nonNegativeNumber(costValue.total, `${name}.cost.total`)
8261
+ }
8262
+ };
8263
+ }
8264
+ function nonNegativeNumber(value, name) {
8265
+ if (!isFiniteNumber(value) || value < 0) {
8266
+ throw new Error(`${name} must be a non-negative finite number`);
8267
+ }
8268
+ return value;
8269
+ }
8270
+ function nonNegativeInteger2(value, name) {
8271
+ const number = nonNegativeNumber(value, name);
8272
+ if (!Number.isSafeInteger(number)) throw new Error(`${name} must be a non-negative safe integer`);
8273
+ return number;
8274
+ }
8275
+
7585
8276
  // src/runtime.ts
8277
+ var SESSION_TEARDOWN_TIMEOUT_MS = 1e3;
7586
8278
  var ShadowMindRuntime = class {
7587
8279
  constructor(pi) {
7588
8280
  this.pi = pi;
7589
- this.batcher = new ReportBatcher(this.configStore.current.resultBatchWindowMs, (reports) => this.deliverReports(reports));
8281
+ this.batcher = new ReportBatcher(
8282
+ this.configStore.current.resultBatchWindowMs,
8283
+ (reports) => this.deliverReports(reports)
8284
+ );
7590
8285
  }
7591
8286
  agentDir = getAgentDir();
7592
8287
  configStore = new ConfigStore(this.agentDir);
7593
8288
  registry = new ShadowRegistry(this.agentDir);
7594
- entityStore = new EntityStore(this.registry, this.configStore.configPath);
8289
+ entityStore = new EntityStore(
8290
+ this.registry,
8291
+ this.configStore.configPath
8292
+ );
7595
8293
  runner = new ShadowRunner();
8294
+ usageStore = new UsageStore(this.agentDir);
7596
8295
  active = /* @__PURE__ */ new Map();
8296
+ finalResponseQueue = new FinalResponseQueue(
8297
+ {
8298
+ currentEpoch: () => this.epoch,
8299
+ maxParallel: () => this.configStore.current.maxParallelShadows,
8300
+ activeCount: () => this.active.size,
8301
+ activeShadowIds: () => new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
8302
+ launch: (pending) => this.launchShadow(
8303
+ pending.ctx,
8304
+ pending.shadow,
8305
+ pending.mainModel,
8306
+ pending.fullModelId,
8307
+ pending.context,
8308
+ pending.availableTools
8309
+ )
8310
+ }
8311
+ );
7597
8312
  recentEvents = [];
8313
+ recentRuns = [];
7598
8314
  batcher;
7599
8315
  sessionLifetime = new SessionLifetime();
7600
8316
  epoch = 0;
@@ -7603,10 +8319,16 @@ var ShadowMindRuntime = class {
7603
8319
  panelVisible = false;
7604
8320
  latestContext;
7605
8321
  diagnostics = [];
8322
+ sessionUsage = zeroUsage();
7606
8323
  shadowCount = 0;
8324
+ completedWithFinalText = false;
7607
8325
  random = Math.random;
7608
8326
  register() {
7609
- registerManagementTools(this.pi, this.entityStore, () => this.configStore.current);
8327
+ registerManagementTools(
8328
+ this.pi,
8329
+ this.entityStore,
8330
+ () => this.configStore.current
8331
+ );
7610
8332
  this.registerEvents();
7611
8333
  this.registerUi();
7612
8334
  }
@@ -7615,16 +8337,23 @@ var ShadowMindRuntime = class {
7615
8337
  this.sessionLifetime.activate();
7616
8338
  this.latestContext = ctx;
7617
8339
  this.modelCalls = 0;
8340
+ this.completedWithFinalText = false;
8341
+ this.sessionUsage = zeroUsage();
8342
+ this.recentRuns.length = 0;
7618
8343
  await this.configStore.initialize();
8344
+ await this.usageStore.initialize();
7619
8345
  this.random = createRandom(this.configStore.current.randomSeed);
7620
8346
  await this.registry.initialize();
7621
8347
  await this.refresh(ctx);
7622
- this.record("session-config", { randomSeed: this.configStore.current.randomSeed ?? "random" });
8348
+ this.record("session-config", {
8349
+ randomSeed: this.configStore.current.randomSeed ?? "random"
8350
+ });
7623
8351
  this.updateStatus(ctx);
7624
8352
  });
7625
8353
  this.pi.on("input", (event, ctx) => {
7626
8354
  this.latestContext = ctx;
7627
8355
  if (event.source === "extension") return;
8356
+ this.completedWithFinalText = false;
7628
8357
  this.epoch += 1;
7629
8358
  this.abortAll("new-user-input");
7630
8359
  });
@@ -7635,11 +8364,29 @@ var ShadowMindRuntime = class {
7635
8364
  this.pi.on("turn_end", async (event, ctx) => {
7636
8365
  this.latestContext = ctx;
7637
8366
  if (!shouldEvaluateHeartbeat(event.toolResults)) {
7638
- this.record("heartbeat-skipped", { reason: "no-tool-activity", modelCalls: this.modelCalls });
8367
+ this.record("heartbeat-skipped", {
8368
+ reason: "no-tool-activity",
8369
+ modelCalls: this.modelCalls
8370
+ });
7639
8371
  return;
7640
8372
  }
7641
8373
  await this.onHeartbeat(ctx);
7642
8374
  });
8375
+ this.pi.on("agent_end", (event, ctx) => {
8376
+ this.latestContext = ctx;
8377
+ this.completedWithFinalText = shouldEvaluateFinalResponse(event.messages);
8378
+ });
8379
+ this.pi.on("agent_settled", async (_event, ctx) => {
8380
+ this.latestContext = ctx;
8381
+ if (!this.completedWithFinalText) {
8382
+ this.record("final-response-skipped", {
8383
+ reason: "no-final-assistant-text"
8384
+ });
8385
+ return;
8386
+ }
8387
+ this.completedWithFinalText = false;
8388
+ await this.onFinalResponse(ctx);
8389
+ });
7643
8390
  this.pi.on("session_shutdown", async (event, ctx) => {
7644
8391
  this.latestContext = ctx;
7645
8392
  if (event.reason === "quit" && (ctx.mode === "print" || ctx.mode === "json")) {
@@ -7647,6 +8394,14 @@ var ShadowMindRuntime = class {
7647
8394
  }
7648
8395
  this.epoch += 1;
7649
8396
  this.abortAll("session-shutdown");
8397
+ if (this.active.size > 0) {
8398
+ const result = await waitForSettled({
8399
+ timeoutMs: SESSION_TEARDOWN_TIMEOUT_MS,
8400
+ isSettled: () => this.active.size === 0
8401
+ });
8402
+ if (!result.settled) this.active.clear();
8403
+ }
8404
+ await this.usageStore.flush();
7650
8405
  ctx.ui.setStatus("shadow-mind", void 0);
7651
8406
  ctx.ui.setWidget("shadow-mind-panel", void 0);
7652
8407
  this.sessionLifetime.deactivate();
@@ -7672,7 +8427,10 @@ var ShadowMindRuntime = class {
7672
8427
  }
7673
8428
  if (command === "status") {
7674
8429
  await this.refresh(ctx);
7675
- ctx.ui.notify(this.statusLines().join("\n"), this.diagnostics.length ? "warning" : "info");
8430
+ ctx.ui.notify(
8431
+ this.statusLines().join("\n"),
8432
+ this.diagnostics.length ? "warning" : "info"
8433
+ );
7676
8434
  } else if (command === "hide") {
7677
8435
  this.panelVisible = false;
7678
8436
  ctx.ui.setWidget("shadow-mind-panel", void 0);
@@ -7690,25 +8448,36 @@ var ShadowMindRuntime = class {
7690
8448
  this.setPaused(!this.paused, ctx);
7691
8449
  }
7692
8450
  });
7693
- this.pi.registerMessageRenderer("shadow-report", (message, _options, theme) => {
7694
- const content = typeof message.content === "string" ? message.content : "Shadow report";
7695
- const prefix = theme.fg("accent", "\u{1F419} shadow \xB7 ");
7696
- return new Text(`${prefix}${content}`, 0, 0);
7697
- });
8451
+ this.pi.registerMessageRenderer(
8452
+ "shadow-report",
8453
+ (message, _options, theme) => {
8454
+ const content = typeof message.content === "string" ? message.content : "Shadow report";
8455
+ const prefix = theme.fg("accent", "\u{1F419} shadow \xB7 ");
8456
+ return new Text(`${prefix}${content}`, 0, 0);
8457
+ }
8458
+ );
7698
8459
  }
7699
8460
  async onHeartbeat(ctx) {
7700
8461
  await this.refresh(ctx);
7701
8462
  if (this.paused || !ctx.model) {
7702
- this.record("heartbeat-skipped", { reason: this.paused ? "paused" : "no-model", modelCalls: this.modelCalls });
8463
+ this.record("heartbeat-skipped", {
8464
+ reason: this.paused ? "paused" : "no-model",
8465
+ modelCalls: this.modelCalls
8466
+ });
7703
8467
  return;
7704
8468
  }
7705
8469
  const snapshot = await this.registry.load();
7706
8470
  const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
7707
8471
  const decision = decideHeartbeat({
7708
8472
  heartbeatProbability: this.configStore.current.heartbeatProbability,
7709
- availableSlots: Math.max(0, this.configStore.current.maxParallelShadows - this.active.size),
8473
+ availableSlots: Math.max(
8474
+ 0,
8475
+ this.configStore.current.maxParallelShadows - this.active.size
8476
+ ),
7710
8477
  shadows: snapshot.shadows,
7711
- activeShadowIds: new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
8478
+ activeShadowIds: new Set(
8479
+ [...this.active.values()].map(({ shadow }) => shadow.id)
8480
+ ),
7712
8481
  mainModelId: fullModelId,
7713
8482
  random: this.random
7714
8483
  });
@@ -7716,19 +8485,76 @@ var ShadowMindRuntime = class {
7716
8485
  modelCalls: this.modelCalls,
7717
8486
  roll: decision.heartbeatRoll,
7718
8487
  candidates: decision.candidates,
7719
- activated: decision.activated.map(({ shadow, roll }) => ({ id: shadow.id, roll })),
8488
+ activated: decision.activated.map(({ shadow, roll }) => ({
8489
+ id: shadow.id,
8490
+ roll
8491
+ })),
7720
8492
  ...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
7721
8493
  ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
7722
8494
  });
7723
8495
  if (!decision.activated.length) return;
7724
- const context = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
7725
- const availableTools = new Set(this.pi.getAllTools().map((tool2) => tool2.name));
8496
+ const context = buildSessionContext(
8497
+ ctx.sessionManager.getEntries(),
8498
+ ctx.sessionManager.getLeafId()
8499
+ );
8500
+ const availableTools = new Set(
8501
+ this.pi.getAllTools().map((tool2) => tool2.name)
8502
+ );
7726
8503
  for (const { shadow } of decision.activated) {
7727
- this.launchShadow(ctx, shadow, ctx.model, fullModelId, context, availableTools);
8504
+ this.launchShadow(
8505
+ ctx,
8506
+ shadow,
8507
+ ctx.model,
8508
+ fullModelId,
8509
+ context,
8510
+ availableTools
8511
+ );
8512
+ }
8513
+ }
8514
+ async onFinalResponse(ctx) {
8515
+ await this.refresh(ctx);
8516
+ if (this.paused || !ctx.model) {
8517
+ this.record("final-response-skipped", {
8518
+ reason: this.paused ? "paused" : "no-model"
8519
+ });
8520
+ return;
7728
8521
  }
8522
+ const snapshot = await this.registry.load();
8523
+ const mainModel = ctx.model;
8524
+ const fullModelId = `${mainModel.provider}/${mainModel.id}`;
8525
+ const decision = decideFinalResponse({
8526
+ shadows: snapshot.shadows,
8527
+ mainModelId: fullModelId
8528
+ });
8529
+ this.record("final-response", {
8530
+ candidates: decision.candidates,
8531
+ activated: decision.activated.map(({ shadow }) => shadow.id),
8532
+ ...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
8533
+ ...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
8534
+ });
8535
+ if (!decision.activated.length) return;
8536
+ const context = buildSessionContext(
8537
+ ctx.sessionManager.getEntries(),
8538
+ ctx.sessionManager.getLeafId()
8539
+ );
8540
+ const availableTools = new Set(
8541
+ this.pi.getAllTools().map((tool2) => tool2.name)
8542
+ );
8543
+ this.finalResponseQueue.enqueue(
8544
+ decision.activated.map(({ shadow }) => ({
8545
+ epoch: this.epoch,
8546
+ shadowId: shadow.id,
8547
+ ctx,
8548
+ shadow,
8549
+ mainModel,
8550
+ fullModelId,
8551
+ context,
8552
+ availableTools
8553
+ }))
8554
+ );
7729
8555
  }
7730
8556
  launchShadow(ctx, shadow, mainModel, fullModelId, context, availableTools) {
7731
- const runId = randomUUID();
8557
+ const runId = randomUUID3();
7732
8558
  const runEpoch = this.epoch;
7733
8559
  const { tools, missing } = resolveShadowTools(shadow.tools, availableTools);
7734
8560
  this.active.set(runId, { shadow, epoch: runEpoch });
@@ -7747,6 +8573,7 @@ var ShadowMindRuntime = class {
7747
8573
  cwd: ctx.cwd,
7748
8574
  agentDir: this.agentDir,
7749
8575
  mainSystemPrompt: ctx.getSystemPrompt(),
8576
+ // SAFETY: buildSessionContext returns Pi message objects; the runner treats them as read-only generic records.
7750
8577
  messages: context.messages,
7751
8578
  mainModel,
7752
8579
  tools,
@@ -7762,12 +8589,29 @@ var ShadowMindRuntime = class {
7762
8589
  reason: "error",
7763
8590
  error: error instanceof Error ? error.message : String(error)
7764
8591
  });
8592
+ this.finalResponseQueue.slotAvailable();
7765
8593
  });
7766
8594
  }
7767
8595
  handleRunEnd(runId, shadow, result) {
8596
+ const activeRun = this.active.get(runId);
7768
8597
  this.active.delete(runId);
8598
+ const persisted = this.usageStore.add(result.usage);
8599
+ if (activeRun?.epoch !== this.epoch) return;
8600
+ this.sessionUsage = addUsage(this.sessionUsage, result.usage);
8601
+ this.recentRuns.push({
8602
+ shadowName: shadow.name,
8603
+ completedAt: (/* @__PURE__ */ new Date()).toISOString(),
8604
+ result
8605
+ });
8606
+ if (this.recentRuns.length > 5) this.recentRuns.shift();
7769
8607
  this.record("run-end", { runId, shadowId: shadow.id, ...result });
7770
- if (this.latestContext && this.sessionLifetime.isActive) this.updateStatus(this.latestContext);
8608
+ if (this.latestContext && this.sessionLifetime.isActive)
8609
+ this.updateStatus(this.latestContext);
8610
+ this.finalResponseQueue.slotAvailable();
8611
+ void persisted.then(() => {
8612
+ if (this.latestContext && this.sessionLifetime.isActive)
8613
+ this.updateStatus(this.latestContext);
8614
+ });
7771
8615
  }
7772
8616
  acceptReport(report) {
7773
8617
  if (report.epoch !== this.epoch) return;
@@ -7777,15 +8621,26 @@ var ShadowMindRuntime = class {
7777
8621
  const current = reports.filter((report) => report.epoch === this.epoch);
7778
8622
  if (!current.length) return;
7779
8623
  const content = formatReportBatch(current);
7780
- this.record("report-delivered", { runIds: current.map((report) => report.runId), count: current.length });
8624
+ this.record("report-delivered", {
8625
+ runIds: current.map((report) => report.runId),
8626
+ count: current.length
8627
+ });
7781
8628
  const idle = this.latestContext?.isIdle() ?? true;
7782
8629
  this.sessionLifetime.run(() => {
7783
- this.pi.sendMessage({
7784
- customType: "shadow-report",
7785
- content,
7786
- display: true,
7787
- details: { reports: current.map(({ shadowId, runId }) => ({ shadowId, runId })) }
7788
- }, { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" });
8630
+ this.pi.sendMessage(
8631
+ {
8632
+ customType: "shadow-report",
8633
+ content,
8634
+ display: true,
8635
+ details: {
8636
+ reports: current.map(({ shadowId, runId }) => ({
8637
+ shadowId,
8638
+ runId
8639
+ }))
8640
+ }
8641
+ },
8642
+ { triggerTurn: true, deliverAs: idle ? "followUp" : "steer" }
8643
+ );
7789
8644
  });
7790
8645
  }
7791
8646
  async refresh(ctx) {
@@ -7795,57 +8650,93 @@ var ShadowMindRuntime = class {
7795
8650
  this.shadowCount = registry.shadows.length;
7796
8651
  this.diagnostics = [
7797
8652
  ...config.error ? [`config: ${config.error}`] : [],
7798
- ...registry.diagnostics.map((item) => `${item.filePath}: ${item.message}`)
8653
+ ...registry.diagnostics.map(
8654
+ (item) => `${item.filePath}: ${item.message}`
8655
+ )
7799
8656
  ];
7800
8657
  this.updateStatus(ctx);
7801
8658
  }
7802
8659
  abortAll(reason) {
7803
8660
  this.runner.abortAll();
8661
+ this.finalResponseQueue.clear();
7804
8662
  this.batcher.clear();
7805
8663
  this.record("runs-aborted", { reason, count: this.active.size });
7806
8664
  }
7807
8665
  async drainHeadless(ctx) {
7808
8666
  if (this.active.size === 0 && !this.batcher.hasPending) return;
7809
8667
  const timeoutMs = this.configStore.current.headlessDrainTimeoutSeconds * 1e3;
7810
- this.record("headless-drain-start", { timeoutMs, active: this.active.size });
8668
+ this.record("headless-drain-start", {
8669
+ timeoutMs,
8670
+ active: this.active.size
8671
+ });
7811
8672
  const result = await waitForSettled({
7812
8673
  timeoutMs,
7813
8674
  isSettled: () => this.active.size === 0 && !this.batcher.hasPending && ctx.isIdle() && !ctx.hasPendingMessages()
7814
8675
  });
7815
- this.record(result.settled ? "headless-drain-complete" : "headless-drain-timeout", {
7816
- durationMs: result.durationMs,
7817
- active: this.active.size
7818
- });
8676
+ this.record(
8677
+ result.settled ? "headless-drain-complete" : "headless-drain-timeout",
8678
+ {
8679
+ durationMs: result.durationMs,
8680
+ active: this.active.size
8681
+ }
8682
+ );
7819
8683
  if (!result.settled) this.abortAll("headless-drain-timeout");
7820
8684
  }
7821
8685
  record(kind, data) {
7822
- const event = { at: (/* @__PURE__ */ new Date()).toISOString(), kind, epoch: this.epoch, data };
8686
+ const event = {
8687
+ at: (/* @__PURE__ */ new Date()).toISOString(),
8688
+ kind,
8689
+ epoch: this.epoch,
8690
+ data
8691
+ };
7823
8692
  this.recentEvents.push(event);
7824
8693
  if (this.recentEvents.length > 20) this.recentEvents.shift();
7825
- this.sessionLifetime.run(() => this.pi.appendEntry("shadow-mind-event", event));
8694
+ this.sessionLifetime.run(
8695
+ () => this.pi.appendEntry("shadow-mind-event", event)
8696
+ );
7826
8697
  }
7827
8698
  setPaused(paused, ctx) {
7828
8699
  this.paused = paused;
7829
8700
  if (paused) this.abortAll("paused");
7830
- ctx.ui.notify(paused ? "Shadow Mind paused" : "Shadow Mind resumed", "info");
8701
+ ctx.ui.notify(
8702
+ paused ? "Shadow Mind paused" : "Shadow Mind resumed",
8703
+ "info"
8704
+ );
7831
8705
  this.updateStatus(ctx);
7832
8706
  }
7833
8707
  updateStatus(ctx) {
7834
8708
  this.sessionLifetime.run(() => {
7835
- const warning = this.diagnostics.length || this.hasRecentRunErrors() ? " !" : "";
7836
- ctx.ui.setStatus("shadow-mind", this.paused ? `\u{1F419} Paused${warning}` : `\u{1F419} ${this.active.size}${warning}`);
7837
- if (this.panelVisible) ctx.ui.setWidget("shadow-mind-panel", this.statusLines(), { placement: "aboveEditor" });
8709
+ const diagnostics = this.usageStore.error ? [...this.diagnostics, `usage: ${this.usageStore.error}`] : this.diagnostics;
8710
+ const warning = diagnostics.length || this.hasRecentRunErrors() ? " !" : "";
8711
+ const usage = `${formatUsageTokens(this.sessionUsage.totalTokens)} \xB7 ${formatUsageCost(this.sessionUsage.cost.total)}`;
8712
+ ctx.ui.setStatus(
8713
+ "shadow-mind",
8714
+ this.paused ? `\u{1F419} Paused \xB7 ${usage}${warning}` : `\u{1F419} ${this.active.size} \xB7 ${usage}${warning}`
8715
+ );
8716
+ if (this.panelVisible)
8717
+ ctx.ui.setWidget("shadow-mind-panel", this.statusLines(), {
8718
+ placement: "aboveEditor"
8719
+ });
7838
8720
  });
7839
8721
  }
7840
8722
  hasRecentRunErrors() {
7841
- return this.recentEvents.slice(-3).some((event) => event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout"));
8723
+ return this.recentEvents.slice(-3).some(
8724
+ (event) => event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout")
8725
+ );
7842
8726
  }
7843
8727
  statusLines() {
7844
8728
  const config = this.configStore.current;
8729
+ const diagnostics = this.usageStore.error ? [...this.diagnostics, `usage: ${this.usageStore.error}`] : this.diagnostics;
7845
8730
  return [
7846
8731
  `\u{1F419} Shadow Mind \xB7 ${this.paused ? "paused" : "active"} \xB7 running ${this.active.size}/${config.maxParallelShadows}`,
7847
8732
  `heartbeat ${formatNumber(config.heartbeatProbability)} \xB7 batch ${config.resultBatchWindowMs}ms \xB7 timeout ${config.defaultShadowTimeoutSeconds}s \xB7 drain ${config.headlessDrainTimeoutSeconds}s \xB7 thinking ${config.defaultThinkingLevel}`,
7848
8733
  `definitions: ${this.shadowCount} valid \xB7 ${this.diagnostics.length} invalid`,
8734
+ formatUsageDetail("session", this.sessionUsage),
8735
+ `usage lifetime \xB7 ${formatUsageSummary(this.usageStore.current)}`,
8736
+ ...this.recentRuns.slice(-3).map(
8737
+ ({ shadowName, completedAt, result }) => `recent run \xB7 ${formatCompletedAt(completedAt)} \xB7 ${shadowName} \xB7 ${result.reason} \xB7 ${formatUsageSummary(result.usage)}`
8738
+ ),
8739
+ ...diagnostics.map((diagnostic) => `diagnostic: ${diagnostic}`),
7849
8740
  ...this.recentEvents.slice(-5).map((event) => {
7850
8741
  const failed = event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout");
7851
8742
  const detail = failed ? ` ${event.data?.error ?? event.data?.reason}` : "";
@@ -7858,11 +8749,22 @@ var ShadowMindRuntime = class {
7858
8749
  function resolveModel(ctx, fullId) {
7859
8750
  const separator = fullId.indexOf("/");
7860
8751
  if (separator <= 0 || separator === fullId.length - 1) return void 0;
7861
- return ctx.modelRegistry.find(fullId.slice(0, separator), fullId.slice(separator + 1));
8752
+ return ctx.modelRegistry.find(
8753
+ fullId.slice(0, separator),
8754
+ fullId.slice(separator + 1)
8755
+ );
7862
8756
  }
7863
8757
  function formatNumber(value) {
7864
8758
  return Number(value.toFixed(3)).toString();
7865
8759
  }
8760
+ function formatCompletedAt(completedAt) {
8761
+ return new Date(completedAt).toLocaleTimeString("en-GB", {
8762
+ hour: "2-digit",
8763
+ minute: "2-digit",
8764
+ second: "2-digit",
8765
+ hourCycle: "h23"
8766
+ });
8767
+ }
7866
8768
 
7867
8769
  // src/index.ts
7868
8770
  function shadowMindExtension(pi) {