dsh-workplan 1.5.0 → 1.5.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 +173 -0
- package/client.js +153 -33
- package/lib/host.js +1 -1
- package/lib/plan.js +112 -8
- package/lib/runtime.js +83 -12
- package/package.json +1 -1
- package/src/client.ts +151 -34
- package/src/host.ts +1 -1
- package/src/plan.ts +110 -8
- package/src/runtime.ts +83 -11
package/lib/plan.js
CHANGED
|
@@ -1039,8 +1039,18 @@ export function applyStatus(statuses, subtaskId, patch) {
|
|
|
1039
1039
|
if (patch.error === undefined)
|
|
1040
1040
|
next.error = null;
|
|
1041
1041
|
}
|
|
1042
|
-
if (patch.status === 'in_progress'
|
|
1043
|
-
next.startedAt
|
|
1042
|
+
if (patch.status === 'in_progress') {
|
|
1043
|
+
if (next.startedAt === null)
|
|
1044
|
+
next.startedAt = Date.now();
|
|
1045
|
+
/*
|
|
1046
|
+
* [1.5.2] 「正在做」不允许自称 100%。
|
|
1047
|
+
*
|
|
1048
|
+
* 工具允许 `{status:'in_progress', progress:100}`,界面于是出现
|
|
1049
|
+
* 「正在做 + 进度 100%」,计划层再叠一个环 0% / 加权 100%(T5 C8)。
|
|
1050
|
+
* 完成只能由 status='done' 表达,progress 到 99 就是这一步的上限。
|
|
1051
|
+
*/
|
|
1052
|
+
if (Number(next.progress) >= 100)
|
|
1053
|
+
next.progress = 99;
|
|
1044
1054
|
}
|
|
1045
1055
|
if (patch.status === 'blocked' && next.finishedAt === null) {
|
|
1046
1056
|
next.finishedAt = Date.now();
|
|
@@ -1169,6 +1179,16 @@ export function deriveAggregates(statuses) {
|
|
|
1169
1179
|
departments.set(item.department, department);
|
|
1170
1180
|
}
|
|
1171
1181
|
const total = statuses.length;
|
|
1182
|
+
const allDone = total > 0 && done === total;
|
|
1183
|
+
/*
|
|
1184
|
+
* [1.5.2] 加权进度在「未全部完成」时不得四舍五入成 100。
|
|
1185
|
+
*
|
|
1186
|
+
* 旧算法直接 `round(progressSum / total)`:11 个 done + 1 个 blocked@95 得到
|
|
1187
|
+
* 100,卡片上就出现「总体进度 100%」旁边还有「1 卡住了 / 11/12 做好了」——
|
|
1188
|
+
* 与截图同类的自相矛盾(T4 实测 A.5)。这里的封顶只作用于展示口径,不影响
|
|
1189
|
+
* 任何计数;全部完成时仍严格为 100,从而与 `done === total` 一一对应。
|
|
1190
|
+
*/
|
|
1191
|
+
const raw = total === 0 ? 0 : Math.round(progressSum / total);
|
|
1172
1192
|
return {
|
|
1173
1193
|
stats: {
|
|
1174
1194
|
total,
|
|
@@ -1176,7 +1196,7 @@ export function deriveAggregates(statuses) {
|
|
|
1176
1196
|
blocked,
|
|
1177
1197
|
inProgress,
|
|
1178
1198
|
pending,
|
|
1179
|
-
progress:
|
|
1199
|
+
progress: allDone ? 100 : Math.min(99, raw),
|
|
1180
1200
|
},
|
|
1181
1201
|
departments: [...departments.values()],
|
|
1182
1202
|
};
|
|
@@ -1203,13 +1223,18 @@ export function computeStats(statuses) {
|
|
|
1203
1223
|
return derivePlanSnapshot(statuses).stats;
|
|
1204
1224
|
}
|
|
1205
1225
|
/**
|
|
1206
|
-
* Invariant checker for tests and CI
|
|
1207
|
-
* pure derivation is already self-consistent.
|
|
1226
|
+
* Invariant checker for tests and CI.
|
|
1208
1227
|
*
|
|
1209
1228
|
* Returns an empty array when every invariant holds. Each message names the
|
|
1210
1229
|
* field and the observed value so it can be pasted straight into a report.
|
|
1230
|
+
*
|
|
1231
|
+
* [1.5.2] `phase` 是新增的可选入参:1.4.3 的 I1–I7 只校验计数内部自洽,
|
|
1232
|
+
* 完全不看 `phase` 与 `stats` 的关系,因此「14/14 且 progress=100 却写着
|
|
1233
|
+
* 正在收尾」这种**跨字段**矛盾一路畅通到界面。I8/I9 专门封住这一类:
|
|
1234
|
+
* 只要调用方把本次真正要下发的 phase 传进来,任何「进度与状态文案打架」
|
|
1235
|
+
* 的载荷都会在这里被判为违规,而不是等到用户看截图。
|
|
1211
1236
|
*/
|
|
1212
|
-
export function aggregateInvariantViolations(snapshot) {
|
|
1237
|
+
export function aggregateInvariantViolations(snapshot, phase) {
|
|
1213
1238
|
const violations = [];
|
|
1214
1239
|
const { statuses, stats, departments } = snapshot;
|
|
1215
1240
|
if (stats.total !== statuses.length) {
|
|
@@ -1271,19 +1296,98 @@ export function aggregateInvariantViolations(snapshot) {
|
|
|
1271
1296
|
if (sums.inProgress !== stats.inProgress) {
|
|
1272
1297
|
violations.push(`I7 各部门 inProgress 之和=${sums.inProgress} 与 stats.inProgress=${stats.inProgress} 不一致`);
|
|
1273
1298
|
}
|
|
1299
|
+
/*
|
|
1300
|
+
* [1.5.2] I8/I9 —— 完成语义跨字段不变量(用户截图那条「100% + 正在收尾」)。
|
|
1301
|
+
*
|
|
1302
|
+
* I8:工作项全部 done ⇔ phase 必须是 done。这是一条双向断言:
|
|
1303
|
+
* - 全 done 却是 pending/planning/executing/integrating/paused/blocked/degraded
|
|
1304
|
+
* ⇒ 卡片会同时出现 100% 与进行中文案(本次事故);
|
|
1305
|
+
* - phase=done 却还有未完成项 ⇒ 反向矛盾(「已完成」下面挂着没做完的活)。
|
|
1306
|
+
* I9:加权进度与计数同口径:全 done ⇒ 100;未全 done ⇒ 不得为 100。
|
|
1307
|
+
* 旧算法 round(progressSum/total) 让 11 done + 1 blocked@95 算出 100。
|
|
1308
|
+
*/
|
|
1309
|
+
if (stats.total > 0) {
|
|
1310
|
+
const allDone = stats.done === stats.total;
|
|
1311
|
+
if (allDone && phase !== undefined && phase !== null && phase !== 'done') {
|
|
1312
|
+
violations.push(`I8 stats.done=${stats.done}/${stats.total} 已全部完成,但 phase=${String(phase)} 仍表示未完成`);
|
|
1313
|
+
}
|
|
1314
|
+
if (!allDone && phase === 'done') {
|
|
1315
|
+
violations.push(`I8 phase=done 但 stats.done=${stats.done}/${stats.total} 尚未全部完成`);
|
|
1316
|
+
}
|
|
1317
|
+
if (allDone && stats.progress !== 100) {
|
|
1318
|
+
violations.push(`I9 全部完成但 stats.progress=${stats.progress}`);
|
|
1319
|
+
}
|
|
1320
|
+
if (!allDone && stats.progress >= 100) {
|
|
1321
|
+
violations.push(`I9 未全部完成(${stats.done}/${stats.total})却显示 stats.progress=${stats.progress}`);
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
/*
|
|
1325
|
+
* I10:单个工作项也不能自相矛盾——`in_progress` 意味着「还没做完」,
|
|
1326
|
+
* 它的进度必须 < 100(旧载荷里出现过「正在做 + 100%」,T5 C8)。
|
|
1327
|
+
*/
|
|
1328
|
+
for (const item of statuses) {
|
|
1329
|
+
if (item?.status === 'in_progress' && Number(item?.progress) >= 100) {
|
|
1330
|
+
violations.push(`I10 子任务 ${String(item?.subtaskId)} 状态为 in_progress 但 progress=${item?.progress}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1274
1333
|
return violations;
|
|
1275
1334
|
}
|
|
1276
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* 计划级阶段,由状态列表唯一决定。
|
|
1337
|
+
*
|
|
1338
|
+
* [1.5.2] 修复「一边 100%、一边正在收尾」的根因。
|
|
1339
|
+
*
|
|
1340
|
+
* 旧实现的最后一步是 `return resultSeen ? 'done' : 'integrating'`:工作项全部
|
|
1341
|
+
* done 时仍要等「结果消息落盘」才晋升终态。问题在于 `resultSeen` 只是**持久化
|
|
1342
|
+
* 回放**才知道的副作用,内存路径(`updateStatus` / `serialisePlan`)拿不到它:
|
|
1343
|
+
*
|
|
1344
|
+
* - 主 Agent 用 `dsh_workplan_update` 补完最后几项时,phase 永远停在 integrating;
|
|
1345
|
+
* - 该路径不会再派发,也就永远不会写结果消息,于是 integrating 永久留存;
|
|
1346
|
+
* - 同期 `stats.done/total` 已是 14/14、百分比 100%,卡片就同时显示
|
|
1347
|
+
* 「100% · 都做完了」和「正在收尾」,并留在「进行中」组(用户截图)。
|
|
1348
|
+
*
|
|
1349
|
+
* 结论:完成语义只能有一个事实源——状态列表。工作项全部完成即计划完成,
|
|
1350
|
+
* 不需要也不允许等另一个副作用来「确认」。`resultSeen` 参数仅为兼容旧调用点保留,
|
|
1351
|
+
* 不再参与判定(旧的 integrating 窗口已由 dispatch 内的 `executing` 覆盖)。
|
|
1352
|
+
*/
|
|
1353
|
+
export function planPhase(statuses, _resultSeen = false) {
|
|
1277
1354
|
if (statuses.length === 0)
|
|
1278
1355
|
return 'pending';
|
|
1279
1356
|
if (statuses.every((item) => item.status === 'done'))
|
|
1280
|
-
return
|
|
1357
|
+
return 'done';
|
|
1281
1358
|
if (statuses.some((item) => item.status === 'blocked'))
|
|
1282
1359
|
return 'blocked';
|
|
1283
1360
|
if (statuses.some((item) => item.status === 'in_progress' || item.status === 'done'))
|
|
1284
1361
|
return 'executing';
|
|
1285
1362
|
return 'pending';
|
|
1286
1363
|
}
|
|
1364
|
+
/**
|
|
1365
|
+
* 把「缓存的 phase」与「由状态列表派生的 phase」对账成唯一结果。
|
|
1366
|
+
*
|
|
1367
|
+
* phase 是缓存(dispatch/预算/取消会写它),statuses 是事实源。只要两者可能
|
|
1368
|
+
* 分叉,就必须在出口处对账,否则同一个会话会因读取路径不同而翻转(实时
|
|
1369
|
+
* integrating / 刷新后 done,T2 场景 B)。
|
|
1370
|
+
*
|
|
1371
|
+
* 规则只有四条,全部可由 `aggregateInvariantViolations` 断言:
|
|
1372
|
+
* 1. 工作项全部 done ⇒ 一定是 done(缓存不得把它拖回任何进行中状态);
|
|
1373
|
+
* 2. 缓存是 done/integrating 但工作项没全 done ⇒ 缓存过期,回退到派生值;
|
|
1374
|
+
* 3. pending/planning/executing 是「状态列表能直接回答」的轮次状态:一旦
|
|
1375
|
+
* 有工作项开始(派生值不再是 pending),缓存里的旧 pending/planning 必须
|
|
1376
|
+
* 让位;而派生值仍是 pending 时保留缓存(dispatch 刚开始、状态尚未落盘)。
|
|
1377
|
+
* 4. 其余情况保留缓存的轮次状态(paused/cancelled/blocked/degraded)——
|
|
1378
|
+
* 它们携带状态列表里没有的信息(预算暂停、用户取消)。
|
|
1379
|
+
*/
|
|
1380
|
+
export function reconcilePhase(phase, statuses) {
|
|
1381
|
+
const derived = planPhase(statuses);
|
|
1382
|
+
if (derived === 'done')
|
|
1383
|
+
return 'done';
|
|
1384
|
+
if (phase === 'done' || phase === 'integrating')
|
|
1385
|
+
return derived;
|
|
1386
|
+
if (phase === 'pending' || phase === 'planning' || phase === 'executing') {
|
|
1387
|
+
return derived === 'pending' ? phase : derived;
|
|
1388
|
+
}
|
|
1389
|
+
return phase ?? derived;
|
|
1390
|
+
}
|
|
1287
1391
|
export function nextReadyInStage(plan, statuses, stageId) {
|
|
1288
1392
|
const byId = new Map(statuses.map((item) => [item.subtaskId, item]));
|
|
1289
1393
|
const stage = plan.stages.find((item) => item.id === stageId);
|
package/lib/runtime.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* in this file is allowed to escape as an unhandled rejection from a listener.
|
|
13
13
|
*/
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
|
-
import { MAX_CONCURRENT, PlanError, applyStatus, attachPlanMeta, buildChildPrompt, buildCancelMessage, buildDegradedMessage, buildPlanMessage, buildPlannerPrompt, buildResultMessage, createStatuses, derivePlanSnapshot, extractJson, flattenSubtasks, isRecord, isSocialMessage, isStatusQuestion, isVagueContinuation, markDependentsBlocked, messageText, readySubtasks, MAX_PLAN_RESUMES, canResumePlan, resetBlockedStatuses, parseCancelMessage, parseResultMessage, parseWorkplanMessage, planPhase, truncate, validatePlan, applyResultStatuses, } from './plan.js';
|
|
15
|
+
import { MAX_CONCURRENT, PlanError, aggregateInvariantViolations, applyStatus, attachPlanMeta, buildChildPrompt, buildCancelMessage, buildDegradedMessage, buildPlanMessage, buildPlannerPrompt, buildResultMessage, createStatuses, derivePlanSnapshot, extractJson, flattenSubtasks, isRecord, isSocialMessage, isStatusQuestion, isVagueContinuation, markDependentsBlocked, messageText, readySubtasks, MAX_PLAN_RESUMES, canResumePlan, resetBlockedStatuses, parseCancelMessage, parseResultMessage, parseWorkplanMessage, planPhase, reconcilePhase, truncate, validatePlan, applyResultStatuses, } from './plan.js';
|
|
16
16
|
import { createProgressWatchdog, describeError } from './watchdog.js';
|
|
17
17
|
export const PLANNER_TIMEOUT_MS = 120000;
|
|
18
18
|
/**
|
|
@@ -785,21 +785,31 @@ export async function dispatchPlan(ctx, record, options) {
|
|
|
785
785
|
});
|
|
786
786
|
}
|
|
787
787
|
record.statuses = reset;
|
|
788
|
-
record.phase = 'paused';
|
|
789
788
|
const total = record.statuses.length;
|
|
790
789
|
const done = record.statuses.filter((item) => item.status === 'done').length;
|
|
790
|
+
/*
|
|
791
|
+
* [1.5.2] 预算到点 ≠ 一定是「暂停」。
|
|
792
|
+
*
|
|
793
|
+
* 旧代码在这里无条件 `record.phase = 'paused'`:如果最后一个子任务恰好在
|
|
794
|
+
* 截止时刻之后完成(done === total),计划会以 14/14、progress=100 的载荷
|
|
795
|
+
* 带着 paused 返回,界面就是「100% + 预算用完,可续跑」(T5 C4)。
|
|
796
|
+
* 全部完成时直接进终态,并且不再写「其余 0 项保留为未完成」这种自相矛盾的说明。
|
|
797
|
+
*/
|
|
798
|
+
const allDoneNow = total > 0 && done === total;
|
|
799
|
+
record.phase = allDoneNow ? 'done' : 'paused';
|
|
791
800
|
const minutes = Math.max(1, Math.round((options.totalTimeoutMs ?? TOTAL_TIMEOUT_MS) / 60000));
|
|
792
|
-
record.error =
|
|
801
|
+
record.error = allDoneNow
|
|
802
|
+
? null
|
|
803
|
+
: `本轮执行预算(${minutes} 分钟)已到:已完成 ${done}/${total},其余 ${total - done} 项保留为未完成,再次派发本计划即可续跑(已完成的不重跑)`;
|
|
793
804
|
/*
|
|
794
805
|
* Drop the failure lines the aborted children wrote on their way out.
|
|
795
806
|
* They used to survive into the result payload as
|
|
796
807
|
* `["T4: 总执行超时", "总执行超时", …]`, so the model read a failure
|
|
797
808
|
* report for subtasks that were explicitly *not* failures.
|
|
798
809
|
*/
|
|
799
|
-
record.errors = [
|
|
800
|
-
|
|
801
|
-
record.error
|
|
802
|
-
].slice(-20);
|
|
810
|
+
record.errors = (record.errors ?? []).filter((line) => !/总执行超时/.test(String(line)));
|
|
811
|
+
if (!allDoneNow)
|
|
812
|
+
record.errors = [...record.errors, record.error].slice(-20);
|
|
803
813
|
}
|
|
804
814
|
else {
|
|
805
815
|
/*
|
|
@@ -850,15 +860,30 @@ function serialisePlan(record, session) {
|
|
|
850
860
|
: []);
|
|
851
861
|
const statuses = snapshot.statuses;
|
|
852
862
|
const stats = snapshot.stats;
|
|
853
|
-
|
|
863
|
+
/*
|
|
864
|
+
* [1.5.2] 出口对账,而不是无条件相信内存缓存。
|
|
865
|
+
*
|
|
866
|
+
* 旧写法 `record.phase ?? planPhase(statuses)` 让缓存一旦存在就完全绕过
|
|
867
|
+
* statuses:一次 `updateStatus` 把计划写成 integrating 之后,即使 14/14 全部
|
|
868
|
+
* 完成,下发的 phase 仍是 integrating,界面于是「100% + 正在收尾」。这里改成
|
|
869
|
+
* `reconcilePhase`:全部完成一律 done,缓存过期的 done/integrating 回退到派生值。
|
|
870
|
+
*/
|
|
871
|
+
let phase = reconcilePhase(record.phase ?? (plan ? planPhase(statuses) : 'degraded'), statuses);
|
|
854
872
|
if (phase === 'pending' && !record.dispatchedAt && plan?.createdAt && Date.now() - plan.createdAt > STALE_PENDING_MS) {
|
|
855
873
|
phase = 'cancelled';
|
|
856
874
|
}
|
|
875
|
+
/*
|
|
876
|
+
* 每次真正下发前跑一遍跨字段不变量(I1–I10)。正常情况下恒为空;一旦某个
|
|
877
|
+
* 未来改动重新引入「进度与状态打架」,这里会把它连同具体字段值一起带出去,
|
|
878
|
+
* 而不是等用户截图发现。附在 payload 上而不是抛错:展示层不应因自检而白屏。
|
|
879
|
+
*/
|
|
880
|
+
const violations = aggregateInvariantViolations(snapshot, phase);
|
|
857
881
|
return {
|
|
858
882
|
sessionId: session.id,
|
|
859
883
|
planId: record.planId ?? plan?.planId ?? null,
|
|
860
884
|
title: plan?.title ?? record.title ?? '任务进度',
|
|
861
885
|
phase,
|
|
886
|
+
...(violations.length ? { violations } : {}),
|
|
862
887
|
degraded: record.degraded === true,
|
|
863
888
|
error: record.error ? truncate(record.error, 2000) : null,
|
|
864
889
|
errors: (record.errors ?? []).slice(-20),
|
|
@@ -1024,6 +1049,15 @@ export function reconstructFromSession(session, targetPlanId) {
|
|
|
1024
1049
|
phase = 'paused';
|
|
1025
1050
|
if (cancelled)
|
|
1026
1051
|
phase = 'cancelled';
|
|
1052
|
+
/*
|
|
1053
|
+
* [1.5.2] 回放也必须走同一套对账。
|
|
1054
|
+
*
|
|
1055
|
+
* 回放曾经因为 `resultSeen=true`(结果消息在会话里)得到 done,而同一时刻的
|
|
1056
|
+
* 实时缓存因为没有这个副作用得到 integrating —— 同一个会话刷新前后翻转。
|
|
1057
|
+
* `reconcilePhase` 让两条链路都以 statuses 为准:全部完成即 done(包括历史上
|
|
1058
|
+
* 「先完成、后收到取消消息」的会话,不再出现「100% + 已取消」)。
|
|
1059
|
+
*/
|
|
1060
|
+
phase = reconcilePhase(phase, statuses);
|
|
1027
1061
|
/*
|
|
1028
1062
|
* An old plan that was never dispatched used to be relabelled `cancelled`,
|
|
1029
1063
|
* which also made it permanently unresumable — the card said "已取消" and no
|
|
@@ -1195,7 +1229,16 @@ export function createWorkplanRuntime(ctx, options = {}) {
|
|
|
1195
1229
|
if (status === 'blocked') {
|
|
1196
1230
|
record.statuses = markDependentsBlocked(record.plan, record.statuses, subtaskId);
|
|
1197
1231
|
}
|
|
1198
|
-
|
|
1232
|
+
/*
|
|
1233
|
+
* [1.5.2] 这里曾是「100% + 正在收尾」的直接入口。
|
|
1234
|
+
*
|
|
1235
|
+
* 旧代码 `record.phase = planPhase(record.statuses)` 用不到 `resultSeen`,
|
|
1236
|
+
* 所以主 Agent 用 update 工具补完最后一项时得到的 phase 永远是 integrating;
|
|
1237
|
+
* 而这条路径之后不会再派发,也就永远不会写结果消息把它晋升为 done。
|
|
1238
|
+
* 走 `reconcilePhase` 后,全部完成即 done,且已 done 的计划不会被一次
|
|
1239
|
+
* 空更新打回 integrating(T4 实测 A.2 的回退路径)。
|
|
1240
|
+
*/
|
|
1241
|
+
record.phase = reconcilePhase(record.phase, record.statuses);
|
|
1199
1242
|
record.updatedAt = Date.now();
|
|
1200
1243
|
const snapshot = derivePlanSnapshot(record.statuses);
|
|
1201
1244
|
return {
|
|
@@ -1223,6 +1266,29 @@ export function createWorkplanRuntime(ctx, options = {}) {
|
|
|
1223
1266
|
const reason = typeof args?.reason === 'string' && args.reason.trim()
|
|
1224
1267
|
? args.reason.trim()
|
|
1225
1268
|
: '用户取消';
|
|
1269
|
+
/*
|
|
1270
|
+
* [1.5.2] 已完成即终态:取消不再把 100% 的计划改写成「已取消」。
|
|
1271
|
+
*
|
|
1272
|
+
* T4 实测 A.4:2/2 全部 done 的计划被取消后会得到 `phase=cancelled` +
|
|
1273
|
+
* `done=2/2` + `percent=100` —— 又一个「一边说全好了、一边说被取消」的
|
|
1274
|
+
* 自相矛盾组合,且会让本可续跑/查看的完成记录永远退出「已完成」组。
|
|
1275
|
+
* 这里直接把取消变成 no-op 并如实返回 done。
|
|
1276
|
+
*/
|
|
1277
|
+
const before = derivePlanSnapshot(record.statuses);
|
|
1278
|
+
if (before.stats.total > 0 && before.stats.done === before.stats.total) {
|
|
1279
|
+
record.phase = 'done';
|
|
1280
|
+
record.updatedAt = Date.now();
|
|
1281
|
+
state.records.set(session.id, record);
|
|
1282
|
+
return {
|
|
1283
|
+
ok: true,
|
|
1284
|
+
planId: record.plan.planId,
|
|
1285
|
+
phase: 'done',
|
|
1286
|
+
reason,
|
|
1287
|
+
alreadyDone: true,
|
|
1288
|
+
abortedRunning: false,
|
|
1289
|
+
stats: before.stats,
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1226
1292
|
if (record.dispatchPromise) {
|
|
1227
1293
|
/*
|
|
1228
1294
|
* Cancel must always work.
|
|
@@ -1409,9 +1475,14 @@ export function createWorkplanRuntime(ctx, options = {}) {
|
|
|
1409
1475
|
ctx.logger?.warn?.(`dsh-workplan: 写入结果消息失败:${error instanceof Error ? error.message : String(error)}`);
|
|
1410
1476
|
}
|
|
1411
1477
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1478
|
+
/*
|
|
1479
|
+
* [1.5.2] 终态不再依赖「结果消息写成功」这个副作用。
|
|
1480
|
+
*
|
|
1481
|
+
* 旧代码只有 `record.resultWritten === true` 时才把 phase 升成 done,
|
|
1482
|
+
* 于是 session.append 抛错(或该轮根本没有派发)就会永久停在 integrating。
|
|
1483
|
+
* 完成语义由 statuses 决定;结果消息只负责持久化,不负责定义完成。
|
|
1484
|
+
*/
|
|
1485
|
+
record.phase = reconcilePhase(record.phase, record.statuses);
|
|
1415
1486
|
/*
|
|
1416
1487
|
* The model's dispatch result reports the same snapshot the dashboard will
|
|
1417
1488
|
* show. It used to send `record.stats` while the status list below was read
|
package/package.json
CHANGED