openclaw-lark-multi-agent 1.8.19 → 1.8.21
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/dist/config.d.ts +15 -1
- package/dist/config.js +14 -2
- package/dist/feishu-bot.d.ts +47 -7
- package/dist/feishu-bot.js +423 -163
- package/dist/go-controller.d.ts +2 -0
- package/dist/go-controller.js +24 -8
- package/dist/input-status-display.d.ts +45 -0
- package/dist/input-status-display.js +67 -0
- package/dist/live-status.d.ts +22 -3
- package/dist/live-status.js +93 -25
- package/dist/message-store.d.ts +68 -2
- package/dist/message-store.js +249 -13
- package/dist/openclaw-client.d.ts +0 -6
- package/dist/openclaw-client.js +178 -67
- package/dist/queued-presentation.d.ts +8 -1
- package/dist/queued-presentation.js +109 -4
- package/dist/terminal-errors.d.ts +12 -0
- package/dist/terminal-errors.js +23 -3
- package/docs/releases/1.8.20.md +30 -0
- package/package.json +4 -3
package/dist/go-controller.d.ts
CHANGED
|
@@ -75,6 +75,8 @@ export type GoClient = {
|
|
|
75
75
|
export type GoControllerDeps = {
|
|
76
76
|
botName: string;
|
|
77
77
|
fallbackModel: string;
|
|
78
|
+
/** OpenClaw profile that owns this bot's sessions (default "main"). */
|
|
79
|
+
profile?: string;
|
|
78
80
|
store: import("./message-store.js").MessageStore;
|
|
79
81
|
client: GoClient;
|
|
80
82
|
/** Executor session key for a chat (e.g. lma-<bot>-<chatId>). */
|
package/dist/go-controller.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
/** Independent LMA supervision. MessageStore owns objective, criteria and completion. */
|
|
2
2
|
import { goSnapshotBlocked } from "./go-runtime-gate.js";
|
|
3
|
+
/** Logical (profile-free) part of a session key. */
|
|
4
|
+
const withoutAgentPrefix = (key) => key.replace(/^agent:[^:]+:/, '');
|
|
5
|
+
/** Physical session key for a logical executor key under this bot's profile. */
|
|
6
|
+
const physicalKey = (key, profile) => {
|
|
7
|
+
if (/^agent:[^:]+:/.test(key))
|
|
8
|
+
return key;
|
|
9
|
+
const agent = String(profile || "main").trim() || "main";
|
|
10
|
+
return `agent:${agent}:${key}`;
|
|
11
|
+
};
|
|
3
12
|
const CHECK_DELAY_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_CHECK_DELAY_MS || 20000);
|
|
4
13
|
const CHECKER_TIMEOUT_MS = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_CHECKER_TIMEOUT_MS || 10 * 60 * 1000);
|
|
5
14
|
const MAX_CONTINUES = Number(process.env.OPENCLAW_LARK_MULTI_AGENT_GO_MAX_CONTINUES || 10);
|
|
@@ -120,7 +129,7 @@ export class GoController {
|
|
|
120
129
|
const executor = sessionKey === job.executorSessionKey;
|
|
121
130
|
const result = executor ? await client.collectGoExecutorRun({ runId: receipt.runId, sessionKey, sessionId: expectedSessionId, timeoutMs: CHECKER_TIMEOUT_MS }) : null;
|
|
122
131
|
if (result && (result.runId !== receipt.runId || result.sessionId !== expectedSessionId ||
|
|
123
|
-
(result.sessionKey !== sessionKey && result.sessionKey !==
|
|
132
|
+
(result.sessionKey !== sessionKey && result.sessionKey !== physicalKey(sessionKey, this.d.profile))))
|
|
124
133
|
throw new Error("Executor result identity mismatch");
|
|
125
134
|
const text = result ? result.text : await client.collectGoRun({ runId: receipt.runId, sessionKey, sessionId: expectedSessionId, timeoutMs: CHECKER_TIMEOUT_MS });
|
|
126
135
|
this.d.store.recordGoRunResult(jobId, actionKey, { state: "complete", runId: receipt.runId, text, ...(result ? { executorResult: result } : {}) });
|
|
@@ -204,7 +213,10 @@ export class GoController {
|
|
|
204
213
|
await this.d.reply(messageId, `⚠️ 当前已有活动 Go #${existing.id}:\n目标:${existing.goal.slice(0, 200)}\n如需更换目标,请先 /go stop。`);
|
|
205
214
|
return;
|
|
206
215
|
}
|
|
207
|
-
|
|
216
|
+
// The Go supervision path talks to the Gateway directly, so it uses the
|
|
217
|
+
// PHYSICAL key throughout (profile included) exactly like the executor
|
|
218
|
+
// session itself.
|
|
219
|
+
const executorKey = physicalKey(this.d.executorSessionKeyFor(chatId), this.d.profile);
|
|
208
220
|
if (await this.safetyUnavailable(executorKey)) {
|
|
209
221
|
await this.d.reply(messageId, "❌ Go 未启动:当前运行时尚未验证 Checker 工具隔离、精确子孙任务/投递门禁及条件发送。不能仅凭提示词安全开启自动监督。独立模式无需原生 Goal;普通聊天不受影响。");
|
|
210
222
|
return;
|
|
@@ -230,7 +242,10 @@ export class GoController {
|
|
|
230
242
|
|| (typeof session.model === "string" && session.model
|
|
231
243
|
? (typeof session.modelProvider === "string" && session.modelProvider && !session.model.includes("/") ? `${session.modelProvider}/${session.model}` : session.model)
|
|
232
244
|
: this.d.fallbackModel);
|
|
233
|
-
|
|
245
|
+
// The checker session NAME stays profile-free (lma- -> lma-go-), exactly like
|
|
246
|
+
// every other logical key LMA stores. It is addressed under the bot's
|
|
247
|
+
// profile only when it is actually handed to the Gateway.
|
|
248
|
+
const checkerKey = physicalKey(`${withoutAgentPrefix(executorKey).replace(/^lma-/, "lma-go-")}-${crypto.randomUUID()}`, this.d.profile);
|
|
234
249
|
try {
|
|
235
250
|
await this.d.client.createSession({ key: checkerKey, model: resolvedModel, label: `Go Checker: ${this.d.botName} ${chatId.slice(-6)}` });
|
|
236
251
|
}
|
|
@@ -349,7 +364,7 @@ export class GoController {
|
|
|
349
364
|
if (saved?.state !== "complete") {
|
|
350
365
|
const result = await this.d.client.readGoExecutorRunResult?.({ ...send, runId: receipt.runId });
|
|
351
366
|
if (!result || result.runId !== receipt.runId || result.sessionId !== job.executorSessionId ||
|
|
352
|
-
![job.executorSessionKey,
|
|
367
|
+
![job.executorSessionKey, physicalKey(job.executorSessionKey, this.d.profile)].includes(result.sessionKey)) {
|
|
353
368
|
this.d.store.recordGoRunResult(job.id, send.actionKey, { state: "unknown", runId: receipt.runId });
|
|
354
369
|
continue;
|
|
355
370
|
}
|
|
@@ -510,16 +525,17 @@ export class GoController {
|
|
|
510
525
|
// ---------------- check cycle ----------------
|
|
511
526
|
async executorBusy(chatId, job) {
|
|
512
527
|
try {
|
|
513
|
-
|
|
528
|
+
const executorPhysicalKey = job.executorSessionKey;
|
|
529
|
+
if (await this.safetyUnavailable(executorPhysicalKey))
|
|
514
530
|
return "运行时安全能力未验证";
|
|
515
|
-
const info = await (this.d.client.getGoSessionInfo?.bind(this.d.client) ?? this.d.client.getSessionInfo.bind(this.d.client))(
|
|
531
|
+
const info = await (this.d.client.getGoSessionInfo?.bind(this.d.client) ?? this.d.client.getSessionInfo.bind(this.d.client))(executorPhysicalKey);
|
|
516
532
|
if (info?.session?.sessionId !== job.executorSessionId || info?.session?.archived)
|
|
517
533
|
return "执行会话代际已改变或归档";
|
|
518
534
|
const identity = this.d.store.getGoNativeIdentity(job.id);
|
|
519
535
|
if (!identity)
|
|
520
536
|
return "Missing independent run binding";
|
|
521
|
-
const snapshot = await this.d.client.readGoRuntimeSnapshot(
|
|
522
|
-
const blocked = goSnapshotBlocked(snapshot, { sessionKey:
|
|
537
|
+
const snapshot = await this.d.client.readGoRuntimeSnapshot(executorPhysicalKey, job.executorSessionId);
|
|
538
|
+
const blocked = goSnapshotBlocked(snapshot, { sessionKey: executorPhysicalKey, sessionId: job.executorSessionId,
|
|
523
539
|
goalId: identity.nativeGoalId, goalRevision: identity.goalRevision, executorRunIds: identity.executorRunIds });
|
|
524
540
|
if (blocked) {
|
|
525
541
|
if (blocked === "identity/version changed")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Display-only references. Never read by admission, routing, reactions or replay. */
|
|
2
|
+
export type InputHintKind = 'received' | 'queued' | 'submitting' | 'append_queued' | 'accepted' | 'processing' | 'waiting' | 'paused' | 'unconfirmed' | 'delivery_pending' | 'partial' | 'completed' | 'no_reply' | 'replied' | 'failed' | 'blocked';
|
|
3
|
+
export type InputHint = {
|
|
4
|
+
rowId?: number;
|
|
5
|
+
messageId?: string;
|
|
6
|
+
kind: InputHintKind;
|
|
7
|
+
detail?: string;
|
|
8
|
+
at: number;
|
|
9
|
+
preview?: string;
|
|
10
|
+
previewSource?: 'visible';
|
|
11
|
+
};
|
|
12
|
+
export type InputStatusContext = {
|
|
13
|
+
botName?: string;
|
|
14
|
+
appId?: string;
|
|
15
|
+
chatId?: string;
|
|
16
|
+
stopEpoch?: number;
|
|
17
|
+
items: InputHint[];
|
|
18
|
+
note?: InputHint;
|
|
19
|
+
};
|
|
20
|
+
export type InputHintChange = {
|
|
21
|
+
kind: InputHintKind;
|
|
22
|
+
detail?: string;
|
|
23
|
+
ids?: number[];
|
|
24
|
+
at: number;
|
|
25
|
+
};
|
|
26
|
+
export type InputDisplayLimits = {
|
|
27
|
+
preview: number;
|
|
28
|
+
members: number;
|
|
29
|
+
activity: number;
|
|
30
|
+
};
|
|
31
|
+
export declare const LIVE_CARD_BYTES = 30000;
|
|
32
|
+
export declare const INPUT_DISPLAY_LIMITS: InputDisplayLimits[];
|
|
33
|
+
export declare function hintLabel(kind: InputHintKind, en: boolean): string;
|
|
34
|
+
export declare function hintDetail(kind: InputHintKind, en: boolean): string;
|
|
35
|
+
export declare function displayClip(value: string, max: number): string;
|
|
36
|
+
/** Explicit visible human ingress: escaping/clipping only. Never infer media or
|
|
37
|
+
* generated prompts from text that merely looks like a system annotation. */
|
|
38
|
+
export declare function visibleInputPreview(content: string, max: number, en: boolean): string;
|
|
39
|
+
/** Legacy stored/hydrated body fallback; explicit visible ingress never uses it. */
|
|
40
|
+
export declare function inputPreview(content: string, max: number, en: boolean): string;
|
|
41
|
+
/** Use the existing markdown component as inert text, not as a user-authored
|
|
42
|
+
* card/mention/link language. Entity introducers/platform tags are neutralized
|
|
43
|
+
* before markdown punctuation is escaped. No media or URL is fetched. */
|
|
44
|
+
export declare function inertInputText(value: string): string;
|
|
45
|
+
export declare function selectInputHints(items: InputHint[], max: number): InputHint[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export const LIVE_CARD_BYTES = 30_000;
|
|
2
|
+
export const INPUT_DISPLAY_LIMITS = [
|
|
3
|
+
{ preview: 160, members: 12, activity: 180 }, { preview: 96, members: 10, activity: 120 },
|
|
4
|
+
{ preview: 48, members: 6, activity: 80 }, { preview: 20, members: 2, activity: 24 },
|
|
5
|
+
];
|
|
6
|
+
const labels = {
|
|
7
|
+
received: ['📥 已收件', '📥 Received'], queued: ['⏳ 本地排队', '⏳ Queued'], submitting: ['📨 提交待确认', '📨 Submission pending'],
|
|
8
|
+
append_queued: ['⏳ 追加排队', '⏳ Append queued'], accepted: ['✅ 已接收', '✅ Accepted'], processing: ['⚙️ 处理中', '⚙️ Processing'],
|
|
9
|
+
waiting: ['⏳ 等待后续', '⏳ Waiting'], paused: ['⏸️ 等待暂停', '⏸️ Wait paused'], unconfirmed: ['❔ 结果待确认', '❔ Result unconfirmed'],
|
|
10
|
+
delivery_pending: ['📨 投递待确认', '📨 Delivery pending'], partial: ['⚠️ 部分确认', '⚠️ Partial'], completed: ['✅ 本轮结束', '✅ Round ended'],
|
|
11
|
+
no_reply: ['💤 无正文', '💤 No content'], replied: ['💬 关联已回复', '💬 Linked reply'], failed: ['⚠️ 执行中断', '⚠️ Interrupted'], blocked: ['⛔ 尚未提交', '⛔ Not submitted'],
|
|
12
|
+
};
|
|
13
|
+
const details = {
|
|
14
|
+
received: ['尚未确认 Gateway 接收', 'Gateway acceptance unconfirmed'], queued: ['等待提交确认', 'Waiting for admission'],
|
|
15
|
+
submitting: ['已开始提交;Gateway 接收尚未确认', 'Submission started; Gateway acceptance unconfirmed'],
|
|
16
|
+
append_queued: ['实际接收方、提交及处理结果未确认', 'Actual receiver, commitment and result unconfirmed'],
|
|
17
|
+
accepted: ['Gateway 已接收;等待执行活动', 'Gateway accepted; waiting for activity'],
|
|
18
|
+
partial: ['root 结果仅覆盖普通输入;追加输入提交及处理仍未确认,不重放', 'Root result covers ordinary inputs only; appended commitment/result remain unconfirmed, no replay'],
|
|
19
|
+
waiting: ['保留后续结果观察;不自动重发输入', 'Continuing result observation; no input replay'],
|
|
20
|
+
paused: ['仅暂停等待;未因此停止任务或重发输入', 'Waiting paused only; no task stop or input replay'],
|
|
21
|
+
unconfirmed: ['最终结果尚未确认', 'Final result unconfirmed'], delivery_pending: ['平台投递回执尚未确认;不重复发送', 'Platform receipt unconfirmed; no duplicate send'],
|
|
22
|
+
completed: ['本轮结果仅覆盖实际普通输入', 'This round covers only its ordinary inputs'],
|
|
23
|
+
no_reply: ['本轮未产生用户可见正文', 'No user-visible content this round'],
|
|
24
|
+
replied: ['关联轮次回复已送达;不据此确认追加输入提交', 'Linked run reply delivered; input commitment remains separate'],
|
|
25
|
+
failed: ['本轮执行中断;不据此归因用户停止', 'Execution interrupted; no user-stop attribution'],
|
|
26
|
+
blocked: ['保留原输入;不迁移或自动重发', 'Input retained; no migration or automatic replay'],
|
|
27
|
+
};
|
|
28
|
+
export function hintLabel(kind, en) { return `【${(labels[kind] || labels.unconfirmed)[en ? 1 : 0]}】`; }
|
|
29
|
+
export function hintDetail(kind, en) { return details[kind]?.[en ? 1 : 0] || ''; }
|
|
30
|
+
export function displayClip(value, max) {
|
|
31
|
+
// Code-point safe, including malformed isolated surrogates; no split emoji
|
|
32
|
+
// surrogate, JSON fragment or half a UTF-8 sequence is emitted.
|
|
33
|
+
const points = Array.from(String(value), ch => { const n = ch.codePointAt(0); return n >= 0xd800 && n <= 0xdfff ? '�' : ch; });
|
|
34
|
+
return points.length > max ? points.slice(0, Math.max(0, max - 1)).join('') + '…' : points.join('');
|
|
35
|
+
}
|
|
36
|
+
/** Explicit visible human ingress: escaping/clipping only. Never infer media or
|
|
37
|
+
* generated prompts from text that merely looks like a system annotation. */
|
|
38
|
+
export function visibleInputPreview(content, max, en) {
|
|
39
|
+
const text = String(content || '').replace(/\s+/g, ' ').trim();
|
|
40
|
+
return displayClip(text || (en ? '[Message body unavailable]' : '[消息正文暂不可用]'), max);
|
|
41
|
+
}
|
|
42
|
+
/** Legacy stored/hydrated body fallback; explicit visible ingress never uses it. */
|
|
43
|
+
export function inputPreview(content, max, en) {
|
|
44
|
+
const filename = (value) => value.trim().split(/[\\/]/).at(-1) || (en ? 'file' : '文件');
|
|
45
|
+
const raw = String(content || '').trim();
|
|
46
|
+
if (raw === '请回复上面最近一条用户消息。')
|
|
47
|
+
return en ? '[Message body unavailable]' : '[消息正文暂不可用]';
|
|
48
|
+
const text = raw
|
|
49
|
+
.replace(/\n\n\[飞书文档已由 LMA 用机器人权限读取并转换为 Markdown 附件,OpenClaw 可直接读取附件内容。\][\s\S]*$/, '')
|
|
50
|
+
.replace(/\[(?:Image|Audio|Sticker)(?::[^\]]*)?\]/gi, match => /^\[Image/i.test(match) ? (en ? '[Image]' : '[图片]') : /^\[Audio/i.test(match) ? (en ? '[Audio]' : '[语音]') : (en ? '[Sticker]' : '[贴纸]'))
|
|
51
|
+
.replace(/\[File:\s*([^\]]*)\]/gi, (_all, value) => `[${en ? 'File' : '文件'}: ${filename(value.split(/\s+->\s+|\s+- download failed/)[0])}]`)
|
|
52
|
+
.replace(/\[(?:FeishuDoc|Attachment):[^\]]*\]/gi, en ? '[Document]' : '[文档]')
|
|
53
|
+
.replace(/\s+/g, ' ').trim();
|
|
54
|
+
return displayClip(text || (en ? '[Message body unavailable]' : '[消息正文暂不可用]'), max);
|
|
55
|
+
}
|
|
56
|
+
/** Use the existing markdown component as inert text, not as a user-authored
|
|
57
|
+
* card/mention/link language. Entity introducers/platform tags are neutralized
|
|
58
|
+
* before markdown punctuation is escaped. No media or URL is fetched. */
|
|
59
|
+
export function inertInputText(value) {
|
|
60
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/[\\`*_{}\[\]()#+!|~]/g, '\\$&');
|
|
61
|
+
}
|
|
62
|
+
export function selectInputHints(items, max) {
|
|
63
|
+
if (items.length <= max)
|
|
64
|
+
return items;
|
|
65
|
+
const first = Math.max(1, Math.floor(max / 3));
|
|
66
|
+
return [...items.slice(0, first), ...items.slice(-(max - first))];
|
|
67
|
+
}
|
package/dist/live-status.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputHintChange, InputHintKind, InputStatusContext } from "./input-status-display.js";
|
|
1
2
|
import type { ProgressEvent } from "./openclaw-client";
|
|
2
3
|
/** One activity line shown in the card content area. */
|
|
3
4
|
export type LiveStatusLine = {
|
|
@@ -5,9 +6,13 @@ export type LiveStatusLine = {
|
|
|
5
6
|
text: string;
|
|
6
7
|
/** Relative time (seconds since run start) when this line was produced. */
|
|
7
8
|
at: number;
|
|
9
|
+
/** Delivery hint text is rendered through exact input references, not activity markdown. */
|
|
10
|
+
inputHint?: boolean;
|
|
8
11
|
};
|
|
9
12
|
/** Structured snapshot the renderer turns into a Feishu interactive card. */
|
|
10
13
|
export type LiveStatusView = {
|
|
14
|
+
/** UI-only source references; bodies are resolved at the actual card boundary. */
|
|
15
|
+
inputStatus?: InputStatusContext;
|
|
11
16
|
/** Card title, e.g. "Claude 正在执行" / "✅ Claude 已完成". */
|
|
12
17
|
title: string;
|
|
13
18
|
/** Activity lines (oldest first); when finished this holds a summary instead. */
|
|
@@ -35,6 +40,7 @@ export type LiveStatusFinalMeta = {
|
|
|
35
40
|
thinkingLevel?: string;
|
|
36
41
|
locale: "zh" | "en";
|
|
37
42
|
noReply?: boolean;
|
|
43
|
+
inputStatus?: InputStatusContext;
|
|
38
44
|
};
|
|
39
45
|
/** Optional external presentation owner (e.g. the durable outbox). A local
|
|
40
46
|
* execution outcome is NOT a platform delivery receipt. Once handed off, late
|
|
@@ -61,6 +67,8 @@ export declare class LiveStatusEffectRejectedError extends Error {
|
|
|
61
67
|
export declare class LiveStatusEffectSuppressedError extends Error {
|
|
62
68
|
}
|
|
63
69
|
export type LiveStatusCallbacks = {
|
|
70
|
+
/** UI-only update/read; cannot grant admission, progress or input completion. */
|
|
71
|
+
inputStatus?: (change?: InputHintChange) => InputStatusContext | undefined;
|
|
64
72
|
/** Create the status card; returns the new message id. */
|
|
65
73
|
create: (view: LiveStatusView, effect: LiveStatusEffect<string | undefined>) => Promise<string | undefined>;
|
|
66
74
|
/** Patch the existing status card. Reject with LiveStatusEffectRejectedError
|
|
@@ -73,6 +81,7 @@ export type LiveStatusCallbacks = {
|
|
|
73
81
|
unconfirmedInputs?: (outcome: "done" | "failed") => {
|
|
74
82
|
title: string;
|
|
75
83
|
detail: string;
|
|
84
|
+
ordinaryIds?: number[];
|
|
76
85
|
} | undefined;
|
|
77
86
|
};
|
|
78
87
|
export type LiveStatusOptions = {
|
|
@@ -116,6 +125,7 @@ export declare class LiveStatusController {
|
|
|
116
125
|
private generation;
|
|
117
126
|
/** Frozen/terminal presentation wins over every older progress snapshot. */
|
|
118
127
|
private pinnedView?;
|
|
128
|
+
private pinnedInputGuard?;
|
|
119
129
|
private terminalOwner?;
|
|
120
130
|
private pendingEdit?;
|
|
121
131
|
private editPump?;
|
|
@@ -136,17 +146,19 @@ export declare class LiveStatusController {
|
|
|
136
146
|
private toolCallCount;
|
|
137
147
|
/** True when finished via noReply(): show a "no content" summary. */
|
|
138
148
|
private noReplyResult;
|
|
149
|
+
private fallbackInputHint?;
|
|
139
150
|
constructor(callbacks: LiveStatusCallbacks, opts: LiveStatusOptions);
|
|
140
151
|
/** Receipt is durable locally, NOT Gateway admission. */
|
|
141
|
-
received(detail: string): void;
|
|
152
|
+
received(detail: string, ids?: number[], kind?: InputHintKind): void;
|
|
142
153
|
/** A proven never-attempted ordinary recovery may reuse its queued card.
|
|
143
154
|
* Merely observing an expected root never calls this method. */
|
|
144
155
|
resumeQueued(): void;
|
|
145
156
|
/** Enqueue/local receipt remains waiting, not actual-owner/model evidence.
|
|
146
157
|
* Keep one latest pinned view while allowing another input to coalesce. */
|
|
147
|
-
showInputWaiting(detail: string, title?: string): Promise<void>;
|
|
158
|
+
showInputWaiting(detail: string, title?: string, ids?: number[], kind?: InputHintKind): Promise<void>;
|
|
148
159
|
/** Only the exact ACK owner calls this; admission is not model execution. */
|
|
149
160
|
admitted(successor?: boolean): Promise<void>;
|
|
161
|
+
submitting(): void;
|
|
150
162
|
get id(): string | undefined;
|
|
151
163
|
start(initialDetail?: string): void;
|
|
152
164
|
setMetadata(model?: string, thinkingLevel?: string): void;
|
|
@@ -156,7 +168,7 @@ export declare class LiveStatusController {
|
|
|
156
168
|
showWaitingForResult(detail: string): Promise<void>;
|
|
157
169
|
/** Release only local observation of a now-idle session, without claiming
|
|
158
170
|
* the previous task succeeded or was aborted. Normal progress is unchanged. */
|
|
159
|
-
pauseForUnconfirmedResult(detail: string, title?: string): Promise<void>;
|
|
171
|
+
pauseForUnconfirmedResult(detail: string, title?: string, kind?: InputHintKind): Promise<void>;
|
|
160
172
|
/** Close only the display after the associated run's original-chat reply is
|
|
161
173
|
* platform-confirmed. This deliberately does not claim inserted-input commit. */
|
|
162
174
|
linkedReplyDelivered(deliveredAt: number, preserveReplyPayload?: boolean, retryZeroEffect?: boolean): Promise<void>;
|
|
@@ -181,6 +193,10 @@ export declare class LiveStatusController {
|
|
|
181
193
|
*/
|
|
182
194
|
noReply(): Promise<void>;
|
|
183
195
|
dispose(): void;
|
|
196
|
+
/** Refresh only the existing UI-owned card after recall. Never create a
|
|
197
|
+
* card, restart ticking, change phase or invoke a business terminal owner.
|
|
198
|
+
* Unknown effects remain fenced; a later known receipt retains this guard. */
|
|
199
|
+
redactRecalledInput(effectGuard: () => boolean): Promise<void>;
|
|
184
200
|
private pinView;
|
|
185
201
|
private finishView;
|
|
186
202
|
private reconcilePinnedView;
|
|
@@ -194,6 +210,9 @@ export declare class LiveStatusController {
|
|
|
194
210
|
private appendText;
|
|
195
211
|
private normalizeText;
|
|
196
212
|
private clip;
|
|
213
|
+
private terminalInputHint;
|
|
214
|
+
private updateInputHint;
|
|
215
|
+
private inputSnapshot;
|
|
197
216
|
private pushLine;
|
|
198
217
|
private stopTimers;
|
|
199
218
|
private ensureCreatedNow;
|
package/dist/live-status.js
CHANGED
|
@@ -47,6 +47,7 @@ export class LiveStatusController {
|
|
|
47
47
|
generation = 0;
|
|
48
48
|
/** Frozen/terminal presentation wins over every older progress snapshot. */
|
|
49
49
|
pinnedView;
|
|
50
|
+
pinnedInputGuard;
|
|
50
51
|
terminalOwner;
|
|
51
52
|
pendingEdit;
|
|
52
53
|
editPump;
|
|
@@ -67,6 +68,7 @@ export class LiveStatusController {
|
|
|
67
68
|
toolCallCount = 0;
|
|
68
69
|
/** True when finished via noReply(): show a "no content" summary. */
|
|
69
70
|
noReplyResult = false;
|
|
71
|
+
fallbackInputHint;
|
|
70
72
|
constructor(callbacks, opts) {
|
|
71
73
|
this.callbacks = callbacks;
|
|
72
74
|
this.opts = opts;
|
|
@@ -84,11 +86,13 @@ export class LiveStatusController {
|
|
|
84
86
|
this.noReplyResult = restored.view.noReply;
|
|
85
87
|
this.waitingForResult = restored.view.phase === "waiting";
|
|
86
88
|
this.updatesFrozen = this.waitingForResult;
|
|
87
|
-
if (restored.view.state
|
|
89
|
+
if (['done', 'failed', 'replied'].includes(restored.view.state)) {
|
|
88
90
|
this.finalized = true;
|
|
89
91
|
this.terminalOutcome = true;
|
|
90
92
|
this.updatesFrozen = true;
|
|
91
93
|
}
|
|
94
|
+
if (this.waitingForResult || this.terminalOutcome)
|
|
95
|
+
this.pinnedView = restored.view;
|
|
92
96
|
}
|
|
93
97
|
// Attempted/unknown create survives process loss as a no-create fence.
|
|
94
98
|
if (restored.blockCreate && !this.messageId)
|
|
@@ -96,10 +100,11 @@ export class LiveStatusController {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
/** Receipt is durable locally, NOT Gateway admission. */
|
|
99
|
-
received(detail) {
|
|
103
|
+
received(detail, ids, kind = "received") {
|
|
100
104
|
if (this.finalized || this.disposed)
|
|
101
105
|
return;
|
|
102
|
-
this.
|
|
106
|
+
this.updateInputHint(kind, detail, ids);
|
|
107
|
+
this.pushLine("lifecycle", detail, true);
|
|
103
108
|
this.callbacks.desired?.(this.buildView());
|
|
104
109
|
void this.safeEdit(this.buildView());
|
|
105
110
|
}
|
|
@@ -113,6 +118,7 @@ export class LiveStatusController {
|
|
|
113
118
|
this.waitingForResult = false;
|
|
114
119
|
this.waitingCreateAllowed = false;
|
|
115
120
|
this.state = "queued";
|
|
121
|
+
this.updateInputHint("queued");
|
|
116
122
|
this.generation++;
|
|
117
123
|
this.pinnedView = undefined;
|
|
118
124
|
this.pendingEdit = undefined;
|
|
@@ -126,13 +132,14 @@ export class LiveStatusController {
|
|
|
126
132
|
}
|
|
127
133
|
/** Enqueue/local receipt remains waiting, not actual-owner/model evidence.
|
|
128
134
|
* Keep one latest pinned view while allowing another input to coalesce. */
|
|
129
|
-
async showInputWaiting(detail, title) {
|
|
135
|
+
async showInputWaiting(detail, title, ids, kind = "waiting") {
|
|
130
136
|
if (this.disposed || this.terminalOutcome)
|
|
131
137
|
return;
|
|
132
138
|
this.updatesFrozen = true;
|
|
133
139
|
this.waitingForResult = true;
|
|
134
140
|
this.waitingCreateAllowed = !this.messageId && !this.disabled;
|
|
135
|
-
this.
|
|
141
|
+
this.updateInputHint(kind, detail, ids);
|
|
142
|
+
this.pushLine("lifecycle", detail, true);
|
|
136
143
|
this.stopTimers();
|
|
137
144
|
const view = this.buildView();
|
|
138
145
|
if (title)
|
|
@@ -152,10 +159,18 @@ export class LiveStatusController {
|
|
|
152
159
|
return;
|
|
153
160
|
this.waitingForResult = false;
|
|
154
161
|
this.state = "admitted";
|
|
155
|
-
this.
|
|
162
|
+
this.updateInputHint("accepted");
|
|
163
|
+
this.pushLine("lifecycle", this.opts.locale === "en" ? "Gateway accepted; waiting for activity" : "Gateway 已接收,等待执行活动", true);
|
|
156
164
|
this.callbacks.desired?.(this.buildView());
|
|
157
165
|
await this.safeEdit(this.buildView());
|
|
158
166
|
}
|
|
167
|
+
submitting() {
|
|
168
|
+
if (this.finalized || this.disposed)
|
|
169
|
+
return;
|
|
170
|
+
this.updateInputHint("submitting");
|
|
171
|
+
this.callbacks.desired?.(this.buildView());
|
|
172
|
+
void this.safeEdit(this.buildView());
|
|
173
|
+
}
|
|
159
174
|
get id() { return this.messageId; }
|
|
160
175
|
start(initialDetail) {
|
|
161
176
|
if (this.createSuppressed) {
|
|
@@ -236,6 +251,7 @@ export class LiveStatusController {
|
|
|
236
251
|
}
|
|
237
252
|
this.waitingForResult = false;
|
|
238
253
|
this.state = "running";
|
|
254
|
+
this.updateInputHint("processing");
|
|
239
255
|
this.callbacks.desired?.(this.buildView());
|
|
240
256
|
await this.ensureCreatedNow();
|
|
241
257
|
await this.safeEdit(this.buildView());
|
|
@@ -247,16 +263,18 @@ export class LiveStatusController {
|
|
|
247
263
|
return;
|
|
248
264
|
this.updatesFrozen = true;
|
|
249
265
|
this.waitingForResult = true;
|
|
250
|
-
this.
|
|
266
|
+
this.updateInputHint("waiting", detail);
|
|
267
|
+
this.pushLine("lifecycle", detail, true);
|
|
251
268
|
this.stopTimers();
|
|
252
269
|
await this.finishView(this.buildView());
|
|
253
270
|
}
|
|
254
271
|
/** Release only local observation of a now-idle session, without claiming
|
|
255
272
|
* the previous task succeeded or was aborted. Normal progress is unchanged. */
|
|
256
|
-
async pauseForUnconfirmedResult(detail, title) {
|
|
273
|
+
async pauseForUnconfirmedResult(detail, title, kind = "unconfirmed") {
|
|
257
274
|
if (this.finalized)
|
|
258
275
|
return;
|
|
259
|
-
this.
|
|
276
|
+
this.updateInputHint(kind, detail);
|
|
277
|
+
this.pushLine("lifecycle", detail, true);
|
|
260
278
|
this.waitingForResult = true;
|
|
261
279
|
this.finalized = true;
|
|
262
280
|
this.stopTimers();
|
|
@@ -271,6 +289,7 @@ export class LiveStatusController {
|
|
|
271
289
|
if (this.disposed || this.terminalOutcome && !safeRetry || !Number.isFinite(deliveredAt) || deliveredAt < this.startedAt)
|
|
272
290
|
return;
|
|
273
291
|
this.state = "replied";
|
|
292
|
+
this.updateInputHint("replied");
|
|
274
293
|
this.waitingForResult = false;
|
|
275
294
|
this.updatesFrozen = true;
|
|
276
295
|
this.finalized = true;
|
|
@@ -294,9 +313,9 @@ export class LiveStatusController {
|
|
|
294
313
|
async complete() {
|
|
295
314
|
if (this.disposed || this.terminalOutcome)
|
|
296
315
|
return;
|
|
297
|
-
const pending = this.
|
|
316
|
+
const pending = this.terminalInputHint("completed", "done");
|
|
298
317
|
if (pending) {
|
|
299
|
-
await this.showInputWaiting(pending.detail, pending.title);
|
|
318
|
+
await this.showInputWaiting(pending.detail, pending.title, [], pending.ordinaryIds?.length ? "partial" : "queued");
|
|
300
319
|
return;
|
|
301
320
|
}
|
|
302
321
|
this.terminalOutcome = true;
|
|
@@ -317,9 +336,9 @@ export class LiveStatusController {
|
|
|
317
336
|
async prepareTerminalDelivery(noReply = false, owner) {
|
|
318
337
|
if (this.disposed || (this.terminalOutcome && this.state !== "done"))
|
|
319
338
|
return undefined;
|
|
320
|
-
const pending = this.
|
|
339
|
+
const pending = this.terminalInputHint(noReply ? "no_reply" : "delivery_pending", "done");
|
|
321
340
|
if (pending) {
|
|
322
|
-
await this.showInputWaiting(pending.detail, pending.title);
|
|
341
|
+
await this.showInputWaiting(pending.detail, pending.title, [], pending.ordinaryIds?.length ? "partial" : "queued");
|
|
323
342
|
return undefined;
|
|
324
343
|
}
|
|
325
344
|
if (owner) {
|
|
@@ -350,6 +369,7 @@ export class LiveStatusController {
|
|
|
350
369
|
thinkingLevel: this.opts.thinkingLevel,
|
|
351
370
|
locale: this.opts.locale === "en" ? "en" : "zh",
|
|
352
371
|
noReply: this.noReplyResult,
|
|
372
|
+
inputStatus: this.inputSnapshot(),
|
|
353
373
|
};
|
|
354
374
|
}
|
|
355
375
|
async prepareFinalDelivery(owner) {
|
|
@@ -358,9 +378,9 @@ export class LiveStatusController {
|
|
|
358
378
|
async fail() {
|
|
359
379
|
if (this.disposed || this.terminalOutcome)
|
|
360
380
|
return;
|
|
361
|
-
const pending = this.
|
|
381
|
+
const pending = this.terminalInputHint("failed", "failed");
|
|
362
382
|
if (pending) {
|
|
363
|
-
await this.showInputWaiting(pending.detail, pending.title);
|
|
383
|
+
await this.showInputWaiting(pending.detail, pending.title, [], pending.ordinaryIds?.length ? "partial" : "queued");
|
|
364
384
|
return;
|
|
365
385
|
}
|
|
366
386
|
this.terminalOutcome = true;
|
|
@@ -377,9 +397,9 @@ export class LiveStatusController {
|
|
|
377
397
|
async noReply() {
|
|
378
398
|
if (this.disposed || this.terminalOutcome)
|
|
379
399
|
return;
|
|
380
|
-
const pending = this.
|
|
400
|
+
const pending = this.terminalInputHint("no_reply", "done");
|
|
381
401
|
if (pending) {
|
|
382
|
-
await this.showInputWaiting(pending.detail, pending.title);
|
|
402
|
+
await this.showInputWaiting(pending.detail, pending.title, [], pending.ordinaryIds?.length ? "partial" : "queued");
|
|
383
403
|
return;
|
|
384
404
|
}
|
|
385
405
|
this.terminalOutcome = true;
|
|
@@ -396,7 +416,26 @@ export class LiveStatusController {
|
|
|
396
416
|
this.pendingEdit = undefined;
|
|
397
417
|
this.stopTimers();
|
|
398
418
|
}
|
|
399
|
-
|
|
419
|
+
/** Refresh only the existing UI-owned card after recall. Never create a
|
|
420
|
+
* card, restart ticking, change phase or invoke a business terminal owner.
|
|
421
|
+
* Unknown effects remain fenced; a later known receipt retains this guard. */
|
|
422
|
+
async redactRecalledInput(effectGuard) {
|
|
423
|
+
if (this.disposed)
|
|
424
|
+
return;
|
|
425
|
+
this.lines = this.lines.filter(line => line.kind !== 'steer');
|
|
426
|
+
const base = this.pinnedView || this.buildView();
|
|
427
|
+
const view = { ...base, lines: base.lines.filter(line => line.kind !== 'steer'), inputStatus: this.inputSnapshot() };
|
|
428
|
+
if (this.terminalOwner) {
|
|
429
|
+
this.callbacks.desired?.(view);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.pinView(view, effectGuard);
|
|
433
|
+
if (!this.messageId || this.blockedEdits || this.unknownEdits.size || !effectGuard())
|
|
434
|
+
return;
|
|
435
|
+
await this.queueEdit(view, true, true, effectGuard);
|
|
436
|
+
}
|
|
437
|
+
pinView(view, effectGuard) {
|
|
438
|
+
this.pinnedInputGuard = effectGuard;
|
|
400
439
|
this.generation++;
|
|
401
440
|
this.callbacks.desired?.(view);
|
|
402
441
|
this.pinnedView = view;
|
|
@@ -414,7 +453,7 @@ export class LiveStatusController {
|
|
|
414
453
|
if (!this.pinnedView || this.disposed)
|
|
415
454
|
return;
|
|
416
455
|
if (!this.terminalOwner) {
|
|
417
|
-
await this.queueEdit(this.pinnedView, true, force);
|
|
456
|
+
await this.queueEdit(this.pinnedView, true, force, this.pinnedInputGuard);
|
|
418
457
|
return;
|
|
419
458
|
}
|
|
420
459
|
const meta = this.finalMeta();
|
|
@@ -452,7 +491,35 @@ export class LiveStatusController {
|
|
|
452
491
|
const maxChars = this.opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
453
492
|
return text.length > maxChars ? `${text.slice(0, Math.max(0, maxChars - 1))}\u2026` : text;
|
|
454
493
|
}
|
|
455
|
-
|
|
494
|
+
terminalInputHint(kind, outcome) {
|
|
495
|
+
const pending = this.callbacks.unconfirmedInputs?.(outcome);
|
|
496
|
+
// Existing custody decides membership FIRST. Presentation never invents
|
|
497
|
+
// an admitted run or upgrades an unconfirmed/appended input's outcome.
|
|
498
|
+
if (!pending)
|
|
499
|
+
this.updateInputHint(kind);
|
|
500
|
+
else if (pending.ordinaryIds?.length)
|
|
501
|
+
this.updateInputHint(kind, undefined, pending.ordinaryIds);
|
|
502
|
+
return pending;
|
|
503
|
+
}
|
|
504
|
+
updateInputHint(kind, detail, ids) {
|
|
505
|
+
const at = Math.max(0, Math.floor((Date.now() - this.startedAt) / 1000));
|
|
506
|
+
this.fallbackInputHint = { kind, detail, at };
|
|
507
|
+
try {
|
|
508
|
+
this.callbacks.inputStatus?.({ kind, detail, ids, at });
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
this.callbacks.warn?.("input display checkpoint unavailable", err);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
inputSnapshot() {
|
|
515
|
+
try {
|
|
516
|
+
return this.callbacks.inputStatus?.() || (this.fallbackInputHint ? { items: [this.fallbackInputHint] } : undefined);
|
|
517
|
+
}
|
|
518
|
+
catch {
|
|
519
|
+
return { items: [{ kind: "unconfirmed", at: 0 }] };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
pushLine(kind, text, inputHint = false) {
|
|
456
523
|
const clean = this.normalizeText(text);
|
|
457
524
|
if (!clean)
|
|
458
525
|
return;
|
|
@@ -462,7 +529,7 @@ export class LiveStatusController {
|
|
|
462
529
|
if (last && last.kind === kind && last.text === clipped)
|
|
463
530
|
return;
|
|
464
531
|
const at = Math.max(0, Math.floor((Date.now() - this.startedAt) / 1000));
|
|
465
|
-
this.lines.push({ kind, text: clipped, at });
|
|
532
|
+
this.lines.push({ kind, text: clipped, at, ...(inputHint ? { inputHint: true } : {}) });
|
|
466
533
|
const limit = this.opts.historySize ?? DEFAULT_HISTORY;
|
|
467
534
|
if (this.lines.length > limit)
|
|
468
535
|
this.lines = this.lines.slice(this.lines.length - limit);
|
|
@@ -559,7 +626,7 @@ export class LiveStatusController {
|
|
|
559
626
|
await this.queueEdit(view, false);
|
|
560
627
|
}
|
|
561
628
|
/** One flight + one latest pending snapshot, regardless of tick/event rate. */
|
|
562
|
-
queueEdit(view, priority, force = false) {
|
|
629
|
+
queueEdit(view, priority, force = false, effectGuard) {
|
|
563
630
|
this.callbacks.desired?.(view);
|
|
564
631
|
if (this.blockedEdits)
|
|
565
632
|
return Promise.resolve();
|
|
@@ -568,7 +635,7 @@ export class LiveStatusController {
|
|
|
568
635
|
return Promise.resolve();
|
|
569
636
|
if (!priority)
|
|
570
637
|
this.generation++;
|
|
571
|
-
this.pendingEdit = { view, generation: this.generation, priority, force, started: false, settled: false, accepted: false };
|
|
638
|
+
this.pendingEdit = { view, generation: this.generation, priority, force, effectGuard, started: false, settled: false, accepted: false };
|
|
572
639
|
return this.flushEdits();
|
|
573
640
|
}
|
|
574
641
|
canRunPendingEdit() {
|
|
@@ -628,7 +695,7 @@ export class LiveStatusController {
|
|
|
628
695
|
};
|
|
629
696
|
const effect = {
|
|
630
697
|
run: async (send) => {
|
|
631
|
-
if (!current()) {
|
|
698
|
+
if (!current() || request.effectGuard && !request.effectGuard()) {
|
|
632
699
|
request.settled = true;
|
|
633
700
|
return;
|
|
634
701
|
}
|
|
@@ -689,7 +756,7 @@ export class LiveStatusController {
|
|
|
689
756
|
/** Dedupe key includes elapsed so the footer timer ticks even without new
|
|
690
757
|
* activity, while identical content within the same second is still skipped. */
|
|
691
758
|
signature(view) {
|
|
692
|
-
return `${view.state}|${view.title}|${view.elapsed}|${view.lines.map((l) => `${l.kind}@${l.at}:${l.text}`).join("|")}`;
|
|
759
|
+
return `${view.state}|${view.title}|${view.elapsed}|${JSON.stringify(view.inputStatus || null)}|${view.lines.map((l) => `${l.kind}@${l.at}:${l.text}`).join("|")}`;
|
|
693
760
|
}
|
|
694
761
|
isNoReplyText(text) {
|
|
695
762
|
return text.trim().toUpperCase() === "NO_REPLY";
|
|
@@ -741,6 +808,7 @@ export class LiveStatusController {
|
|
|
741
808
|
return {
|
|
742
809
|
title,
|
|
743
810
|
lines,
|
|
811
|
+
inputStatus: this.inputSnapshot(),
|
|
744
812
|
elapsed: this.formatElapsed(),
|
|
745
813
|
model: this.opts.model,
|
|
746
814
|
thinkingLevel: this.opts.thinkingLevel,
|