@eddyskywalker/dsh-chatgpt-subscription 0.43.0 → 0.44.0

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.
Files changed (33) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/cordis.patch.yml +5 -0
  3. package/lib/client.js +157 -46
  4. package/lib/client.js.map +1 -1
  5. package/lib/entry-progress-updates-CkGJr_fe.js +1004 -0
  6. package/lib/index.js +237 -991
  7. package/lib/progress-updates.js +2 -0
  8. package/lib/types/client/locales-ru/qoder.d.ts.map +1 -1
  9. package/lib/types/client/progress-updates/host-source.d.ts +16 -0
  10. package/lib/types/client/progress-updates/host-source.d.ts.map +1 -1
  11. package/lib/types/client/progress-updates/index.d.ts.map +1 -1
  12. package/lib/types/client/progress-updates/settings-binding.d.ts +5 -0
  13. package/lib/types/client/progress-updates/settings-binding.d.ts.map +1 -1
  14. package/lib/types/client/qoder/QoderSection.d.ts.map +1 -1
  15. package/lib/types/client/qoder/locales.d.ts +9 -1
  16. package/lib/types/client/qoder/locales.d.ts.map +1 -1
  17. package/lib/types/client/qoder/styles.d.ts.map +1 -1
  18. package/lib/types/compat.d.ts +1 -1
  19. package/lib/types/entry-progress-updates.d.ts +43 -0
  20. package/lib/types/entry-progress-updates.d.ts.map +1 -0
  21. package/lib/types/host/proxy-manager.d.ts +6 -0
  22. package/lib/types/host/proxy-manager.d.ts.map +1 -1
  23. package/lib/types/host/qoder/client.d.ts.map +1 -1
  24. package/lib/types/host/qoder/mapper.d.ts +74 -2
  25. package/lib/types/host/qoder/mapper.d.ts.map +1 -1
  26. package/lib/types/index.d.ts +21 -25
  27. package/lib/types/index.d.ts.map +1 -1
  28. package/lib/types/shared/qoder-contracts.d.ts +10 -0
  29. package/lib/types/shared/qoder-contracts.d.ts.map +1 -1
  30. package/locale/progress-updates/en.json +6 -0
  31. package/locale/progress-updates/zh.json +6 -0
  32. package/package.json +13 -3
  33. package/progress-updates.svg +9 -0
