@keyou007/agent-bot 0.1.24-alpha.0 → 0.1.24-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +24 -4
- package/README.zh.md +24 -4
- package/dist/cli/SelfUpdateRunner.js +2 -0
- package/dist/cli/SelfUpdateRunner.js.map +1 -1
- package/dist/codex/CodexEventMapper.js +90 -2
- package/dist/codex/CodexEventMapper.js.map +1 -1
- package/dist/codex/CodexRuntime.js +160 -12
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +14 -1
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +42 -11
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMarkdown.js +70 -4
- package/dist/feishu/FeishuMarkdown.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +2 -1
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/local-files/TurnPreviewPage.js +16 -1
- package/dist/local-files/TurnPreviewPage.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +37 -7
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +58 -38
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +2 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +7 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +12 -0
- package/src/cli/SelfUpdateRunner.ts +2 -0
- package/src/codex/CodexEventMapper.ts +97 -3
- package/src/codex/CodexRuntime.ts +163 -13
- package/src/commands/CommandRouter.ts +13 -2
- package/src/commands/commandTypes.ts +2 -2
- package/src/feishu/CardRenderer.ts +48 -12
- package/src/feishu/FeishuMarkdown.ts +69 -3
- package/src/feishu/FeishuTurnPresenter.ts +1 -0
- package/src/index.ts +4 -0
- package/src/local-files/TurnPreviewPage.ts +16 -1
- package/src/presentation/TurnStateReducer.ts +49 -9
- package/src/presentation/turnViewTypes.ts +4 -0
- package/src/proxy/ProxySessionController.ts +61 -40
- package/src/runtime/types.ts +13 -1
- package/src/supervision/SafeRestartNotifier.ts +2 -0
- package/src/supervision/SafeRestartScheduler.ts +7 -0
|
@@ -54,11 +54,25 @@ function includeLocalFileLineReferences(
|
|
|
54
54
|
projectCwd?: string,
|
|
55
55
|
localFileUrl?: LocalFileUrlResolver,
|
|
56
56
|
): string {
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
const codeSpans = inlineCodeSpans(markdown);
|
|
58
|
+
return markdown.replace(MARKDOWN_LINK, (link, imageMarker: string, label: string, rawTarget: string, offset: number) => {
|
|
59
|
+
if (imageMarker || codeSpans.some(([start, end]) => offset >= start && offset < end)) return link;
|
|
59
60
|
|
|
60
61
|
const target = unwrapMarkdownTarget(rawTarget.trim());
|
|
61
|
-
if (!isLocalFileTarget(target))
|
|
62
|
+
if (!isLocalFileTarget(target)) {
|
|
63
|
+
const relative = resolveRelativeFileTarget(target, projectCwd);
|
|
64
|
+
if (!relative || isLocalImageTarget(relative.filePath)) return link;
|
|
65
|
+
const viewerUrl = localFileUrl?.(relative.filePath, relative.reference);
|
|
66
|
+
if (!viewerUrl) {
|
|
67
|
+
const displayPath = displayAbsolutePath(relative.filePath) + (relative.reference ?? "");
|
|
68
|
+
return label ? `${label}(${inlineCode(displayPath)})` : inlineCode(displayPath);
|
|
69
|
+
}
|
|
70
|
+
const url = new URL(viewerUrl);
|
|
71
|
+
if (relative.hash) url.hash = relative.hash;
|
|
72
|
+
const normalizedLabel = relative.reference && !label.endsWith(relative.reference)
|
|
73
|
+
? `${label}${relative.reference}` : label;
|
|
74
|
+
return `[${normalizedLabel}](${url.toString()})`;
|
|
75
|
+
}
|
|
62
76
|
if (isLocalImageTarget(target)) return link;
|
|
63
77
|
|
|
64
78
|
const reference = LOCAL_FILE_LINE_REFERENCE.exec(target)?.[1];
|
|
@@ -84,6 +98,58 @@ function includeLocalFileLineReferences(
|
|
|
84
98
|
});
|
|
85
99
|
}
|
|
86
100
|
|
|
101
|
+
interface RelativeFileTarget {
|
|
102
|
+
filePath: string;
|
|
103
|
+
reference?: string;
|
|
104
|
+
hash?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveRelativeFileTarget(target: string, projectCwd?: string): RelativeFileTarget | undefined {
|
|
108
|
+
if (!projectCwd || !target || /^(?:#|\?|\/\/)/u.test(target)) return undefined;
|
|
109
|
+
const pathApi = usesWindowsPaths(projectCwd) ? path.win32 : path.posix;
|
|
110
|
+
if (!pathApi.isAbsolute(projectCwd)) return undefined;
|
|
111
|
+
const suffixStart = target.search(/[?#]/u);
|
|
112
|
+
const pathTarget = suffixStart < 0 ? target : target.slice(0, suffixStart);
|
|
113
|
+
const lineSuffix = LOCAL_FILE_LINE_REFERENCE.exec(pathTarget)?.[1];
|
|
114
|
+
// A basename with a line suffix (report.md:12) is a file, not a URI scheme.
|
|
115
|
+
const fileWithLine = /^[^:/\\]+\.[^:/\\]+:\d+(?::\d+)?$/u.test(pathTarget);
|
|
116
|
+
if (!fileWithLine && /^[a-z][a-z\d+.-]*:/iu.test(target)) return undefined;
|
|
117
|
+
const encodedPath = lineSuffix ? pathTarget.slice(0, -lineSuffix.length) : pathTarget;
|
|
118
|
+
try {
|
|
119
|
+
const decodedPath = decodeURIComponent(encodedPath);
|
|
120
|
+
if (!decodedPath || /[\x00-\x1f\x7f]/u.test(decodedPath)
|
|
121
|
+
|| pathApi.isAbsolute(decodedPath) || /^[a-z]:/iu.test(decodedPath)) return undefined;
|
|
122
|
+
const filePath = pathApi.resolve(projectCwd, decodedPath);
|
|
123
|
+
const hashStart = target.indexOf("#");
|
|
124
|
+
const hash = hashStart < 0 ? undefined : new URL(target.slice(hashStart), "http://localhost/").hash;
|
|
125
|
+
const hashLine = /^#L(\d+)(?:C\d+)?(?:-L?\d+(?:C\d+)?)?$/u.exec(hash ?? "")?.[1];
|
|
126
|
+
const validHashLine = hashLine && Number.isSafeInteger(Number(hashLine)) && Number(hashLine) > 0;
|
|
127
|
+
return {
|
|
128
|
+
filePath,
|
|
129
|
+
reference: validHashLine ? `:${Number(hashLine)}` : hash ? undefined : lineSuffix,
|
|
130
|
+
hash: validHashLine ? `#L${Number(hashLine)}` : hash,
|
|
131
|
+
};
|
|
132
|
+
} catch {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function inlineCodeSpans(markdown: string): Array<[number, number]> {
|
|
138
|
+
const runs = [...markdown.matchAll(/`+/gu)];
|
|
139
|
+
const spans: Array<[number, number]> = [];
|
|
140
|
+
for (let index = 0; index < runs.length; index += 1) {
|
|
141
|
+
const opener = runs[index]!;
|
|
142
|
+
const backslashes = /\\*$/u.exec(markdown.slice(0, opener.index))?.[0].length ?? 0;
|
|
143
|
+
if (backslashes % 2 !== 0) continue;
|
|
144
|
+
const closing = runs.findIndex((run, candidate) => candidate > index && run[0].length === opener[0].length);
|
|
145
|
+
if (closing < 0) continue;
|
|
146
|
+
const closer = runs[closing]!;
|
|
147
|
+
spans.push([opener.index, closer.index + closer[0].length]);
|
|
148
|
+
index = closing;
|
|
149
|
+
}
|
|
150
|
+
return spans;
|
|
151
|
+
}
|
|
152
|
+
|
|
87
153
|
function projectFilePathLabel(
|
|
88
154
|
target: string,
|
|
89
155
|
projectCwd: string,
|
|
@@ -684,6 +684,7 @@ function eventPriority(event: AgentEvent): "normal" | "critical" {
|
|
|
684
684
|
|| event.type === "approval_resolved"
|
|
685
685
|
|| event.type === "tool_started"
|
|
686
686
|
|| event.type === "context_compaction"
|
|
687
|
+
|| (event.type === "progress" && event.severity === "warning")
|
|
687
688
|
) {
|
|
688
689
|
return "critical";
|
|
689
690
|
}
|
package/src/index.ts
CHANGED
|
@@ -192,6 +192,10 @@ if (feishuOutbound && config.updates?.enabled !== false) {
|
|
|
192
192
|
const controller = new ProxySessionController(config, store, runtimes, outbound, logger, {
|
|
193
193
|
supervised,
|
|
194
194
|
restart: requestRestart,
|
|
195
|
+
forceSafeRestart: async (scheduleId) => {
|
|
196
|
+
if (shuttingDown || restartRequested) return false;
|
|
197
|
+
return safeRestart.forceScheduled(scheduleId);
|
|
198
|
+
},
|
|
195
199
|
cancelAutomaticUpdate: async (action) => {
|
|
196
200
|
if (!dailyUpdateMonitor) throw new Error("自动更新功能未启用。");
|
|
197
201
|
await dailyUpdateMonitor.cancel(action);
|
|
@@ -46,6 +46,9 @@ const PREVIEW_LABELS = {
|
|
|
46
46
|
model: "模型",
|
|
47
47
|
tools: "个工具",
|
|
48
48
|
turnTokens: "本轮",
|
|
49
|
+
totalTokens: "总计",
|
|
50
|
+
cachedTokens: "缓存命中",
|
|
51
|
+
nonCachedTokens: "非缓存",
|
|
49
52
|
compactionTokens: "压缩",
|
|
50
53
|
compactionAfterTokens: "压缩后",
|
|
51
54
|
characters: "字符",
|
|
@@ -84,6 +87,9 @@ const PREVIEW_LABELS = {
|
|
|
84
87
|
model: "Model",
|
|
85
88
|
tools: "tools",
|
|
86
89
|
turnTokens: "Turn",
|
|
90
|
+
totalTokens: "Total",
|
|
91
|
+
cachedTokens: "Cache hit",
|
|
92
|
+
nonCachedTokens: "Non-cached",
|
|
87
93
|
compactionTokens: "Compaction",
|
|
88
94
|
compactionAfterTokens: "After compaction",
|
|
89
95
|
characters: "characters",
|
|
@@ -302,6 +308,7 @@ export function renderTurnPreviewSnapshot(
|
|
|
302
308
|
: "",
|
|
303
309
|
timeline ? `<section class="timeline">${timeline}</section>` : `<div class="empty">${escapeHtml(language === "zh" ? "正在等待 Agent 返回进度…" : "Waiting for Agent progress…")}</div>`,
|
|
304
310
|
state.fileSummary.length > 0 ? renderFileSummary(state, language, resolveFileUrl) : "",
|
|
311
|
+
state.approval ? `<section class="notice"><h2>${escapeHtml(state.approval.title)}</h2><div class="markdown">${renderText(state.approval.reason ?? "")}</div><p>${escapeHtml(language === "zh" ? "请在飞书任务卡片中确认或拒绝。" : "Approve or reject using the task card in Feishu.")}</p></section>` : "",
|
|
305
312
|
state.error ? `<section class="result error-result"><h2>${escapeHtml(labels.error)}</h2><pre>${escapeHtml(state.error)}</pre></section>` : "",
|
|
306
313
|
state.finalResponse
|
|
307
314
|
? `<section class="result final-result"><h2>${escapeHtml(labels.finalAnswer)}</h2><div class="markdown">${renderText(state.finalResponse)}</div></section>`
|
|
@@ -355,6 +362,9 @@ function renderActivity(
|
|
|
355
362
|
): string {
|
|
356
363
|
switch (activity.kind) {
|
|
357
364
|
case "assistant":
|
|
365
|
+
if (activity.id.startsWith("commentary:runtime-error:")) {
|
|
366
|
+
return `<article class="activity message runtime-error" data-activity="${escapeAttribute(activity.id)}"><pre>${escapeHtml(activity.text)}</pre></article>`;
|
|
367
|
+
}
|
|
358
368
|
return renderMessageActivity(activity.id, activity.text, "commentary", renderText);
|
|
359
369
|
case "user":
|
|
360
370
|
return renderMessageActivity(activity.id, activity.text, "user", renderText);
|
|
@@ -619,7 +629,11 @@ function renderMetadata(state: TurnViewState, language: TurnPreviewLanguage): st
|
|
|
619
629
|
return [
|
|
620
630
|
elapsed,
|
|
621
631
|
state.model?.trim() ? `<span title="${escapeAttribute(labels.model)}">${escapeHtml(state.model.trim())}</span>` : "",
|
|
622
|
-
state.totalTokens === undefined ? "" :
|
|
632
|
+
state.totalTokens === undefined ? "" : state.cachedInputTokens === undefined
|
|
633
|
+
? `<span title="${escapeAttribute(labels.turnTokens)}">${formatTokenCount(state.totalTokens)} tokens</span>`
|
|
634
|
+
: `<span title="${escapeAttribute(labels.nonCachedTokens)}: ${formatNumber(state.totalTokens)} tokens">${escapeHtml(labels.nonCachedTokens)} ${formatTokenCount(state.totalTokens)} tokens</span>`,
|
|
635
|
+
state.totalTokensIncludingCache === undefined ? "" : `<span title="${escapeAttribute(labels.totalTokens)}: ${formatNumber(state.totalTokensIncludingCache)} tokens">${escapeHtml(labels.totalTokens)} ${formatTokenCount(state.totalTokensIncludingCache)} tokens</span>`,
|
|
636
|
+
state.cachedInputTokens === undefined ? "" : `<span title="${escapeAttribute(labels.cachedTokens)}: ${formatNumber(state.cachedInputTokens)} tokens">${escapeHtml(labels.cachedTokens)} ${formatTokenCount(state.cachedInputTokens)} tokens</span>`,
|
|
623
637
|
state.contextCompactionAfterTokens === undefined ? "" : state.contextCompactionBeforeTokens === undefined
|
|
624
638
|
? `<span title="${escapeAttribute(labels.compactionAfterTokens)}">${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`
|
|
625
639
|
: `<span title="${escapeAttribute(labels.compactionTokens)}">${formatTokenCount(state.contextCompactionBeforeTokens)} → ${formatTokenCount(state.contextCompactionAfterTokens)} tokens</span>`,
|
|
@@ -802,6 +816,7 @@ summary:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px;
|
|
|
802
816
|
.tool-meta span { white-space: nowrap; }
|
|
803
817
|
.tool-header-timing { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
804
818
|
.tool-header-timing:empty, .tool-footer-timing:empty { display: none; }
|
|
819
|
+
.runtime-error pre { white-space: pre-wrap; overflow-wrap: anywhere; border-left: 2px solid var(--danger); }
|
|
805
820
|
.error-output, .error-result h2 { color: var(--danger); }
|
|
806
821
|
.muted, .empty { color: var(--muted); }
|
|
807
822
|
.notice, .empty { padding: 10px 0; }
|
|
@@ -53,7 +53,14 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
53
53
|
case "turn_started":
|
|
54
54
|
return { ...state, status: "running", startedAt: event.startedAt };
|
|
55
55
|
case "agent_text_delta":
|
|
56
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
assistantText: bound(`${state.assistantText}${event.text}`),
|
|
59
|
+
...(event.replacesActivityId ? {
|
|
60
|
+
activities: (state.activities ?? []).filter((activity) => activity.id !== event.replacesActivityId),
|
|
61
|
+
progressText: undefined,
|
|
62
|
+
} : {}),
|
|
63
|
+
};
|
|
57
64
|
case "token_usage_updated": {
|
|
58
65
|
const lastTokens = normalizeTokenCount(event.lastTokens);
|
|
59
66
|
const cumulativeTokens = normalizeTokenCount(event.cumulativeTokens);
|
|
@@ -61,10 +68,22 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
61
68
|
const delta = previousCumulative === undefined
|
|
62
69
|
? lastTokens
|
|
63
70
|
: Math.max(0, cumulativeTokens - previousCumulative);
|
|
71
|
+
const total = accumulateReportedTokens(
|
|
72
|
+
event.lastTotalTokens, event.cumulativeTotalTokens,
|
|
73
|
+
state.totalTokensIncludingCache, state.tokenUsageTotalCumulative, previousCumulative !== undefined,
|
|
74
|
+
);
|
|
75
|
+
const cached = accumulateReportedTokens(
|
|
76
|
+
event.lastCachedTokens, event.cumulativeCachedTokens,
|
|
77
|
+
state.cachedInputTokens, state.tokenUsageCachedCumulative, previousCumulative !== undefined,
|
|
78
|
+
);
|
|
64
79
|
return {
|
|
65
80
|
...state,
|
|
66
81
|
totalTokens: (state.totalTokens ?? 0) + delta,
|
|
67
82
|
tokenUsageCumulative: Math.max(previousCumulative ?? 0, cumulativeTokens),
|
|
83
|
+
totalTokensIncludingCache: total.value,
|
|
84
|
+
tokenUsageTotalCumulative: total.cumulative,
|
|
85
|
+
cachedInputTokens: cached.value,
|
|
86
|
+
tokenUsageCachedCumulative: cached.cumulative,
|
|
68
87
|
latestContextTokens: event.contextTokens === undefined
|
|
69
88
|
? state.latestContextTokens
|
|
70
89
|
: normalizeTokenCount(event.contextTokens),
|
|
@@ -155,11 +174,13 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
155
174
|
};
|
|
156
175
|
}
|
|
157
176
|
case "progress": {
|
|
177
|
+
const activities = state.activities ?? [];
|
|
178
|
+
const warning = event.severity === "warning";
|
|
158
179
|
const activityUpdate = upsertReasoningActivity(
|
|
159
|
-
|
|
180
|
+
warning ? activities.filter((activity) => activity.id !== (event.activityId ?? "progress")) : activities,
|
|
160
181
|
event.activityId ?? "progress",
|
|
161
|
-
event.text,
|
|
162
|
-
event.append === true,
|
|
182
|
+
warning && !event.activityId?.startsWith("commentary:runtime-error:") ? bound(event.text) : event.text,
|
|
183
|
+
!warning && event.append === true,
|
|
163
184
|
event.activityId?.startsWith("commentary:") ? "assistant" : "reasoning",
|
|
164
185
|
);
|
|
165
186
|
return {
|
|
@@ -179,7 +200,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
179
200
|
return {
|
|
180
201
|
...state,
|
|
181
202
|
...tracking,
|
|
182
|
-
status: "tool_running",
|
|
203
|
+
status: state.approval ? "waiting_for_approval" : "tool_running",
|
|
183
204
|
activeTool: bounded,
|
|
184
205
|
activities: activityUpdate.activities,
|
|
185
206
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -193,6 +214,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
193
214
|
case "approval_requested":
|
|
194
215
|
return { ...state, status: "waiting_for_approval", approval: event.request };
|
|
195
216
|
case "approval_resolved":
|
|
217
|
+
if (state.approval?.id !== event.requestId) return state;
|
|
196
218
|
return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
|
|
197
219
|
case "turn_completed": {
|
|
198
220
|
const durationMs = Math.max(0, event.durationMs ?? Date.now() - state.startedAt);
|
|
@@ -230,7 +252,7 @@ export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnVi
|
|
|
230
252
|
approval: undefined,
|
|
231
253
|
completedAt,
|
|
232
254
|
durationMs: Math.max(0, completedAt - state.startedAt),
|
|
233
|
-
error:
|
|
255
|
+
error: event.message,
|
|
234
256
|
};
|
|
235
257
|
}
|
|
236
258
|
}
|
|
@@ -312,7 +334,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
312
334
|
...tracking,
|
|
313
335
|
fullToolOutputs,
|
|
314
336
|
fullToolErrors,
|
|
315
|
-
status: "tool_running",
|
|
337
|
+
status: state.approval ? "waiting_for_approval" : "tool_running",
|
|
316
338
|
activeTool: bounded,
|
|
317
339
|
activities: activityUpdate.activities,
|
|
318
340
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -328,7 +350,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
328
350
|
...tracking,
|
|
329
351
|
fullToolOutputs,
|
|
330
352
|
fullToolErrors,
|
|
331
|
-
status: activeTool ? "tool_running" : "running",
|
|
353
|
+
status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
332
354
|
activeTool,
|
|
333
355
|
activities: activityUpdate.activities,
|
|
334
356
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -343,7 +365,7 @@ function reduceToolUpdate(state: TurnViewState, tool: ToolState, fullOutput?: st
|
|
|
343
365
|
...tracking,
|
|
344
366
|
fullToolOutputs,
|
|
345
367
|
fullToolErrors,
|
|
346
|
-
status: activeTool ? "tool_running" : "running",
|
|
368
|
+
status: state.approval ? "waiting_for_approval" : activeTool ? "tool_running" : "running",
|
|
347
369
|
activeTool,
|
|
348
370
|
activities: activityUpdate.activities,
|
|
349
371
|
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
@@ -463,6 +485,24 @@ function bound(value: string): string {
|
|
|
463
485
|
return `${value.slice(0, MAX_TEXT - 1)}…`;
|
|
464
486
|
}
|
|
465
487
|
|
|
488
|
+
function accumulateReportedTokens(
|
|
489
|
+
last: number | undefined,
|
|
490
|
+
cumulative: number | undefined,
|
|
491
|
+
value: number | undefined,
|
|
492
|
+
previous: number | undefined,
|
|
493
|
+
hasPriorUsage: boolean,
|
|
494
|
+
): { value?: number; cumulative?: number } {
|
|
495
|
+
if (last === undefined || cumulative === undefined || !Number.isFinite(last) || !Number.isFinite(cumulative)
|
|
496
|
+
|| last < 0 || cumulative < 0) return {};
|
|
497
|
+
// A saved or partial turn without a baseline cannot acquire a complete breakdown later.
|
|
498
|
+
if (hasPriorUsage && (previous === undefined || value === undefined)) return {};
|
|
499
|
+
const normalizedCumulative = normalizeTokenCount(cumulative);
|
|
500
|
+
return {
|
|
501
|
+
value: (value ?? 0) + (previous === undefined ? normalizeTokenCount(last) : Math.max(0, normalizedCumulative - previous)),
|
|
502
|
+
cumulative: Math.max(previous ?? 0, normalizedCumulative),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
466
506
|
function normalizeTokenCount(value: number): number {
|
|
467
507
|
return Math.max(0, Math.round(value));
|
|
468
508
|
}
|
|
@@ -37,6 +37,10 @@ export interface TurnViewState {
|
|
|
37
37
|
durationMs?: number;
|
|
38
38
|
totalTokens?: number;
|
|
39
39
|
tokenUsageCumulative?: number;
|
|
40
|
+
totalTokensIncludingCache?: number;
|
|
41
|
+
cachedInputTokens?: number;
|
|
42
|
+
tokenUsageTotalCumulative?: number;
|
|
43
|
+
tokenUsageCachedCumulative?: number;
|
|
40
44
|
contextCompactionStatus?: "running" | "completed";
|
|
41
45
|
contextCompactionCount?: number;
|
|
42
46
|
contextCompactionId?: string;
|
|
@@ -61,6 +61,7 @@ import type {
|
|
|
61
61
|
ApprovalDecision,
|
|
62
62
|
ConversationTurn,
|
|
63
63
|
ModelOption,
|
|
64
|
+
ModelListResult,
|
|
64
65
|
PermissionMode,
|
|
65
66
|
RemoteCompletedTurnSummary,
|
|
66
67
|
RemoteSessionActivity,
|
|
@@ -234,13 +235,13 @@ const HELP_COMMAND_SECTIONS: Array<{
|
|
|
234
235
|
commands: [
|
|
235
236
|
{
|
|
236
237
|
command: "/new",
|
|
237
|
-
usage: "[title] [--dir <cwd> | --nodir]",
|
|
238
|
-
description: "
|
|
238
|
+
usage: "[title] [--agent <name>] [--dir <cwd> | --nodir]",
|
|
239
|
+
description: "使用默认或指定 Agent 创建任务;--nodir 创建 Projectless 任务",
|
|
239
240
|
},
|
|
240
241
|
{
|
|
241
242
|
command: "/newgroup",
|
|
242
|
-
usage: "[title] [--dir <cwd> | --nodir]",
|
|
243
|
-
description: "创建飞书群和新任务",
|
|
243
|
+
usage: "[title] [--agent <name>] [--dir <cwd> | --nodir]",
|
|
244
|
+
description: "使用默认或指定 Agent 创建飞书群和新任务",
|
|
244
245
|
},
|
|
245
246
|
{
|
|
246
247
|
command: "/dir",
|
|
@@ -500,6 +501,7 @@ export interface ProxyLifecycle {
|
|
|
500
501
|
supervised?: boolean;
|
|
501
502
|
restart(contextKey: string, force: boolean, replyTarget?: MessageReplyTarget): Promise<void>;
|
|
502
503
|
cancelSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
504
|
+
forceSafeRestart?(scheduleId: number): Promise<boolean>;
|
|
503
505
|
cancelAutomaticUpdate?(action: CardAction): Promise<void>;
|
|
504
506
|
rememberFeishuUserOpenId?(userOpenId: string): Promise<void> | void;
|
|
505
507
|
persistAgentExecutionDefaults?(
|
|
@@ -1049,6 +1051,18 @@ export class ProxySessionController {
|
|
|
1049
1051
|
});
|
|
1050
1052
|
} else if (kind === "queued_prompt_cancel") {
|
|
1051
1053
|
await this.cancelQueuedPrompt(scopedAction);
|
|
1054
|
+
} else if (kind === "safe_restart_force") {
|
|
1055
|
+
const scheduleId = Number(scopedAction.value.scheduleId);
|
|
1056
|
+
if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0) {
|
|
1057
|
+
throw new Error("安全重启卡片无效,请使用最新的状态卡片。");
|
|
1058
|
+
}
|
|
1059
|
+
if (!this.lifecycle?.forceSafeRestart) {
|
|
1060
|
+
throw new Error("当前运行方式不支持立即重启。");
|
|
1061
|
+
}
|
|
1062
|
+
const restarted = await this.lifecycle.forceSafeRestart(scheduleId);
|
|
1063
|
+
if (!restarted) {
|
|
1064
|
+
await this.outbound.sendText(contextKey, "该安全重启计划已失效,请查看最新状态卡片。");
|
|
1065
|
+
}
|
|
1052
1066
|
} else if (kind === "safe_restart_cancel") {
|
|
1053
1067
|
const scheduleId = Number(scopedAction.value.scheduleId);
|
|
1054
1068
|
if (!Number.isSafeInteger(scheduleId) || scheduleId <= 0) {
|
|
@@ -1164,7 +1178,9 @@ export class ProxySessionController {
|
|
|
1164
1178
|
await this.outbound.sendText(contextKey, STOP_TASK_STARTED_MESSAGE);
|
|
1165
1179
|
await this.stopSessionReference(contextKey, sessionId);
|
|
1166
1180
|
if (scopedAction.value.cardView === "status") await this.refreshStatusCardFromAction(scopedAction, sessionId);
|
|
1167
|
-
else
|
|
1181
|
+
else if (scopedAction.value.cardView !== "safe_restart") {
|
|
1182
|
+
await this.refreshSessionsCardFromAction(scopedAction, { forceSwitchTaskId: sessionId });
|
|
1183
|
+
}
|
|
1168
1184
|
} else if (kind === "session_status") {
|
|
1169
1185
|
await this.outbound.sendText(contextKey, STATUS_STARTED_MESSAGE);
|
|
1170
1186
|
await this.status(contextKey, String(scopedAction.value.sessionId ?? ""));
|
|
@@ -2006,14 +2022,16 @@ export class ProxySessionController {
|
|
|
2006
2022
|
case "file":
|
|
2007
2023
|
await this.sendCurrentTaskFile(contextKey, command.filePath);
|
|
2008
2024
|
return;
|
|
2009
|
-
case "new":
|
|
2010
|
-
|
|
2025
|
+
case "new": {
|
|
2026
|
+
const agentName = command.agentName ?? context.defaultAgent;
|
|
2027
|
+
const agent = this.ensureAgent(agentName);
|
|
2028
|
+
if (command.projectless && agent.kind !== "app-server") {
|
|
2011
2029
|
throw new Error("/new --nodir 仅支持 App Server Agent。");
|
|
2012
2030
|
}
|
|
2013
2031
|
await this.outbound.sendText(contextKey, CREATE_TASK_STARTED_MESSAGE);
|
|
2014
2032
|
await this.createSession(
|
|
2015
2033
|
contextKey,
|
|
2016
|
-
|
|
2034
|
+
agentName,
|
|
2017
2035
|
command.projectless
|
|
2018
2036
|
? undefined
|
|
2019
2037
|
: command.cwd === undefined
|
|
@@ -2024,11 +2042,14 @@ export class ProxySessionController {
|
|
|
2024
2042
|
undefined,
|
|
2025
2043
|
undefined,
|
|
2026
2044
|
command.title,
|
|
2027
|
-
this.inheritedExecutionSettings(contextKey,
|
|
2045
|
+
this.inheritedExecutionSettings(contextKey, agentName),
|
|
2028
2046
|
);
|
|
2029
2047
|
return;
|
|
2030
|
-
|
|
2031
|
-
|
|
2048
|
+
}
|
|
2049
|
+
case "newgroup": {
|
|
2050
|
+
const agentName = command.agentName ?? context.defaultAgent;
|
|
2051
|
+
const agent = this.ensureAgent(agentName);
|
|
2052
|
+
if (command.projectless && agent.kind !== "app-server") {
|
|
2032
2053
|
throw new Error("/newgroup --nodir 仅支持 App Server Agent。");
|
|
2033
2054
|
}
|
|
2034
2055
|
if (!userId?.startsWith("ou_")) {
|
|
@@ -2037,13 +2058,14 @@ export class ProxySessionController {
|
|
|
2037
2058
|
await this.outbound.sendText(contextKey, CREATE_GROUP_STARTED_MESSAGE);
|
|
2038
2059
|
await this.createFeishuGroup(
|
|
2039
2060
|
contextKey,
|
|
2040
|
-
|
|
2061
|
+
agentName,
|
|
2041
2062
|
command.title,
|
|
2042
2063
|
userId,
|
|
2043
2064
|
command.cwd === undefined ? undefined : resolveUserPath(command.cwd),
|
|
2044
2065
|
command.projectless === true,
|
|
2045
2066
|
);
|
|
2046
2067
|
return;
|
|
2068
|
+
}
|
|
2047
2069
|
case "clone":
|
|
2048
2070
|
case "clonegroup": {
|
|
2049
2071
|
let source = this.currentSession(contextKey);
|
|
@@ -2136,7 +2158,7 @@ export class ProxySessionController {
|
|
|
2136
2158
|
await this.openExecutionSettings(contextKey, "model");
|
|
2137
2159
|
return;
|
|
2138
2160
|
case "provider":
|
|
2139
|
-
await this.
|
|
2161
|
+
await this.openExecutionSettings(contextKey, "provider");
|
|
2140
2162
|
return;
|
|
2141
2163
|
case "thinking":
|
|
2142
2164
|
await this.openExecutionSettings(contextKey, "thinking");
|
|
@@ -5110,7 +5132,7 @@ export class ProxySessionController {
|
|
|
5110
5132
|
const providerSupported = loaded.runtime.kind === "codex" && Boolean(loaded.runtime.listModelProviders);
|
|
5111
5133
|
const providers = providerSupported ? await this.modelProviderOptions(loaded) : [];
|
|
5112
5134
|
const currentProvider = loaded.session.modelProvider ?? providers.find((provider) => provider.isDefault)?.id;
|
|
5113
|
-
const models = await loaded.runtime
|
|
5135
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, currentProvider, loaded.session.model);
|
|
5114
5136
|
const currentModel = models.find((model) => model.id === loaded.session.model)
|
|
5115
5137
|
?? models.find((model) => model.isDefault)
|
|
5116
5138
|
?? models[0];
|
|
@@ -5127,6 +5149,7 @@ export class ProxySessionController {
|
|
|
5127
5149
|
providers,
|
|
5128
5150
|
providerSupported,
|
|
5129
5151
|
models,
|
|
5152
|
+
warning,
|
|
5130
5153
|
reasoningOptions: currentModel?.supportedReasoningEfforts ?? [],
|
|
5131
5154
|
};
|
|
5132
5155
|
}
|
|
@@ -5156,21 +5179,6 @@ export class ProxySessionController {
|
|
|
5156
5179
|
await this.openExecutionSettings(contextKey, "agent");
|
|
5157
5180
|
}
|
|
5158
5181
|
|
|
5159
|
-
private async openProviderSettings(contextKey: string): Promise<void> {
|
|
5160
|
-
const loaded = await this.loadSession(this.requireCurrentSession(contextKey));
|
|
5161
|
-
const providers = loaded.runtime.kind === "codex" && loaded.runtime.listModelProviders
|
|
5162
|
-
? await this.modelProviderOptions(loaded)
|
|
5163
|
-
: [];
|
|
5164
|
-
const current = loaded.session.modelProvider?.trim()
|
|
5165
|
-
|| providers.find((provider) => provider.isDefault)?.id
|
|
5166
|
-
|| "运行时默认";
|
|
5167
|
-
if (providers.length <= 1) {
|
|
5168
|
-
await this.outbound.sendText(contextKey, `当前 Provider:${current}\n当前没有其他 Provider 可以切换。`);
|
|
5169
|
-
return;
|
|
5170
|
-
}
|
|
5171
|
-
await this.openExecutionSettings(contextKey, "provider", { sessionId: loaded.record.localSessionId });
|
|
5172
|
-
}
|
|
5173
|
-
|
|
5174
5182
|
private async selectProvider(
|
|
5175
5183
|
contextKey: string,
|
|
5176
5184
|
modelProvider: string,
|
|
@@ -5210,13 +5218,14 @@ export class ProxySessionController {
|
|
|
5210
5218
|
): Promise<void> {
|
|
5211
5219
|
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5212
5220
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5213
|
-
const models = await loaded.runtime
|
|
5221
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, loaded.session.model);
|
|
5214
5222
|
const card = this.cardRenderer.renderModelSelector({
|
|
5215
5223
|
sessionId,
|
|
5216
5224
|
contextKey,
|
|
5217
5225
|
currentModel: selectProviderModel(models, loaded.session.model)?.id,
|
|
5218
5226
|
reasoningEffort: loaded.session.reasoningEffort,
|
|
5219
5227
|
models,
|
|
5228
|
+
warning,
|
|
5220
5229
|
modelProvider,
|
|
5221
5230
|
permissionMode,
|
|
5222
5231
|
unifiedSettings: true,
|
|
@@ -5234,7 +5243,7 @@ export class ProxySessionController {
|
|
|
5234
5243
|
): Promise<void> {
|
|
5235
5244
|
const loaded = await this.loadSession(this.requireSession(contextKey, sessionId));
|
|
5236
5245
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5237
|
-
const models = await loaded.runtime
|
|
5246
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
5238
5247
|
const selected = models.find((candidate) => candidate.id === model);
|
|
5239
5248
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5240
5249
|
const options = selected.supportedReasoningEfforts.length > 0
|
|
@@ -5252,6 +5261,7 @@ export class ProxySessionController {
|
|
|
5252
5261
|
model,
|
|
5253
5262
|
currentEffort,
|
|
5254
5263
|
options,
|
|
5264
|
+
warning,
|
|
5255
5265
|
permissionMode,
|
|
5256
5266
|
unifiedSettings: true,
|
|
5257
5267
|
});
|
|
@@ -5296,7 +5306,7 @@ export class ProxySessionController {
|
|
|
5296
5306
|
await this.outbound.sendText(contextKey, `正在切换到 Provider ${settings.provider},请稍后。`);
|
|
5297
5307
|
}
|
|
5298
5308
|
const loaded = await this.loadSession(record);
|
|
5299
|
-
await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5309
|
+
const warning = await this.assertProviderModelSettings(loaded, settings.provider, settings.model, settings.effort);
|
|
5300
5310
|
const session = await this.changeProviderSettings(loaded, {
|
|
5301
5311
|
modelProvider: settings.provider,
|
|
5302
5312
|
model: settings.model,
|
|
@@ -5317,7 +5327,7 @@ export class ProxySessionController {
|
|
|
5317
5327
|
notice,
|
|
5318
5328
|
});
|
|
5319
5329
|
} else {
|
|
5320
|
-
await this.outbound.sendText(contextKey, notice.replaceAll("`", ""));
|
|
5330
|
+
await this.outbound.sendText(contextKey, [notice.replaceAll("`", ""), warning].filter(Boolean).join("\n"));
|
|
5321
5331
|
}
|
|
5322
5332
|
}
|
|
5323
5333
|
|
|
@@ -5352,15 +5362,23 @@ export class ProxySessionController {
|
|
|
5352
5362
|
modelProvider: string,
|
|
5353
5363
|
model: string,
|
|
5354
5364
|
effort: string,
|
|
5355
|
-
): Promise<
|
|
5365
|
+
): Promise<string | undefined> {
|
|
5356
5366
|
await this.assertModelProvider(loaded, modelProvider);
|
|
5357
|
-
const
|
|
5367
|
+
const { models, warning } = await this.modelOptions(loaded.runtime, modelProvider, model);
|
|
5368
|
+
const selected = models.find((candidate) => candidate.id === model);
|
|
5358
5369
|
if (!selected) throw new Error(`未知模型:${model}`);
|
|
5359
5370
|
if (selected.supportedReasoningEfforts.length > 0
|
|
5360
5371
|
&& !selected.supportedReasoningEfforts.some((option) => option.value === effort)) {
|
|
5361
5372
|
const supported = selected.supportedReasoningEfforts.map((option) => option.value).join("、") || "无";
|
|
5362
5373
|
throw new Error(`模型 ${model} 不支持思考强度 ${effort}。支持的强度:${supported}`);
|
|
5363
5374
|
}
|
|
5375
|
+
return warning;
|
|
5376
|
+
}
|
|
5377
|
+
|
|
5378
|
+
private async modelOptions(runtime: AgentRuntime, provider?: string, fallbackModel?: string): Promise<ModelListResult> {
|
|
5379
|
+
return runtime.listModelsWithStatus
|
|
5380
|
+
? runtime.listModelsWithStatus(provider, fallbackModel)
|
|
5381
|
+
: { models: await runtime.listModels(provider, fallbackModel) };
|
|
5364
5382
|
}
|
|
5365
5383
|
|
|
5366
5384
|
private async assertModelProvider(loaded: LoadedSession, modelProvider: string): Promise<void> {
|
|
@@ -7027,10 +7045,12 @@ export class ProxySessionController {
|
|
|
7027
7045
|
activeTurnId = remoteActive ? remote?.lastTurnId ?? activeTurnId : activeTurnId;
|
|
7028
7046
|
queued = this.store.countQueuedPrompts(current.localSessionId);
|
|
7029
7047
|
snapshot = turnViewSnapshot(current.lastTurnId ? this.store.getTurnSnapshot(current.lastTurnId) : undefined);
|
|
7030
|
-
const
|
|
7031
|
-
|
|
7032
|
-
:
|
|
7033
|
-
|
|
7048
|
+
const waitingForApproval = snapshot?.turnId === current.lastTurnId && snapshot?.status === "waiting_for_approval";
|
|
7049
|
+
const statusLabel = waitingForApproval ? "等待确认"
|
|
7050
|
+
: remoteActive && remote && localCurrent && !isBotOwnedActiveTurn(localCurrent, remote)
|
|
7051
|
+
? "外部执行中"
|
|
7052
|
+
: sessionStatusLabel(current.status, activeTurnId);
|
|
7053
|
+
const resultLabel = waitingForApproval ? "等待确认" : remoteActive
|
|
7034
7054
|
? "执行中"
|
|
7035
7055
|
: remoteTurnStatusLabel(remote?.lastTurnStatus ?? current.lastTurnStatus);
|
|
7036
7056
|
taskLines.push(
|
|
@@ -8259,7 +8279,8 @@ function executionDetailLines(
|
|
|
8259
8279
|
|
|
8260
8280
|
function finalResultLines(snapshot?: TurnViewState, remote?: RemoteSessionSummary): string[] {
|
|
8261
8281
|
const relevantSnapshot = !remote?.lastTurnId || snapshot?.turnId === remote.lastTurnId ? snapshot : undefined;
|
|
8262
|
-
if (relevantSnapshot?.status === "
|
|
8282
|
+
if (relevantSnapshot?.status === "waiting_for_approval") return ["任务正在等待确认,尚无最终结果。"];
|
|
8283
|
+
if (relevantSnapshot?.status === "running" || relevantSnapshot?.status === "tool_running"
|
|
8263
8284
|
|| isRemoteSessionActive(remote)) {
|
|
8264
8285
|
return ["任务仍在执行,尚无最终结果。"];
|
|
8265
8286
|
}
|
package/src/runtime/types.ts
CHANGED
|
@@ -58,6 +58,7 @@ export interface ToolState {
|
|
|
58
58
|
|
|
59
59
|
export interface ApprovalRequest {
|
|
60
60
|
id: string;
|
|
61
|
+
kind?: "mode_change";
|
|
61
62
|
title: string;
|
|
62
63
|
command?: string;
|
|
63
64
|
reason?: string;
|
|
@@ -66,13 +67,17 @@ export interface ApprovalRequest {
|
|
|
66
67
|
|
|
67
68
|
export type AgentEvent =
|
|
68
69
|
| { type: "turn_started"; sessionId: string; turnId: string; startedAt: number }
|
|
69
|
-
| { type: "agent_text_delta"; sessionId: string; turnId: string; text: string }
|
|
70
|
+
| { type: "agent_text_delta"; sessionId: string; turnId: string; text: string; replacesActivityId?: string }
|
|
70
71
|
| {
|
|
71
72
|
type: "token_usage_updated";
|
|
72
73
|
sessionId: string;
|
|
73
74
|
turnId: string;
|
|
74
75
|
lastTokens: number;
|
|
75
76
|
cumulativeTokens: number;
|
|
77
|
+
lastTotalTokens?: number;
|
|
78
|
+
cumulativeTotalTokens?: number;
|
|
79
|
+
lastCachedTokens?: number;
|
|
80
|
+
cumulativeCachedTokens?: number;
|
|
76
81
|
contextTokens?: number;
|
|
77
82
|
}
|
|
78
83
|
| {
|
|
@@ -92,6 +97,7 @@ export type AgentEvent =
|
|
|
92
97
|
text: string;
|
|
93
98
|
activityId?: string;
|
|
94
99
|
append?: boolean;
|
|
100
|
+
severity?: "warning";
|
|
95
101
|
}
|
|
96
102
|
| { type: "plan_updated"; sessionId: string; turnId: string; steps: PlanStep[] }
|
|
97
103
|
| { type: "tool_started"; sessionId: string; turnId: string; tool: ToolState }
|
|
@@ -224,6 +230,11 @@ export interface ModelOption {
|
|
|
224
230
|
defaultReasoningEffort?: string;
|
|
225
231
|
}
|
|
226
232
|
|
|
233
|
+
export interface ModelListResult {
|
|
234
|
+
models: ModelOption[];
|
|
235
|
+
warning?: string;
|
|
236
|
+
}
|
|
237
|
+
|
|
227
238
|
export interface ModelProviderOption {
|
|
228
239
|
id: string;
|
|
229
240
|
displayName?: string;
|
|
@@ -285,6 +296,7 @@ export interface AgentRuntime {
|
|
|
285
296
|
): Promise<RuntimeSession>;
|
|
286
297
|
respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
|
|
287
298
|
listModels(modelProvider?: string, fallbackModel?: string): Promise<ModelOption[]>;
|
|
299
|
+
listModelsWithStatus?(modelProvider?: string, fallbackModel?: string): Promise<ModelListResult>;
|
|
288
300
|
listModelProviders?(): Promise<ModelProviderOption[]>;
|
|
289
301
|
release?(options?: { force?: boolean }): Promise<RuntimeReleaseResult>;
|
|
290
302
|
onEvent(listener: (event: RuntimeEvent) => void): () => void;
|
|
@@ -121,6 +121,8 @@ export class SafeRestartNotifier {
|
|
|
121
121
|
.filter((session) => session.status === "running" || this.store.countQueuedPrompts(session.localSessionId) > 0)
|
|
122
122
|
.map((session) => ({
|
|
123
123
|
id: session.remoteSessionId ?? session.localSessionId,
|
|
124
|
+
localSessionId: session.localSessionId,
|
|
125
|
+
isRunning: session.status === "running",
|
|
124
126
|
title: session.title,
|
|
125
127
|
}));
|
|
126
128
|
await Promise.all(targets.map(async ({ contextKey, replyMessageId, reason }) => {
|
|
@@ -93,6 +93,13 @@ export class SafeRestartScheduler {
|
|
|
93
93
|
return this.cancelScheduled(this.scheduleId);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
async forceScheduled(expectedScheduleId: number): Promise<boolean> {
|
|
97
|
+
const reason = this.reason;
|
|
98
|
+
if (!reason || this.scheduleId !== expectedScheduleId) return false;
|
|
99
|
+
await this.restart(reason, expectedScheduleId, this.options.readActivity());
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
96
103
|
async cancelScheduled(expectedScheduleId: number): Promise<boolean> {
|
|
97
104
|
const reason = this.reason;
|
|
98
105
|
if (!reason || this.scheduleId !== expectedScheduleId) return false;
|