pi-shadow-mind 0.1.21 → 0.1.23

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.
@@ -61,3 +61,15 @@
61
61
  - 发生:PR #11 与 issue #10 记录到同一用户任务内的反复整改:终检报告触发 Main 修订后,新的最终回复再次触发同一 Shadow,缺少停止条件;heartbeat 报告接力也会持续唤醒整改轮。
62
62
  - 分析:跳过由 shadow-report 触发的修复轮会失去对修订结果的复核,统一预算又涉及整个调度层、不适合放进单个 Shadow 定义。因此停止条件放在单个 Shadow 的完成检查上:整批审查进入终态后才提交轮次,被新输入、新报告或会话切换失效的检查不消耗配额;超时或错误的运行同样提交一次尝试,避免对同一用户任务重复重试。存在待交付的 heartbeat 报告时先交付已知问题,避免在旧文本上启动终检并浪费有限的验收轮次。缺省与 0 都表示不限制,维持现有默认行为。
63
63
  - 改变:新增 `final_response_rounds` frontmatter 字段与调度过滤;独立的 FinalResponseBudget 拥有轮次语义、计数与重置,Runtime 只接线 Session 生命周期与整批完成回调;CompletionReview 增加完成回调,FinalResponseQueue 增加启动门禁,batcher 有待交付报告时优先刷新。heartbeat 接力、跨 Shadow 统一预算与 stop 后不自动 follow-up 仍留在 issue #10。
64
+
65
+ ## 2026-09-15 · active_for_models 支持排除语法与模型短名
66
+
67
+ - 发生:用户希望仅对特定主力模型(如 `gpt-6-astra`)关闭某 Shadow,而对其余模型保持启用。
68
+ - 分析:此前仅支持全量 `*` 或显式枚举白名单,针对特定模型禁用必须穷举所有其他合法模型,配置维护成本高;应支持以 `!` 开头的排除规则,支持模型短名与 glob 通配,并在纯排除配置时默认全量模型候选。
69
+ - 改变:`matchesModel` 区分正向与否定规则;命中任一排除规则立即过滤;纯排除列表默认继承 `*`;补充短名、通配、格式破坏与调度过滤回归。
70
+
71
+ ## 2026-09-21 · Shadow 会话不加载按会话记录的扩展
72
+
73
+ - 发生:同时安装 Run 归档扩展 pi-experiencev2 时,用户归档的 1098 个 Run 里有 381 个(35%)来自 Shadow 会话,缺摘要率 21.0%,高于用户会话的 9.3%;摘要成功的 301 条也按 Main 的目标撰写,再因为 Shadow 只回 `NOT_RELEVANT` 判成任务未完成,在归档列表里与用户 Run 混排。Shadow 会话不走 CLI 的 `session_start` / `session_shutdown`,这类扩展的关闭路径也不触发,归档 `writers` 表残留 26 条记录。
74
+ - 分析:Shadow 的结论本来就通过 `report_to_main` 进入 Main 会话记录,381 次运行只有 8 次产出报告,单独归档价值接近零。整体关闭扩展不可行,provider 扩展要提供模型。因此按名单排除,默认排除 `pi-experiencev2`,写 `[]` 恢复原状。匹配只能基于扩展文件路径:`DefaultResourceLoader` 先调用 `extensionsOverride`,再调用 `applyExtensionSourceInfo`,回调拿到的 `sourceInfo` 还是 `source: "local"` / `origin: "top-level"` 的占位值,`baseDir` 指向文件所在目录而非包根。包名改从文件向上最近的 `package.json` 读取,遇到 `node_modules` 或 home 目录即停,`index.js` 这类入口文件名不作为标识,避免一个通用词排除掉所有打包扩展。
75
+ - 改变:新增 `excluded_extensions` 配置与 `src/extension-filter.ts`(`isSelfExtension` 一并迁入),`bootstrapSession` 的 `extensionsOverride` 同时应用两个判据。实测对照:`excluded_extensions: []` 时归档 2 条 Run 且残留 1 条 writer,默认值下归档 1 条、writer 0 条,同一次运行的 Shadow debug 日志与 usage 证明 Shadow 确实执行过。
package/DESIGN.md CHANGED
@@ -47,6 +47,8 @@ Shadow 的 Markdown 定义持久存在,但运行实例不保留长期记忆。
47
47
 
