wezard 2.2.2 → 2.4.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.
- package/CLAUDE.md +9 -6
- package/README.md +5 -5
- package/config.example.jsonc +12 -0
- package/dist/cli/init.js +2 -1
- package/dist/cli/init.js.map +1 -1
- package/dist/daemon/approval.js +90 -6
- package/dist/daemon/approval.js.map +1 -1
- package/dist/daemon/chat-log.js +6 -1
- package/dist/daemon/chat-log.js.map +1 -1
- package/dist/daemon/chat-name.js +15 -0
- package/dist/daemon/chat-name.js.map +1 -1
- package/dist/daemon/config-api.js +265 -70
- package/dist/daemon/config-api.js.map +1 -1
- package/dist/daemon/danger.js +15 -0
- package/dist/daemon/danger.js.map +1 -1
- package/dist/daemon/detail.js +49 -0
- package/dist/daemon/detail.js.map +1 -1
- package/dist/daemon/handoff.js +12 -5
- package/dist/daemon/handoff.js.map +1 -1
- package/dist/daemon/idle-nudge.js +55 -0
- package/dist/daemon/idle-nudge.js.map +1 -0
- package/dist/daemon/inbound.js +19 -14
- package/dist/daemon/inbound.js.map +1 -1
- package/dist/daemon/index.js +1509 -259
- package/dist/daemon/index.js.map +1 -1
- package/dist/daemon/jobs.js +180 -32
- package/dist/daemon/jobs.js.map +1 -1
- package/dist/daemon/last-response.js +15 -0
- package/dist/daemon/last-response.js.map +1 -1
- package/dist/daemon/memory-steward.js +124 -68
- package/dist/daemon/memory-steward.js.map +1 -1
- package/dist/daemon/mirror-bridge.js +506 -124
- package/dist/daemon/mirror-bridge.js.map +1 -1
- package/dist/daemon/mirror-store.js.map +1 -1
- package/dist/daemon/model-select.js +102 -0
- package/dist/daemon/model-select.js.map +1 -1
- package/dist/daemon/notices.js +147 -33
- package/dist/daemon/notices.js.map +1 -1
- package/dist/daemon/peers.js +261 -124
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/pending-items.js +171 -0
- package/dist/daemon/pending-items.js.map +1 -0
- package/dist/daemon/receipts.js +428 -80
- package/dist/daemon/receipts.js.map +1 -1
- package/dist/daemon/route.js +133 -19
- package/dist/daemon/route.js.map +1 -1
- package/dist/daemon/spawn-tmux.js +24 -13
- package/dist/daemon/spawn-tmux.js.map +1 -1
- package/dist/daemon/steward-guard.js +32 -0
- package/dist/daemon/steward-guard.js.map +1 -0
- package/dist/daemon/task-registry.js +1 -1
- package/dist/daemon/task-registry.js.map +1 -1
- package/dist/daemon/tasks.js +12 -4
- package/dist/daemon/tasks.js.map +1 -1
- package/dist/daemon/usage.js +4 -11
- package/dist/daemon/usage.js.map +1 -1
- package/dist/daemon/wizard-memory.js +36 -4
- package/dist/daemon/wizard-memory.js.map +1 -1
- package/dist/daemon/wizard.js +174 -87
- package/dist/daemon/wizard.js.map +1 -1
- package/dist/mcp/server.js +209 -132
- package/dist/mcp/server.js.map +1 -1
- package/dist/shared/charter-view.js +35 -0
- package/dist/shared/charter-view.js.map +1 -0
- package/dist/shared/chat-http.js +134 -32
- package/dist/shared/chat-http.js.map +1 -1
- package/dist/shared/chat-render.js +6 -3
- package/dist/shared/chat-render.js.map +1 -1
- package/dist/shared/chat-view.js +17 -6
- package/dist/shared/chat-view.js.map +1 -1
- package/dist/shared/cli-backends.js +2 -0
- package/dist/shared/cli-backends.js.map +1 -1
- package/dist/shared/config-meta.js +91 -0
- package/dist/shared/config-meta.js.map +1 -0
- package/dist/shared/config-writer.js +5 -6
- package/dist/shared/config-writer.js.map +1 -1
- package/dist/shared/config.js +273 -294
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/daemon-token.js +18 -0
- package/dist/shared/daemon-token.js.map +1 -0
- package/dist/shared/detail-render.js +233 -20
- package/dist/shared/detail-render.js.map +1 -1
- package/dist/shared/detail-store.js +33 -12
- package/dist/shared/detail-store.js.map +1 -1
- package/dist/shared/effort.js +11 -0
- package/dist/shared/effort.js.map +1 -0
- package/dist/shared/keepalive.js +5 -5
- package/dist/shared/keepalive.js.map +1 -1
- package/dist/shared/model-prices.js +258 -0
- package/dist/shared/model-prices.js.map +1 -0
- package/dist/shared/pricing.js +55 -0
- package/dist/shared/pricing.js.map +1 -0
- package/dist/shared/reminder.js +18 -4
- package/dist/shared/reminder.js.map +1 -1
- package/dist/shared/role-render.js +210 -8
- package/dist/shared/role-render.js.map +1 -1
- package/dist/shared/role-view.js +104 -26
- package/dist/shared/role-view.js.map +1 -1
- package/dist/shared/session-label.js +4 -0
- package/dist/shared/session-label.js.map +1 -1
- package/dist/shared/task-file.js +16 -1
- package/dist/shared/task-file.js.map +1 -1
- package/dist/shared/turn-state.js +24 -0
- package/dist/shared/turn-state.js.map +1 -0
- package/dist/shared/world.js +116 -13
- package/dist/shared/world.js.map +1 -1
- package/dist/svr/index.js +2 -1
- package/dist/svr/index.js.map +1 -1
- package/hooks/pre-tool-use.sh +15 -3
- package/package.json +1 -1
- package/web/chat.css +226 -74
- package/web/chat.js +1075 -370
package/dist/daemon/peers.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
// attachments) and the graph runner (which drives them) both compose on top.
|
|
12
12
|
import { existsSync, openSync, readSync, closeSync, statSync } from "node:fs";
|
|
13
13
|
import { backendForPath } from "../shared/cli-backends.js";
|
|
14
|
-
import { truncate, truncateWithCount } from "../shared/std.js";
|
|
14
|
+
import { clipLine, truncate, truncateWithCount } from "../shared/std.js";
|
|
15
|
+
import { tildePath } from "./notices.js";
|
|
15
16
|
import { isKeepalivePingText, withoutKeepalive } from "../shared/keepalive.js";
|
|
16
17
|
import { envelopeAttrs, parseEnvelope, renderReminder } from "../shared/reminder.js";
|
|
17
18
|
/** Strip ANSI SGR/CSI + OSC so captured pane text is safe to embed / match on. */
|
|
@@ -59,12 +60,39 @@ const readTailBytes = (jsonlPath, want) => {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
};
|
|
63
|
+
const HEAD_BYTES = 256 * 1024;
|
|
64
|
+
/** transcript 第一条带时间戳的记录的时刻 (ms) —— 会话真正开始的那一刻。只读文件头;
|
|
65
|
+
* 正文里的 `timestamp` 是转义过的 (`\"timestamp\"`), 正则不会误中。读不到 → undefined。 */
|
|
66
|
+
export const firstStamp = (jsonlPath) => {
|
|
67
|
+
if (!jsonlPath || !existsSync(jsonlPath))
|
|
68
|
+
return undefined;
|
|
69
|
+
let fd;
|
|
70
|
+
try {
|
|
71
|
+
const buf = Buffer.allocUnsafe(HEAD_BYTES);
|
|
72
|
+
fd = openSync(jsonlPath, "r");
|
|
73
|
+
const read = readSync(fd, buf, 0, HEAD_BYTES, 0);
|
|
74
|
+
const iso = /"timestamp":"([^"]+)"/.exec(buf.subarray(0, read).toString("utf8"))?.[1];
|
|
75
|
+
const t = iso ? Date.parse(iso) : NaN;
|
|
76
|
+
return Number.isFinite(t) ? t : undefined;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
if (fd !== undefined) {
|
|
83
|
+
try {
|
|
84
|
+
closeSync(fd);
|
|
85
|
+
}
|
|
86
|
+
catch { /* ignore */ }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
62
90
|
/** 从文件尾读一段并解析; `enough` 说不够就翻四倍重读, 直到够了、读完整个文件、
|
|
63
91
|
* 或者撞上上限。窗口是手段不是目的 —— 调用方只说"我要几轮", 不该关心一条
|
|
64
92
|
* attachment 有多大。 */
|
|
65
|
-
const readTailUntil = (jsonlPath, parse, enough) => {
|
|
93
|
+
const readTailUntil = (jsonlPath, parse, enough, max = TAIL_BYTES_MAX) => {
|
|
66
94
|
const size = fileSize(jsonlPath);
|
|
67
|
-
const ceiling = Math.min(size || TAIL_BYTES,
|
|
95
|
+
const ceiling = Math.min(size || TAIL_BYTES, max);
|
|
68
96
|
const walk = (win) => {
|
|
69
97
|
const parsed = parse(readTailBytes(jsonlPath, win));
|
|
70
98
|
return enough(parsed) || win >= ceiling ? parsed : walk(Math.min(win * 4, ceiling));
|
|
@@ -95,14 +123,21 @@ const blockText = (content) => {
|
|
|
95
123
|
/** Last `n` user/assistant turns of a transcript, oldest first. Backend-agnostic:
|
|
96
124
|
* the line shape is normalized through the owning CLI's dialect adapter. */
|
|
97
125
|
export const tailTurns = (jsonlPath, n = 3, keepLines = false) => readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, keepLines), (ts) => ts.length >= n).slice(-n);
|
|
126
|
+
/** 最后一句进到模型的话 (带信封), 往回读到有为止 —— 中间隔着多少轮工具都不怕。 */
|
|
127
|
+
export const lastUserTurn = (jsonlPath) => readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, true), (ts) => ts.some((t) => t.role === "user"))
|
|
128
|
+
.filter((t) => t.role === "user").at(-1);
|
|
98
129
|
/** `tailTurns` 去掉保温 ping/pong 之后的最后 `n` 轮 —— 凡是交给另一个 wizard (或人)
|
|
99
130
|
* **读**的都走这里: 挂机一晚的会话尾巴上全是 ping/pong, 照 tailTurns 数出来的
|
|
100
|
-
* 「最近 6 轮」一句真话都没有, 「最后一条回复」是一个 pong
|
|
101
|
-
|
|
102
|
-
export const talkTurns = (jsonlPath, n, pingSigs = [], keepLines = false) => {
|
|
131
|
+
* 「最近 6 轮」一句真话都没有, 「最后一条回复」是一个 pong。 */
|
|
132
|
+
export const talkTurns = (jsonlPath, n, pingSigs = [], keepLines = false, marks = false) => {
|
|
103
133
|
const clean = (ts) => withoutKeepalive(ts, pingSigs);
|
|
104
|
-
return clean(readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, keepLines), (ts) => clean(ts).length >= n)).slice(-n);
|
|
134
|
+
return clean(readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, keepLines, marks), (ts) => clean(ts).length >= n)).slice(-n);
|
|
105
135
|
};
|
|
136
|
+
/** 从 `sinceMs` 起的全部来回 (带标记): 往回读到越过它的第一行为止, **不受 TAIL_BYTES_MAX
|
|
137
|
+
* 限制**。按条数读尾巴的 talkTurns 管不了一轮很长的情形: 一轮里的大段工具输出 (读文件、
|
|
138
|
+
* 跑命令) 能把开这一轮的那句推到 2MB 之外 —— 回执就再也定位不到问话, 对方答完了也只能
|
|
139
|
+
* 报 silent。问话一定晚于发话时刻, 所以读到发话时刻之前就够了, 再大的文件也只读这一段。 */
|
|
140
|
+
export const turnsSince = (jsonlPath, sinceMs, pingSigs = []) => withoutKeepalive(readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, true, true), (ts) => ts.some((t) => !!t.ms && t.ms < sinceMs), Infinity), pingSigs);
|
|
106
141
|
/** 一个会话的来回, 每个来回 = 一句问话 + 它之后的全部回答 (途中的话在前, 终句在
|
|
107
142
|
* 最后), 旧的在前。读多深由时间窗定, 调用方再按时刻与条数裁:
|
|
108
143
|
* 给了 `since` → 一直读到比它更早的那一句 (窗口内的一句不漏);
|
|
@@ -130,8 +165,8 @@ export const talkRounds = (jsonlPath, rounds, pingSigs = [], win = {}) => {
|
|
|
130
165
|
};
|
|
131
166
|
/** 纯解析: 一段 transcript 原文 → 里面的对话轮次。`keepLines` 保留换行 —— 摘要与
|
|
132
167
|
* 预览要压成一行, 但交给另一个 wizard 读的回复不能: 表格 / 列表 / 代码块压成一行
|
|
133
|
-
*
|
|
134
|
-
const parseTurns = (jsonlPath, raw, keepLines = false) => {
|
|
168
|
+
* 之后对模型就是一团字。`marks` 把 CLI 自插的 user 行留成空的 `notice` 轮 (见 Turn)。 */
|
|
169
|
+
const parseTurns = (jsonlPath, raw, keepLines = false, marks = false) => {
|
|
135
170
|
if (!raw)
|
|
136
171
|
return [];
|
|
137
172
|
const normalize = backendForPath(jsonlPath).normalizeTranscriptLine;
|
|
@@ -148,6 +183,12 @@ const parseTurns = (jsonlPath, raw, keepLines = false) => {
|
|
|
148
183
|
}
|
|
149
184
|
// The first line of a truncated tail is usually a fragment — normalize
|
|
150
185
|
// returning null (or a throw) is the expected outcome, not an error.
|
|
186
|
+
const att = parsed.attachment;
|
|
187
|
+
if (marks && att?.type === "queued_command" && typeof att.prompt === "string" && !parsed.isSidechain) {
|
|
188
|
+
const env = parseEnvelope(att.prompt);
|
|
189
|
+
const ms = Date.parse(String(parsed.timestamp ?? ""));
|
|
190
|
+
return env ? [{ role: "user", text: "", ms, env, queued: true }] : [];
|
|
191
|
+
}
|
|
151
192
|
let row;
|
|
152
193
|
try {
|
|
153
194
|
row = normalize(parsed);
|
|
@@ -170,10 +211,12 @@ const parseTurns = (jsonlPath, raw, keepLines = false) => {
|
|
|
170
211
|
// keepaliveStamps to mtime fallback, and let the keepalive's own ping
|
|
171
212
|
// writes (plus snapshot/summary churn) read as REAL activity that reset
|
|
172
213
|
// the round budget without bound.
|
|
173
|
-
const
|
|
174
|
-
const ms = typeof rawTs === "number" ? rawTs : Date.parse(String(rawTs ?? ""));
|
|
214
|
+
const ms = stampMs(parsed.timestamp);
|
|
175
215
|
const mid = role === "assistant" && row.message?.stop_reason === "tool_use";
|
|
176
|
-
|
|
216
|
+
if (!text && marks && role === "user" && raw.includes("<task-notification"))
|
|
217
|
+
return [{ role, text: "", ms, notice: true }];
|
|
218
|
+
const apiError = role === "assistant" && parsed.isApiErrorMessage === true;
|
|
219
|
+
return text ? [{ role, text, ms, ...(env ? { env } : {}), ...(mid ? { mid: true } : {}), ...(apiError ? { apiError: true } : {}) }] : [];
|
|
177
220
|
});
|
|
178
221
|
};
|
|
179
222
|
// Flatten a message's content for QUOTE DEDUP only — unlike `blockText` this
|
|
@@ -360,12 +403,29 @@ const parseOpenTools = (jsonlPath, raw) => {
|
|
|
360
403
|
* `RESULT: …`, 这里把它捞出来。取**最后一个**匹配 —— 中间复述过这个格式的那些
|
|
361
404
|
* 不算数。捞不到返回 "", 调用方退回整段 lastText, 所以不遵守约定也只是退化。 */
|
|
362
405
|
export const extractResult = (text, max = 800) => {
|
|
406
|
+
const c = parseClosing(text, max);
|
|
407
|
+
return c.kind === "result" ? c.text : "";
|
|
408
|
+
};
|
|
409
|
+
// NEED 只认全大写: "what I need: …" / 「需要: …」在普通答话里太常见, 误认一次就是
|
|
410
|
+
// 一份不计数的回执。
|
|
411
|
+
const CLOSING_RE = /(?:^|[\s>*|-])(RESULT|Result|result|结论|NEED)\s*[::]\s*/gm;
|
|
412
|
+
const ARTIFACT_RE = /^[ \t>*|-]*ARTIFACT[ \t]*[::][ \t]*(\S+)(?:[ \t]+(?:—|--|-|:)[ \t]*(.*))?$/gm;
|
|
413
|
+
export const parseClosing = (text, max = 800) => {
|
|
414
|
+
const artifacts = [...text.matchAll(ARTIFACT_RE)]
|
|
415
|
+
.map((m) => ({ path: m[1].replace(/^`|`$/g, ""), note: (m[2] ?? "").trim() }))
|
|
416
|
+
.filter((a) => !/^<.*>$/.test(a.path)); // 复述格式时的 `<路径>` 占位
|
|
363
417
|
// 标记之后的**全部**内容, 而不是"那一行" —— 结论常常不止一行 (路径一行、说明
|
|
364
418
|
// 一行)。约定是收口在最后, 所以标记之后剩下的就是结论。取最后一个匹配: 模型常
|
|
365
|
-
// 先复述一遍格式要求再给答案。
|
|
366
|
-
const
|
|
367
|
-
const
|
|
368
|
-
|
|
419
|
+
// 先复述一遍格式要求再给答案。ARTIFACT 行不算结论正文。
|
|
420
|
+
const after = (m) => text.slice((m.index ?? 0) + m[0].length).replace(ARTIFACT_RE, "").trim().slice(0, max);
|
|
421
|
+
const marks = [...text.matchAll(CLOSING_RE)];
|
|
422
|
+
// 交差优先: 写了 RESULT 就是交差, 哪怕后面跟一句 `NEED: 无` —— 当成反问, 工单就卡到超时。
|
|
423
|
+
const result = marks.filter((m) => m[1] !== "NEED").at(-1);
|
|
424
|
+
if (result)
|
|
425
|
+
return { kind: "result", text: after(result), artifacts };
|
|
426
|
+
const need = marks.at(-1);
|
|
427
|
+
const q = need ? after(need) : "";
|
|
428
|
+
return q && !/^(?:无|没有|none|n\/a|-+)[。.]?$/i.test(q) ? { kind: "need", text: q, artifacts } : { kind: "none", text: "", artifacts };
|
|
369
429
|
};
|
|
370
430
|
/** The peer's most recent assistant message — the handoff payload when one
|
|
371
431
|
* agent drives another ("take #fix's conclusion and review it"). */
|
|
@@ -380,9 +440,11 @@ export const clipMiddle = (s, max = 4000, head = Math.floor(max / 4)) => s.lengt
|
|
|
380
440
|
* 发话时刻是最便宜的关联 id (A2A 的 taskId 在这里的等价物): 早于它的一律不算。
|
|
381
441
|
* 没有时间戳的行 (ms=0) 无从判断, 放行 —— 退化成旧行为, 而不是把回复吞掉。
|
|
382
442
|
* 保温的 pong 不算回复: 它比真正的答案晚, 不剔掉的话等的人拿到的就是一个 "pong"。 */
|
|
383
|
-
export const lastReply = (jsonlPath, sinceMs = 0, pingSigs = []) => talkTurns(jsonlPath, 40, pingSigs, true)
|
|
443
|
+
export const lastReply = (jsonlPath, sinceMs = 0, pingSigs = []) => talkTurns(jsonlPath, 40, pingSigs, true, true)
|
|
384
444
|
.filter((t) => t.role === "assistant" && (!t.ms || t.ms >= sinceMs))
|
|
385
445
|
.at(-1)?.text ?? "";
|
|
446
|
+
/** 报错之后叫它接着干的那一句 (`continue` / `继续` …)。 */
|
|
447
|
+
const RESUME_RE = /^(?:continue|go on|resume|继续|接着(?:干|做)?)[\s.!。!]*$/i;
|
|
386
448
|
/** paste 到那一行落盘之间的抖动 —— 问话的时间戳可能比我们记的发话时刻早一点。 */
|
|
387
449
|
const ASK_SLACK_MS = 60_000;
|
|
388
450
|
/** 「`from` 那一句的回答」—— 比 lastReply 严一档, 自动回执靠它定位。
|
|
@@ -390,44 +452,45 @@ const ASK_SLACK_MS = 60_000;
|
|
|
390
452
|
* 发话时刻 (lastReply 的 sinceMs) 分不开两件事: 目标正忙时我们这一句是排队的,
|
|
391
453
|
* 它先吐出来的是**上一件事**的结论, 而那条回复同样晚于我们 paste 的时刻 —— 按时刻
|
|
392
454
|
* 取就会拿旧结论当新回执。信封是更硬的锚: 我们注入的那一句在它的 transcript 里挂着
|
|
393
|
-
* `kind=peer from=.我`, 从那一句往后数, 答的就一定是我们问的。找不到那一句 (
|
|
394
|
-
*
|
|
395
|
-
*
|
|
396
|
-
*
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
return r && r.text;
|
|
455
|
+
* `kind=peer from=.我`, 从那一句往后数, 答的就一定是我们问的。找不到那一句 (还排在
|
|
456
|
+
* 输入框里没读进) 返回 `undefined`; 找到了问话但它后面还没有回答返回 "" —— 两种都要
|
|
457
|
+
* 接着等, 绝不能退回按时刻取把上一件事的结论当成这一次的回执 (receipts 只用前者分辨
|
|
458
|
+
* 「它在忙前面的轮」与「读进了却没答」)。 */
|
|
459
|
+
/** 找问话的那一段: 有发话时刻就读到它之前 (问话可以离尾巴很远), 没有 (老调用) 退回最近 80 条。 */
|
|
460
|
+
const askWindow = (jsonlPath, sinceMs, pingSigs) => sinceMs > 0 ? turnsSince(jsonlPath, sinceMs - ASK_SLACK_MS, pingSigs) : talkTurns(jsonlPath, 80, pingSigs, true, true);
|
|
461
|
+
export const replyToPeer = (jsonlPath, fromName, sinceMs = 0, pingSigs = [], turn) => {
|
|
462
|
+
const r = answerOf(askWindow(jsonlPath, sinceMs, pingSigs), fromName, sinceMs, turn);
|
|
463
|
+
return r && { text: r.text, ...(r.error ? { error: true } : {}) };
|
|
403
464
|
};
|
|
404
465
|
/** 交接那一刻, `from` 那一句在旧会话里**收口**了没有 —— 比 replyToPeer 再严一档。
|
|
405
466
|
*
|
|
406
467
|
* 自我交接是在某一轮**中途**调的: 那一轮最后一条 assistant 是"已交接"之类的收尾,
|
|
407
468
|
* 不是答案。所以只认在 `untilMs` 之前就被下一句 user 行关上的那一轮: 问话那一轮
|
|
408
469
|
* 还开着 (就是交接这一轮) → "", 这份回执的义务跟着简报转进新会话。三态同 replyToPeer。 */
|
|
409
|
-
export const replyClosedBefore = (jsonlPath, fromName, sinceMs, untilMs, pingSigs = []) => {
|
|
410
|
-
const r = answerOf(
|
|
411
|
-
return r && (r.closedAt !== undefined && r.closedAt < untilMs ? r.text : "");
|
|
412
|
-
};
|
|
413
|
-
/** `
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
*
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
*
|
|
420
|
-
|
|
421
|
-
const answerOf = (ts, fromName, sinceMs) => {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
470
|
+
export const replyClosedBefore = (jsonlPath, fromName, sinceMs, untilMs, pingSigs = [], turn) => {
|
|
471
|
+
const r = answerOf(askWindow(jsonlPath, sinceMs, pingSigs), fromName, sinceMs, turn);
|
|
472
|
+
return r && (r.closedAt !== undefined && r.closedAt < untilMs && !r.error ? r.text : "");
|
|
473
|
+
};
|
|
474
|
+
/** 这一行是在续上一轮 (报错之后没有信封的 `continue` / 「继续」), 不起新的一轮。 */
|
|
475
|
+
const resumesAt = (ts, i) => !!ts[i - 1]?.apiError && !ts[i].env && !ts[i].notice && RESUME_RE.test(ts[i].text.trim());
|
|
476
|
+
/** 开最近这一轮的那句 user 行 (`marks` 读出来的 turns): 排队吃进的、后台通知、报错后的
|
|
477
|
+
* 续跑都不算 —— 它们不起新的一轮, 这一轮是谁发起的要看再往前那一句。 */
|
|
478
|
+
export const openingOf = (ts) => ts.filter((t, i) => t.role === "user" && !t.queued && !t.notice && !resumesAt(ts, i)).at(-1);
|
|
479
|
+
/** 开最近这一轮的那句 user 行, 往回读到有为止 (不受 TAIL_BYTES_MAX 限制) —— 一轮再长,
|
|
480
|
+
* 「这一轮是谁发起的」也要认得出 (父 k、链头、打断会不会误伤别人的活都靠它)。 */
|
|
481
|
+
export const openingTurn = (jsonlPath, pingSigs = []) => openingOf(withoutKeepalive(readTailUntil(jsonlPath, (raw) => parseTurns(jsonlPath, raw, false, true), (ts) => !!openingOf(withoutKeepalive(ts, pingSigs)), Infinity), pingSigs));
|
|
482
|
+
const answerOf = (ts, fromName, sinceMs, turn) => {
|
|
483
|
+
const lastAsk = (hit) => ts.reduce((at, t, i) => (t.role === "user" && t.env && hit(t.env) && (!t.ms || t.ms >= sinceMs - ASK_SLACK_MS) ? i : at), -1);
|
|
484
|
+
const byTurn = turn ? lastAsk((e) => !e.receipt && e.turn === turn) : -1;
|
|
485
|
+
// 退回按人找时只认没有件号的老信封: 带件号的那些是别的件, 不是这一件。
|
|
486
|
+
const direct = byTurn >= 0 ? byTurn : lastAsk((e) => !e.receipt && e.kind === "peer" && e.from === fromName && !(turn && e.turn));
|
|
487
|
+
const asked = Math.max(direct, lastAsk((e) => !!e.receipt && e.replyTo === fromName));
|
|
425
488
|
if (asked < 0)
|
|
426
489
|
return undefined;
|
|
427
|
-
const next = ts.findIndex((t, i) => i > asked && t.role === "user");
|
|
490
|
+
const next = ts.findIndex((t, i) => i > asked && t.role === "user" && !t.queued && !resumesAt(ts, i));
|
|
428
491
|
const last = ts.slice(asked + 1, next < 0 ? undefined : next).filter((t) => t.role === "assistant").at(-1);
|
|
429
492
|
const text = last && !(last.mid && next < 0) ? last.text : "";
|
|
430
|
-
return { text, ...(next < 0 ? {} : { closedAt: ts[next].ms ?? 0 }) };
|
|
493
|
+
return { text, ...(next < 0 ? {} : { closedAt: ts[next].ms ?? 0 }), ...(text && last?.apiError ? { error: true } : {}) };
|
|
431
494
|
};
|
|
432
495
|
// ── wizard → wizard 的信封 ────────────────────────────────────────────
|
|
433
496
|
// 同伴的话是原样落进输入框的, 而落进输入框的东西在模型眼里都是「用户说的」。没有
|
|
@@ -448,40 +511,89 @@ export { parseEnvelope };
|
|
|
448
511
|
* 谁、在哪说的; 住在与一个人的单聊里的, 默认值就是对的, 不为它每轮多付一段。 */
|
|
449
512
|
export const renderHumanEnvelope = (user, chat) => renderReminder(envelopeAttrs.human(user, chat), [`这一轮是 \`${user}\` 在群 **${chat}** 里说的, 你的回复发回那个群。`]);
|
|
450
513
|
/** 定时任务放的那一轮: 不标的话, 记录里它就成了「人说的」。 */
|
|
451
|
-
export const renderTaskEnvelope = (taskId) => renderReminder(envelopeAttrs.task(taskId),
|
|
514
|
+
export const renderTaskEnvelope = (taskId, quiet) => renderReminder(envelopeAttrs.task(taskId, quiet?.turn), quiet
|
|
515
|
+
? [`这一轮是定时任务 \`${taskId}\` 到点放进来的, 不是人此刻说的, 而且是**有事才说**的那种: 这一轮私下跑, 没人在看。`,
|
|
516
|
+
"照常执行。看完**没有值得人知道的**, 最后一条消息就只写一行 `QUIET`, 什么也不会发出去; 有, 就把最后一条消息写给人看 —— 守护进程原样转进群。",
|
|
517
|
+
"要派活给别的 wizard, 就在这一轮里用 `wait_peer` 把结果拿回来再收口 —— 回执要等这一轮结束才到, 那时已经没人替你转发。"]
|
|
518
|
+
: [`这一轮是定时任务 \`${taskId}\` 到点放进来的, 不是人此刻说的 —— 照常执行, 回复照常发进群。`]);
|
|
452
519
|
/** `from` = 发话方的称呼 (`.name`); `chat` 给了 = 公开轮 (那个群的名字, 可以是 ""),
|
|
453
520
|
* 不给 = 私聊。 */
|
|
454
|
-
export const renderPeerEnvelope = (from, chat) => renderReminder(envelopeAttrs.peer(from, chat),
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
521
|
+
export const renderPeerEnvelope = (from, chat, t) => renderReminder(envelopeAttrs.peer(from, chat, t), [
|
|
522
|
+
...(chat === undefined
|
|
523
|
+
? [
|
|
524
|
+
`这一轮是 wizard \`${from}\` 发来的**私聊**, 不是人说的: 人看不见这一轮, 读你回复的是 \`${from}\`。`,
|
|
525
|
+
...(t?.quiet
|
|
526
|
+
? [`\`${from}\` **不收回执**: 你这一轮的终句不会送到任何地方 —— 别 tell_peer 回它, 也别为这一轮 notify。`]
|
|
527
|
+
: [
|
|
528
|
+
`你这一轮的**最后一条消息**就是给它的回执 —— 你一停下, 守护进程自动把它送进 \`${from}\` 的输入框, **你不需要再调任何工具**: 不要 send_peer 回它 (那会让它多跑一轮), 也不要为这一轮 notify。`,
|
|
529
|
+
"对 wizard 直说: 不寒暄、不加对人的称呼、不复述它的话; 结论收口成末尾的 `RESULT: …` (交付物写进文件就回传路径); 要它补信息才能干下去, 就收口成 `NEED: <问题>` (这一份不算交差); 交付了文件, 每个加一行 `ARTIFACT: <路径> — 一句话`。",
|
|
530
|
+
]),
|
|
531
|
+
]
|
|
532
|
+
: [
|
|
533
|
+
`这一轮是 wizard \`${from}\` 在群${chat ? ` **${chat}** ` : ""}里**公开**对你说的: 你的回复会直接发进那个群, 人和 \`${from}\` 都看得到。`,
|
|
534
|
+
`照对人说话的方式答; 不要再 send_peer 把同一段话回给 \`${from}\` —— 你停下之后守护进程会自动把这段结论送回它那里。`,
|
|
535
|
+
]),
|
|
536
|
+
...(t?.re ? [`这是对件 \`${t.turn}\` 的**续问**: 接着那件活说, 不是一件新活。`] : []),
|
|
537
|
+
// 这句话要怎么接 —— 只有发话方知道, 不写出来对方一律当成一件活去干。
|
|
538
|
+
...(t?.act === "ask" ? [`这是一个**提问**: 只答这一问, 几句话${t.quiet ? "" : "收口 (`RESULT: <答案>`)"}, 别为它开新活; 你手上原来的活答完接着干。`] : []),
|
|
539
|
+
...(t?.act === "fyi" ? ["这是一条**知会**: 不用回复、不用收口, 也别为它开新活 —— 与手头的活相关就顺手吸收, 无关就略过; 一句话收住这一轮即可。"] : []),
|
|
540
|
+
// 来回多了就收口: 不在守护进程里拒绝 —— 拒了模型会换条通路接着乒乓。
|
|
541
|
+
...((t?.legs ?? 0) > 3 ? [`这件活已经来回 ${t.legs - 1} 次: 这一次请直接给 \`RESULT:\`, 拿不准的写清你的假设。`] : []),
|
|
542
|
+
...(t?.deadline ? [`期限 ${hhmm(t.deadline)}: 到点还没答完, \`${from}\` 收到的是一份超时回执。`] : []),
|
|
543
|
+
]);
|
|
544
|
+
const hhmm = (ms) => new Date(ms).toTimeString().slice(0, 5);
|
|
464
545
|
/** 回执的信封 —— 这一轮是对方干完活、守护进程自动送回来的结论, 不是新派的活。
|
|
465
546
|
*
|
|
466
547
|
* 为什么必须有它: 回执是原样 paste 进输入框的, 而落进输入框的东西在模型眼里都是
|
|
467
548
|
* 「有人在对我说话」。没有这一段, 它会把一份结论当成一件新活去做, 或者客气地回它
|
|
468
549
|
* 一句 —— 而回那一句又会被对方当成新的一轮。所以这里写死三件事: 这是回执、原话在
|
|
469
550
|
* 下面、读完接着干你自己的, 别回话。 */
|
|
470
|
-
export const renderReceiptEnvelope = (from, chat, job) => renderReminder(envelopeAttrs.receipt(from, chat, job), [
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
551
|
+
export const renderReceiptEnvelope = (from, chat, job, status = "done", turn, route) => renderReminder(envelopeAttrs.receipt(from, chat, job, status, turn, route), [
|
|
552
|
+
...(status === "done"
|
|
553
|
+
? [`这是 wizard \`${from}\` 对你某一次 send_peer 的**回执**: 它那一轮干完了, 守护进程把它的结论自动送到你这里 —— 不是人说的, 也不是新派给你的活。`]
|
|
554
|
+
: status === "need"
|
|
555
|
+
? [`这是 wizard \`${from}\` 对你某一次 send_peer 的**反问**: 它缺信息干不下去, 下面 \`NEED:\` 后面就是它要的 —— 按下面的件号续问答它; 这一份**不计入工单**, 它拿到答复干完会再送一份回执。`]
|
|
556
|
+
: status === "error"
|
|
557
|
+
? [`这是 wizard \`${from}\` 对你某一次 send_peer 的**失败回执**: 它那一轮以 CLI 报错收尾, 下面是报错原文, 不是它的结论, **不计入工单**。它若被续跑、之后答出来, 结论会照常再送来一份; 等不及就 peek_peer 看它, 或者再 tell_peer 它一次。`]
|
|
558
|
+
: [`这是 wizard \`${from}\` 对你某一次 send_peer 的**失败回执**: ${FAILED[status]}, 这一份不会再有答案了 (工单里按已落定计)。自己判断: 换人、再 tell_peer 追问, 或者在汇总里如实写缺了这一份。下面是守护进程的说明和它最后说的一句。`]),
|
|
559
|
+
// 去向由守护进程按派活链算好 (receipts.routeOf), 这里只告诉模型终句会落到哪。
|
|
560
|
+
...(route?.replyTo?.startsWith(".")
|
|
561
|
+
? [`你这一轮的**最后一条消息会作为回执送回 \`${route.replyTo}\`** (它当初把这件活派给你, 你转给了 \`${from}\`) —— 据此收口, 末尾写 \`RESULT: …\`。`]
|
|
562
|
+
: route?.replyTo
|
|
563
|
+
? [`你这一轮的**最后一条消息会发进群「${route.replyTo}」**, ${route.audience ? `\`${route.audience}\` ` : "人"}在等这件事的结论 —— 写给人看: 做成了什么、没做成什么, 不复述 \`${from}\` 的原话。`]
|
|
564
|
+
: route?.pending && !(job && job.job)
|
|
565
|
+
? [`还有 ${route.pending} 份没回: 这一轮的回复只记在 rolepage, 不外发 —— 先记下这一份, 等最后一份到了再收口。`]
|
|
566
|
+
: chat === undefined
|
|
567
|
+
? [`这段对话是私聊, 人看不见。`]
|
|
568
|
+
: [`这一轮的对话在群${chat ? ` **${chat}** ` : ""}里, 人看得见你们俩。`]),
|
|
569
|
+
...(status === "done"
|
|
570
|
+
? [
|
|
571
|
+
...(route?.replyTo ? [] : ["下面就是它的原话。据此接着干你自己手上那件事; **不要为收到回执而回话** —— 不要 send_peer 回它 (除非你确实有新的东西要问它), 也不要把这段原话复述给人, 只说你据此做了什么、结论是什么。"]),
|
|
572
|
+
"它可能还没干完全部 —— 读 `RESULT:` 那一行 (如果有) 作为它的收口结论; 要更多细节用 peek_peer 读它的对话, 不要猜。",
|
|
573
|
+
]
|
|
574
|
+
: []),
|
|
575
|
+
...(turn ? [`这件活是 \`${turn}\`: 要就它追问、补充, 用 \`tell_peer({name: "${from}", re: "${turn}"})\` —— 沿用同一个频道, 工单还开着就沿用工单。`] : []),
|
|
477
576
|
// 「齐了吗」是守护进程数的, 不让模型在上下文里自己数 —— 异步回执是 N 个独立
|
|
478
577
|
// 的轮次陆续进来的, 数错一个就会提前收口或者永远等。
|
|
479
|
-
...(job && job.job
|
|
480
|
-
? job.
|
|
481
|
-
? [
|
|
482
|
-
:
|
|
578
|
+
...(job && job.job && status !== "need" && !(job.req && status === "error")
|
|
579
|
+
? job.closed
|
|
580
|
+
? [`工单 \`${job.job}\` 已经收工 (close_job 过了), 这一份是收工之后才到的: 不必再 close_job, 也不计入那次汇总${status === "done" && !route?.replyTo ? " —— 这段结论还有用, 就自己补一句交代 (notify 给人 / 回给上游)" : ""}。`]
|
|
581
|
+
: job.req
|
|
582
|
+
// 需求根单: 不走「汇总 → close_job」—— 关单 = 归档, 要等人验收。
|
|
583
|
+
? [status === "done"
|
|
584
|
+
? `这是需求根单 \`${job.job}\` 的交付: lead 交差了。向人交代结果后**别 close_job**, 等人验收 —— 认可 → \`close_job({job: "${job.job}"})\` 归档并回收 lead; 说不对 → \`tell_peer({name: "${from}", re: "${turn}", job: "${job.job}"})\` 返工 (同一张单)。对人别提单号。`
|
|
585
|
+
: `这是需求根单 \`${job.job}\`, 但 lead 没交差 (${status}): 向人说明卡在哪。lead 还在 → \`tell_peer({name: "${from}", re: "${turn}", job: "${job.job}"})\` 叫它续; 否则 \`close_job({job: "${job.job}"})\` 关掉, 需要的话重新 \`dispatch({lead:true})\` 开新单。对人别提单号。`]
|
|
586
|
+
: job.done >= job.total
|
|
587
|
+
? [`这是工单 \`${job.job}\` 的**最后一份** (${job.done}/${job.total}, 全部到齐): 现在可以汇总收口了 —— close_job(summary) 留档并回收临时分身 (不发群); 给人的结论就是你这一轮的最后一条消息, 用你自己的话写${route?.replyTo && !route.replyTo.startsWith(".") ? " —— 它会进群" : ""}。`]
|
|
588
|
+
: [`这是工单 \`${job.job}\` 的第 ${job.done}/${job.total} 份, **还差 ${job.total - job.done} 份**: 先把这一份记住 (或落到文件里), 不要现在汇总、也不要向人汇报进度; 等最后一份到了会明确告诉你「全部到齐」。`]
|
|
483
589
|
: []),
|
|
484
590
|
]);
|
|
591
|
+
const FAILED = {
|
|
592
|
+
timeout: "等到期限它还没答完",
|
|
593
|
+
silent: "它停下了几次, 都没有答这一句",
|
|
594
|
+
dead: "它的 pane 没了 (被收掉或崩了)",
|
|
595
|
+
canceled: "它这件活被人收掉 / 打断了",
|
|
596
|
+
};
|
|
485
597
|
/** Prompt-token size of the session's most recent turn: input + both cache
|
|
486
598
|
* tiers = how full the context window is, i.e. exactly what a cold cache would
|
|
487
599
|
* have to re-write at 1.25x. Read from the last assistant usage snapshot in
|
|
@@ -517,6 +629,66 @@ const pickContextTokens = (jsonlPath, raw) => {
|
|
|
517
629
|
}
|
|
518
630
|
return 0;
|
|
519
631
|
};
|
|
632
|
+
/** Prompt-cache TTL (s) this session actually writes with, read off the last
|
|
633
|
+
* usage's per-tier split (`cache_creation.ephemeral_{1h,5m}_input_tokens`):
|
|
634
|
+
* Claude Code on a subscription writes 1h entries, the bare API default is 5min.
|
|
635
|
+
* 0 = no tiered write on record (backend without the split, or no turn yet) —
|
|
636
|
+
* the caller falls back to config. */
|
|
637
|
+
export const cacheTtlSec = (jsonlPath) => readTailUntil(jsonlPath, (raw) => pickCacheTtl(jsonlPath, raw), (v) => v > 0);
|
|
638
|
+
const pickCacheTtl = (jsonlPath, raw) => {
|
|
639
|
+
const normalize = backendForPath(jsonlPath).normalizeTranscriptLine;
|
|
640
|
+
const lines = raw.split("\n").filter((l) => l.includes("ephemeral_"));
|
|
641
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
642
|
+
let row;
|
|
643
|
+
try {
|
|
644
|
+
row = normalize(JSON.parse(lines[i]));
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const cc = row?.message?.usage?.cache_creation;
|
|
650
|
+
if ((cc?.ephemeral_1h_input_tokens ?? 0) > 0)
|
|
651
|
+
return 3600;
|
|
652
|
+
if ((cc?.ephemeral_5m_input_tokens ?? 0) > 0)
|
|
653
|
+
return 300;
|
|
654
|
+
}
|
|
655
|
+
return 0;
|
|
656
|
+
};
|
|
657
|
+
/** When this session's prompt cache was last touched: the START of its newest answered
|
|
658
|
+
* API request (= the user / tool_result row it was sent after), since TTL runs from
|
|
659
|
+
* the start of the request that reads or writes the entry. File mtime marks the
|
|
660
|
+
* END — a minute later at p99, several on a long generation — and also moves on
|
|
661
|
+
* metadata rows that touch no cache. Synthetic replies (`<synthetic>`, all-zero
|
|
662
|
+
* usage) sent nothing. A request still in flight is not counted (its user row may be a
|
|
663
|
+
* local command that sends nothing) — older = colder, the safe side.
|
|
664
|
+
* 0 = no request on record — the caller falls back to mtime. */
|
|
665
|
+
export const cacheTouchMs = (jsonlPath) => readTailUntil(jsonlPath, (raw) => pickCacheTouch(jsonlPath, raw), (v) => v > 0);
|
|
666
|
+
const pickCacheTouch = (jsonlPath, raw) => {
|
|
667
|
+
const normalize = backendForPath(jsonlPath).normalizeTranscriptLine;
|
|
668
|
+
return raw.split("\n").reduce((acc, line) => {
|
|
669
|
+
let parsed;
|
|
670
|
+
try {
|
|
671
|
+
parsed = JSON.parse(line);
|
|
672
|
+
}
|
|
673
|
+
catch {
|
|
674
|
+
return acc;
|
|
675
|
+
}
|
|
676
|
+
const row = normalize(parsed);
|
|
677
|
+
if (!row || row.isSidechain)
|
|
678
|
+
return acc;
|
|
679
|
+
const ms = stampMs(parsed.timestamp);
|
|
680
|
+
if (row.type === "user")
|
|
681
|
+
return ms ? { ...acc, sentAt: ms } : acc;
|
|
682
|
+
const u = row.type === "assistant" ? row.message?.usage : undefined;
|
|
683
|
+
const sent = (u?.input_tokens ?? 0) + (u?.cache_read_input_tokens ?? 0) + (u?.cache_creation_input_tokens ?? 0) > 0;
|
|
684
|
+
return sent ? { ...acc, touch: acc.sentAt || ms } : acc;
|
|
685
|
+
}, { sentAt: 0, touch: 0 }).touch;
|
|
686
|
+
};
|
|
687
|
+
/** A row's `timestamp` as epoch ms (Claude: ISO string, CodeBuddy: number); 0 if absent / unparsable. */
|
|
688
|
+
const stampMs = (ts) => {
|
|
689
|
+
const ms = typeof ts === "number" ? ts : Date.parse(String(ts ?? ""));
|
|
690
|
+
return Number.isNaN(ms) ? 0 : ms;
|
|
691
|
+
};
|
|
520
692
|
/** 这段上下文里读过 / 改过的文件 (旧的在前、去重, 同一文件按最后一次碰它的位置排)。
|
|
521
693
|
*
|
|
522
694
|
* 「它的上下文里装着什么」是可以算出来的事实: 从文件尾往回, 撞上 compact 边界就停
|
|
@@ -616,8 +788,8 @@ const lastToolMs = (jsonlPath) => {
|
|
|
616
788
|
return m;
|
|
617
789
|
const content = row.message?.content;
|
|
618
790
|
const tooly = Array.isArray(content) && content.some((b) => /^tool_(use|result)$/.test(b?.type ?? ""));
|
|
619
|
-
const ms =
|
|
620
|
-
return tooly &&
|
|
791
|
+
const ms = stampMs(parsed.timestamp);
|
|
792
|
+
return tooly && ms ? Math.max(m, ms) : m;
|
|
621
793
|
}
|
|
622
794
|
catch {
|
|
623
795
|
return m;
|
|
@@ -652,64 +824,29 @@ export const paneIsBusy = (paneText) => stripAnsi(paneText)
|
|
|
652
824
|
.filter(Boolean)
|
|
653
825
|
.slice(-FOOTER_ROWS)
|
|
654
826
|
.some((l) => BUSY_MARKERS.some((re) => re.test(l)));
|
|
655
|
-
const stallRows = (jsonlPath) => {
|
|
656
|
-
const normalize = backendForPath(jsonlPath).normalizeTranscriptLine;
|
|
657
|
-
return readTailBytes(jsonlPath, TAIL_BYTES)
|
|
658
|
-
.split("\n")
|
|
659
|
-
.flatMap((line) => {
|
|
660
|
-
if (!line.trim())
|
|
661
|
-
return [];
|
|
662
|
-
try {
|
|
663
|
-
const parsed = JSON.parse(line);
|
|
664
|
-
const row = normalize(parsed);
|
|
665
|
-
const role = row?.message?.role;
|
|
666
|
-
if (!row || row.isMeta || row.isSidechain || (role !== "user" && role !== "assistant"))
|
|
667
|
-
return [];
|
|
668
|
-
const content = row.message?.content;
|
|
669
|
-
const blocks = Array.isArray(content) ? content : [];
|
|
670
|
-
const ms = typeof parsed.timestamp === "number" ? parsed.timestamp : Date.parse(String(parsed.timestamp ?? ""));
|
|
671
|
-
return [{
|
|
672
|
-
role, ms: Number.isNaN(ms) ? 0 : ms,
|
|
673
|
-
apiError: parsed.isApiErrorMessage === true,
|
|
674
|
-
toolResult: blocks.length > 0 && blocks.every((b) => b?.type === "tool_result"),
|
|
675
|
-
text: stripMeta(blockText(content)).trim(),
|
|
676
|
-
}];
|
|
677
|
-
}
|
|
678
|
-
catch {
|
|
679
|
-
return [];
|
|
680
|
-
} // truncated first line of the tail window
|
|
681
|
-
});
|
|
682
|
-
};
|
|
683
|
-
export const transcriptStalled = (jsonlPath, pingSigs, quietMs, now = Date.now()) => {
|
|
684
|
-
const rows = stallRows(jsonlPath);
|
|
685
|
-
const last = rows.at(-1);
|
|
686
|
-
if (!last || !last.ms || now - last.ms < quietMs)
|
|
687
|
-
return false;
|
|
688
|
-
if (last.toolResult)
|
|
689
|
-
return true;
|
|
690
|
-
if (!(last.role === "assistant" && last.apiError))
|
|
691
|
-
return false;
|
|
692
|
-
if (/\/login\b|log(ged)? ?in|auth/i.test(last.text))
|
|
693
|
-
return false;
|
|
694
|
-
const opener = [...rows].reverse().find((r) => r.role === "user" && !r.toolResult);
|
|
695
|
-
return !!opener && !isKeepalivePingText(opener.text, pingSigs);
|
|
696
|
-
};
|
|
697
827
|
/** Machinery, not conversation: the `<system-reminder>` wrapper is the same
|
|
698
828
|
* marker the mirror's meta-stripper and `tailTurns` already drop, so the hint
|
|
699
829
|
* reaches the agent's context without leaking into WeCom bubbles or peer
|
|
700
|
-
* summaries.
|
|
701
|
-
|
|
830
|
+
* summaries. A wizard already introduced within the recent injections (`seen`)
|
|
831
|
+
* only gets its name re-listed — the full row is still in context; the usage
|
|
832
|
+
* rule rides along only when the window has none. `cwd` = the recipient's own
|
|
833
|
+
* workspace: a peer working in the same one doesn't repeat it. */
|
|
834
|
+
export const peerMentionPart = (mentions, cwd = "") => (seen) => {
|
|
702
835
|
if (mentions.length === 0)
|
|
703
|
-
return "";
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
"
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
836
|
+
return { text: "", keys: [] };
|
|
837
|
+
const fresh = mentions.filter((m) => !seen(`m:.${m.tag}`));
|
|
838
|
+
const known = mentions.filter((m) => seen(`m:.${m.tag}`));
|
|
839
|
+
const rule = !seen("rule:mention");
|
|
840
|
+
const row = (m) => `- .${m.tag}${m.job ? ` job="${clipLine(m.job, 80).replace(/"/g, "'")}"` : ""}${m.foreign ? ` chat=${m.chat || "(other)"}` : ""}${m.cwd && m.cwd !== cwd ? ` cwd=${tildePath(m.cwd)}` : ""}`;
|
|
841
|
+
return {
|
|
842
|
+
text: renderReminder({ wezard: "mention", names: mentions.map((m) => `.${m.tag}`).join(" ") }, [
|
|
843
|
+
...(rule
|
|
844
|
+
? ["`.name` tokens above are live wizards, not literal text. To involve / check / relay to one: tell_peer · peek_peer · wait_peer(name) — never answer for it; wizard_roster for all. A passing mention needs no tool."]
|
|
845
|
+
: []),
|
|
846
|
+
...fresh.map(row),
|
|
847
|
+
...(known.length ? [`${known.map((m) => `.${m.tag}`).join(" ")}: wizards (described earlier)`] : []),
|
|
848
|
+
]),
|
|
849
|
+
keys: [...(rule ? ["rule:mention"] : []), ...fresh.map((m) => `m:.${m.tag}`)],
|
|
850
|
+
};
|
|
714
851
|
};
|
|
715
852
|
//# sourceMappingURL=peers.js.map
|