pi-shadow-mind 0.1.22 → 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.
@@ -67,3 +67,9 @@
67
67
  - 发生:用户希望仅对特定主力模型(如 `gpt-6-astra`)关闭某 Shadow,而对其余模型保持启用。
68
68
  - 分析:此前仅支持全量 `*` 或显式枚举白名单,针对特定模型禁用必须穷举所有其他合法模型,配置维护成本高;应支持以 `!` 开头的排除规则,支持模型短名与 glob 通配,并在纯排除配置时默认全量模型候选。
69
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
 
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
@@ -7921,8 +7929,7 @@ function isStaleExtensionContextError(error) {
7921
7929
 
7922
7930
  // src/shadow-runner.ts
7923
7931
  import { mkdir as mkdir3 } from "node:fs/promises";
7924
- import { fileURLToPath } from "node:url";
7925
- import { basename as basename3, dirname as dirname2, join as join4, normalize, resolve } from "node:path";
7932
+ import { join as join5 } from "node:path";
7926
7933
  import { Type as Type2 } from "typebox";
7927
7934
  import {
7928
7935
  DefaultResourceLoader,
@@ -8136,6 +8143,90 @@ function compactJson(value) {
8136
8143
  }
8137
8144
  }
8138
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
+
8139
8230
  // src/shadow-runner.ts
8140
8231
  function resolveShadowTools(whitelist, available, defaults = DEFAULT_READ_TOOLS) {
8141
8232
  const builtin = /* @__PURE__ */ new Set(["report_to_main"]);
@@ -8147,7 +8238,6 @@ function resolveShadowTools(whitelist, available, defaults = DEFAULT_READ_TOOLS)
8147
8238
  }
8148
8239
  return { tools, missing };
8149
8240
  }