48
48
  每个实例同时快照激活时 Main 的当前工作目录,并在整个临时 AgentSession 中保持不变。Shadow 的工具都以该目录为 `cwd`;Main 后续切换目录只影响新激活的 Shadow。
49
49
 
50
+ 临时 AgentSession 加载的扩展是 Main 的子集:去掉本插件自身,再去掉 `excluded_extensions` 列出的扩展。Shadow 需要 provider 扩展提供模型,因此不整体关闭扩展。排除项按扩展文件路径匹配,可用的标识是包名、包目录名、单文件扩展的文件名和绝对路径,都是精确匹配,不支持通配。
51
+
50
52
  ## 3. Shadow Mind 实体
51
53
 
52
54
  每个 Shadow Mind 使用一个 Markdown 文件描述。用户可以创建和调整它,主 Agent 也可以通过插件工具创建和调整它。
@@ -65,7 +67,7 @@ Shadow 定义统一从全局目录加载:
65
67
 
66
68
  该目录属于用户数据,不放入插件安装目录,也不随当前项目切换。插件级配置保存在 `config.json`;registry 只扫描目录顶层的 `.md` 文件,不读取 `config.json`,也不递归读取 `logs/`。
67
69
 
68
- `config.json` 保存默认 Shadow 模型、`default_thinking_level`、`heartbeat_probability`、`heartbeat_tools`、`max_parallel_shadows`、`default_shadow_timeout_seconds`、`headless_drain_timeout_seconds`、`result_batch_window_ms` 和可选的 `random_seed` 等全局调度配置。`default_shadow_model` 省略时,插件使用激活时的当前 Main 模型;用户也可以配置一个固定默认模型。`default_thinking_level` 的内置默认值为 `low`。
70
+ `config.json` 保存默认 Shadow 模型、`default_thinking_level`、`heartbeat_probability`、`heartbeat_tools`、`max_parallel_shadows`、`default_shadow_timeout_seconds`、`headless_drain_timeout_seconds`、`result_batch_window_ms`、`excluded_extensions` 和可选的 `random_seed` 等全局调度配置。`excluded_extensions` 的内置默认值是 `["pi-experiencev2"]`,写成 `[]` 即加载 Main 的全部扩展。`default_shadow_model` 省略时,插件使用激活时的当前 Main 模型;用户也可以配置一个固定默认模型。`default_thinking_level` 的内置默认值为 `low`。
69
71
 
70
72
  每次 heartbeat 判断或 final-response 调度前,插件检查并重新加载发生变化的 `config.json`。纯文本轮次不会进入 heartbeat,但 Main 的最终文字可以触发 final-response 检查。新配置只影响后续调度和新建实例;已经运行的 Shadow 继续使用启动时取得的配置快照。
71
73
 
@@ -111,7 +113,7 @@ frontmatter 包含以下运行字段:
111
113
  | `trigger` | 激活方式,可包含 `heartbeat`、`final_response` 或两者;默认 `[heartbeat]` |
112
114
  | `final_response_rounds` | 主 Agent 输出结束后的最大运行轮数。设为 `1` 时仅运行一次,避免反馈循环导致持续对话;缺省或 `0` 表示不限制 |
113
115
  | `activation_tools` | heartbeat 候选工具过滤;该 Main 轮次命中任意指定工具名即可,默认 `[]` 表示不限制;final_response 独立于此过滤 |
114
- | `active_for_models` | 适用于哪些 Main 模型;`"*"` 表示全部模型,省略时默认 `["*"]` |
116
+ | `active_for_models` | 适用于哪些 Main 模型;`"*"` 表示全部模型,省略时默认 `["*"]`;支持 `"!pattern"` 排除语法及通配符 |
115
117
  | `run_with_model` | Shadow 自己使用的模型;省略时使用插件默认模型 |
116
118
  | `thinking_level` | Shadow 使用的 thinking level;省略时使用插件默认值,再回退到 Main 会话当前生效等级 |
117
119
  | `timeout_seconds` | Shadow 单次运行超时;省略时使用插件默认超时 |
@@ -119,7 +121,7 @@ frontmatter 包含以下运行字段:
119
121
 
