@zhushanwen/pi-ext-guards 0.4.0 → 0.4.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-ext-guards",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Shared runtime-environment guards for Pi extensions — oncePerProcess(key, fn) dedupes cross-session side-effect operations against pi factory re-invocation and handler accumulation (shared library, not a Pi extension, zero dependencies).",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -1,11 +1,12 @@
1
1
  // src/__tests__/ext-guards.test.ts
2
2
  //
3
- // oncePerProcess 单测——验收条款逐条对应(u-guards-pkg ①):
3
+ // oncePerProcess 单测——验收条款逐条对应(u-guards-pkg ①;失败语义按加固审查裁决
4
+ // 修订为「失败不缓存、下次调用可重试」):
4
5
  // a. key 隔离去重:不同 key 互不影响,各自首次调用执行
5
6
  // b. 同 key 双调 fn 仅执行一次
6
- // c. fn 抛错不释放 key:同 key 再调不再执行(每进程至多一次的字面语义)
7
+ // c. fn 抛错不吞不包装,且不缓存——同 key 再调重新执行(可重试)
7
8
  // d. 结果缓存形态:值原样重放(严格同一引用)、Promise 实例重放
8
- // (含 rejected Promise 缓存——不因 rejection 释放 key)
9
+ // e. rejected Promise:落定后释放 key,下次调用重新执行
9
10
  //
10
11
  // 模块级 Map 是被测状态:测试间共享,每个用例用全文件唯一的 key 隔离。
11
12
 
@@ -37,21 +38,68 @@ describe("oncePerProcess", () => {
37
38
  expect(fnB).toHaveBeenCalledTimes(1);
38
39
  });
39
40
 