@@ -0,0 +1,1004 @@
1
+ import { createUserMessage } from "@deepseek-ai/dsh-llm";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ import { z } from "zod";
4
+ /**
5
+ * 判断是否该催促。达到阈值即触发,注入之后调用方把计数清零。
6
+ * @param rounds - 自上次进展更新以来的连续轮次数。
7
+ * @returns 是否达到 {@link NUDGE_THRESHOLD}。
8
+ */
9
+ function shouldNudge(rounds) {
10
+ return rounds >= 8;
11
+ }
12
+ /**
13
+ * 把进展列表截断到环形缓冲上限,保留最新的一批。
14
+ * @param list - 任意长度的进展列表(不会被就地修改)。
15
+ * @returns 截断后的新数组。
16
+ */
17
+ function capUpdates(list) {
18
+ return list.length <= 50 ? [...list] : list.slice(list.length - 50);
19
+ }
20
+ /** 进程内自增序号,只用于拼出稳定 id;不含任何运行时依赖。 */
21
+ let sequence = 0;
22
+ /**
23
+ * 生成一条进展的稳定 id。
24
+ * @param at - 产生时刻(Unix 毫秒)。
25
+ * @returns 形如 `pu-<时间戳>-<序号>` 的字符串。
26
+ */
27
+ function nextUpdateId(at) {
28
+ sequence += 1;
29
+ return `pu-${at}-${sequence}`;
30
+ }
31
+ /**
32
+ * 创建一个空的进展更新 store。
33
+ * @returns 新的会话维度 store。
34
+ */
35
+ function createProgressUpdateStore() {
36
+ let current = {
37
+ updates: [],
38
+ roundsSinceUpdate: 0,
39
+ nudges: 0
40
+ };
41
+ return {
42
+ state: () => current,
43
+ push: (update) => {
44
+ current = {
45
+ ...current,
46
+ updates: capUpdates([...current.updates, update])
47
+ };
48
+ return current;
49
+ },
50
+ bumpRound: () => {
51
+ current = {
52
+ ...current,
53
+ roundsSinceUpdate: current.roundsSinceUpdate + 1
54
+ };
55
+ return current.roundsSinceUpdate;
56
+ },
57
+ markUpdated: () => {
58
+ current = {
59
+ ...current,
60
+ roundsSinceUpdate: 0
61
+ };
62
+ return current;
63
+ },
64
+ markNudged: () => {
65
+ current = {
66
+ ...current,
67
+ roundsSinceUpdate: 0,
68
+ nudges: current.nudges + 1
69
+ };
70
+ return current;
71
+ },
72
+ reset: () => {
73
+ current = {
74
+ updates: [],
75
+ roundsSinceUpdate: 0,
76
+ nudges: 0
77
+ };
78
+ return current;
79
+ }
80
+ };
81
+ }
82
+ //#endregion
83
+ //#region src/host/progress-updates/tool.ts
84
+ /** 注册的工具名。冻结契约的一部分,不要改动。 */
85
+ const NOTIFY_USER_TOOL = "progress_update";
86
+ /** 工具关闭时返回给模型的文本(canonical 输出的 render,而不是抛错)。 */
87
+ const DISABLED_OUTPUT_TEXT = "Progress updates are disabled; the update was not displayed.";
88
+ /** 工具描述的最终全文。 */
89
+ const PROGRESS_UPDATE_DESCRIPTION = [
90
+ "Report progress to the user without ending your turn.",
91
+ "Two updates are part of the job, not optional narration: send one BEFORE your first tool call of a multi-step task (the plan), and one BEFORE your final answer (what was done and what was verified).",
92
+ "Use it during the rest of the work so the user can follow along and correct course early:",
93
+ "state your plan near the beginning of the task (what you will do, and in what order);",
94
+ "report a meaningful conclusion the moment you have it (a key fact, the root cause, a verified assumption) rather than narrating every step;",
95
+ "give a stage judgement when one phase ends and the next begins (where things stand, what happens next);",
96
+ "explain what you are waiting for before a long wait such as a test run, an install, or a build;",
97
+ "and say what blocks you — and what you will try next, or what you need from the user — when you are stuck.",
98
+ "Each update has one short title line (plain text, no Markdown, no trailing period) with the details in the message body.",
99
+ "Do not repeat a status that has not changed.",
100
+ "When you run as a subagent, describe only your own subtask: finishing your subtask is not the same as finishing the overall task."
101
+ ].join(" ");
102
+ /**
103
+ * 构造 progress_update 工具。
104
+ * @param deps - 依赖:当前开关与进展接收端。
105
+ * @returns 可注册的工具定义。
106
+ */
107
+ function createProgressUpdateTool(deps) {
108
+ return defineTool({
109
+ name: NOTIFY_USER_TOOL,
110
+ description: PROGRESS_UPDATE_DESCRIPTION,
111
+ parameters: {
112
+ title: {
113
+ type: "string",
114
+ description: "One line, about 60 characters at most, in the user's language: plain text, no Markdown, no trailing period."
115
+ },
116
+ message: {
117
+ type: "string",
118
+ required: true,
119
+ description: "The details, about 1000 characters at most, in light Markdown: one paragraph of prose plus a few bullets. Tables are supported: when several items share fields, use a GFM pipe table (header row plus a | --- | separator row) instead of one item per line."
120
+ }
121
+ },
122
+ output: {
123
+ schema: {
124
+ type: "object",
125
+ additionalProperties: false,
126
+ properties: { displayed: {
127
+ type: "boolean",
128
+ description: "Whether the update was actually shown to the user."
129
+ } }
130
+ },
131
+ render(_args, value) {
132
+ return [{
133
+ type: "text",
134
+ text: value.displayed ? "Progress update displayed." : DISABLED_OUTPUT_TEXT
135
+ }];
136
+ }
137
+ },
138
+ async execute(args, exec) {
139
+ if (!deps.isEnabled()) return { displayed: false };
140
+ const sink = deps.sink?.();
141
+ if (sink === void 0) return { displayed: false };
142
+ const at = Date.now();
143
+ const source = deps.sourceOf?.(exec);
144
+ const update = {
145
+ id: nextUpdateId(at),
146
+ message: args.message,
147
+ at,
148
+ ...args.title === void 0 || args.title.trim() === "" ? {} : { title: args.title.trim() },
149
+ ...source === void 0 ? {} : { source }
150
+ };
151
+ return { displayed: sink.push(update) };
152
+ }
153
+ });
154
+ }
155
+ //#endregion
156
+ //#region src/host/progress-updates/nudge.ts
157
+ /** 注入到模型上下文里的催促正文。 */
158
+ const NUDGE_TEXT = [
159
+ "You have made a run of tool calls without reporting progress to the user.",
160
+ "If you are still in the middle of multi-step work, send one progress update now with the progress_update tool:",
161
+ "one short title line and the details in the body — what you have established so far, where things stand, and what you are doing next.",
162
+ "Skip it when there is genuinely nothing new to report."
163
+ ].join(" ");
164
+ /** 催促上下文在消息来源上的 kind(供客户端与日志区分这类注入)。 */
165
+ const NUDGE_SOURCE_KIND = "progress-updates-nudge";
166
+ /**
167
+ * 造一条催促上下文。
168
+ * @param sequence - 本会话第几次催促,写进 notice 摘要便于日志辨认。
169
+ * @returns 可直接放进 `additionalContexts` 的用户角色消息。
170
+ */
171
+ function createNudgeContext(sequence) {
172
+ return createUserMessage({
173
+ content: [{
174
+ type: "text",
175
+ text: NUDGE_TEXT
176
+ }],
177
+ source: {
178
+ kind: NUDGE_SOURCE_KIND,
179
+ form: "notice",
180
+ summary: `progress update nudge #${sequence}`
181
+ }
182
+ });
183
+ }
184
+ /**
185
+ * 找出一次工具调用所属的**计数会话**:子会话归并到最近的顶层祖先,整棵委派
186
+ * 树共用一个计数器。
187
+ * @param agent - 事件里的调用方 agent。
188
+ * @param sessions - 会话查找表,缺省时退化为只认调用方自己的会话。
189
+ * @returns 计数用的会话 id,无法判定时返回 undefined。
190
+ */
191
+ function counterSessionIdOf(agent, sessions) {
192
+ const own = stringOf$1(agent?.session?.header?.id) ?? stringOf$1(agent?.id);
193
+ if (own === void 0) return void 0;
194
+ if (sessions === void 0) return own;
195
+ let current = own;
196
+ let header = agent?.session?.header;
197
+ for (let depth = 0; depth < 16 && current !== void 0; depth += 1) {
198
+ const resolved = header ?? sessions.get(current)?.header;
199
+ if (resolved === void 0) return current;
200
+ const parentId = stringOf$1(resolved.parentSession);
201
+ if (parentId === void 0) return current;
202
+ const parent = sessions.get(parentId);
203
+ if (parent === void 0) return current;
204
+ header = parent.header;
205
+ current = parentId;
206
+ }
207
+ return current;
208
+ }
209
+ /**
210
+ * 读一次工具调用所属的「步」标识,用于把同一个模型步里的多条工具结果合并成一轮。
211
+ *
212
+ * 拿不到 turnBoundary(没有 agent-loop,或宿主不是真实会话)时返回 undefined,
213
+ * 调用方退化为按工具结果计数——宁可多计,也不要漏掉催促。
214
+ * @param agent - 事件里的调用方 agent。
215
+ * @param projections - 会话投影服务,可选。
216
+ * @returns 该步的唯一键,或 undefined。
217
+ */
218
+ function stepKeyOf(agent, projections) {
219
+ if (projections === void 0) return void 0;
220
+ const session = agent?.session;
221
+ if (session === void 0) return void 0;
222
+ try {
223
+ const seq = projections.stateOf(session, "turnBoundary")?.lastStepStartSeq;
224
+ return typeof seq === "number" ? `step:${seq}` : void 0;
225
+ } catch {
226
+ return;
227
+ }
228
+ }
229
+ /**
230
+ * 读一个未知字段为字符串。
231
+ * @param value - 候选值。
232
+ * @returns 字符串,或任何其它类型的 undefined。
233
+ */
234
+ function stringOf$1(value) {
235
+ return typeof value === "string" && value !== "" ? value : void 0;
236
+ }
237
+ //#endregion
238
+ //#region src/host/progress-updates/prompt.ts
239
+ /**
240
+ * 系统提示词引导:让模型知道「多步任务里要主动汇报进展」这件事该怎么用。
241
+ *
242
+ * 文本用函数形式提供(而不是静态字符串):开关关闭时返回空串,装配期就地
243
+ * 抹掉引导,不需要重启会话、也不需要重挂插件行。
244
+ *
245
+ * ## 为什么是英文
246
+ *
247
+ * 提示词是写给模型看的,不是写给用户看的:同一段要求在英文下最短、歧义最少,
248
+ * 也和工具描述(`tool.ts` 的 `PROGRESS_UPDATE_DESCRIPTION`,本来就是英文)保持
249
+ * 同一套措辞。用户看到的语言由 {@link progressLanguageDirective} 单独钉住——
250
+ * 「指令用英文」与「输出用中文」是两件事,不要混在一起。
251
+ *
252
+ * @module dsh-chatgpt-subscription/host/progress-updates/prompt
253
+ */
254
+ /**
255
+ * 本模块提示词分节的名称,全局唯一(重复注册会抛错)。
256
+ *
257
+ * 保留原插件的 `plugin:progress-updates` 拼写:会话里已有的分节名不能因为搬家
258
+ * 而变成第二个分节。
259
+ */
260
+ const PROGRESS_SECTION_NAME = "plugin:progress-updates";
261
+ /** 允许的取值,供设置解析做白名单校验(配置里写错就等于没写)。 */
262
+ const PROGRESS_UPDATE_LANGUAGES = [
263
+ "auto",
264
+ "zh",
265
+ "en"
266
+ ];
267
+ /**
268
+ * 引导词正文。写法对齐 Kimi 的「进展更新」:先说清多步任务早期的思路,再报
269
+ * 有意义的结论、阶段判断、长等待与阻塞;每条给一行标题,细节放正文;不重复
270
+ * 没有变化的状态。开头与结尾两条被提成硬要求——没有它们,面板在任务的两端
271
+ * 都是空的。
272
+ */
273
+ const PROGRESS_GUIDANCE = [
274
+ "## Progress updates",
275
+ "",
276
+ "During multi-step work you can call `progress_update` to report progress without ending your turn and without spending a question: report, then keep working.",
277
+ "",
278
+ "**Two updates are mandatory — missing either one means the job is not finished:**",
279
+ "",
280
+ "- **Opening**: before the first tool call of the task, send one update stating the plan (which steps, what you touch first). The user must be able to redirect you before you disappear into execution.",
281
+ "- **Closing**: after the work is done and before your final answer, send one update stating the result (what changed, what was verified, what is left). The panel and the conversation both close on this line.",
282
+ "",
283
+ "Beyond those two, these moments are worth a report:",
284
+ "- **Early planning**: the opening update covers it; report again only when the plan really changes (different approach, smaller scope, a broken premise).",
285
+ "- **A meaningful conclusion**: a key fact, a root cause, a verified assumption — report the conclusion itself, not \"I just read a file\".",
286
+ "- **A stage judgement**: one phase ends and the next begins — one line on where things stand and what happens next.",
287
+ "- **Before a long wait**: a test run, an install, a build — say what you are waiting for and roughly how long.",
288
+ "- **When blocked**: missing information, missing permission, an external failure — say where you are stuck, what you will try next, or what you need from the user.",
289
+ "",
290
+ "How to write one:",
291
+ "- One short title line (plain text, no Markdown, no trailing period); the details go in the message body.",
292
+ "- The body uses light Markdown: one paragraph of prose plus a few bullets. **When several rows share fields, use a GFM pipe table** (header row plus a `| --- |` separator row) instead of one item per line.",
293
+ "- Do not repeat a status that has not changed: if the last update already said it is running, it needs no second report.",
294
+ "- A progress update is for the user, not for the log: write it in human words, do not restate tool arguments.",
295
+ "",
296
+ "When you run as a subagent: describe only your own subtask progress. Finishing your subtask is not the same as finishing the overall task — do not report the overall progress as done."
297
+ ].join("\n");
298
+ /**
299
+ * 每个语言档位对应的一句指令。
300
+ *
301
+ * 只约束输出的标题与正文,不约束引导词本身;三条都必须自洽——`auto` 不许在
302
+ * 用户说中文时滑回英文,`zh`/`en` 则明确压过对话语言。
303
+ */
304
+ const PROGRESS_LANGUAGE_DIRECTIVES = {
305
+ auto: "Write the title and the message of every progress update in the language the user is writing in, whatever it is. Follow the conversation; do not fall back to English unless the user does.",
306
+ zh: "Write the title and the message of every progress update in Simplified Chinese, even when the conversation is in another language.",
307
+ en: "Write the title and the message of every progress update in English, even when the conversation is in another language."
308
+ };
309
+ /**
310
+ * 取一个语言档位的指令文本;未知取值按 `auto` 处理。
311
+ * @param language - 配置里读到的语言档位。
312
+ * @returns 追加到引导词末尾的一句指令。
313
+ */
314
+ function progressLanguageDirective(language) {
315
+ return PROGRESS_LANGUAGE_DIRECTIVES[language] ?? PROGRESS_LANGUAGE_DIRECTIVES.auto;
316
+ }
317
+ /**
318
+ * 生成提示词分节的文本提供者。
319
+ *
320
+ * 文本与开关无关,这是刻意的:系统提示是缓存前缀的第一段,会话中途改一个字
321
+ * 就会让整段前缀失效、整段重新计费(Kimi 的缓存是内容前缀匹配)。开关的语义
322
+ * 因此只留给工具自己——关闭时 `execute` 直接返回 `displayed:false`。
323
+ * @param language - 读取当前输出语言的函数;缺省按 `auto` 处理。
324
+ * @returns 引导词加语言指令。
325
+ */
326
+ function progressSectionText(language = () => "auto") {
327
+ return () => PROGRESS_GUIDANCE + "\n\n" + progressLanguageDirective(language());
328
+ }
329
+ //#endregion
330
+ //#region src/host/progress-updates/projection.ts
331
+ /**
332
+ * 「进展更新」的会话投影:把日志里的 progress_update 调用折叠成一条条进展。
333
+ *
334
+ * ## 为什么要有它
335
+ *
336
+ * 进展更新原来只活在进程内的一个 Map 里(`store.ts` 的环形缓冲):重开对话、
337
+ * 刷新页面、宿主重启之后那份状态就没了,用户看到的是「进展面板空了」。而数据其实
338
+ * 一直没丢 —— `progress_update` 的每次调用本身就是一条 `tool/call` 会话事件,
339
+ * 早已被 DSH 持久化。所以这里把**日志**当成唯一真源:事件提交时折叠一次,读的
340
+ * 时候就是一份可以直接回放的历史。
341
+ *
342
+ * ## 为什么不 import 投影包
343
+ *
344
+ * `@deepseek-ai/dsh-session-projection` **不在** peerDependencies 里:本插件要
345
+ * 同时支持 0.1.2-alpha.5 ~ 0.2.1-rc.2,那一串宿主里有的根本没有这个包,运行时
346
+ * import 会让整个插件在那代宿主上装载失败。所以这里按**结构化类型**自己声明
347
+ * 契约(与 `nudge.ts` 的 `NudgeSessionLookup` / `TurnBoundaryLookup` 同一套
348
+ * 做法),运行时只经 `ctx.get('sessionProjections')` 拿服务。
349
+ *
350
+ * ## 折叠是日志重放路径,任何情况都不许抛错
351
+ *
352
+ * `apply` 在每条提交事件上跑一次。这里抛出一次,毁掉的不只是本单元:重放链路
353
+ * 会当场断掉,会话可能连读都读不出来。所以对事件形状一律**不信任** —— 不是
354
+ * tool/call、不是 progress_update、arguments 不是合法 JSON、message 不是字符串、
355
+ * seq/time 不是数字 —— 全部原样返回**同一个 state 引用**(`Object.is` 相等,
356
+ * 下游零工作)。
357
+ *
358
+ * ## id 为什么用 seq,还要带上会话 id
359
+ *
360
+ * 客户端(`src/client/progress-updates/host-source.ts` 的 `sameList`)按 id 比较
361
+ * 两份快照,面板列表也拿 id 当 React key。时间戳会随重放时的时钟漂移,seq 不会:
362
+ * 同一段日志重放两次必须得到**逐字相同**的 id。会话 id 后缀解决的是另一半问题 ——
363
+ * 读值要合并整棵委派树(见 `index.ts` 的 `projectionUpdatesOf`),而 seq 在
364
+ * 每个会话里都从 0 开始,只带 seq 的话根会话与子会话的第一条进展会撞 id。
365
+ *
366
+ * @module dsh-chatgpt-subscription/host/progress-updates/projection
367
+ */
368
+ /** 投影键:宿主读状态、客户端取 wire 值都用这个名字。 */
369
+ const PROGRESS_UPDATES_PROJECTION_KEY = "progressUpdates";
370
+ /** 单条进展的 schema:与 `ProgressUpdate` 逐字同形。 */
371
+ const UPDATE_SCHEMA = z.object({
372
+ id: z.string(),
373
+ title: z.string().optional(),
374
+ message: z.string(),
375
+ at: z.number(),
376
+ source: z.string().optional()
377
+ });
378
+ /** 折叠状态的 schema(宿主状态)。 */
379
+ const progressUpdatesStateSchema = z.object({
380
+ updates: z.array(UPDATE_SCHEMA),
381
+ source: z.string().optional()
382
+ });
383
+ /** wire 值的 schema(客户端看到的数组)。 */
384
+ const progressUpdatesViewSchema = z.array(UPDATE_SCHEMA);
385
+ /**
386
+ * 造一条进展的稳定 id。
387
+ * @param seq - 事件在会话日志里的序号(重放稳定)。
388
+ * @param source - 所属会话 id;拿不到时省略后缀。
389
+ * @returns 形如 `pu-<seq>-<sessionId>` 的字符串。
390
+ */
391
+ function updateIdOf(seq, source) {
392
+ return source === void 0 ? "pu-" + seq : "pu-" + seq + "-" + source;
393
+ }
394
+ /**
395
+ * 读一个未知值为非空字符串。
396
+ * @param value - 候选值。
397
+ * @returns 非空字符串,其它类型的 undefined。
398
+ */
399
+ function stringOf(value) {
400
+ return typeof value === "string" && value !== "" ? value : void 0;
401
+ }
402
+ /**
403
+ * 把 `tool/call` 里的 `arguments` 原文解析成进展内容。
404
+ *
405
+ * **任何一点不对就返回 undefined**(而不是抛错,也不是造一条半截进展):这是重放
406
+ * 路径,一条形状坏掉的历史事件只该被跳过。
407
+ * @param raw - 工具入参 JSON 原文。
408
+ * @returns 正文与(去空白后的)可选标题。
409
+ */
410
+ function parseArguments(raw) {
411
+ if (typeof raw !== "string") return void 0;
412
+ let value;
413
+ try {
414
+ value = JSON.parse(raw);
415
+ } catch {
416
+ return;
417
+ }
418
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
419
+ const record = value;
420
+ if (typeof record.message !== "string") return void 0;
421
+ const title = typeof record.title === "string" ? record.title.trim() : "";
422
+ return title === "" ? { message: record.message } : {
423
+ message: record.message,
424
+ title
425
+ };
426
+ }
427
+ /**
428
+ * 把 PTC 子调用里的 `arguments`(**已解析的对象**)读成进展内容。
429
+ *
430
+ * 与 {@link parseArguments} 只差一层:那条路径拿到的是模型吐出的 JSON 原文,
431
+ * 这条拿到的是宿主已经归一化过的对象(见 `dsh-tools` 的 `PtcDispatchEventData`)。
432
+ * 同样是"任何一点不对就返回 undefined",同样不抛错。
433
+ * @param raw - 已归一化的入参对象。
434
+ * @returns 正文与(去空白后的)可选标题。
435
+ */
436
+ function parseDispatchedArguments(raw) {
437
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return void 0;
438
+ const record = raw;
439
+ if (typeof record.message !== "string") return void 0;
440
+ const title = typeof record.title === "string" ? record.title.trim() : "";
441
+ return title === "" ? { message: record.message } : {
442
+ message: record.message,
443
+ title
444
+ };
445
+ }
446
+ /**
447
+ * 造一个进展折叠单元。
448
+ *
449
+ * 每次登记都要一份**新的**定义对象:它是纯函数集合,没有跨会话的状态,但把
450
+ * `init` 闭包里的会话头漏进来就会把不同会话串成一份。
451
+ * @returns 可交给 `sessionProjections.register` 的定义。
452
+ */
453
+ function progressUpdatesProjection() {
454
+ return {
455
+ key: PROGRESS_UPDATES_PROJECTION_KEY,
456
+ stateVersion: 2,
457
+ stateSchema: progressUpdatesStateSchema,
458
+ init(header) {
459
+ const source = stringOf(header?.id);
460
+ return source === void 0 ? { updates: [] } : {
461
+ updates: [],
462
+ source
463
+ };
464
+ },
465
+ apply(state, event) {
466
+ const isNative = event?.type === "tool/call";
467
+ const isDispatch = event?.type === "tool/ptc-dispatch";
468
+ if (!isNative && !isDispatch) return state;
469
+ const data = event.data;
470
+ if (typeof data !== "object" || data === null) return state;
471
+ if (data.name !== "progress_update") return state;
472
+ const body = isNative ? parseArguments(data.arguments) : parseDispatchedArguments(data.arguments);
473
+ if (body === void 0) return state;
474
+ const seq = event.seq;
475
+ if (typeof seq !== "number" || !Number.isFinite(seq)) return state;
476
+ const at = event.time;
477
+ if (typeof at !== "number" || !Number.isFinite(at)) return state;
478
+ const update = {
479
+ id: updateIdOf(seq, state.source),
480
+ message: body.message,
481
+ at,
482
+ ...body.title === void 0 ? {} : { title: body.title },
483
+ ...state.source === void 0 ? {} : { source: state.source }
484
+ };
485
+ return {
486
+ ...state,
487
+ updates: capUpdates([...state.updates, update])
488
+ };
489
+ },
490
+ wire: {
491
+ viewSchema: progressUpdatesViewSchema,
492
+ view: (state) => state.updates
493
+ }
494
+ };
495
+ }
496
+ /**
497
+ * 读一个会话上折叠出来的进展。
498
+ *
499
+ * 两条读取路径,都按**不可用就当没读到**处理(返回 undefined,调用方回退到内存
500
+ * 实现):先问 `stateOf`(宿主状态,最直接),没有再问 `snapshot`(wire 切面,
501
+ * 老一点的投影服务上只有它)。读出来的值一律过 schema,形状不对就当投影不可用 ——
502
+ * 宁可用进程内那份,也不要把一份坏数据渲染成历史。
503
+ * @param projections - 会话投影服务。
504
+ * @param session - 会话对象。
505
+ * @returns 该会话的进展;投影不可用时 undefined。
506
+ */
507
+ function readProjectedUpdates(projections, session) {
508
+ const stateOf = projections.stateOf;
509
+ if (typeof stateOf === "function") {
510
+ let state;
511
+ try {
512
+ state = stateOf.call(projections, session, PROGRESS_UPDATES_PROJECTION_KEY);
513
+ } catch {
514
+ return;
515
+ }
516
+ if (state === void 0) return void 0;
517
+ const parsed = progressUpdatesStateSchema.safeParse(state);
518
+ return parsed.success ? parsed.data.updates : void 0;
519
+ }
520
+ const snapshot = projections.snapshot;
521
+ if (typeof snapshot !== "function") return void 0;
522
+ let values;
523
+ try {
524
+ values = snapshot.call(projections, session, [PROGRESS_UPDATES_PROJECTION_KEY])?.values;
525
+ } catch {
526
+ return;
527
+ }
528
+ if (values === void 0) return void 0;
529
+ const parsed = progressUpdatesViewSchema.safeParse(values[PROGRESS_UPDATES_PROJECTION_KEY]);
530
+ return parsed.success ? parsed.data : void 0;
531
+ }
532
+ /**
533
+ * 把多个会话的进展合并成一份。
534
+ *
535
+ * 按 `at` 升序排(同一毫秒时按输入顺序,JS 的排序是稳定的:传入方把"被查询的
536
+ * 会话自己"放在最前面),再按 {@link MAX_UPDATES} 截断保留最新的。整棵委派树共用
537
+ * 一份进展是今天的语义,所以合并后的上限与单会话一致,不随子会话数量膨胀。
538
+ * @param lists - 各会话各自的进展(每条已各自截断过)。
539
+ * @returns 合并后的进展,最多 {@link MAX_UPDATES} 条。
540
+ */
541
+ function mergeProjectedUpdates(lists) {
542
+ const merged = [].concat(...lists.map((list) => [...list]));
543
+ merged.sort((left, right) => left.at - right.at);
544
+ return capUpdates(merged);
545
+ }
546
+ //#endregion
547
+ //#region src/host/progress-updates/routes.ts
548
+ /** 进展读取路由的完整路径。 */
549
+ const PROGRESS_UPDATES_PATH = "/api/dsh-progress-updates/updates";
550
+ /** 路由默认的 JSON 响应头。 */
551
+ const JSON_HEADERS = {
552
+ "content-type": "application/json; charset=utf-8",
553
+ "referrer-policy": "no-referrer"
554
+ };
555
+ /**
556
+ * 断言本次请求来自本机。
557
+ *
558
+ * 进展内容会包含用户在会话里做的事,属于个人数据,所以只有 loopback(桌面端
559
+ * 自己)能读:socket 地址是权威判断,Host 头必须也是 loopback 权威(挡住 DNS
560
+ * 重绑定),`X-Forwarded-For` 永远不信任。
561
+ * @param request - 进来的 HTTP 请求。
562
+ * @returns 是否放行。
563
+ */
564
+ function isLoopbackRequest(request) {
565
+ if (!isLoopbackAddress(request.socket.remoteAddress)) return false;
566
+ const host = request.headers.host;
567
+ if (typeof host !== "string" || host === "") return false;
568
+ let hostname;
569
+ try {
570
+ hostname = new URL("http://" + host).hostname.toLowerCase();
571
+ } catch {
572
+ return false;
573
+ }
574
+ if (!isLoopbackHostname(hostname)) return false;
575
+ return request.headers["sec-fetch-site"] !== "cross-site";
576
+ }
577
+ /** socket 远端地址是否落在 loopback 范围(127/8、::1、IPv4-mapped)。 */
578
+ function isLoopbackAddress(address) {
579
+ if (address === void 0) return false;
580
+ const normalized = address.toLowerCase();
581
+ if (normalized === "::1") return true;
582
+ if (normalized.startsWith("::ffff:")) return isIPv4Loopback(normalized.slice(7));
583
+ return isIPv4Loopback(normalized);
584
+ }
585
+ /** IPv4 127/8 判定(四段十进制,首段为 127)。 */
586
+ function isIPv4Loopback(v4) {
587
+ const parts = v4.split(".");
588
+ return parts.length === 4 && parts[0] === "127" && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255);
589
+ }
590
+ /** 主机名是否指向 loopback 权威(localhost、[::1]、127/8)。 */
591
+ function isLoopbackHostname(hostname) {
592
+ if (hostname === "localhost" || hostname === "[::1]") return true;
593
+ return isIPv4Loopback(hostname);
594
+ }
595
+ /**
596
+ * 写一个 JSON 响应。
597
+ * @param res - 响应对象。
598
+ * @param status - HTTP 状态码。
599
+ * @param body - 要序列化的值。
600
+ * @param headers - 追加或覆盖的头。
601
+ */
602
+ function writeJson(res, status, body, headers = {}) {
603
+ res.writeHead(status, {
604
+ ...JSON_HEADERS,
605
+ ...headers
606
+ });
607
+ res.end(JSON.stringify(body));
608
+ }
609
+ /**
610
+ * 进展读取路由:`GET /api/dsh-progress-updates/updates?session=<id>`。
611
+ *
612
+ * 返回 `{ updates: ProgressUpdate[] }`。响应带 no-store:这个列表随 agent 干活
613
+ * 实时变化,缓存住就等于展示过期进展。
614
+ * @param reader - 进展读取面。
615
+ * @returns 可注册的 route。
616
+ */
617
+ function makeProgressUpdatesRoute(reader) {
618
+ return {
619
+ kind: "exact",
620
+ path: PROGRESS_UPDATES_PATH,
621
+ handler: (req, res) => {
622
+ if (!isLoopbackRequest(req)) {
623
+ writeJson(res, 403, {
624
+ ok: false,
625
+ error: "forbidden: loopback-only"
626
+ });
627
+ return;
628
+ }
629
+ const session = new URL(req.url ?? "/", "http://127.0.0.1").searchParams.get("session") ?? "";
630
+ writeJson(res, 200, { updates: reader.updatesOf(session) }, { "cache-control": "no-store" });
631
+ }
632
+ };
633
+ }
634
+ //#endregion
635
+ //#region src/host/progress-updates/index.ts
636
+ /**
637
+ * 读取一个配置字段,沿着 volatile 活引用取当前值。
638
+ * @param field - 字段本身。
639
+ * @returns 当前值,或字段缺失时的 undefined。
640
+ */
641
+ function readProgressConfigField(field) {
642
+ if (field === void 0 || field === null) return void 0;
643
+ if (typeof field === "object" && typeof field.get === "function") return field.get();
644
+ return field;
645
+ }
646
+ /**
647
+ * 解析一次读取的有效设置:每次调用都重读活引用,因此设置保存后立刻生效。
648
+ * @param config - 主入口收到的插件配置(活引用或普通值)。
649
+ * @returns 生效的开关与催促阈值。
650
+ */
651
+ function resolveProgressUpdatesSettings(config) {
652
+ const enabled = readProgressConfigField(config?.progressUpdates);
653
+ const threshold = readProgressConfigField(config?.progressUpdatesNudgeThreshold);
654
+ const language = readProgressConfigField(config?.progressUpdatesLanguage);
655
+ return {
656
+ enabled: enabled !== false,
657
+ nudgeThreshold: typeof threshold === "number" && Number.isSafeInteger(threshold) && threshold > 0 ? threshold : 8,
658
+ language: language !== void 0 && PROGRESS_UPDATE_LANGUAGES.includes(language) ? language : "auto"
659
+ };
660
+ }
661
+ /** 血缘回溯的深度上限:与 `nudge.ts` 的 `counterSessionIdOf` 同一个数。 */
662
+ const LINEAGE_DEPTH_LIMIT = 16;
663
+ /**
664
+ * 找出一个会话的全部子孙(广度优先,只看 `sessions.list()` 里活跃的那些)。
665
+ *
666
+ * 重开对话之后历史子孙并不在 `list()` 里 —— 那时它们各自的进展本来就取不到,
667
+ * 这里能做的只是把**当前活跃**的那几支合进来,剩下的等它们 attach。
668
+ * @param rootId - 祖先会话 id。
669
+ * @param sessions - 会话服务。
670
+ * @returns 子孙会话(不含根自己),按发现顺序。
671
+ */
672
+ function descendantSessionsOf(rootId, sessions) {
673
+ const list = sessions.list;
674
+ if (typeof list !== "function") return [];
675
+ let all;
676
+ try {
677
+ all = list.call(sessions);
678
+ } catch {
679
+ return [];
680
+ }
681
+ if (!Array.isArray(all) || all.length === 0) return [];
682
+ /** 父会话 id → 直接子会话。 */
683
+ const childrenOf = /* @__PURE__ */ new Map();
684
+ for (const session of all) {
685
+ const parent = typeof session?.header?.parentSession === "string" ? session.header.parentSession : void 0;
686
+ if (parent === void 0) continue;
687
+ const bucket = childrenOf.get(parent);
688
+ if (bucket === void 0) childrenOf.set(parent, [session]);
689
+ else bucket.push(session);
690
+ }
691
+ const found = [];
692
+ const seen = /* @__PURE__ */ new Set([rootId]);
693
+ let frontier = [rootId];
694
+ for (let depth = 0; depth < LINEAGE_DEPTH_LIMIT && frontier.length > 0; depth += 1) {
695
+ const next = [];
696
+ for (const parentId of frontier) for (const child of childrenOf.get(parentId) ?? []) {
697
+ const childId = typeof child.header?.id === "string" ? child.header.id : void 0;
698
+ if (childId !== void 0) {
699
+ if (seen.has(childId)) continue;
700
+ seen.add(childId);
701
+ next.push(childId);
702
+ }
703
+ found.push(child);
704
+ }
705
+ frontier = next;
706
+ }
707
+ return found;
708
+ }
709
+ /**
710
+ * 已挂载的模块登记表,走一个 global symbol:同一个包的两份模块实例(npm 拷贝与
711
+ * 仓库 link)仍然共享一次判定,而重复注册同名工具或同名提示词分节会让宿主启动
712
+ * 失败。
713
+ */
714
+ const MOUNTED = Symbol.for("dsh-chatgpt-subscription.mounted-modules");
715
+ /** 本模块在登记表里的名字。 */
716
+ const MODULE_ID = "progress-updates";
717
+ /**
718
+ * 把一条催促挂到一个 post-execute 决策上,同时保住该决策原有的附加上下文。
719
+ * @param decision - `next()` 交回来的决策。
720
+ * @param context - 要追加的催促上下文。
721
+ * @returns 追加后的决策(形状不变)。
722
+ */
723
+ function withNudge(decision, context) {
724
+ const additionalContexts = [...decision.additionalContexts ?? [], context];
725
+ if (decision.kind === "block") return {
726
+ kind: "block",
727
+ feedback: decision.feedback,
728
+ additionalContexts
729
+ };
730
+ if (decision.value !== void 0) return {
731
+ kind: "accept",
732
+ value: decision.value,
733
+ additionalContexts
734
+ };
735
+ if (decision.content !== void 0) return {
736
+ kind: "accept",
737
+ content: decision.content,
738
+ additionalContexts
739
+ };
740
+ return {
741
+ kind: "accept",
742
+ additionalContexts
743
+ };
744
+ }
745
+ /**
746
+ * 单实例守卫:本模块已挂载时放弃本次安装。
747
+ * @param ctx - 宿主上下文。
748
+ * @returns 释放函数(交出挂载权);已经挂载过时返回 undefined。
749
+ */
750
+ function claimMount(ctx) {
751
+ const registry = globalThis;
752
+ const existing = registry[MOUNTED];
753
+ const mounted = existing instanceof Set ? existing : /* @__PURE__ */ new Set();
754
+ if (existing !== mounted) registry[MOUNTED] = mounted;
755
+ if (mounted.has(MODULE_ID)) return void 0;
756
+ mounted.add(MODULE_ID);
757
+ const release = () => {
758
+ mounted.delete(MODULE_ID);
759
+ };
760
+ if (typeof ctx.effect === "function") ctx.effect(() => release, "dsh-chatgpt-subscription: progress updates mount");
761
+ return release;
762
+ }
763
+ /**
764
+ * 挂载「进展更新」的宿主半边。
765
+ * @param ctx - 宿主插件上下文。
766
+ * @param options - 挂载选项(设置读取器)。
767
+ * @returns 卸载函数;重复挂载时返回一个空操作。
768
+ */
769
+ function installProgressUpdates(ctx, options) {
770
+ const release = claimMount(ctx);
771
+ if (release === void 0) return () => void 0;
772
+ /** 每次调用重新读服务:宿主树可能在本模块挂载之后才 settle,读一次存下来会漏。 */
773
+ const serviceOf = (name) => {
774
+ const read = ctx.get;
775
+ return typeof read === "function" ? read.call(ctx, name) : void 0;
776
+ };
777
+ /** 登记一个副作用;上下文没有 effect 时立即执行(测试用假上下文)。 */
778
+ const track = (execute, label) => {
779
+ if (typeof ctx.effect === "function") ctx.effect(execute, label);
780
+ else execute();
781
+ };
782
+ /** 会话查找表:够把子会话回溯到顶层祖先即可。 */
783
+ const sessionsOf = () => serviceOf("sessions");
784
+ /** 会话投影服务:只用来读 turnBoundary,把同一个模型步里的多条工具结果合并成一轮。 */
785
+ const projectionsOf = () => serviceOf("sessionProjections");
786
+ /**
787
+ * 会话投影服务:登记/读取进展折叠。
788
+ *
789
+ * 与 {@link projectionsOf} 是同一个服务的两份结构化视图(`nudge` 那边只读
790
+ * `turnBoundary`,这里读写进展键),分开写是为了让各自的类型面只声明自己真正
791
+ * 用到的成员。服务缺席(老宿主、没装 agent-loop)时是 undefined。
792
+ */
793
+ const sessionProjectionsOf = () => serviceOf("sessionProjections");
794
+ /**
795
+ * 登记进展折叠单元。服务缺席时**安静跳过**:这是可选服务,缺了只是回到内存
796
+ * 实现(今天的行为),不是一次安装失败。
797
+ */
798
+ track(() => {
799
+ const projections = sessionProjectionsOf();
800
+ if (typeof projections?.register !== "function") return () => void 0;
801
+ try {
802
+ return projections.register(progressUpdatesProjection());
803
+ } catch (error) {
804
+ serviceOf("logger")?.warn("[dsh-chatgpt-subscription] progress updates projection not registered: " + String(error));
805
+ return () => void 0;
806
+ }
807
+ }, "dsh-chatgpt-subscription: progress update projection");
808
+ /** 会话维度状态;键是计数会话 id(子会话已归并到顶层祖先)。 */
809
+ const stores = /* @__PURE__ */ new Map();
810
+ /** 已经计过数的「步」:同一个模型步里的后续工具结果不再重复计数。 */
811
+ const countedSteps = /* @__PURE__ */ new Map();
812
+ /**
813
+ * 取一个计数会话的 store,按需创建。
814
+ * @param key - 计数会话 id。
815
+ * @returns 该会话的 store。
816
+ */
817
+ const storeOf = (key) => {
818
+ let store = stores.get(key);
819
+ if (store === void 0) {
820
+ store = createProgressUpdateStore();
821
+ stores.set(key, store);
822
+ }
823
+ return store;
824
+ };
825
+ /**
826
+ * 一次工具调用归属的计数会话 id。
827
+ * @param exec - 工具执行上下文(或任意带 agent 的对象)。
828
+ * @returns 计数会话 id;判定不出来时归入 `session` 这一桶。
829
+ */
830
+ const counterKeyOf = (exec) => counterSessionIdOf(exec?.agent, sessionsOf()) ?? "session";
831
+ /**
832
+ * 从会话日志折叠出来的进展 —— 面板的**首选**取数路径。
833
+ *
834
+ * 读三样东西,任缺其一就返回 undefined,让调用方回退到内存实现:
835
+ * 会话服务、投影服务,以及"这个 id 对应的会话对象"。最后一件事在重开对话之后
836
+ * 并不必然成立:`sessions` 里只有**活跃**会话,历史会话要等宿主 attach 上来
837
+ * 才读得到 —— 那之前面板显示的是内存里那一份(通常为空),attach 之后同一个
838
+ * 轮询请求就能读到完整历史。
839
+ *
840
+ * 合并范围是**整棵委派树**:今天的语义是一个子会话的进展归并到顶层祖先的桶里,
841
+ * 折叠之后靠 `header.parentSession` 把子孙找回来(与 `nudge.ts` 的 lineage
842
+ * 走法同一套,深度上限 16 是防一条损坏的日志把这里变成死循环),按 `at` 升序
843
+ * 排 —— 子会话汇报的进展因此落在它真正发生的那个时间点上,而不是全排在最后。
844
+ * @param sessionId - 浏览器问的会话 id。
845
+ * @returns 合并后的进展;这条路径不可用时 undefined。
846
+ */
847
+ const projectionUpdatesOf = (sessionId) => {
848
+ if (sessionId === "") return void 0;
849
+ const sessions = sessionsOf();
850
+ if (sessions === void 0) return void 0;
851
+ const projections = sessionProjectionsOf();
852
+ if (projections === void 0) return void 0;
853
+ let own;
854
+ try {
855
+ own = sessions.get?.(sessionId);
856
+ } catch {
857
+ return;
858
+ }
859
+ if (own === void 0) return void 0;
860
+ const updates = readProjectedUpdates(projections, own);
861
+ if (updates === void 0) return void 0;
862
+ const lists = [updates];
863
+ for (const child of descendantSessionsOf(sessionId, sessions)) {
864
+ const childUpdates = readProjectedUpdates(projections, child);
865
+ if (childUpdates !== void 0) lists.push(childUpdates);
866
+ }
867
+ return lists.length === 1 ? updates : mergeProjectedUpdates(lists);
868
+ };
869
+ /**
870
+ * 浏览器半边的读取面:给定会话 id 就交出那个会话的进展快照。
871
+ *
872
+ * **先投影、后内存**:投影是从已持久化的会话日志折出来的,重开对话、刷新页面、
873
+ * 宿主重启之后都还在;内存 `stores` 只在投影这条路不可用时兜底(老宿主没有
874
+ * 投影服务、会话还没 attach、或者压根没装会话服务)。
875
+ *
876
+ * 请求不带 session(或带一个本进程没见过的 id)时,回退到那个"合成会话桶":
877
+ * 这是单会话部署里的常态,也是插件第一次被调用、祖先血缘还没建立时的落点。
878
+ */
879
+ const reader = { updatesOf: (sessionId) => {
880
+ const projected = projectionUpdatesOf(sessionId);
881
+ if (projected !== void 0) return projected;
882
+ const direct = stores.get(sessionId);
883
+ if (direct !== void 0) return direct.state().updates;
884
+ if (sessionId !== "") {
885
+ const merged = counterSessionIdOf({ id: sessionId }, sessionsOf());
886
+ const parent = merged === void 0 ? void 0 : stores.get(merged);
887
+ if (parent !== void 0) return parent.state().updates;
888
+ return [];
889
+ }
890
+ const fallback = stores.get("session");
891
+ return fallback === void 0 ? [] : fallback.state().updates;
892
+ } };
893
+ track(() => {
894
+ const tools = serviceOf("tools");
895
+ if (tools === void 0) {
896
+ serviceOf("logger")?.warn("[dsh-chatgpt-subscription] progress updates tool skipped: no tools service");
897
+ return () => void 0;
898
+ }
899
+ const tool = createProgressUpdateTool({
900
+ isEnabled: () => options.settings().enabled,
901
+ sink: () => ({ push: (update) => {
902
+ if (!options.settings().enabled) return false;
903
+ storeOf(update.source ?? "session").push(update);
904
+ return true;
905
+ } }),
906
+ sourceOf: (exec) => counterKeyOf(exec)
907
+ });
908
+ return tools.register(tool);
909
+ }, "dsh-chatgpt-subscription: progress update tool");
910
+ track(() => {
911
+ const systemPrompt = serviceOf("systemPrompt");
912
+ if (systemPrompt === void 0) return () => void 0;
913
+ return systemPrompt.section({
914
+ name: PROGRESS_SECTION_NAME,
915
+ order: 220,
916
+ text: progressSectionText(() => options.settings().language)
917
+ });
918
+ }, "dsh-chatgpt-subscription: progress update prompt section");
919
+ track(() => {
920
+ const webServer = serviceOf("webServer");
921
+ if (webServer === void 0) return () => void 0;
922
+ return webServer.register(makeProgressUpdatesRoute(reader));
923
+ }, "dsh-chatgpt-subscription: progress update route");
924
+ const disposeListener = ctx.on?.("tools/post-execute", (async (exec, _result, next) => {
925
+ const decision = await next();
926
+ const current = options.settings();
927
+ if (!current.enabled) return decision;
928
+ const key = counterKeyOf(exec);
929
+ const store = storeOf(key);
930
+ if (exec.name === "progress_update") {
931
+ store.markUpdated();
932
+ const reportedStep = stepKeyOf(exec.agent, projectionsOf());
933
+ if (reportedStep !== void 0) countedSteps.set(key, reportedStep);
934
+ return decision;
935
+ }
936
+ const step = stepKeyOf(exec.agent, projectionsOf());
937
+ if (step !== void 0) {
938
+ if (countedSteps.get(key) === step) return decision;
939
+ countedSteps.set(key, step);
940
+ }
941
+ const rounds = store.bumpRound();
942
+ if (!(current.nudgeThreshold === 8 ? shouldNudge(rounds) : rounds >= current.nudgeThreshold)) return decision;
943
+ const nudges = store.markNudged().nudges;
944
+ return withNudge(decision, createNudgeContext(nudges));
945
+ }));
946
+ return () => {
947
+ disposeListener?.();
948
+ stores.clear();
949
+ countedSteps.clear();
950
+ release();
951
+ };
952
+ }
953
+ //#endregion
954
+ //#region src/entry-progress-updates.ts
955
+ /** Loader 行名;patch 里的行 id 另由 bundle 决定。 */
956
+ const name = "dsh-chatgpt-subscription-progress-updates";
957
+ /**
958
+ * 这一行不为任何服务等待。
959
+ *
960
+ * `installProgressUpdates` 逐个用 `ctx.get()` 探可选服务(tools、systemPrompt、
961
+ * sessions、sessionProjections、webServer),缺哪个就退化成更弱的实现;把它们的
962
+ * 名字写进 `inject` 反而会让没装 agent-loop 的宿主整行挂不上——那是这一行原本的
963
+ * 独立性所要避免的事。
964
+ */
965
+ const inject = [];
966
+ /**
967
+ * 设置是怎么到这一行的。
968
+ *
969
+ * 字段声明在核心入口(见文件头的理由)。行顺序不归这一行管,所以两种顺序都要成立:
970
+ * 核心先挂上就直接递交;这一行先挂上就把读取器寄存下来,核心一调用就取走。
971
+ */
972
+ let attachedSettings;
973
+ let waitingForSettings;
974
+ /**
975
+ * 由核心入口在挂载时调用,把它的 `volatile` 设置读面交给这一行。
976
+ *
977
+ * 幂等:重复递交以最后一次为准(核心重挂时设置引用会换一份新的)。
978
+ * @param read - 每次调用都重读活引用,返回当前的开关、阈值与语言。
979
+ */
980
+ function attachProgressSettings(read) {
981
+ attachedSettings = read;
982
+ const waiting = waitingForSettings;
983
+ waitingForSettings = void 0;
984
+ waiting?.(read);
985
+ }
986
+ /**
987
+ * 还没有设置就不挂。
988
+ *
989
+ * 用一个写死的默认值假装在跑,会让「设置显示打开」与「实际没挂」同时成立——那正是
990
+ * 这一行第一版的样子。等核心把它交过来,再挂。
991
+ * @returns 一个读完引用就返回当前设置的函数。
992
+ */
993
+ async function readSettings() {
994
+ if (attachedSettings !== void 0) return attachedSettings;
995
+ return await new Promise((resolve) => {
996
+ waitingForSettings = resolve;
997
+ });
998
+ }
999
+ async function apply(ctx) {
1000
+ const read = await readSettings();
1001
+ ctx.effect(() => installProgressUpdates(ctx, { settings: read }), "dsh-progress-updates: host half");
1002
+ }
1003
+ //#endregion
1004
+ export { resolveProgressUpdatesSettings as a, name as i, attachProgressSettings as n, inject as r, apply as t };