120
122
  `name` 只用于 `shadow-report` 和状态界面展示,不参与身份判断;省略时回退到最终解析出的 `id`。Markdown 正文就是 Shadow 的认知定义、长期职责和行为要求。
121
123
 
122
- `active_for_models` 绑定的是被观察的 Main 模型,`run_with_model` 则指定 Shadow 自己运行时使用的模型。匹配前由 Pi 将 Main 的别名或简写解析为完整 `provider/model-id`;模型过滤使用完整 ID 和精确值 `"*"` 进行匹配。模型选择优先级为:Shadow 的 `run_with_model` → 插件的 `default_shadow_model` → 激活时的当前 Main 模型。
124
+ `active_for_models` 绑定的是被观察的 Main 模型,`run_with_model` 则指定 Shadow 自己运行时使用的模型。匹配前由 Pi 将 Main 的别名或简写解析为完整 `provider/model-id`;模型过滤使用完整 ID、纯模型短名、精确值 `"*"` 及通配符进行匹配;支持以 `!` 开头的排除规则(如 `"!gpt-6-astra"` 或 `"!openai-codex/*"`),命中任一排除项即跳过该 Shadow;当列表仅包含排除规则时,默认正向范围为全量 `*`。模型选择优先级为:Shadow 的 `run_with_model` → 插件的 `default_shadow_model` → 激活时的当前 Main 模型。
123
125
 
124
126
  如果显式配置的 `run_with_model` 当前不存在、未认证或不可用,本次激活失败并写入轻量运行事件,不自动换用其他模型。只有省略该字段时才使用插件默认 Shadow 模型。
125
127
 
package/README.md CHANGED
@@ -97,6 +97,8 @@ On the first session start, the extension creates:
97
97
 
98
98
  No default Shadow Mind is created. The global runtime timeout defaults to 300 seconds, and individual Shadows may override it with `timeout_seconds`.
99
99
 
100
+ A Shadow session loads the same extensions as the main session, minus this plugin and anything listed in `excluded_extensions`. Extensions that archive or record whole sessions would otherwise file every Shadow run as one more user session, so `pi-experiencev2` is excluded by default. An entry is an exact identifier, not a pattern: a package name (`pi-experiencev2`, `@scope/name`), a package directory name, a single-file extension's file name, or an absolute path. Set `"excluded_extensions": []` to load everything the main session loads. Shadows need their model provider, so keep provider extensions off this list.
101
+
100
102
  Press `F6` or `Alt+S` to pause or resume Shadow Mind for the current session. On macOS, you may need `Fn+F6`; `Alt+S` corresponds to `Option+S` and depends on the terminal's Option key mapping. The paused footer reads `🐙 Paused` without a redundant zero count. Use `/shadow` to toggle the status panel, `/shadow status` for a summary, or `/shadow toggle`, `/shadow pause`, and `/shadow resume` for command-based control. In TUI mode, `/shadow reports` opens a bounded, scrollable viewer for the five most recently delivered reports, independent of the recent-run list. Use ↑/↓, PageUp/PageDown, or Home/End to scroll; Escape or `/shadow reports hide` closes it. The viewer shows a snapshot; reopen to see new deliveries. Report history is session-local and clears on session replacement or reload; persisted reports remain in the conversation. The status panel keeps its compact summary. Management tools can list, create, update, enable, disable, and delete Shadow Minds, as well as read or update the global configuration. Every write requires user confirmation.
101
103
 
102
104
  ## Using with DSH
package/README.zh-CN.md CHANGED
@@ -94,6 +94,8 @@ pi install npm:pi-shadow-mind
94
94
 
95
95
  扩展不会默认创建 Shadow Mind。全局默认运行超时为 300 秒,单个 Shadow 可以通过 `timeout_seconds` 覆盖。
96
96
 