8150
- var SELF_PATH = normalize(resolve(fileURLToPath(import.meta.url)));
8151
8241
  function runReason(timedOut, reported, aborted, failed = false) {
8152
8242
  if (timedOut) return "timeout";
8153
8243
  if (reported) return "report";
@@ -8225,6 +8315,7 @@ var ShadowRunner = class {
8225
8315
  async bootstrapSession(request, model, controller, state, sessionManager) {
8226
8316
  const thinkingLevel = resolveRunThinkingLevel(model, request);
8227
8317
  const settingsManager = SettingsManager.create(request.cwd, request.agentDir);
8318
+ const isExcluded = createExtensionExcluder(request.config.excludedExtensions);
8228
8319
  const resourceLoader = new DefaultResourceLoader({
8229
8320
  cwd: request.cwd,
8230
8321
  agentDir: request.agentDir,
@@ -8236,9 +8327,14 @@ var ShadowRunner = class {
8236
8327
  appendSystemPrompt: [],
8237
8328
  agentsFilesOverride: () => ({ agentsFiles: [] }),
8238
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.
8239
8333
  extensionsOverride: (base) => ({
8240
8334
  ...base,
8241
- extensions: base.extensions.filter((extension) => !isSelfExtension(extension.resolvedPath))
8335
+ extensions: base.extensions.filter(
8336
+ (extension) => !isSelfExtension(extension.resolvedPath) && !isExcluded(extension.resolvedPath)
8337
+ )
8242
8338
  })
8243
8339
  });
8244
8340
  await resourceLoader.reload();
@@ -8331,7 +8427,7 @@ function createReportTool(onReport) {
8331
8427
  }
8332
8428
  async function createShadowSessionManager(request) {
8333
8429
  if (!request.shadow.debug) return SessionManager.inMemory(request.cwd);
8334
- const directory = join4(request.agentDir, "shadow-minds", "logs", request.shadow.id);
8430
+ const directory = join5(request.agentDir, "shadow-minds", "logs", request.shadow.id);
8335
8431
  await mkdir3(directory, { recursive: true });
8336
8432
  const manager = SessionManager.create(request.cwd, directory);
8337
8433
  manager.appendCustomEntry("shadow-mind-run", { runId: request.runId, epoch: request.epoch, shadowId: request.shadow.id });
@@ -8363,12 +8459,6 @@ function assertTrajectoryFits(model, messages) {
8363
8459
  throw new Error(`trajectory ~${estimated} tokens exceeds ${model.provider}/${model.id} context window (${model.contextWindow})`);
8364
8460
  }
8365
8461
  }
8366
- function isSelfExtension(candidate) {
8367
- const normalized = normalize(resolve(candidate));
8368
- if (normalized === SELF_PATH) return true;
8369
- const sourceDirectory = normalize(resolve(dirname2(SELF_PATH)));
8370
- return dirname2(normalized) === sourceDirectory && (basename3(normalized) === "index.ts" || basename3(normalized) === "index.js");
8371
- }
8372
8462
 
8373
8463
  // src/shutdown-drain.ts
8374
8464
  async function waitForSettled(options) {
@@ -8394,12 +8484,12 @@ async function waitForSettled(options) {
8394
8484
  // src/usage-store.ts
8395
8485
  import { randomUUID as randomUUID2 } from "node:crypto";
8396
8486
  import { mkdir as mkdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
8397
- import { dirname as dirname3, join as join6 } from "node:path";
8487
+ import { dirname as dirname3, join as join7 } from "node:path";
8398
8488
 
8399
8489
  // src/usage-file-lock.ts
8400
8490
  import { randomUUID } from "node:crypto";
8401
8491
  import { mkdir as mkdir4, readFile as readFile4, rename, rm, stat as stat2, utimes, writeFile as writeFile3 } from "node:fs/promises";
8402
- import { join as join5 } from "node:path";
8492
+ import { join as join6 } from "node:path";
8403
8493
  var USAGE_LOCK_OWNER_FILE = "owner";
8404
8494
  var USAGE_LOCK_OPERATION_DIRECTORY = ".operation";
8405
8495
  async function acquireUsageFileLock(usagePath, timing = {}) {
@@ -8436,7 +8526,7 @@ function startLease(lockPath, owner, timing) {
8436
8526
  return;
8437
8527
  }
8438
8528
  const now = /* @__PURE__ */ new Date();
8439
- await utimes(join5(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
8529
+ await utimes(join6(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
8440
8530
  }).catch(() => void 0);
8441
8531
  }, timing.heartbeatMs);
8442
8532
  timer.unref();
@@ -8450,7 +8540,7 @@ function startLease(lockPath, owner, timing) {
8450
8540
  async function recoverStaleUsageLock(lockPath, timing) {
8451
8541
  const observed = await readUsageLockState(lockPath);
8452
8542
  if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return;
8453
- const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8543
+ const operationPath = join6(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8454
8544
  if (!await claimUsageLockOperation(operationPath, timing)) return;
8455
8545
  try {
8456
8546
  const current = await readUsageLockState(lockPath);
@@ -8468,7 +8558,7 @@ async function recoverStaleUsageLock(lockPath, timing) {
8468
8558
  }
8469
8559
  }
8470
8560
  async function releaseUsageLock(lockPath, token, timing) {
8471
- const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8561
+ const operationPath = join6(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
8472
8562
  while (true) {
8473
8563
  const observed = await readUsageLockState(lockPath);
8474
8564
  if (!observed || observed.owner?.token !== token) return;
@@ -8523,7 +8613,7 @@ async function claimUsageLockOperation(operationPath, timing) {
8523
8613
  return false;
8524
8614
  }
8525
8615
  async function writeOwner(path, owner) {
8526
- await writeFile3(join5(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
8616
+ await writeFile3(join6(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
8527
8617
  encoding: "utf8",
8528
8618
  flag: "wx"
8529
8619
  });
@@ -8537,7 +8627,7 @@ async function readUsageLockState(lockPath) {
8537
8627
  throw error;
8538
8628
  }
8539
8629
  try {
8540
- const ownerPath = join5(lockPath, USAGE_LOCK_OWNER_FILE);
8630
+ const ownerPath = join6(lockPath, USAGE_LOCK_OWNER_FILE);
8541
8631
  const [raw, ownerFile] = await Promise.all([readFile4(ownerPath, "utf8"), stat2(ownerPath)]);
8542
8632
  return {
8543
8633
  owner: parseOwner(raw),
@@ -8595,7 +8685,7 @@ var UsageStore = class {
8595
8685
  initialization;
8596
8686
  pendingWrite = Promise.resolve();
8597
8687
  constructor(agentDir) {
8598
- this.usagePath = join6(agentDir, "shadow-minds", "usage.json");
8688
+ this.usagePath = join7(agentDir, "shadow-minds", "usage.json");
8599
8689
  }
8600
8690
  initialize() {
8601
8691
  this.initialization ??= this.load();