40
- it("fn 同步抛错:错误原样上抛(不吞不包装),key 不释放——同 key 再调不再执行且重抛同一错误", () => {
41
+ it("fn 同步抛错:错误原样上抛(不吞不包装),不写缓存——同 key 再调重新执行(可重试)", () => {
41
42
  const boom = new Error("reap failed");
42
- const failing = vi.fn(() => {
43
- throw boom;
43
+ let attempts = 0;
44
+ const fn = vi.fn(() => {
45
+ attempts += 1;
46
+ if (attempts === 1) throw boom;
47
+ return "recovered";
44
48
  });
45
49
 
46
- expect(() => oncePerProcess("dedupe:throws", failing)).toThrow(boom);
50
+ expect(() => oncePerProcess("dedupe:throws-retry", fn)).toThrow(boom);
51
+ expect(fn).toHaveBeenCalledTimes(1);
52
+
53
+ // 失败不缓存:下次同 key 调用重新执行 fn,成功结果照常缓存
54
+ expect(oncePerProcess("dedupe:throws-retry", fn)).toBe("recovered");
55
+ expect(fn).toHaveBeenCalledTimes(2);
56
+ // 成功后去重恢复:不再执行
57
+ expect(oncePerProcess("dedupe:throws-retry", fn)).toBe("recovered");
58
+ expect(fn).toHaveBeenCalledTimes(2);
59
+ });
60
+
61
+ it("async fn 抛错:rejected Promise 落定后释放 key——下次调用重新执行(可重试)", async () => {
62
+ const boom = new Error("async reap failed");
63
+ let attempts = 0;
64
+ const fn = vi.fn(async () => {
65
+ attempts += 1;
66
+ if (attempts === 1) throw boom;
67
+ return "recovered";
68
+ });
47
69
 
48
- // 再调换一个「本会成功」的 fn:若 key 被释放,它会执行并返回——这是本条
49
- // 的核心断言(失败不重置,双跑窗口不重新打开)
50
- const shouldNeverRun = vi.fn(() => "second attempt");
51
- expect(() => oncePerProcess("dedupe:throws", shouldNeverRun)).toThrow(boom);
70
+ const first = oncePerProcess("dedupe:async-rejected-retry", fn);
71
+ // 先 attach 断言再消费,避免 unhandledRejection
72
+ await expect(first).rejects.toThrow(boom);
73
+ expect(fn).toHaveBeenCalledTimes(1);
52
74
 
53
- expect(failing).toHaveBeenCalledTimes(1);
54
- expect(shouldNeverRun).not.toHaveBeenCalled();
75
+ // rejection 已落定、key 已释放:下次调用重新执行 fn
76
+ const second = oncePerProcess("dedupe:async-rejected-retry", fn);
77
+ expect(second).not.toBe(first);
78
+ await expect(second).resolves.toBe("recovered");
79
+ expect(fn).toHaveBeenCalledTimes(2);
80
+ });
81
+
82
+ it("async fn:rejection 落定前的同 key 双调仍重放同一实例(落定后释放)", async () => {
83
+ const boom = new Error("async reap failed");
84
+ const fn = vi.fn(async () => {
85
+ throw boom;
86
+ });
87
+
88
+ const first = oncePerProcess("dedupe:async-inflight", fn);
89
+ // 落定前第二次调用:同一 Promise 实例(在飞去重不双跑)
90
+ const second = oncePerProcess("dedupe:async-inflight", fn);
91
+ expect(second).toBe(first);
92
+ // 先 attach 断言再消费,避免 unhandledRejection
93
+ const firstRejection = expect(first).rejects.toThrow(boom);
94
+ await firstRejection;
95
+ await Promise.resolve(); // 让释放 handler 的微任务走完
96
+ expect(fn).toHaveBeenCalledTimes(1);
97
+
98
+ // 落定后第三次调用:key 已释放,fn 重新执行
99
+ const third = oncePerProcess("dedupe:async-inflight", fn);
100
+ expect(third).not.toBe(first);
101
+ await expect(third).rejects.toThrow(boom);
102
+ expect(fn).toHaveBeenCalledTimes(2);
55
103
  });
56
104
 
57
105
  it("结果缓存形态:对象返回值严格同一引用(重放非重新求值)", () => {
@@ -78,20 +126,14 @@ describe("oncePerProcess", () => {
78
126
  expect(fn).toHaveBeenCalledTimes(1);
79
127
  });
80
128
 
81
- it("async fn 抛错:rejected Promise 被缓存——同 key 再调重放同一实例、fn 不再执行", async () => {
82
- const boom = new Error("async reap failed");
83
- const fn = vi.fn(async () => {
84
- throw boom;
85
- });
129
+ it("fn 返回 undefined:值仍被缓存(不因 undefined 误判未执行)", () => {
130
+ const fn = vi.fn(() => undefined);
86
131
 
87
- const first = oncePerProcess("dedupe:async-rejected", fn);
88
- // 先 attach 断言再消费,避免 unhandledRejection
89
- const firstRejection = expect(first).rejects.toThrow(boom);
90
- const second = oncePerProcess("dedupe:async-rejected", fn);
132
+ const first = oncePerProcess("dedupe:undefined-value", fn);
133
+ const second = oncePerProcess("dedupe:undefined-value", fn);
91
134
 
92
- expect(second).toBe(first);
93
- await firstRejection;
94
- await expect(second).rejects.toThrow(boom);
135
+ expect(first).toBeUndefined();
136
+ expect(second).toBeUndefined();
95
137
  expect(fn).toHaveBeenCalledTimes(1);
96
138
  });
97
139
  });
@@ -3,7 +3,7 @@
3
3
  // guardStaleCtx 单测——验收条款逐条对应(crash-resilience u1-ext-guard):
4
4
  // a. 前置代际检查:isCtxStale=true → fn 不执行、stale 静默降级(返回 undefined)
5
5
  // b. 非 stale 错误原样上抛(守卫不吞真实 bug,同步 throw 与 async rejection 双面)
6
- // c. onStale 回调:前置命中无参调用;文案分诊命中携带错误调用;缺省走 XYZ_AGENT_DEBUG=1 stderr
6
+ // c. onStale 回调:前置命中无参调用;文案分诊命中携带错误调用;缺省走 TAIJI_AGENT_DEBUG=1 stderr
7
7
  // d. 文案子串匹配:STALE_CTX_MARKER 命中即静默(isCtxStale=false 时文案兜底仍生效)
8
8
  // e. async fn:stale rejection → resolve undefined;非 stale rejection → 原样 reject
9
9
  // f. 正常路径:返回值/resolve 值原样透传(守卫不改变正常路径,A2 负面验证的单测面)
@@ -140,8 +140,8 @@ describe("guardStaleCtx async fn(F2 形态异步面,scheduler tick 迁移形
140
140
  });
141
141
 
142
142
  describe("guardStaleCtx onStale 缺省(默认 debug 日志)", () => {
143
- it("XYZ_AGENT_DEBUG=1:stderr 写入守卫降级行(含 label 与错误文案)", () => {
144
- vi.stubEnv("XYZ_AGENT_DEBUG", "1");
143
+ it("TAIJI_AGENT_DEBUG=1:stderr 写入守卫降级行(含 label 与错误文案)", () => {
144
+ vi.stubEnv("TAIJI_AGENT_DEBUG", "1");
145
145
  const stderr = vi.spyOn(process.stderr, "write").mockReturnValue(true);
146
146
 
147
147
  guardStaleCtx(() => {
@@ -154,8 +154,8 @@ describe("guardStaleCtx onStale 缺省(默认 debug 日志)", () => {
154
154
  expect(line).toContain(STALE_CTX_MARKER);
155
155
  });
156
156
 
157
- it("XYZ_AGENT_DEBUG 未设:静默(不写 stderr)", () => {
158
- vi.stubEnv("XYZ_AGENT_DEBUG", "");
157
+ it("TAIJI_AGENT_DEBUG 未设:静默(不写 stderr)", () => {
158
+ vi.stubEnv("TAIJI_AGENT_DEBUG", "");
159
159
  const stderr = vi.spyOn(process.stderr, "write").mockReturnValue(true);
160
160
 
161
161
  guardStaleCtx(() => {
@@ -59,14 +59,14 @@ describe("isEnoentError", () => {
59
59
  });
60
60
 
61
61
  describe("isSubagentProcess", () => {
62
- it("XYZ_AGENT_SUBAGENT=1 命中(引擎 spawn 链恒注入的现行标记)", () => {
63
- expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "1" })).toBe(true);
62
+ it("TAIJI_AGENT_SUBAGENT=1 命中(引擎 spawn 链恒注入的现行标记)", () => {
63
+ expect(isSubagentProcess({ TAIJI_AGENT_SUBAGENT: "1" })).toBe(true);
64
64
  });
65
65
 
66
66
  it("值非 \"1\" 不命中(标记必须是引擎注入的字面 \"1\")", () => {
67
- expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "0" })).toBe(false);
68
- expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "" })).toBe(false);
69
- expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "true" })).toBe(false);
67
+ expect(isSubagentProcess({ TAIJI_AGENT_SUBAGENT: "0" })).toBe(false);
68
+ expect(isSubagentProcess({ TAIJI_AGENT_SUBAGENT: "" })).toBe(false);
69
+ expect(isSubagentProcess({ TAIJI_AGENT_SUBAGENT: "true" })).toBe(false);
70
70
  });
71
71
 
72
72
  it("缺失不命中(顶层会话 → 消费方不降级)", () => {
@@ -74,28 +74,28 @@ describe("isSubagentProcess", () => {
74
74
  });
75
75
 
76
76
  it("env 参数注入隔离:传入的 env 与宿主 process.env 无关(宿主即使带标记,空 env 也不命中)", () => {
77
- const saved = process.env.XYZ_AGENT_SUBAGENT;
77
+ const saved = process.env.TAIJI_AGENT_SUBAGENT;
78
78
  try {
79
- process.env.XYZ_AGENT_SUBAGENT = "1";
79
+ process.env.TAIJI_AGENT_SUBAGENT = "1";
80
80
  // 显式传 env:宿主的标记不渗透,判据只看参数
81
81
  expect(isSubagentProcess({})).toBe(false);
82
- expect(isSubagentProcess({ XYZ_AGENT_SUBAGENT: "1" })).toBe(true);
82
+ expect(isSubagentProcess({ TAIJI_AGENT_SUBAGENT: "1" })).toBe(true);
83
83
  } finally {
84
- if (saved === undefined) delete process.env.XYZ_AGENT_SUBAGENT;
85
- else process.env.XYZ_AGENT_SUBAGENT = saved;
84
+ if (saved === undefined) delete process.env.TAIJI_AGENT_SUBAGENT;
85
+ else process.env.TAIJI_AGENT_SUBAGENT = saved;
86
86
  }
87
87
  });
88
88
 
89
89
  it("缺省参数读 process.env(不传 env 时以宿主环境为判据)", () => {
90
- const saved = process.env.XYZ_AGENT_SUBAGENT;
90
+ const saved = process.env.TAIJI_AGENT_SUBAGENT;
91
91
  try {
92
- process.env.XYZ_AGENT_SUBAGENT = "1";
92
+ process.env.TAIJI_AGENT_SUBAGENT = "1";
93
93
  expect(isSubagentProcess()).toBe(true);
94
- delete process.env.XYZ_AGENT_SUBAGENT;
94
+ delete process.env.TAIJI_AGENT_SUBAGENT;
95
95
  expect(isSubagentProcess()).toBe(false);
96
96
  } finally {
97
- if (saved === undefined) delete process.env.XYZ_AGENT_SUBAGENT;
98
- else process.env.XYZ_AGENT_SUBAGENT = saved;
97
+ if (saved === undefined) delete process.env.TAIJI_AGENT_SUBAGENT;
98
+ else process.env.TAIJI_AGENT_SUBAGENT = saved;
99
99
  }
100
100
  });
101
101
 
package/src/index.ts CHANGED
@@ -11,10 +11,10 @@
11
11
  // reaper 双跑即此形态)。本包把这类「pi 运行环境隐式坑」的守卫集中一处,业务
12
12
  // extension 引入即用,防线不再散落各包内联。
13
13
 
14
- /** 一次执行的结果记录:正常返回记值、抛错记错误——两者都不释放 key。 */
15
- type ExecutionRecord =
16
- | { readonly outcome: "returned"; readonly value: unknown }
17
- | { readonly outcome: "threw"; readonly error: unknown };
14
+ /** 一次执行的结果记录:只缓存成功结果;失败(同步抛错 / Promise rejection)不缓存,下次调用可重试。 */
15
+ interface ExecutionRecord {
16
+ readonly value: unknown;
17
+ }
18
18
 
19
19
  // 「按进程去重」的物理载体 = 模块级 Map。有效前提:同一进程内模块级状态跨 factory
20
20
  // 二调持久——pi extension 缓存按 cwd 失效时 factory 重跑,但 jiti/Node 模块缓存同图
@@ -23,22 +23,24 @@ type ExecutionRecord =
23
23
  const executions = new Map<string, ExecutionRecord>();
24
24
 
25
25
  /**
26
- * 进程内按 key 去重地执行 fn:同一 key 至多执行一次,后续调用重放首次结果。
26
+ * 进程内按 key 去重地执行 fn:同一 key 至多执行一次**成功**,后续调用重放首次成功结果。
27
27
  *
28
- * 语义细节(设计 §3.2 D3 守卫粒度段 + §3.3 D3 守卫语义):
28
+ * 语义细节(设计 §3.2 D3 守卫粒度段 + §3.3 D3 守卫语义;失败语义按加固审查裁决修订
29
+ * ——「恢复幂等,下次启动重试」承诺优先,见下方失败段):
29
30
  *
30
31
  * - **结果缓存形态(非跳过)**:首次调用执行 fn 并缓存结果;后续同 key 调用不执行
31
32
  * fn,返回首次的返回值——原样重放:对象返回严格同一引用(toBe 级,非重新求值);
32
- * fn 返回 Promise 时重放同一实例,因此 **rejected Promise 同样被缓存**、不因
33
- * rejection 释放 key。
34
- * - **fn 抛错不吞、key 不释放**:同步抛错原样上抛(守卫不捕获、不包装),同时记录
35
- * 该错误——后续同 key 调用重抛同一错误实例、不再执行 fn。验收条款「fn 抛错不阻断
36
- * 后续 handler」的准确语义是**守卫不吞 fn 的错误**(调用方 catch 守卫调用即可继续
37
- * handler 后续逻辑,先例:base-tool-enhance runSessionStartMaintenance 的 try/catch
38
- * 形态),而非把 key 释放给二次执行——失败释放会让 factory 二调双跑窗口重新打开
39
- * (本次事故形态),与 u-bte-guard 内联 flag 先例「reap 抛错不重置 flag」同语义。
40
- * 失败重试不归守卫:需要兜底的场景由宿主的其他触发面承接(该先例的失败兜底即交
41
- * runtime 收殓触发面 B)。
33
+ * fn 返回 Promise 时重放同一实例。
34
+ * - **失败不缓存、可重试**:fn 同步抛错原样上抛(守卫不捕获、不包装)且**不写缓存**;
35
+ * fn 返回的 Promise rejection 落定后释放 key(仅释放本条失败记录,期间同 key 重试
36
+ * 成功的新记录不受影响)——下次同 key 调用重新执行 fn。守卫不自动重试,重试节奏
37
+ * 由调用方决定(宿主的其他触发面承接,如下一个 session_start)。代价(有意接受):
38
+ * 首次失败到重试成功之间,factory 二调的双跑窗口重新打开——重试的是**失败过**的
39
+ * 操作,成功即缓存、窗口随即关闭;对幂等的进程级维护(recoverCrashedRuns「恢复
40
+ * 幂等,下次启动重试」、配置迁移、索引重建等)这正是期望行为。验收条款「fn 抛错
41
+ * 不阻断后续 handler」的准确语义仍是**守卫不吞 fn 的错误**(调用方 catch 守卫调用
42
+ * 即可继续 handler 后续逻辑,先例:base-tool-enhance runSessionStartMaintenance
43
+ * 的 try/catch 形态)。
42
44
  * - **粒度边界**:只包「跨 session 副作用操作」(进程级全局维护类,正确频率就是每
43
45
  * 进程至多一次)。session 级幂等操作(如 pending 对账——读当前 session 的 entries
44
46
  * 与 registry,appendEntry 幂等)必须保持每 session_start 执行,不要挂本守卫;需要
@@ -47,29 +49,40 @@ const executions = new Map<string, ExecutionRecord>();
47
49
  * (如 "base-tool-enhance:reap")避免撞 key。
48
50
  *
49
51
  * @param key 去重键(进程内全局,建议带包名前缀)
50
- * @param fn 无参函数——首次调用时求值一次,后续调用不再执行;需要上下文(pi/ctx)
51
- * 的调用方在闭包里捕获,被闭包捕获的是首次调用处的值(这正是「每进程至多一次」
52
- * 的字面语义)
52
+ * @param fn 无参函数——首次调用时求值一次(直到成功),后续调用不再执行;需要上下文
53
+ * (pi/ctx)的调用方在闭包里捕获,被闭包捕获的是首次调用处的值(这正是「每进程
54
+ * 至多一次成功」的字面语义)
53
55
  * @returns 首次执行的返回值(后续调用重放同一结果;async fn 重放同一 Promise 实例)
54
56
  */
55
57
  export function oncePerProcess<T>(key: string, fn: () => T): T {
56
58
  const hit = executions.get(key);
57
59
  if (hit !== undefined) {
58
- if (hit.outcome === "threw") {
59
- throw hit.error;
60
- }
61
60
  // 同 key 的值由首次调用的泛型参数化记录,模块级 Map 无法按 key 参数化类型,
62
61
  // 断言仅收窄回该泛型(构造点与重放点同函数,无跨来源混装)。
63
62
  return hit.value as T;
64
63
  }
65
- try {
66
- const result = fn();
67
- executions.set(key, { outcome: "returned", value: result });
68
- return result;
69
- } catch (error) {
70
- executions.set(key, { outcome: "threw", error });
71
- throw error;
64
+ const result = fn();
65
+ // 只缓存成功结果:同步抛错不经过本行(原样上抛 + 无缓存,下次调用可重试);
66
+ // Promise 的 rejection 在落定后经下方 handler 释放 key(同样可重试)。
67
+ executions.set(key, { value: result });
68
+ if (isPromiseLike(result)) {
69
+ // 内部挂 rejection 释放 handler(同时消除本条 rejection 的 unhandled 面——
70
+ // 调用方是否自行接 rejection 均安全)。仅当缓存里仍是本条失败记录时删除:
71
+ // rejection 落定前同 key 已被重试成功覆盖的场景不误删新记录。
72
+ void Promise.resolve(result).catch(() => {
73
+ const record = executions.get(key);
74
+ if (record !== undefined && record.value === result) executions.delete(key);
75
+ });
72
76
  }
77
+ return result;
78
+ }
79
+
80
+ /**
81
+ * 测试专用:清空 oncePerProcess 的进程级守卫 Map。供静态 import 形态的测试重置
82
+ * 去重状态(无 resetModules 重建模块图时的等价手段);生产代码禁止调用。
83
+ */
84
+ export function _resetOncePerProcessForTest(): void {
85
+ executions.clear();
73
86
  }
74
87
 
75
88
  /**
@@ -109,14 +122,14 @@ export function isEnoentError(err: unknown): boolean {
109
122
  // (base-tool-enhance background 降级——bte background 生命周期设计的 D14 项,
110
123
  // commit 8599abd0c,该设计未在现行 docs 留档 / smart-context R6 不注册工具,
111
124
  // 规则定义见 smart-context src/index.ts R 规则族注释)。判据锚
112
- // XYZ_AGENT_SUBAGENT=1——subagent-engine-sdk env.ts 的 buildEngineChildEnv 对每个引擎
125
+ // TAIJI_AGENT_SUBAGENT=1——subagent-engine-sdk env.ts 的 buildEngineChildEnv 对每个引擎
113
126
  // 子进程恒注入(L0 层最后写入),再经引擎内二次 spawn(buildOutboundChildEnv 全量
114
127
  // 继承)传到 subagent 内的 bash。旧判据(PI_SUBAGENT_ROOT_SESSION_ID /
115
128
  // PI_SUBAGENT_SELF_RECORD_ID 两键)在引擎协议化后已无写入方(20260914 探针证实,
116
129
  // 报告 .tmp/dev-flow/ext-simplify-17-d4-probe.md),判据失效态由 D4 重锚收敛于此。
117
130
 
118
131
  /** subagent 进程标记 env 键(引擎 spawn 链恒注入 "1",subagent-engine-sdk env.ts L0)。 */
119
- export const SUBAGENT_MARKER_ENV = "XYZ_AGENT_SUBAGENT";
132
+ export const SUBAGENT_MARKER_ENV = "TAIJI_AGENT_SUBAGENT";
120
133
 
121
134
  /**
122
135
  * 判断当前进程是否引擎链上的 subagent 子进程。
@@ -137,7 +150,7 @@ export function isSubagentProcess(
137
150
 
138
151
  // ── stale ctx 守卫(guardStaleCtx,崩溃韧性 D1)────────────────────────
139
152
  //
140
- // 背景(docs/design/crash-resilience.md §3.3 D1 / §2.2 事件 E1):pi 的 extension API
153
+ // 背景:pi 的 extension API
141
154
  // 对象(pi / ctx)在 session 替换(newSession/fork/switchSession/reload)后被 runner
142
155
  // 标记 stale,再调用其方法会**同步抛错**(loader.js/runner.js 的 assertActive,错误文案
143
156
  // 含 STALE_CTX_MARKER)。跨 session 生命周期存活的异步回调(compact 的 onComplete/onError、
@@ -172,7 +185,7 @@ export interface GuardStaleCtxOptions {
172
185
  isCtxStale?: () => boolean;
173
186
  /**
174
187
  * stale 降级通知(静默降级的观测点):前置检查命中时无参调用;fn 抛错被分诊为
175
- * stale 时以捕获的错误为参调用。缺省 = 仅 XYZ_AGENT_DEBUG=1 时写一行 stderr
188
+ * stale 时以捕获的错误为参调用。缺省 = 仅 TAIJI_AGENT_DEBUG=1 时写一行 stderr
176
189
  * debug(本包零依赖、无 pi handle,不能走 extension-logger;需要落盘日志的
177
190
  * 接入方传入自己的 debugLog/logger.warn)。
178
191
  */
@@ -189,9 +202,9 @@ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
189
202
  );
190
203
  }
191
204
 
192
- /** 缺省降级通知:XYZ_AGENT_DEBUG=1 时 stderr 一行(对齐 extensions 包 XYZ_AGENT_DEBUG 惯例)。 */
205
+ /** 缺省降级通知:TAIJI_AGENT_DEBUG=1 时 stderr 一行(对齐 extensions 包 TAIJI_AGENT_DEBUG 惯例)。 */
193
206
  function defaultOnStale(label: string, error?: unknown): void {
194
- if (process.env.XYZ_AGENT_DEBUG !== "1") return;
207
+ if (process.env.TAIJI_AGENT_DEBUG !== "1") return;
195
208
  const detail =
196
209
  error === undefined
197
210
  ? "generation check (isCtxStale)"