97
+ Shadow 会话加载的扩展与 Main 相同,只去掉本插件和 `excluded_extensions` 里列出的扩展。按会话归档或记录的扩展会把每次 Shadow 运行也记成一条用户会话,因此默认排除 `pi-experiencev2`。每个条目是精确标识,不是通配:包名(`pi-experiencev2`、`@scope/name`)、包目录名、单文件扩展的文件名,或绝对路径。写 `"excluded_extensions": []` 就恢复加载 Main 的全部扩展。Shadow 需要自己的模型 provider,不要把 provider 扩展写进这个列表。
98
+
97
99
  按 `F6` 或 `Alt+S` 可以暂停或恢复当前 Session 的 Shadow Mind。macOS 上可能需要按 `Fn+F6`;`Alt+S` 对应 `Option+S`,是否生效取决于终端的 Option 键映射。暂停时底部状态显示为 `🐙 Paused`,不再显示没有信息量的零计数。使用 `/shadow` 显示或隐藏状态面板,`/shadow status` 查看摘要,也可以通过 `/shadow toggle`、`/shadow pause` 和 `/shadow resume` 控制状态。TUI 模式下,`/shadow reports` 直接打开限高、可滚动的查看器,展示最近交付的 5 份报告,独立于最近运行列表。使用 ↑/↓、PageUp/PageDown 或 Home/End 滚动,Escape 或 `/shadow reports hide` 关闭。查看器显示打开时的快照,重新打开可查看新报告。报告历史仅保留在当前 Session 内存中,切换会话或 reload 时清空;已持久化的报告仍可在对话记录中查看。状态面板保持简短摘要。管理工具可以查询、创建、更新、启用、禁用和删除 Shadow Mind,以及读取或修改全局配置。所有写操作都需要用户确认。
98
100
 
99
101
  ## 在 DSH 中使用
package/dist/index.js CHANGED
@@ -158,6 +158,7 @@ function isThinkingLevel(value) {
158
158
  }
159
159
 
160
160
  // src/config.ts
