dsh-workplan 1.5.1 → 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 +145 -2
- package/client.js +152 -32
- 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 +149 -32
- package/src/host.ts +1 -1
- package/src/plan.ts +110 -8
- package/src/runtime.ts +83 -11
package/src/runtime.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { randomUUID } from 'node:crypto'
|
|
|
17
17
|
import {
|
|
18
18
|
MAX_CONCURRENT,
|
|
19
19
|
PlanError,
|
|
20
|
+
aggregateInvariantViolations,
|
|
20
21
|
applyStatus,
|
|
21
22
|
attachPlanMeta,
|
|
22
23
|
buildChildPrompt,
|
|
@@ -43,6 +44,7 @@ import {
|
|
|
43
44
|
parseResultMessage,
|
|
44
45
|
parseWorkplanMessage,
|
|
45
46
|
planPhase,
|
|
47
|
+
reconcilePhase,
|
|
46
48
|
truncate,
|
|
47
49
|
validatePlan,
|
|
48
50
|
applyResultStatuses,
|
|
@@ -800,21 +802,30 @@ export async function dispatchPlan(ctx: any, record: any, options: any) {
|
|
|
800
802
|
})
|
|
801
803
|
}
|
|
802
804
|
record.statuses = reset
|
|
803
|
-
record.phase = 'paused'
|
|
804
805
|
const total = record.statuses.length
|
|
805
806
|
const done = record.statuses.filter((item: any) => item.status === 'done').length
|
|
807
|
+
/*
|
|
808
|
+
* [1.5.2] 预算到点 ≠ 一定是「暂停」。
|
|
809
|
+
*
|
|
810
|
+
* 旧代码在这里无条件 `record.phase = 'paused'`:如果最后一个子任务恰好在
|
|
811
|
+
* 截止时刻之后完成(done === total),计划会以 14/14、progress=100 的载荷
|
|
812
|
+
* 带着 paused 返回,界面就是「100% + 预算用完,可续跑」(T5 C4)。
|
|
813
|
+
* 全部完成时直接进终态,并且不再写「其余 0 项保留为未完成」这种自相矛盾的说明。
|
|
814
|
+
*/
|
|
815
|
+
const allDoneNow = total > 0 && done === total
|
|
816
|
+
record.phase = allDoneNow ? 'done' : 'paused'
|
|
806
817
|
const minutes = Math.max(1, Math.round((options.totalTimeoutMs ?? TOTAL_TIMEOUT_MS) / 60000))
|
|
807
|
-
record.error =
|
|
818
|
+
record.error = allDoneNow
|
|
819
|
+
? null
|
|
820
|
+
: `本轮执行预算(${minutes} 分钟)已到:已完成 ${done}/${total},其余 ${total - done} 项保留为未完成,再次派发本计划即可续跑(已完成的不重跑)`
|
|
808
821
|
/*
|
|
809
822
|
* Drop the failure lines the aborted children wrote on their way out.
|
|
810
823
|
* They used to survive into the result payload as
|
|
811
824
|
* `["T4: 总执行超时", "总执行超时", …]`, so the model read a failure
|
|
812
825
|
* report for subtasks that were explicitly *not* failures.
|
|
813
826
|
*/
|
|
814
|
-
record.errors = [
|
|
815
|
-
|
|
816
|
-
record.error,
|
|
817
|
-
].slice(-20)
|
|
827
|
+
record.errors = (record.errors ?? []).filter((line: string) => !/总执行超时/.test(String(line)))
|
|
828
|
+
if (!allDoneNow) record.errors = [...record.errors, record.error].slice(-20)
|
|
818
829
|
} else {
|
|
819
830
|
/*
|
|
820
831
|
* `blocked` is a terminal state, so it always reports progress 100 — the
|
|
@@ -865,15 +876,30 @@ function serialisePlan(record: any, session: any) {
|
|
|
865
876
|
)
|
|
866
877
|
const statuses = snapshot.statuses
|
|
867
878
|
const stats = snapshot.stats
|
|
868
|
-
|
|
879
|
+
/*
|
|
880
|
+
* [1.5.2] 出口对账,而不是无条件相信内存缓存。
|
|
881
|
+
*
|
|
882
|
+
* 旧写法 `record.phase ?? planPhase(statuses)` 让缓存一旦存在就完全绕过
|
|
883
|
+
* statuses:一次 `updateStatus` 把计划写成 integrating 之后,即使 14/14 全部
|
|
884
|
+
* 完成,下发的 phase 仍是 integrating,界面于是「100% + 正在收尾」。这里改成
|
|
885
|
+
* `reconcilePhase`:全部完成一律 done,缓存过期的 done/integrating 回退到派生值。
|
|
886
|
+
*/
|
|
887
|
+
let phase = reconcilePhase(record.phase ?? (plan ? planPhase(statuses) : 'degraded'), statuses)
|
|
869
888
|
if (phase === 'pending' && !record.dispatchedAt && plan?.createdAt && Date.now() - plan.createdAt > STALE_PENDING_MS) {
|
|
870
889
|
phase = 'cancelled'
|
|
871
890
|
}
|
|
891
|
+
/*
|
|
892
|
+
* 每次真正下发前跑一遍跨字段不变量(I1–I10)。正常情况下恒为空;一旦某个
|
|
893
|
+
* 未来改动重新引入「进度与状态打架」,这里会把它连同具体字段值一起带出去,
|
|
894
|
+
* 而不是等用户截图发现。附在 payload 上而不是抛错:展示层不应因自检而白屏。
|
|
895
|
+
*/
|
|
896
|
+
const violations = aggregateInvariantViolations(snapshot, phase)
|
|
872
897
|
return {
|
|
873
898
|
sessionId: session.id,
|
|
874
899
|
planId: record.planId ?? plan?.planId ?? null,
|
|
875
900
|
title: plan?.title ?? record.title ?? '任务进度',
|
|
876
901
|
phase,
|
|
902
|
+
...(violations.length ? { violations } : {}),
|
|
877
903
|
degraded: record.degraded === true,
|
|
878
904
|
error: record.error ? truncate(record.error, 2000) : null,
|
|
879
905
|
errors: (record.errors ?? []).slice(-20),
|
|
@@ -1028,6 +1054,15 @@ export function reconstructFromSession(session: any, targetPlanId?: string) {
|
|
|
1028
1054
|
let phase = planPhase(statuses, resultSeen)
|
|
1029
1055
|
if (resultPhase === 'paused' && statuses.some((item: any) => item.status !== 'done')) phase = 'paused'
|
|
1030
1056
|
if (cancelled) phase = 'cancelled'
|
|
1057
|
+
/*
|
|
1058
|
+
* [1.5.2] 回放也必须走同一套对账。
|
|
1059
|
+
*
|
|
1060
|
+
* 回放曾经因为 `resultSeen=true`(结果消息在会话里)得到 done,而同一时刻的
|
|
1061
|
+
* 实时缓存因为没有这个副作用得到 integrating —— 同一个会话刷新前后翻转。
|
|
1062
|
+
* `reconcilePhase` 让两条链路都以 statuses 为准:全部完成即 done(包括历史上
|
|
1063
|
+
* 「先完成、后收到取消消息」的会话,不再出现「100% + 已取消」)。
|
|
1064
|
+
*/
|
|
1065
|
+
phase = reconcilePhase(phase, statuses)
|
|
1031
1066
|
/*
|
|
1032
1067
|
* An old plan that was never dispatched used to be relabelled `cancelled`,
|
|
1033
1068
|
* which also made it permanently unresumable — the card said "已取消" and no
|
|
@@ -1206,7 +1241,16 @@ export function createWorkplanRuntime(ctx: any, options: WorkplanRuntimeOptions
|
|
|
1206
1241
|
if (status === 'blocked') {
|
|
1207
1242
|
record.statuses = markDependentsBlocked(record.plan, record.statuses, subtaskId)
|
|
1208
1243
|
}
|
|
1209
|
-
|
|
1244
|
+
/*
|
|
1245
|
+
* [1.5.2] 这里曾是「100% + 正在收尾」的直接入口。
|
|
1246
|
+
*
|
|
1247
|
+
* 旧代码 `record.phase = planPhase(record.statuses)` 用不到 `resultSeen`,
|
|
1248
|
+
* 所以主 Agent 用 update 工具补完最后一项时得到的 phase 永远是 integrating;
|
|
1249
|
+
* 而这条路径之后不会再派发,也就永远不会写结果消息把它晋升为 done。
|
|
1250
|
+
* 走 `reconcilePhase` 后,全部完成即 done,且已 done 的计划不会被一次
|
|
1251
|
+
* 空更新打回 integrating(T4 实测 A.2 的回退路径)。
|
|
1252
|
+
*/
|
|
1253
|
+
record.phase = reconcilePhase(record.phase, record.statuses)
|
|
1210
1254
|
record.updatedAt = Date.now()
|
|
1211
1255
|
const snapshot = derivePlanSnapshot(record.statuses)
|
|
1212
1256
|
return {
|
|
@@ -1231,6 +1275,29 @@ export function createWorkplanRuntime(ctx: any, options: WorkplanRuntimeOptions
|
|
|
1231
1275
|
const reason = typeof args?.reason === 'string' && args.reason.trim()
|
|
1232
1276
|
? args.reason.trim()
|
|
1233
1277
|
: '用户取消'
|
|
1278
|
+
/*
|
|
1279
|
+
* [1.5.2] 已完成即终态:取消不再把 100% 的计划改写成「已取消」。
|
|
1280
|
+
*
|
|
1281
|
+
* T4 实测 A.4:2/2 全部 done 的计划被取消后会得到 `phase=cancelled` +
|
|
1282
|
+
* `done=2/2` + `percent=100` —— 又一个「一边说全好了、一边说被取消」的
|
|
1283
|
+
* 自相矛盾组合,且会让本可续跑/查看的完成记录永远退出「已完成」组。
|
|
1284
|
+
* 这里直接把取消变成 no-op 并如实返回 done。
|
|
1285
|
+
*/
|
|
1286
|
+
const before = derivePlanSnapshot(record.statuses)
|
|
1287
|
+
if (before.stats.total > 0 && before.stats.done === before.stats.total) {
|
|
1288
|
+
record.phase = 'done'
|
|
1289
|
+
record.updatedAt = Date.now()
|
|
1290
|
+
state.records.set(session.id, record)
|
|
1291
|
+
return {
|
|
1292
|
+
ok: true,
|
|
1293
|
+
planId: record.plan.planId,
|
|
1294
|
+
phase: 'done',
|
|
1295
|
+
reason,
|
|
1296
|
+
alreadyDone: true,
|
|
1297
|
+
abortedRunning: false,
|
|
1298
|
+
stats: before.stats,
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1234
1301
|
if (record.dispatchPromise) {
|
|
1235
1302
|
/*
|
|
1236
1303
|
* Cancel must always work.
|
|
@@ -1411,9 +1478,14 @@ export function createWorkplanRuntime(ctx: any, options: WorkplanRuntimeOptions
|
|
|
1411
1478
|
ctx.logger?.warn?.(`dsh-workplan: 写入结果消息失败:${error instanceof Error ? error.message : String(error)}`)
|
|
1412
1479
|
}
|
|
1413
1480
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1481
|
+
/*
|
|
1482
|
+
* [1.5.2] 终态不再依赖「结果消息写成功」这个副作用。
|
|
1483
|
+
*
|
|
1484
|
+
* 旧代码只有 `record.resultWritten === true` 时才把 phase 升成 done,
|
|
1485
|
+
* 于是 session.append 抛错(或该轮根本没有派发)就会永久停在 integrating。
|
|
1486
|
+
* 完成语义由 statuses 决定;结果消息只负责持久化,不负责定义完成。
|
|
1487
|
+
*/
|
|
1488
|
+
record.phase = reconcilePhase(record.phase, record.statuses)
|
|
1417
1489
|
/*
|
|
1418
1490
|
* The model's dispatch result reports the same snapshot the dashboard will
|
|
1419
1491
|
* show. It used to send `record.stats` while the status list below was read
|