161
+ var DEFAULT_EXCLUDED_EXTENSIONS = ["pi-experiencev2"];
161
162
  var DEFAULT_CONFIG = {
162
163
  heartbeatProbability: 1 / 3,
163
164
  heartbeatTools: [],
@@ -165,6 +166,7 @@ var DEFAULT_CONFIG = {
165
166
  defaultShadowTimeoutSeconds: 300,
166
167
  headlessDrainTimeoutSeconds: 120,
167
168
  resultBatchWindowMs: 400,
169
+ excludedExtensions: DEFAULT_EXCLUDED_EXTENSIONS,
168
170
  defaultThinkingLevel: "low"
169
171
  };
170
172
  function parseConfig(input) {
@@ -178,6 +180,7 @@ function parseConfig(input) {
178
180
  const timeout = positiveNumber(value.default_shadow_timeout_seconds, DEFAULT_CONFIG.defaultShadowTimeoutSeconds, "default_shadow_timeout_seconds");
179
181
  const drainTimeout = positiveNumber(value.headless_drain_timeout_seconds, DEFAULT_CONFIG.headlessDrainTimeoutSeconds, "headless_drain_timeout_seconds");
180
182
  const windowMs = nonNegativeInteger(value.result_batch_window_ms, DEFAULT_CONFIG.resultBatchWindowMs, "result_batch_window_ms");
183
+ const excludedExtensions = cleanStringArray(value.excluded_extensions, DEFAULT_CONFIG.excludedExtensions, "excluded_extensions");
181
184
  const model = optionalNonEmptyString(value.default_shadow_model, "default_shadow_model");
182
185
  const randomSeed = optionalSeed(value.random_seed);
183
186
  const thinking = value.default_thinking_level ?? DEFAULT_CONFIG.defaultThinkingLevel;
@@ -191,6 +194,7 @@ function parseConfig(input) {
191
194
  defaultShadowTimeoutSeconds: timeout,
192
195
  headlessDrainTimeoutSeconds: drainTimeout,
193
196
  resultBatchWindowMs: windowMs,
197
+ excludedExtensions,
194
198
  defaultShadowModel: model,
195
199
  defaultThinkingLevel: thinking,
196
200
  randomSeed
@@ -204,6 +208,8 @@ function serializeConfig(config) {
204
208
  default_shadow_timeout_seconds: config.defaultShadowTimeoutSeconds,
205
209
  headless_drain_timeout_seconds: config.headlessDrainTimeoutSeconds,
206
210
  result_batch_window_ms: config.resultBatchWindowMs,
211
+ // Always written, including `[]`: omitting it would silently restore the default.
212
+ excluded_extensions: config.excludedExtensions,
207
213
  ...config.defaultShadowModel ? { default_shadow_model: config.defaultShadowModel } : {},
208
214
  default_thinking_level: config.defaultThinkingLevel,
209
215
  ...config.randomSeed !== void 0 ? { random_seed: config.randomSeed } : {}
@@ -7327,6 +7333,7 @@ function configWriteTool(store, getConfig) {
7327
7333
  Type.Number({ exclusiveMinimum: 0 })
7328
7334
  ),
7329
7335
  result_batch_window_ms: Type.Optional(Type.Integer({ minimum: 0 })),
7336
+ excluded_extensions: Type.Optional(Type.Array(Type.String())),
7330
7337
  default_shadow_model: Type.Optional(Type.String()),
7331
7338
  default_thinking_level: Type.Optional(THINKING),
7332
7339
  random_seed: Type.Optional(
@@ -7343,6 +7350,7 @@ function configWriteTool(store, getConfig) {
7343
7350
  default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
7344
7351
  headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
7345
7352
  result_batch_window_ms: raw.result_batch_window_ms ?? current.resultBatchWindowMs,
7353
+ excluded_extensions: raw.excluded_extensions ?? current.excludedExtensions,
7346
7354
  default_shadow_model: raw.default_shadow_model ?? current.defaultShadowModel,
7347
7355
  default_thinking_level: raw.default_thinking_level ?? current.defaultThinkingLevel,
7348
7356
  random_seed: raw.random_seed ?? current.randomSeed
@@ -7826,7 +7834,49 @@ function decideFinalResponse(options) {
7826
7834
  };
7827
7835
  }
7828
7836
  function matchesModel(shadow, fullModelId) {
7829
- return shadow.activeForModels.includes("*") || shadow.activeForModels.includes(fullModelId);
7837
+ if (!shadow.activeForModels || shadow.activeForModels.length === 0) {
7838
+ return false;
7839
+ }
7840
+ const negatives = [];
7841
+ const positives = [];
7842
+ for (const item of shadow.activeForModels) {
7843
+ const trimmed = item.trim();
7844
+ if (!trimmed) continue;
7845
+ if (trimmed.startsWith("!")) {
7846
+ const pattern = trimmed.slice(1).trim();
7847
+ if (pattern) negatives.push(pattern);
7848
+ } else {
7849
+ positives.push(trimmed);
7850
+ }
7851
+ }
7852
+ for (const neg of negatives) {
7853
+ if (modelPatternMatches(neg, fullModelId)) {
7854
+ return false;
7855
+ }
7856
+ }
7857
+ if (positives.length === 0) {
7858
+ return negatives.length > 0;
7859
+ }
7860
+ return positives.some((pos) => modelPatternMatches(pos, fullModelId));
7861
+ }
7862
+ function modelPatternMatches(pattern, fullModelId) {
7863
+ if (pattern === "*") return true;
7864
+ if (pattern === fullModelId) return true;
7865
+ const slashIndex = fullModelId.indexOf("/");
7866
+ const modelId = slashIndex !== -1 ? fullModelId.slice(slashIndex + 1) : fullModelId;
7867
+ if (!pattern.includes("/") && pattern === modelId) {
7868
+ return true;
7869
+ }
7870
+ if (pattern.includes("*")) {
7871
+ const regex = wildcardToRegex(pattern);
7872
+ if (regex.test(fullModelId)) return true;
7873
+ if (!pattern.includes("/") && regex.test(modelId)) return true;
7874
+ }
7875
+ return false;
7876
+ }
7877
+ function wildcardToRegex(pattern) {
7878
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
7879
+ return new RegExp(`^${escaped.replace(/\*/g, ".*")}$`);
7830
7880
  }
7831
7881
  function matchesActivationTools(shadow, executedTools) {
7832
7882
  if (shadow.activationTools.length === 0) {
@@ -7879,8 +7929,7 @@ function isStaleExtensionContextError(error) {
7879
7929
 
7880
7930
  // src/shadow-runner.ts
7881
7931
  import { mkdir as mkdir3 } from "node:fs/promises";
7882
- import { fileURLToPath } from "node:url";
7883
- import { basename as basename3, dirname as dirname2, join as join4, normalize, resolve } from "node:path";
7932
+ import { join as join5 } from "node:path";
7884
7933
  import { Type as Type2 } from "typebox";
7885
7934
  import {
7886
7935
  DefaultResourceLoader,
@@ -8094,6 +8143,90 @@ function compactJson(value) {
8094
8143
  }
8095
8144
  }
8096
8145
 
8146
+ // src/extension-filter.ts
8147
+ import { readFileSync as readFileSync2 } from "node:fs";
8148
+ import { homedir } from "node:os";
8149
+ import { basename as basename3, dirname as dirname2, join as join4, normalize, resolve } from "node:path";
8150
+ import { fileURLToPath } from "node:url";
8151
+ var SELF_PATH = normalize(resolve(fileURLToPath(import.meta.url)));
8152
+ var SELF_DIRECTORY = dirname2(SELF_PATH);
8153
+ var SELF_ENTRIES = /* @__PURE__ */ new Set(["index.ts", "index.js"]);
8154
+ function isSelfExtension(candidate) {
8155
+ const normalized = normalize(resolve(candidate));
8156
+ if (normalized === SELF_PATH) return true;
8157
+ return dirname2(normalized) === SELF_DIRECTORY && SELF_ENTRIES.has(basename3(normalized));
8158
+ }
8159
+ var PACKAGE_LOOKUP_DEPTH = 4;
8160
+ var GENERIC_ENTRY_FILE = /^index\.[^.]+$/;
8161
+ var packageNameCache = /* @__PURE__ */ new Map();
8162
+ function extensionIdentifiers(resolvedPath) {
8163
+ const normalized = normalize(resolve(resolvedPath));
8164
+ const identifiers = /* @__PURE__ */ new Set([normalized]);
8165
+ const owner = findPackage(dirname2(normalized));
8166
+ if (owner) {
8167
+ identifiers.add(owner.name);
8168
+ identifiers.add(basename3(owner.directory));
8169
+ identifiers.add(owner.directory);
8170
+ }
8171
+ const fileName = basename3(normalized);
8172
+ if (!GENERIC_ENTRY_FILE.test(fileName)) {
8173
+ identifiers.add(fileName);
8174
+ identifiers.add(fileName.replace(/\.[^.]+$/, ""));
8175
+ }
8176
+ return identifiers;
8177
+ }
8178
+ function createExtensionExcluder(entries) {
8179
+ const wanted = /* @__PURE__ */ new Set();
8180
+ for (const entry of entries) {
8181
+ const trimmed = entry.trim();
8182
+ if (!trimmed) continue;
8183
+ wanted.add(trimmed);
8184
+ wanted.add(stripScheme(trimmed));
8185
+ wanted.add(normalize(resolve(expandHome(trimmed))));
8186
+ }
8187
+ if (wanted.size === 0) return () => false;
8188
+ return (resolvedPath) => {
8189
+ for (const identifier of extensionIdentifiers(resolvedPath)) {
8190
+ if (wanted.has(identifier)) return true;
8191
+ }
8192
+ return false;
8193
+ };
8194
+ }
8195
+ function findPackage(directory) {
8196
+ if (packageNameCache.has(directory)) return packageNameCache.get(directory);
8197
+ const home = resolve(homedir());
8198
+ let current = directory;
8199
+ let found;
8200
+ for (let depth = 0; depth < PACKAGE_LOOKUP_DEPTH; depth += 1) {
8201
+ if (basename3(current) === "node_modules" || current === home) break;
8202
+ const name = readPackageName(current);
8203
+ if (name) {
8204
+ found = { name, directory: current };
8205
+ break;
8206
+ }
8207
+ const parent = dirname2(current);
8208
+ if (parent === current) break;
8209
+ current = parent;
8210
+ }
8211
+ packageNameCache.set(directory, found);
8212
+ return found;
8213
+ }
8214
+ function readPackageName(directory) {
8215
+ try {
8216
+ const parsed = JSON.parse(readFileSync2(join4(directory, "package.json"), "utf8"));
8217
+ const name = parsed.name;
8218
+ return typeof name === "string" && name.trim() ? name.trim() : void 0;
8219
+ } catch {
8220
+ return void 0;
8221
+ }
8222
+ }
8223
+ function stripScheme(source) {
8224
+ return source.replace(/^[a-z][a-z0-9+.-]+:/i, "");
8225
+ }
8226
+ function expandHome(entry) {
8227
+ return entry === "~" || entry.startsWith("~/") ? resolve(homedir(), entry.slice(2)) : entry;
8228
+ }
8229
+
8097
8230
  // src/shadow-runner.ts
8098
8231
  function resolveShadowTools(whitelist, available, defaults = DEFAULT_READ_TOOLS) {
8099
8232
  const builtin = /* @__PURE__ */ new Set(["report_to_main"]);
@@ -8105,7 +8238,6 @@ function resolveShadowTools(whitelist, available, defaults = DEFAULT_READ_TOOLS)
8105
8238
  }
8106
8239
  return { tools, missing };
8107
8240
  }
8108
- var SELF_PATH = normalize(resolve(fileURLToPath(import.meta.url)));
8109
8241
  function runReason(timedOut, reported, aborted, failed = false) {
8110
8242
  if (timedOut) return "timeout";
8111
8243
  if (reported) return "report";
@@ -8183,6 +8315,7 @@ var ShadowRunner = class {
8183
8315
  async bootstrapSession(request, model, controller, state, sessionManager) {
8184
8316
  const thinkingLevel = resolveRunThinkingLevel(model, request);
8185
8317
  const settingsManager = SettingsManager.create(request.cwd, request.agentDir);
8318
+ const isExcluded = createExtensionExcluder(request.config.excludedExtensions);
8186
8319
  const resourceLoader = new DefaultResourceLoader({
8187
8320
  cwd: request.cwd,
8188
8321
  agentDir: request.agentDir,
@@ -8194,9 +8327,14 @@ var ShadowRunner = class {
8194
8327
  appendSystemPrompt: [],
8195
8328
  agentsFilesOverride: () => ({ agentsFiles: [] }),
8196
8329
  skillsOverride: (base) => ({ skills: [], diagnostics: base.diagnostics }),
8330
+ // Drop this plugin itself plus every extension the user excluded, so
8331
+ // session-scoped extensions (archivers, recorders) never treat an internal
8332
+ // Shadow run as one more user session.
8197
8333
  extensionsOverride: (base) => ({
8198
8334
  ...base,
8199
- extensions: base.extensions.filter((extension) => !isSelfExtension(extension.resolvedPath))
8335
+ extensions: base.extensions.filter(
8336
+ (extension) => !isSelfExtension(extension.resolvedPath) && !isExcluded(extension.resolvedPath)
8337
+ )
8200
8338
  })
8201
8339
  });
8202
8340
  await resourceLoader.reload();
@@ -8289,7 +8427,7 @@ function createReportTool(onReport) {
8289
8427
  }
8290
8428
  async function createShadowSessionManager(request) {
8291
8429
  if (!request.shadow.debug) return SessionManager.inMemory(request.cwd);
8292
- const directory = join4(request.agentDir, "shadow-minds", "logs", request.shadow.id);
8430
+ const directory = join5(request.agentDir, "shadow-minds", "logs", request.shadow.id);
8293
8431
  await mkdir3(directory, { recursive: true });
8294
8432
  const manager = SessionManager.create(request.cwd, directory);
8295
8433
  manager.appendCustomEntry("shadow-mind-run", { runId: request.runId, epoch: request.epoch, shadowId: request.shadow.id });
@@ -8321,12 +8459,6 @@ function assertTrajectoryFits(model, messages) {
8321
8459
  throw new Error(`trajectory ~${estimated} tokens exceeds ${model.provider}/${model.id} context window (${model.contextWindow})`);
8322
8460
  }
8323
8461
  }
8324
- function isSelfExtension(candidate) {
8325
- const normalized = normalize(resolve(candidate));
8326
- if (normalized === SELF_PATH) return true;
8327
- const sourceDirectory = normalize(resolve(dirname2(SELF_PATH)));
8328
- return dirname2(normalized) === sourceDirectory && (basename3(normalized) === "index.ts" || basename3(normalized) === "index.js");
8329
- }
8330
8462
 
8331
8463
  // src/shutdown-drain.ts
8332
8464
  async function waitForSettled(options) {
@@ -8352,12 +8484,12 @@ async function waitForSettled(options) {
8352
8484
  // src/usage-store.ts
8353
8485
  import { randomUUID as randomUUID2 } from "node:crypto";
8354
8486
  import { mkdir as mkdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
8355
- import { dirname as dirname3, join as join6 } from "node:path";
8487
+ import { dirname as dirname3, join as join7 } from "node:path";
8356
8488
 
8357
8489
  // src/usage-file-lock.ts
8358
8490
  import { randomUUID } from "node:crypto";
8359
8491
  import { mkdir as mkdir4, readFile as readFile4, rename, rm, stat as stat2, utimes, writeFile as writeFile3 } from "node:fs/promises";
8360
- import { join as join5 } from "node:path";
8492
+ import { join as join6 } from "node:path";
8361
8493
  var USAGE_LOCK_OWNER_FILE = "owner";
8362
8494
  var USAGE_LOCK_OPERATION_DIRECTORY = ".operation";
8363
8495
  async function acquireUsageFileLock(usagePath, timing = {}) {
@@ -8394,7 +8526,7 @@ function startLease(lockPath, owner, timing) {
8394
8526
  return;
8395
8527
  }
8396
8528
  const now = /* @__PURE__ */ new Date();
8397
- await utimes(join5(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
8529
+ await utimes(join6(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
8398
8530
  }).catch(() => void 0);
8399
8531
  }, timing.heartbeatMs);
8400
8532
  timer.unref();
@@ -8408,7 +8540,7 @@ function startLease(lockPath, owner, timing) {
8408
8540
  async function recoverStaleUsageLock(lockPath, timing) {
8409
8541
  const observed = await readUsageLockState(lockPath);
8410
8542
  if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return;
8411
- const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8543
+ const operationPath = join6(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8412
8544
  if (!await claimUsageLockOperation(operationPath, timing)) return;
8413
8545
  try {
8414
8546
  const current = await readUsageLockState(lockPath);
@@ -8426,7 +8558,7 @@ async function recoverStaleUsageLock(lockPath, timing) {
8426
8558
  }
8427
8559
  }
8428
8560
  async function releaseUsageLock(lockPath, token, timing) {
8429
- const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8561
+ const operationPath = join6(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8430
8562
  while (true) {
8431
8563
  const observed = await readUsageLockState(lockPath);
8432
8564
  if (!observed || observed.owner?.token !== token) return;
@@ -8481,7 +8613,7 @@ async function claimUsageLockOperation(operationPath, timing) {
8481
8613
  return false;
8482
8614
  }
8483
8615
  async function writeOwner(path, owner) {
8484
- await writeFile3(join5(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
8616
+ await writeFile3(join6(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
8485
8617
  encoding: "utf8",
8486
8618
  flag: "wx"
8487
8619
  });
@@ -8495,7 +8627,7 @@ async function readUsageLockState(lockPath) {
8495
8627
  throw error;
8496
8628
  }
8497
8629
  try {
8498
- const ownerPath = join5(lockPath, USAGE_LOCK_OWNER_FILE);
8630
+ const ownerPath = join6(lockPath, USAGE_LOCK_OWNER_FILE);
8499
8631
  const [raw, ownerFile] = await Promise.all([readFile4(ownerPath, "utf8"), stat2(ownerPath)]);
8500
8632
  return {
8501
8633
  owner: parseOwner(raw),
@@ -8553,7 +8685,7 @@ var UsageStore = class {
8553
8685
  initialization;
8554
8686
  pendingWrite = Promise.resolve();
8555
8687
  constructor(agentDir) {
8556
- this.usagePath = join6(agentDir, "shadow-minds", "usage.json");
8688
+ this.usagePath = join7(agentDir, "shadow-minds", "usage.json");
8557
8689
  }
8558
8690
  initialize() {
8559
8691
  this.initialization ??= this.load();
@@ -8919,10 +9051,7 @@ var ShadowMindRuntime = class {
8919
9051
  const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
8920
9052
  const decision = decideHeartbeat({
8921
9053
  heartbeatProbability: config.heartbeatProbability,
8922
- availableSlots: Math.max(
8923
- 0,
8924
- config.maxParallelShadows - this.active.size
8925
- ),
9054
+ availableSlots: Math.max(0, config.maxParallelShadows - this.active.size),
8926
9055
  shadows: snapshot.shadows,
8927
9056
  activeShadowIds: new Set(
8928
9057
  [...this.active.values()].map(({ shadow }) => shadow.id)