dsh-activity-pane 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +64 -76
- package/package.json +1 -1
- package/scripts/acceptance.mjs +2 -2
- package/scripts/check.mjs +87 -61
- package/src/client.mjs +15 -16
- package/src/core.mjs +49 -60
- package/src/host.mjs +46 -12
package/.dsh-plugin/client.js
CHANGED
|
@@ -1985,58 +1985,30 @@ function durationTime(value) {
|
|
|
1985
1985
|
return Number.isFinite(time) ? time : null;
|
|
1986
1986
|
}
|
|
1987
1987
|
|
|
1988
|
-
/**
|
|
1989
|
-
|
|
1990
|
-
|
|
1988
|
+
/**
|
|
1989
|
+
* 从 history 重放回合/等待边界事件,提取最近一个已结束回合的运行过程耗时(busy 口径:
|
|
1990
|
+
* 起止差值扣除回合内阻塞等待,与 R-01-020 累计口径一致,恒不大于该回合墙钟时长)。
|
|
1991
|
+
* 回合起止不完整、时间逆序或回合运行段不为正时忽略该回合;无可得回合返回 null。
|
|
1992
|
+
*/
|
|
1993
|
+
function lastTurnBusyFromEvents(events) {
|
|
1994
|
+
let state = emptyTurnStats();
|
|
1991
1995
|
let latest = null;
|
|
1992
1996
|
for (const entry of Array.isArray(events) ? events : []) {
|
|
1993
|
-
const
|
|
1994
|
-
const
|
|
1995
|
-
|
|
1996
|
-
if (
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
}
|
|
2001
|
-
if (event.type !== "turn/end") continue;
|
|
2002
|
-
const start = starts.get(turn);
|
|
2003
|
-
if (start === undefined || time < start) continue;
|
|
2004
|
-
if (latest === null || time > latest.end) latest = { end: time, duration: time - start };
|
|
2005
|
-
}
|
|
2006
|
-
return latest;
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
/** 从 turnTimings 提取最近完整回合的结束时刻与固定耗时;两者始终来自同一回合。 */
|
|
2010
|
-
function lastTurnDurationCandidateFromTimings(turnTimings) {
|
|
2011
|
-
if (!(turnTimings instanceof Map)) return null;
|
|
2012
|
-
let latest = null;
|
|
2013
|
-
for (const timing of turnTimings.values()) {
|
|
2014
|
-
const start = durationTime(timing?.startTime);
|
|
2015
|
-
const end = durationTime(timing?.endTime);
|
|
2016
|
-
if (start === null || end === null || end < start) continue;
|
|
2017
|
-
if (latest === null || end > latest.end) latest = { end, duration: end - start };
|
|
1997
|
+
const time = durationTime(eventOf(entry)?.time);
|
|
1998
|
+
const prev = state;
|
|
1999
|
+
state = applyTurnEventToStats(state, entry);
|
|
2000
|
+
if (prev.openTurnStart === null || state.openTurnStart !== null) continue;
|
|
2001
|
+
// 能走到回合闭合的必为时刻有效的 turn/end(applyTurnEventToStats 对无效时刻无效果)。
|
|
2002
|
+
const activeMs = (state.busyMs ?? 0) - (prev.busyMs ?? 0);
|
|
2003
|
+
if (activeMs <= 0) continue;
|
|
2004
|
+
latest = { end: time, duration: activeMs };
|
|
2018
2005
|
}
|
|
2019
|
-
return latest;
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
/** 从 history 提取最近完整回合的固定耗时;回合起止不完整或逆序时忽略该回合。 */
|
|
2023
|
-
function lastTurnDurationFromEvents(events) {
|
|
2024
|
-
return lastTurnDurationCandidateFromEvents(events)?.duration ?? null;
|
|
2006
|
+
return latest?.duration ?? null;
|
|
2025
2007
|
}
|
|
2026
2008
|
|
|
2027
|
-
/**
|
|
2028
|
-
function
|
|
2029
|
-
return
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
/** 在快照与 history 中按最近结束时刻选择同一最新完整回合的固定耗时。 */
|
|
2033
|
-
function lastTurnDuration({ turnTimings = null, history = [] } = {}) {
|
|
2034
|
-
const candidates = [lastTurnDurationCandidateFromTimings(turnTimings), lastTurnDurationCandidateFromEvents(history)].filter(Boolean);
|
|
2035
|
-
let latest = null;
|
|
2036
|
-
for (const candidate of candidates) {
|
|
2037
|
-
if (latest === null || candidate.end > latest.end) latest = candidate;
|
|
2038
|
-
}
|
|
2039
|
-
return latest?.duration ?? null;
|
|
2009
|
+
/** 最近完整回合的固定耗时(busy 口径):从 history 事件重放派生。 */
|
|
2010
|
+
function lastTurnDuration({ history = [] } = {}) {
|
|
2011
|
+
return lastTurnBusyFromEvents(history);
|
|
2040
2012
|
}
|
|
2041
2013
|
|
|
2042
2014
|
/** ask_user_question 工具名:提问/计划审查等待的开启边界(tool/call)与配对结算(tool/result)。 */
|
|
@@ -2052,7 +2024,7 @@ function isBusyBoundaryEvent(type) {
|
|
|
2052
2024
|
|
|
2053
2025
|
/** 空记账状态(无任何有效计时)。 */
|
|
2054
2026
|
function emptyTurnStats() {
|
|
2055
|
-
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null };
|
|
2027
|
+
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null, watermarkTime: null };
|
|
2056
2028
|
}
|
|
2057
2029
|
|
|
2058
2030
|
/** 从任意来源拷贝记账状态:缺失字段归一为 null(旧记录/部分记录兼容)。 */
|
|
@@ -2064,10 +2036,14 @@ function turnStatsFrom(state) {
|
|
|
2064
2036
|
openWaitKind: state?.openWaitKind ?? null,
|
|
2065
2037
|
openWaitId: state?.openWaitId ?? null,
|
|
2066
2038
|
waitedMs: state?.waitedMs ?? null,
|
|
2039
|
+
watermarkTime: state?.watermarkTime ?? null,
|
|
2067
2040
|
};
|
|
2068
2041
|
}
|
|
2069
2042
|
|
|
2070
|
-
/**
|
|
2043
|
+
/**
|
|
2044
|
+
* 记账状态逐字段相等(不含 watermarkSeq——它不是状态字段,由宿主侧随写入一并落盘;
|
|
2045
|
+
* watermarkTime 为水位处已覆盖事件的时刻,参与相等判定以让补写该字段的收敛写入生效)。
|
|
2046
|
+
*/
|
|
2071
2047
|
function turnStatsEqual(a, b) {
|
|
2072
2048
|
return (
|
|
2073
2049
|
a === b ||
|
|
@@ -2076,7 +2052,8 @@ function turnStatsEqual(a, b) {
|
|
|
2076
2052
|
(a?.openWaitStart ?? null) === (b?.openWaitStart ?? null) &&
|
|
2077
2053
|
(a?.openWaitKind ?? null) === (b?.openWaitKind ?? null) &&
|
|
2078
2054
|
(a?.openWaitId ?? null) === (b?.openWaitId ?? null) &&
|
|
2079
|
-
(a?.waitedMs ?? null) === (b?.waitedMs ?? null)
|
|
2055
|
+
(a?.waitedMs ?? null) === (b?.waitedMs ?? null) &&
|
|
2056
|
+
(a?.watermarkTime ?? null) === (b?.watermarkTime ?? null))
|
|
2080
2057
|
);
|
|
2081
2058
|
}
|
|
2082
2059
|
|
|
@@ -2198,15 +2175,18 @@ function totalBusyDisplayMs({ busyMs = null, openTurnStart = null, waitedMs = nu
|
|
|
2198
2175
|
|
|
2199
2176
|
/**
|
|
2200
2177
|
* 回合统计记账的统一收敛(R-01-020/AC-04、AC-05):对全会话事件列表重放出下一份
|
|
2201
|
-
*
|
|
2202
|
-
*
|
|
2203
|
-
*
|
|
2204
|
-
*
|
|
2178
|
+
* 记账,宿主侧懒回填与启动扫描共用。路径选择——无记录、强制重放、持久化水位非法、
|
|
2179
|
+
* 或水位超前于日志最大 seq(事件 seq 空间被重编,如 dsh 0.1.5 V3 迁移)时从空状态
|
|
2180
|
+
* 全量重放(超前水位会把后续全部实时事件封死在守卫之外,openTurnStart 永不清空、
|
|
2181
|
+
* 总耗时无限增长);否则从持久化记账出发仅增量应用 `seq > watermarkSeq` 的事件。
|
|
2182
|
+
* 水位推进处同步记录 `watermarkTime`(水位处已覆盖事件的时刻,与实时登记按效果事件
|
|
2183
|
+
* 写入的口径互为保守——检测只要求该值不超过真实边界事件时刻)——宿主实时登记据此
|
|
2184
|
+
* 识别 seq 空间重编(低 seq 事件携带比水位更新的时刻,增量口径已失效)。
|
|
2205
2185
|
* `closeOpenTurn`(宿主启动扫描):重放后仍存在的开放回合按日志最后事件时刻强制
|
|
2206
2186
|
* 结算关闭——宿主重启后不存在仍在运行的回合,残留起点只会令总耗时无限增长;尾部
|
|
2207
2187
|
* 未配对等待一并按同刻结算(不落到运行时长里)。
|
|
2208
2188
|
*/
|
|
2209
|
-
function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
2189
|
+
function reconcileTurnStats(current, records, { closeOpenTurn = false, forceFresh = false } = {}) {
|
|
2210
2190
|
const list = Array.isArray(records) ? records : [];
|
|
2211
2191
|
let maxSeq = null;
|
|
2212
2192
|
let lastTime = null;
|
|
@@ -2216,23 +2196,32 @@ function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
|
2216
2196
|
const time = durationTime(eventOf(record)?.time);
|
|
2217
2197
|
if (time !== null && (lastTime === null || time > lastTime)) lastTime = time;
|
|
2218
2198
|
}
|
|
2219
|
-
const fresh = !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2199
|
+
const fresh = forceFresh || !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2220
2200
|
let state = emptyTurnStats();
|
|
2221
2201
|
let watermarkSeq = null;
|
|
2202
|
+
let watermarkTime = null;
|
|
2203
|
+
// 水位推进:seq 更大即前推水位;事件时刻有效才更新 watermarkTime(无效不前推)。
|
|
2204
|
+
const advance = (record) => {
|
|
2205
|
+
const seq = Number(record?.seq);
|
|
2206
|
+
if (!Number.isFinite(seq) || (watermarkSeq !== null && seq <= watermarkSeq)) return;
|
|
2207
|
+
watermarkSeq = seq;
|
|
2208
|
+
const time = durationTime(eventOf(record)?.time);
|
|
2209
|
+
if (time !== null) watermarkTime = time;
|
|
2210
|
+
};
|
|
2222
2211
|
if (fresh) {
|
|
2223
2212
|
for (const record of list) {
|
|
2224
2213
|
state = applyTurnEventToStats(state, record);
|
|
2225
|
-
|
|
2226
|
-
if (Number.isFinite(seq) && (watermarkSeq === null || seq > watermarkSeq)) watermarkSeq = seq;
|
|
2214
|
+
advance(record);
|
|
2227
2215
|
}
|
|
2228
2216
|
} else {
|
|
2229
2217
|
state = turnStatsFrom(current);
|
|
2218
|
+
watermarkTime = state.watermarkTime;
|
|
2230
2219
|
watermarkSeq = Number(current.watermarkSeq);
|
|
2231
2220
|
for (const record of list) {
|
|
2232
2221
|
const seq = Number(record?.seq);
|
|
2233
2222
|
if (!Number.isFinite(seq) || seq <= watermarkSeq) continue;
|
|
2234
2223
|
state = applyTurnEventToStats(state, record);
|
|
2235
|
-
|
|
2224
|
+
advance(record);
|
|
2236
2225
|
}
|
|
2237
2226
|
}
|
|
2238
2227
|
if (closeOpenTurn && state.openTurnStart !== null) {
|
|
@@ -2245,7 +2234,7 @@ function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
|
2245
2234
|
state.openWaitId = null;
|
|
2246
2235
|
state.waitedMs = null;
|
|
2247
2236
|
}
|
|
2248
|
-
return { ...state, watermarkSeq };
|
|
2237
|
+
return { ...state, watermarkSeq, watermarkTime };
|
|
2249
2238
|
}
|
|
2250
2239
|
|
|
2251
2240
|
/**
|
|
@@ -3035,9 +3024,11 @@ const CSS = `
|
|
|
3035
3024
|
box-shadow: none;
|
|
3036
3025
|
animation: none;
|
|
3037
3026
|
}
|
|
3027
|
+
/* 会话卡标题(活动卡、子代理卡与最近卡共用 .dap-title)统一常规字重,不加粗
|
|
3028
|
+
(R-01-013/AC-09 及东家 2026-09-11 视觉反馈);无按卡类的字重覆盖。 */
|
|
3038
3029
|
[data-dsh-activity-pane] .dap-title {
|
|
3039
3030
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
3040
|
-
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight:
|
|
3031
|
+
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;
|
|
3041
3032
|
}
|
|
3042
3033
|
/* 标题行最右侧的累计运行时长(R-01-020/AC-01):固定占位不参与标题挤压,
|
|
3043
3034
|
标题过长时以自身省略号让位;色调弱于标题,不与状态点抢视觉。 */
|
|
@@ -3046,10 +3037,6 @@ const CSS = `
|
|
|
3046
3037
|
color: #8a94a3; white-space: nowrap;
|
|
3047
3038
|
}
|
|
3048
3039
|
[data-dsh-activity-pane] .dap-total-time[hidden] { display: none; }
|
|
3049
|
-
/* 最近历史卡标题降为常规字重:历史区不抢占视觉强调(R-01-013/AC-09)。 */
|
|
3050
|
-
[data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-title {
|
|
3051
|
-
font-weight: 400;
|
|
3052
|
-
}
|
|
3053
3040
|
/* 等待卡末行首行「类型胶囊」(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043):圆底类型
|
|
3054
3041
|
图标 + 类型文字,色相随等待类别(--dap-wait-color)——阻塞金/完成绿/错误红;
|
|
3055
3042
|
胶囊为行内元素不自占满宽,随文字内容收缩。 */
|
|
@@ -5055,13 +5042,14 @@ function apply(ctx) {
|
|
|
5055
5042
|
}
|
|
5056
5043
|
}
|
|
5057
5044
|
|
|
5058
|
-
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15
|
|
5059
|
-
|
|
5045
|
+
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15):history 引用
|
|
5046
|
+
* 不变即命中缓存。busy 口径(起止差值扣除回合内阻塞等待),与标题行总耗时同口径,
|
|
5047
|
+
* 保证恒不大于累计值(R-01-020)。 */
|
|
5048
|
+
function memoTurnDuration(detail) {
|
|
5060
5049
|
const history = detail.history ?? null;
|
|
5061
|
-
if (detail.
|
|
5062
|
-
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
5050
|
+
if (detail.memoTurnDurationHistoryOf !== history) {
|
|
5063
5051
|
detail.memoTurnDurationHistoryOf = history;
|
|
5064
|
-
detail.memoTurnDuration = lastTurnDuration({
|
|
5052
|
+
detail.memoTurnDuration = lastTurnDuration({ history });
|
|
5065
5053
|
}
|
|
5066
5054
|
return detail.memoTurnDuration ?? null;
|
|
5067
5055
|
}
|
|
@@ -5966,7 +5954,7 @@ function apply(ctx) {
|
|
|
5966
5954
|
}
|
|
5967
5955
|
}
|
|
5968
5956
|
if (entry.kind === "awaiting" && detail) {
|
|
5969
|
-
entry.elapsedMs = memoTurnDuration(detail
|
|
5957
|
+
entry.elapsedMs = memoTurnDuration(detail);
|
|
5970
5958
|
}
|
|
5971
5959
|
if (detail?.model) {
|
|
5972
5960
|
entry.model = detail.model.model;
|
|
@@ -6038,7 +6026,7 @@ function apply(ctx) {
|
|
|
6038
6026
|
// 非运行(暂停等待):冻结最后已知统计与最近回合耗时,progress 置空隐藏
|
|
6039
6027
|
// 进度条(R-01-009/AC-15);刷新/无留存时回退当前列表投影。
|
|
6040
6028
|
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
6041
|
-
entry.elapsedMs = detail ? memoTurnDuration(detail
|
|
6029
|
+
entry.elapsedMs = detail ? memoTurnDuration(detail) : null;
|
|
6042
6030
|
entry.progress = null;
|
|
6043
6031
|
}
|
|
6044
6032
|
}
|
|
@@ -6061,13 +6049,13 @@ function apply(ctx) {
|
|
|
6061
6049
|
recentTotal = recentCandidates.length;
|
|
6062
6050
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
6063
6051
|
recentHasMore = recent.length < recentTotal;
|
|
6064
|
-
//
|
|
6065
|
-
//
|
|
6052
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从已读 history 取最近完整回合的运行
|
|
6053
|
+
// 过程时长(busy 口径,与标题行总耗时一致,R-01-013/AC-12),缺边界时仅为当前
|
|
6054
|
+
// 可见历史卡安排一次既有 history 补读。
|
|
6066
6055
|
const recentDurationFallbackIds = new Set();
|
|
6067
6056
|
for (const entry of recent) {
|
|
6068
6057
|
const detail = sessionDetailsById.get(entry.id);
|
|
6069
|
-
const
|
|
6070
|
-
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
6058
|
+
const elapsedMs = lastTurnDuration({ history: detail?.history ?? null });
|
|
6071
6059
|
// 累计运行时长(R-01-020/AC-01):最近历史卡同为标题行右侧显示;历史卡无开放回合。
|
|
6072
6060
|
applyTotalBusy(entry, now);
|
|
6073
6061
|
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
package/package.json
CHANGED
package/scripts/acceptance.mjs
CHANGED
|
@@ -139,8 +139,8 @@ const steps = [
|
|
|
139
139
|
"找一个发指令后 agent 运行了较久才结束的会话,待其落入最近历史区后,确认卡片第五行同时显示本地绝对日期时间(跨年份带年份)与相对活动时间,且绝对时间对应最后回复结束时刻(而非发指令时刻);历史区仍按该时刻从新到旧排序。刷新页面(冷会话)后确认时间先按宿主列表时间显示、数据到达后精化为回复结束时刻,相对时间会按分钟级推进。",
|
|
140
140
|
// R-01-013/AC-12 最近历史卡保留最近回合统计
|
|
141
141
|
"让一个已有完整回合的会话结束并经「移入历史」进入最近历史区,确认助手预览行之后、活动时间之前的倒数第二行仍显示最后回合的「tok/s · 缓存 · 输入 · 输出」可用统计,耗时固定在该行最右侧;缺失字段不造假且全部为空时不留空行。刷新页面后确认可从 native 列表投影与回合边界恢复。",
|
|
142
|
-
// R-01-013/AC-09
|
|
143
|
-
"
|
|
142
|
+
// R-01-013/AC-09 会话卡标题统一常规字重
|
|
143
|
+
"确认活动卡与最近历史卡的会话标题均为常规字重(不加粗)。",
|
|
144
144
|
// R-01-013/AC-10 最近卡整体淡化
|
|
145
145
|
"确认最近历史卡整体比活动卡明显更淡(不透明度更低),悬停时仍有高亮反馈。",
|
|
146
146
|
// R-01-013/AC-11 最近卡轮廓可辨
|
package/scripts/check.mjs
CHANGED
|
@@ -58,8 +58,7 @@ import {
|
|
|
58
58
|
catalogModelEntries,
|
|
59
59
|
lastTurnEndFromEvents,
|
|
60
60
|
lastTurnEndFromTimings,
|
|
61
|
-
|
|
62
|
-
lastTurnDurationFromTimings,
|
|
61
|
+
lastTurnBusyFromEvents,
|
|
63
62
|
lastTurnDuration,
|
|
64
63
|
applyTurnEventToStats,
|
|
65
64
|
reconcileTurnStats,
|
|
@@ -1366,6 +1365,50 @@ assert.equal(normalizeBusyMs(Number.NaN), null, "NaN 归一为 null");
|
|
|
1366
1365
|
assert.equal(next.openTurnStart, null, "R-01-020/AC-04 启动扫描后无残留开放回合");
|
|
1367
1366
|
assert.equal(next.waitedMs, null, "R-01-020/AC-04 启动扫描后等待累计清零");
|
|
1368
1367
|
}
|
|
1368
|
+
// 重编检测(R-01-020/AC-04):水位推进处记录 watermarkTime;增量重放同步更新。
|
|
1369
|
+
{
|
|
1370
|
+
const next = reconcileTurnStats(
|
|
1371
|
+
{ busyMs: 500, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2, watermarkTime: 600 },
|
|
1372
|
+
[
|
|
1373
|
+
{ type: "turn/start", seq: 3, time: 10_000 },
|
|
1374
|
+
{ type: "turn/end", seq: 4, time: 11_000 },
|
|
1375
|
+
],
|
|
1376
|
+
);
|
|
1377
|
+
assert.equal(next.busyMs, 1_500, "R-01-020/AC-04 增量重放补齐缺口回合");
|
|
1378
|
+
assert.equal(next.watermarkTime, 11_000, "R-01-020/AC-04 水位推进处记录最后应用边界事件的时刻");
|
|
1379
|
+
}
|
|
1380
|
+
// 部分重叠的 seq 空间重编(水位 ≤ 日志最大 seq,既有水位超前检测不可达):forceFresh
|
|
1381
|
+
// 丢弃旧空间记账、从空状态全量重放,实时登记的重编检测据此自愈;存量记录(无
|
|
1382
|
+
// watermarkTime)同走该路径。
|
|
1383
|
+
{
|
|
1384
|
+
const forced = reconcileTurnStats(
|
|
1385
|
+
{ busyMs: 100, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2, watermarkTime: 600 },
|
|
1386
|
+
[
|
|
1387
|
+
{ type: "turn/start", seq: 1, time: 10_000 },
|
|
1388
|
+
{ type: "turn/end", seq: 2, time: 11_000 },
|
|
1389
|
+
{ type: "turn/start", seq: 3, time: 11_500 },
|
|
1390
|
+
{ type: "turn/end", seq: 4, time: 12_000 },
|
|
1391
|
+
],
|
|
1392
|
+
{ forceFresh: true },
|
|
1393
|
+
);
|
|
1394
|
+
assert.equal(forced.busyMs, 1_500, "R-01-020/AC-04 强制全量重放按新 seq 空间重算累计");
|
|
1395
|
+
assert.equal(forced.watermarkSeq, 4, "R-01-020/AC-04 强制全量重放后水位落在新空间");
|
|
1396
|
+
assert.equal(forced.watermarkTime, 12_000, "R-01-020/AC-04 强制全量重放后水位时刻为最后边界事件");
|
|
1397
|
+
}
|
|
1398
|
+
{
|
|
1399
|
+
const legacy = reconcileTurnStats(
|
|
1400
|
+
{ busyMs: 100, openTurnStart: null, openWaitStart: null, openWaitKind: null, waitedMs: null, watermarkSeq: 2 },
|
|
1401
|
+
[
|
|
1402
|
+
{ type: "turn/start", seq: 1, time: 10_000 },
|
|
1403
|
+
{ type: "turn/end", seq: 2, time: 11_000 },
|
|
1404
|
+
{ type: "tool/call", seq: 3, time: 11_500, data: { callId: "c1", name: "bash" } },
|
|
1405
|
+
],
|
|
1406
|
+
{ forceFresh: true },
|
|
1407
|
+
);
|
|
1408
|
+
assert.equal(legacy.busyMs, 1_000, "R-01-020/AC-04 存量记录(无 watermarkTime)强制重放后按新空间重算");
|
|
1409
|
+
assert.equal(legacy.watermarkSeq, 3, "R-01-020/AC-04 存量记录重放后水位覆盖全部事件(含无效果事件)");
|
|
1410
|
+
assert.equal(legacy.watermarkTime, 11_500, "R-01-020/AC-04 存量记录重放后补上水位时刻");
|
|
1411
|
+
}
|
|
1369
1412
|
// R-01-013/AC-05:历史卡同时显示本地绝对日期时间与相对年龄,异常输入不制造虚假时间。
|
|
1370
1413
|
const relativeMinute = 60_000;
|
|
1371
1414
|
const relativeHour = 60 * relativeMinute;
|
|
@@ -2824,67 +2867,64 @@ assert.equal(
|
|
|
2824
2867
|
2500,
|
|
2825
2868
|
"turnTimings 取最大 endTime,忽略未结束回合",
|
|
2826
2869
|
);
|
|
2827
|
-
// ---- R-01-009/AC-12
|
|
2870
|
+
// ---- R-01-009/AC-12 等待卡保留最近完整回合耗时(busy 口径,R-01-020 语义) ----
|
|
2828
2871
|
assert.equal(
|
|
2829
|
-
|
|
2830
|
-
[1, { startTime: 100, endTime: 900 }],
|
|
2831
|
-
[2, { startTime: 1000 }],
|
|
2832
|
-
[3, { startTime: 2000, endTime: 2500 }],
|
|
2833
|
-
])),
|
|
2834
|
-
500,
|
|
2835
|
-
"turnTimings 取最近已结束回合的起止差值,不取更早回合或开放回合",
|
|
2836
|
-
);
|
|
2837
|
-
assert.equal(
|
|
2838
|
-
lastTurnDurationFromTimings(new Map([[1, { startTime: 900, endTime: 100 }]])),
|
|
2839
|
-
null,
|
|
2840
|
-
"起点晚于终点时不生成虚假回合耗时",
|
|
2841
|
-
);
|
|
2842
|
-
assert.equal(lastTurnDurationFromTimings(new Map([[1, { startTime: 100 }]])), null, "无 endTime 不生成等待耗时");
|
|
2843
|
-
assert.equal(
|
|
2844
|
-
lastTurnDurationFromEvents([
|
|
2872
|
+
lastTurnBusyFromEvents([
|
|
2845
2873
|
{ event: { type: "turn/start", time: 100, data: { turn: 1 } } },
|
|
2846
2874
|
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
2847
2875
|
{ event: { type: "turn/start", time: 1000, data: { turn: 2 } } },
|
|
2848
2876
|
{ event: { type: "turn/end", time: 2500, data: { turn: 2 } } },
|
|
2849
2877
|
]),
|
|
2850
2878
|
1500,
|
|
2851
|
-
"history
|
|
2879
|
+
"history 取最近已结束回合的起止差值",
|
|
2852
2880
|
);
|
|
2853
2881
|
assert.equal(
|
|
2854
|
-
|
|
2855
|
-
{ event: { type: "turn/start", time:
|
|
2856
|
-
{ event: { type: "
|
|
2882
|
+
lastTurnBusyFromEvents([
|
|
2883
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2884
|
+
{ event: { type: "tool/call", time: 2000, data: { callId: "c1", name: "ask_user_question" } } },
|
|
2885
|
+
{ event: { type: "tool/result", time: 7000, data: { message: { source: { kind: "tool", callId: "c1" } } } } },
|
|
2886
|
+
{ event: { type: "turn/end", time: 8000, data: { turn: 1 } } },
|
|
2857
2887
|
]),
|
|
2858
|
-
|
|
2859
|
-
"
|
|
2888
|
+
2000,
|
|
2889
|
+
"最近回合耗时扣除回合内提问等待(busy 口径,与总耗时一致)",
|
|
2860
2890
|
);
|
|
2861
2891
|
assert.equal(
|
|
2862
|
-
|
|
2863
|
-
{ event: { type: "turn/start", time:
|
|
2864
|
-
{ event: { type: "
|
|
2892
|
+
lastTurnBusyFromEvents([
|
|
2893
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2894
|
+
{ event: { type: "approval/asked", time: 2000, data: { id: "a1" } } },
|
|
2895
|
+
{ event: { type: "approval/decided", time: 6000, data: { id: "a1" } } },
|
|
2896
|
+
{ event: { type: "turn/end", time: 7000, data: { turn: 1 } } },
|
|
2897
|
+
]),
|
|
2898
|
+
2000,
|
|
2899
|
+
"最近回合耗时扣除回合内审批等待",
|
|
2900
|
+
);
|
|
2901
|
+
assert.equal(
|
|
2902
|
+
lastTurnBusyFromEvents([
|
|
2903
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2904
|
+
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
2865
2905
|
]),
|
|
2866
2906
|
null,
|
|
2867
|
-
"
|
|
2907
|
+
"起止逆序时不生成虚假回合耗时",
|
|
2868
2908
|
);
|
|
2869
2909
|
assert.equal(
|
|
2870
|
-
|
|
2910
|
+
lastTurnBusyFromEvents([
|
|
2911
|
+
{ event: { type: "turn/start", time: null, data: { turn: 1 } } },
|
|
2912
|
+
{ event: { type: "turn/end", time: 100, data: { turn: 1 } } },
|
|
2913
|
+
]),
|
|
2871
2914
|
null,
|
|
2872
|
-
"
|
|
2915
|
+
"起点缺失时不把 null 当作时间零点",
|
|
2873
2916
|
);
|
|
2874
2917
|
assert.equal(
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
800,
|
|
2883
|
-
"turnTimings 最新孤立 end 不得与更早完整回合错配",
|
|
2918
|
+
lastTurnBusyFromEvents([
|
|
2919
|
+
{ event: { type: "turn/start", time: 1000, data: { turn: 1 } } },
|
|
2920
|
+
{ event: { type: "tool/call", time: 2000, data: { callId: "c1", name: "ask_user_question" } } },
|
|
2921
|
+
{ event: { type: "turn/end", time: 5000, data: { turn: 1 } } },
|
|
2922
|
+
]),
|
|
2923
|
+
1000,
|
|
2924
|
+
"turn/end 强制结算未配对等待:等待尾部不计入最近回合耗时",
|
|
2884
2925
|
);
|
|
2885
2926
|
assert.equal(
|
|
2886
2927
|
lastTurnDuration({
|
|
2887
|
-
turnTimings: new Map(),
|
|
2888
2928
|
history: [
|
|
2889
2929
|
{ event: { type: "turn/start", time: 100, data: { turn: 1 } } },
|
|
2890
2930
|
{ event: { type: "turn/end", time: 900, data: { turn: 1 } } },
|
|
@@ -2892,24 +2932,10 @@ assert.equal(
|
|
|
2892
2932
|
],
|
|
2893
2933
|
}),
|
|
2894
2934
|
800,
|
|
2895
|
-
"
|
|
2896
|
-
);
|
|
2897
|
-
assert.equal(
|
|
2898
|
-
lastTurnDuration({
|
|
2899
|
-
turnTimings: new Map([[1, { startTime: 100, endTime: 900 }]]),
|
|
2900
|
-
history: [
|
|
2901
|
-
{ event: { type: "turn/start", time: 1000, data: { turn: 2 } } },
|
|
2902
|
-
{ event: { type: "turn/end", time: 1300, data: { turn: 2 } } },
|
|
2903
|
-
],
|
|
2904
|
-
}),
|
|
2905
|
-
300,
|
|
2906
|
-
"多来源时按最近结束时刻选择同一最新回合耗时",
|
|
2907
|
-
);
|
|
2908
|
-
assert.equal(
|
|
2909
|
-
lastTurnDuration({ turnTimings: new Map([[1, { startTime: 100, endTime: 900 }]]), history: [] }),
|
|
2910
|
-
800,
|
|
2911
|
-
"无 history 时从 turnTimings 提取回合耗时",
|
|
2935
|
+
"最新孤立 end 不得与更早完整回合错配",
|
|
2912
2936
|
);
|
|
2937
|
+
assert.equal(lastTurnDuration({ history: [] }), null, "空 history 无回合耗时");
|
|
2938
|
+
assert.equal(lastTurnDuration({ history: null }), null, "history 缺失回退 null");
|
|
2913
2939
|
const refineSnap = {
|
|
2914
2940
|
ids: ["sTurn", "sPrompt", "sNone"],
|
|
2915
2941
|
byId: {
|
|
@@ -3836,9 +3862,9 @@ assert.ok(bundle.includes("dataset.traceKey"), "同一流程节点复用 DOM,
|
|
|
3836
3862
|
assert.ok(!bundle.includes(".dap-trace-item[data-status=\"running\"]::before {\n animation: none !important;"), "降低动效设置不关闭运行点脉冲");
|
|
3837
3863
|
|
|
3838
3864
|
// R-01-013/AC-09
|
|
3839
|
-
//
|
|
3840
|
-
assert.ok(bundle.includes(
|
|
3841
|
-
assert.ok(bundle.includes("
|
|
3865
|
+
// 全部会话卡标题(活动卡、子代理卡、最近卡)统一常规字重(不加粗),无按卡类覆盖。
|
|
3866
|
+
assert.ok(bundle.includes("white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;"), "会话卡标题使用常规字重(不加粗)");
|
|
3867
|
+
assert.ok(!bundle.includes('[data-kind="recent"] .dap-title {'), "最近卡标题不再有独立字重覆盖(与活动卡共用常规字重)");
|
|
3842
3868
|
|
|
3843
3869
|
// R-01-013/AC-10
|
|
3844
3870
|
// 最近历史卡整体不透明度低于活动卡,弱化历史区视觉强调。
|
package/src/client.mjs
CHANGED
|
@@ -437,9 +437,11 @@ const CSS = `
|
|
|
437
437
|
box-shadow: none;
|
|
438
438
|
animation: none;
|
|
439
439
|
}
|
|
440
|
+
/* 会话卡标题(活动卡、子代理卡与最近卡共用 .dap-title)统一常规字重,不加粗
|
|
441
|
+
(R-01-013/AC-09 及东家 2026-09-11 视觉反馈);无按卡类的字重覆盖。 */
|
|
440
442
|
[data-dsh-activity-pane] .dap-title {
|
|
441
443
|
flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
|
|
442
|
-
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight:
|
|
444
|
+
white-space: nowrap; font-size: 12px; line-height: 16px; font-weight: 400;
|
|
443
445
|
}
|
|
444
446
|
/* 标题行最右侧的累计运行时长(R-01-020/AC-01):固定占位不参与标题挤压,
|
|
445
447
|
标题过长时以自身省略号让位;色调弱于标题,不与状态点抢视觉。 */
|
|
@@ -448,10 +450,6 @@ const CSS = `
|
|
|
448
450
|
color: #8a94a3; white-space: nowrap;
|
|
449
451
|
}
|
|
450
452
|
[data-dsh-activity-pane] .dap-total-time[hidden] { display: none; }
|
|
451
|
-
/* 最近历史卡标题降为常规字重:历史区不抢占视觉强调(R-01-013/AC-09)。 */
|
|
452
|
-
[data-dsh-activity-pane] .dap-card[data-kind="recent"] .dap-title {
|
|
453
|
-
font-weight: 400;
|
|
454
|
-
}
|
|
455
453
|
/* 等待卡末行首行「类型胶囊」(R-01-002/AC-01、AC-02、AC-09、AC-13,C-043):圆底类型
|
|
456
454
|
图标 + 类型文字,色相随等待类别(--dap-wait-color)——阻塞金/完成绿/错误红;
|
|
457
455
|
胶囊为行内元素不自占满宽,随文字内容收缩。 */
|
|
@@ -2457,13 +2455,14 @@ function apply(ctx) {
|
|
|
2457
2455
|
}
|
|
2458
2456
|
}
|
|
2459
2457
|
|
|
2460
|
-
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15
|
|
2461
|
-
|
|
2458
|
+
/** 最近回合耗时 memo(等待卡与暂停子代理卡共用,R-01-009/AC-12、AC-15):history 引用
|
|
2459
|
+
* 不变即命中缓存。busy 口径(起止差值扣除回合内阻塞等待),与标题行总耗时同口径,
|
|
2460
|
+
* 保证恒不大于累计值(R-01-020)。 */
|
|
2461
|
+
function memoTurnDuration(detail) {
|
|
2462
2462
|
const history = detail.history ?? null;
|
|
2463
|
-
if (detail.
|
|
2464
|
-
detail.memoTurnDurationSnapshotOf = detailSnapshot;
|
|
2463
|
+
if (detail.memoTurnDurationHistoryOf !== history) {
|
|
2465
2464
|
detail.memoTurnDurationHistoryOf = history;
|
|
2466
|
-
detail.memoTurnDuration = lastTurnDuration({
|
|
2465
|
+
detail.memoTurnDuration = lastTurnDuration({ history });
|
|
2467
2466
|
}
|
|
2468
2467
|
return detail.memoTurnDuration ?? null;
|
|
2469
2468
|
}
|
|
@@ -3368,7 +3367,7 @@ function apply(ctx) {
|
|
|
3368
3367
|
}
|
|
3369
3368
|
}
|
|
3370
3369
|
if (entry.kind === "awaiting" && detail) {
|
|
3371
|
-
entry.elapsedMs = memoTurnDuration(detail
|
|
3370
|
+
entry.elapsedMs = memoTurnDuration(detail);
|
|
3372
3371
|
}
|
|
3373
3372
|
if (detail?.model) {
|
|
3374
3373
|
entry.model = detail.model.model;
|
|
@@ -3440,7 +3439,7 @@ function apply(ctx) {
|
|
|
3440
3439
|
// 非运行(暂停等待):冻结最后已知统计与最近回合耗时,progress 置空隐藏
|
|
3441
3440
|
// 进度条(R-01-009/AC-15);刷新/无留存时回退当前列表投影。
|
|
3442
3441
|
Object.assign(entry, mergeRuntimeStats(detail?.lastRuntimeStats, projectionStats));
|
|
3443
|
-
entry.elapsedMs = detail ? memoTurnDuration(detail
|
|
3442
|
+
entry.elapsedMs = detail ? memoTurnDuration(detail) : null;
|
|
3444
3443
|
entry.progress = null;
|
|
3445
3444
|
}
|
|
3446
3445
|
}
|
|
@@ -3463,13 +3462,13 @@ function apply(ctx) {
|
|
|
3463
3462
|
recentTotal = recentCandidates.length;
|
|
3464
3463
|
const recent = recentCandidates.slice(0, recentVisibleCount);
|
|
3465
3464
|
recentHasMore = recent.length < recentTotal;
|
|
3466
|
-
//
|
|
3467
|
-
//
|
|
3465
|
+
// 最近卡统计复用运行卡的列表投影口径;耗时从已读 history 取最近完整回合的运行
|
|
3466
|
+
// 过程时长(busy 口径,与标题行总耗时一致,R-01-013/AC-12),缺边界时仅为当前
|
|
3467
|
+
// 可见历史卡安排一次既有 history 补读。
|
|
3468
3468
|
const recentDurationFallbackIds = new Set();
|
|
3469
3469
|
for (const entry of recent) {
|
|
3470
3470
|
const detail = sessionDetailsById.get(entry.id);
|
|
3471
|
-
const
|
|
3472
|
-
const elapsedMs = lastTurnDuration({ turnTimings: detailSnapshot?.turnTimings, history: detail?.history });
|
|
3471
|
+
const elapsedMs = lastTurnDuration({ history: detail?.history ?? null });
|
|
3473
3472
|
// 累计运行时长(R-01-020/AC-01):最近历史卡同为标题行右侧显示;历史卡无开放回合。
|
|
3474
3473
|
applyTotalBusy(entry, now);
|
|
3475
3474
|
const stats = statsFromProjection(snapshot?.byId?.[entry.id]?.projectionValues, elapsedMs);
|
package/src/core.mjs
CHANGED
|
@@ -1979,58 +1979,30 @@ function durationTime(value) {
|
|
|
1979
1979
|
return Number.isFinite(time) ? time : null;
|
|
1980
1980
|
}
|
|
1981
1981
|
|
|
1982
|
-
/**
|
|
1983
|
-
|
|
1984
|
-
|
|
1982
|
+
/**
|
|
1983
|
+
* 从 history 重放回合/等待边界事件,提取最近一个已结束回合的运行过程耗时(busy 口径:
|
|
1984
|
+
* 起止差值扣除回合内阻塞等待,与 R-01-020 累计口径一致,恒不大于该回合墙钟时长)。
|
|
1985
|
+
* 回合起止不完整、时间逆序或回合运行段不为正时忽略该回合;无可得回合返回 null。
|
|
1986
|
+
*/
|
|
1987
|
+
export function lastTurnBusyFromEvents(events) {
|
|
1988
|
+
let state = emptyTurnStats();
|
|
1985
1989
|
let latest = null;
|
|
1986
1990
|
for (const entry of Array.isArray(events) ? events : []) {
|
|
1987
|
-
const
|
|
1988
|
-
const
|
|
1989
|
-
|
|
1990
|
-
if (
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
if (event.type !== "turn/end") continue;
|
|
1996
|
-
const start = starts.get(turn);
|
|
1997
|
-
if (start === undefined || time < start) continue;
|
|
1998
|
-
if (latest === null || time > latest.end) latest = { end: time, duration: time - start };
|
|
1991
|
+
const time = durationTime(eventOf(entry)?.time);
|
|
1992
|
+
const prev = state;
|
|
1993
|
+
state = applyTurnEventToStats(state, entry);
|
|
1994
|
+
if (prev.openTurnStart === null || state.openTurnStart !== null) continue;
|
|
1995
|
+
// 能走到回合闭合的必为时刻有效的 turn/end(applyTurnEventToStats 对无效时刻无效果)。
|
|
1996
|
+
const activeMs = (state.busyMs ?? 0) - (prev.busyMs ?? 0);
|
|
1997
|
+
if (activeMs <= 0) continue;
|
|
1998
|
+
latest = { end: time, duration: activeMs };
|
|
1999
1999
|
}
|
|
2000
|
-
return latest;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
/** 从 turnTimings 提取最近完整回合的结束时刻与固定耗时;两者始终来自同一回合。 */
|
|
2004
|
-
function lastTurnDurationCandidateFromTimings(turnTimings) {
|
|
2005
|
-
if (!(turnTimings instanceof Map)) return null;
|
|
2006
|
-
let latest = null;
|
|
2007
|
-
for (const timing of turnTimings.values()) {
|
|
2008
|
-
const start = durationTime(timing?.startTime);
|
|
2009
|
-
const end = durationTime(timing?.endTime);
|
|
2010
|
-
if (start === null || end === null || end < start) continue;
|
|
2011
|
-
if (latest === null || end > latest.end) latest = { end, duration: end - start };
|
|
2012
|
-
}
|
|
2013
|
-
return latest;
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
/** 从 history 提取最近完整回合的固定耗时;回合起止不完整或逆序时忽略该回合。 */
|
|
2017
|
-
export function lastTurnDurationFromEvents(events) {
|
|
2018
|
-
return lastTurnDurationCandidateFromEvents(events)?.duration ?? null;
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
/** 从 turnTimings 提取最近完整回合的固定耗时;全部回合未结束或无效时返回 null。 */
|
|
2022
|
-
export function lastTurnDurationFromTimings(turnTimings) {
|
|
2023
|
-
return lastTurnDurationCandidateFromTimings(turnTimings)?.duration ?? null;
|
|
2000
|
+
return latest?.duration ?? null;
|
|
2024
2001
|
}
|
|
2025
2002
|
|
|
2026
|
-
/**
|
|
2027
|
-
export function lastTurnDuration({
|
|
2028
|
-
|
|
2029
|
-
let latest = null;
|
|
2030
|
-
for (const candidate of candidates) {
|
|
2031
|
-
if (latest === null || candidate.end > latest.end) latest = candidate;
|
|
2032
|
-
}
|
|
2033
|
-
return latest?.duration ?? null;
|
|
2003
|
+
/** 最近完整回合的固定耗时(busy 口径):从 history 事件重放派生。 */
|
|
2004
|
+
export function lastTurnDuration({ history = [] } = {}) {
|
|
2005
|
+
return lastTurnBusyFromEvents(history);
|
|
2034
2006
|
}
|
|
2035
2007
|
|
|
2036
2008
|
/** ask_user_question 工具名:提问/计划审查等待的开启边界(tool/call)与配对结算(tool/result)。 */
|
|
@@ -2046,7 +2018,7 @@ export function isBusyBoundaryEvent(type) {
|
|
|
2046
2018
|
|
|
2047
2019
|
/** 空记账状态(无任何有效计时)。 */
|
|
2048
2020
|
export function emptyTurnStats() {
|
|
2049
|
-
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null };
|
|
2021
|
+
return { busyMs: null, openTurnStart: null, openWaitStart: null, openWaitKind: null, openWaitId: null, waitedMs: null, watermarkTime: null };
|
|
2050
2022
|
}
|
|
2051
2023
|
|
|
2052
2024
|
/** 从任意来源拷贝记账状态:缺失字段归一为 null(旧记录/部分记录兼容)。 */
|
|
@@ -2058,10 +2030,14 @@ export function turnStatsFrom(state) {
|
|
|
2058
2030
|
openWaitKind: state?.openWaitKind ?? null,
|
|
2059
2031
|
openWaitId: state?.openWaitId ?? null,
|
|
2060
2032
|
waitedMs: state?.waitedMs ?? null,
|
|
2033
|
+
watermarkTime: state?.watermarkTime ?? null,
|
|
2061
2034
|
};
|
|
2062
2035
|
}
|
|
2063
2036
|
|
|
2064
|
-
/**
|
|
2037
|
+
/**
|
|
2038
|
+
* 记账状态逐字段相等(不含 watermarkSeq——它不是状态字段,由宿主侧随写入一并落盘;
|
|
2039
|
+
* watermarkTime 为水位处已覆盖事件的时刻,参与相等判定以让补写该字段的收敛写入生效)。
|
|
2040
|
+
*/
|
|
2065
2041
|
export function turnStatsEqual(a, b) {
|
|
2066
2042
|
return (
|
|
2067
2043
|
a === b ||
|
|
@@ -2070,7 +2046,8 @@ export function turnStatsEqual(a, b) {
|
|
|
2070
2046
|
(a?.openWaitStart ?? null) === (b?.openWaitStart ?? null) &&
|
|
2071
2047
|
(a?.openWaitKind ?? null) === (b?.openWaitKind ?? null) &&
|
|
2072
2048
|
(a?.openWaitId ?? null) === (b?.openWaitId ?? null) &&
|
|
2073
|
-
(a?.waitedMs ?? null) === (b?.waitedMs ?? null)
|
|
2049
|
+
(a?.waitedMs ?? null) === (b?.waitedMs ?? null) &&
|
|
2050
|
+
(a?.watermarkTime ?? null) === (b?.watermarkTime ?? null))
|
|
2074
2051
|
);
|
|
2075
2052
|
}
|
|
2076
2053
|
|
|
@@ -2192,15 +2169,18 @@ export function totalBusyDisplayMs({ busyMs = null, openTurnStart = null, waited
|
|
|
2192
2169
|
|
|
2193
2170
|
/**
|
|
2194
2171
|
* 回合统计记账的统一收敛(R-01-020/AC-04、AC-05):对全会话事件列表重放出下一份
|
|
2195
|
-
*
|
|
2196
|
-
*
|
|
2197
|
-
*
|
|
2198
|
-
*
|
|
2172
|
+
* 记账,宿主侧懒回填与启动扫描共用。路径选择——无记录、强制重放、持久化水位非法、
|
|
2173
|
+
* 或水位超前于日志最大 seq(事件 seq 空间被重编,如 dsh 0.1.5 V3 迁移)时从空状态
|
|
2174
|
+
* 全量重放(超前水位会把后续全部实时事件封死在守卫之外,openTurnStart 永不清空、
|
|
2175
|
+
* 总耗时无限增长);否则从持久化记账出发仅增量应用 `seq > watermarkSeq` 的事件。
|
|
2176
|
+
* 水位推进处同步记录 `watermarkTime`(水位处已覆盖事件的时刻,与实时登记按效果事件
|
|
2177
|
+
* 写入的口径互为保守——检测只要求该值不超过真实边界事件时刻)——宿主实时登记据此
|
|
2178
|
+
* 识别 seq 空间重编(低 seq 事件携带比水位更新的时刻,增量口径已失效)。
|
|
2199
2179
|
* `closeOpenTurn`(宿主启动扫描):重放后仍存在的开放回合按日志最后事件时刻强制
|
|
2200
2180
|
* 结算关闭——宿主重启后不存在仍在运行的回合,残留起点只会令总耗时无限增长;尾部
|
|
2201
2181
|
* 未配对等待一并按同刻结算(不落到运行时长里)。
|
|
2202
2182
|
*/
|
|
2203
|
-
export function reconcileTurnStats(current, records, { closeOpenTurn = false } = {}) {
|
|
2183
|
+
export function reconcileTurnStats(current, records, { closeOpenTurn = false, forceFresh = false } = {}) {
|
|
2204
2184
|
const list = Array.isArray(records) ? records : [];
|
|
2205
2185
|
let maxSeq = null;
|
|
2206
2186
|
let lastTime = null;
|
|
@@ -2210,23 +2190,32 @@ export function reconcileTurnStats(current, records, { closeOpenTurn = false } =
|
|
|
2210
2190
|
const time = durationTime(eventOf(record)?.time);
|
|
2211
2191
|
if (time !== null && (lastTime === null || time > lastTime)) lastTime = time;
|
|
2212
2192
|
}
|
|
2213
|
-
const fresh = !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2193
|
+
const fresh = forceFresh || !isRecord(current) || !Number.isFinite(current.watermarkSeq) || (maxSeq !== null && Number(current.watermarkSeq) > maxSeq);
|
|
2214
2194
|
let state = emptyTurnStats();
|
|
2215
2195
|
let watermarkSeq = null;
|
|
2196
|
+
let watermarkTime = null;
|
|
2197
|
+
// 水位推进:seq 更大即前推水位;事件时刻有效才更新 watermarkTime(无效不前推)。
|
|
2198
|
+
const advance = (record) => {
|
|
2199
|
+
const seq = Number(record?.seq);
|
|
2200
|
+
if (!Number.isFinite(seq) || (watermarkSeq !== null && seq <= watermarkSeq)) return;
|
|
2201
|
+
watermarkSeq = seq;
|
|
2202
|
+
const time = durationTime(eventOf(record)?.time);
|
|
2203
|
+
if (time !== null) watermarkTime = time;
|
|
2204
|
+
};
|
|
2216
2205
|
if (fresh) {
|
|
2217
2206
|
for (const record of list) {
|
|
2218
2207
|
state = applyTurnEventToStats(state, record);
|
|
2219
|
-
|
|
2220
|
-
if (Number.isFinite(seq) && (watermarkSeq === null || seq > watermarkSeq)) watermarkSeq = seq;
|
|
2208
|
+
advance(record);
|
|
2221
2209
|
}
|
|
2222
2210
|
} else {
|
|
2223
2211
|
state = turnStatsFrom(current);
|
|
2212
|
+
watermarkTime = state.watermarkTime;
|
|
2224
2213
|
watermarkSeq = Number(current.watermarkSeq);
|
|
2225
2214
|
for (const record of list) {
|
|
2226
2215
|
const seq = Number(record?.seq);
|
|
2227
2216
|
if (!Number.isFinite(seq) || seq <= watermarkSeq) continue;
|
|
2228
2217
|
state = applyTurnEventToStats(state, record);
|
|
2229
|
-
|
|
2218
|
+
advance(record);
|
|
2230
2219
|
}
|
|
2231
2220
|
}
|
|
2232
2221
|
if (closeOpenTurn && state.openTurnStart !== null) {
|
|
@@ -2239,7 +2228,7 @@ export function reconcileTurnStats(current, records, { closeOpenTurn = false } =
|
|
|
2239
2228
|
state.openWaitId = null;
|
|
2240
2229
|
state.waitedMs = null;
|
|
2241
2230
|
}
|
|
2242
|
-
return { ...state, watermarkSeq };
|
|
2231
|
+
return { ...state, watermarkSeq, watermarkTime };
|
|
2243
2232
|
}
|
|
2244
2233
|
|
|
2245
2234
|
/**
|
package/src/host.mjs
CHANGED
|
@@ -59,7 +59,9 @@ const domainSpec = defineDomain({
|
|
|
59
59
|
* openTurnStart 当前开放回合起点(无开放回合为 null)、openWaitStart/openWaitKind 当前
|
|
60
60
|
* 未配对等待区间(回合内阻塞等待:approval/question)、waitedMs 本回合已配对等待时长
|
|
61
61
|
* 累计、watermarkSeq 实时登记已覆盖的最大事件 seq(回填与实时登记以水位衔接,seq ≤
|
|
62
|
-
* watermark
|
|
62
|
+
* watermark 的事件不重复应用)、watermarkTime 水位处事件的时刻(实时登记据此识别 seq
|
|
63
|
+
* 空间重编:低 seq 事件携带比水位更新的时刻,增量口径失效,须全量重放)。全部字段
|
|
64
|
+
* 可选/可空。 */
|
|
63
65
|
const turnStatRecord = z.object({
|
|
64
66
|
busyMs: z.number().nullable().optional(),
|
|
65
67
|
openTurnStart: z.number().nullable().optional(),
|
|
@@ -68,6 +70,7 @@ const turnStatRecord = z.object({
|
|
|
68
70
|
openWaitId: z.string().nullable().optional(),
|
|
69
71
|
waitedMs: z.number().nullable().optional(),
|
|
70
72
|
watermarkSeq: z.number().nullable().optional(),
|
|
73
|
+
watermarkTime: z.number().nullable().optional(),
|
|
71
74
|
})
|
|
72
75
|
|
|
73
76
|
/** 独立 domain:dsh-storage-domain 无迁移机制(version 不同在 open 时拒绝),新增表落新 domain,
|
|
@@ -122,8 +125,13 @@ export function apply(ctx) {
|
|
|
122
125
|
resolveDomain()
|
|
123
126
|
// 启动扫描(R-01-020/AC-04):宿主重启后不存在仍在运行的回合——残留开放回合的
|
|
124
127
|
// 记账按日志最后事件时刻强制结算关闭,否则总耗时按 now − openTurnStart 无限增长。
|
|
128
|
+
// 无 watermarkTime 的存量记录(本字段引入前写入)强制全量重放复核一次——无论重编
|
|
129
|
+
// 后日志 seq 是否已超越旧水位(部分重叠重编)都能收敛,重写后带上该字段、不再
|
|
130
|
+
// 重复扫描;此后重编由实时登记的重编检测承接。
|
|
125
131
|
for (const [id, record] of turnStats.entries()) {
|
|
126
|
-
|
|
132
|
+
const closeOpenTurn = record?.openTurnStart != null
|
|
133
|
+
const legacy = record?.watermarkTime == null
|
|
134
|
+
if (closeOpenTurn || legacy) ensureTurnStatsFresh(id, { closeOpenTurn, forceFresh: legacy })
|
|
127
135
|
}
|
|
128
136
|
})
|
|
129
137
|
|
|
@@ -159,10 +167,17 @@ export function apply(ctx) {
|
|
|
159
167
|
* reconcileTurnStats 收敛——无记录、水位非法或水位超前于日志最大 seq(事件 seq 空间
|
|
160
168
|
* 被重编,如 dsh 0.1.5 V3 迁移)的会话自空状态全量重放;已有记录会话仅增量应用
|
|
161
169
|
* seq > watermarkSeq 的事件(宿主离线期间经其它入口发生的回合),以水位衔接保证
|
|
162
|
-
* 不重复、不遗漏。
|
|
163
|
-
*
|
|
164
|
-
|
|
165
|
-
|
|
170
|
+
* 不重复、不遗漏。forceFresh:调用侧已证实水位口径失效(实时登记识别出 seq 空间
|
|
171
|
+
* 重编,或存量记录尚未携带 watermarkTime)时丢弃已有记账、强制从空状态全量重放;
|
|
172
|
+
* 在途回填不得吞掉该选项——命中时在其完成后补一次强制重放。closeOpenTurn(启动
|
|
173
|
+
* 扫描):重放后仍开放的回合按日志最后事件时刻强制结算。读取失败保留已有记账,
|
|
174
|
+
* 随下次请求重试;会话不存在/已删除时静默跳过。 */
|
|
175
|
+
function ensureTurnStatsFresh(id, { closeOpenTurn = false, forceFresh = false } = {}) {
|
|
176
|
+
if (backfills.has(id)) {
|
|
177
|
+
const inflight = backfills.get(id)
|
|
178
|
+
if (!forceFresh) return inflight
|
|
179
|
+
return inflight.then(() => ensureTurnStatsFresh(id, { closeOpenTurn, forceFresh: true }))
|
|
180
|
+
}
|
|
166
181
|
const promise = new Promise((resolve) => {
|
|
167
182
|
ctx.inject(['sessionQuery'], (sessionQuery) => {
|
|
168
183
|
resolve(sessionQuery)
|
|
@@ -170,13 +185,16 @@ export function apply(ctx) {
|
|
|
170
185
|
}).then(async (sessionQuery) => {
|
|
171
186
|
try {
|
|
172
187
|
await domainReady
|
|
173
|
-
const current = turnStats.get(id) ?? null
|
|
188
|
+
const current = forceFresh ? null : turnStats.get(id) ?? null
|
|
174
189
|
const records = await sessionQuery.listEvents(id)
|
|
175
190
|
const next = reconcileTurnStats(current, Array.isArray(records) ? records : [], { closeOpenTurn })
|
|
176
191
|
const changed =
|
|
177
|
-
|
|
192
|
+
forceFresh ||
|
|
193
|
+
current === null ||
|
|
194
|
+
next.watermarkSeq !== (current.watermarkSeq ?? null) ||
|
|
195
|
+
!turnStatsEqual(next, current)
|
|
178
196
|
if (changed) {
|
|
179
|
-
await turnStats.put(id, { ...turnStatsFrom(next), watermarkSeq: next.watermarkSeq })
|
|
197
|
+
await turnStats.put(id, { ...turnStatsFrom(next), watermarkSeq: next.watermarkSeq, watermarkTime: next.watermarkTime })
|
|
180
198
|
broadcastBusy()
|
|
181
199
|
}
|
|
182
200
|
} catch (error) {
|
|
@@ -265,15 +283,31 @@ export function apply(ctx) {
|
|
|
265
283
|
const inflight = backfills.get(id)
|
|
266
284
|
if (inflight) await inflight
|
|
267
285
|
}
|
|
268
|
-
|
|
269
|
-
if (Number.isFinite(current?.watermarkSeq) && seq <= current.watermarkSeq)
|
|
286
|
+
let current = turnStats.get(id)
|
|
287
|
+
if (Number.isFinite(current?.watermarkSeq) && seq <= current.watermarkSeq) {
|
|
288
|
+
// 重编检测(R-01-020/AC-04):稳定 seq 空间内低水位事件的时刻不可能比水位处
|
|
289
|
+
// 事件更新;时刻反而更新说明事件 seq 空间被重编(如 dsh 0.1.5 V3 迁移),
|
|
290
|
+
// 持久化水位已失效,增量口径不再封死后续事件。无 watermarkTime 的存量记录
|
|
291
|
+
//(本字段引入前写入,其水位未经重编检测校验)同样不可信——两者统一丢弃旧
|
|
292
|
+
// 记账强制全量重放自愈;重放若仍覆盖不到本事件(日志写入滞后),落回下方
|
|
293
|
+
// 正常增量路径应用。
|
|
294
|
+
const time = Number(event.time)
|
|
295
|
+
if (!Number.isFinite(time)) return
|
|
296
|
+
if (Number.isFinite(current.watermarkTime) && time <= current.watermarkTime) return
|
|
297
|
+
await ensureTurnStatsFresh(id, { forceFresh: true })
|
|
298
|
+
current = turnStats.get(id)
|
|
299
|
+
if (Number.isFinite(current?.watermarkSeq) && seq <= current.watermarkSeq) return
|
|
300
|
+
}
|
|
270
301
|
const next = applyTurnEventToStats(turnStatsFrom(current), event)
|
|
271
302
|
if (turnStatsEqual(next, current)) {
|
|
272
303
|
// 无效果事件(非提问工具调用、未配对的结算事件等):不落盘不广播,
|
|
273
304
|
// 水位不前移——重放路径对同类事件同样不改变记账状态,口径一致。
|
|
274
305
|
return
|
|
275
306
|
}
|
|
276
|
-
|
|
307
|
+
const time = Number(event.time)
|
|
308
|
+
// 与 reconcile 同口径:时刻无效时保留既有 watermarkTime 不前推(能走到此处的
|
|
309
|
+
// 必为有效果事件、时刻有效,null 分支仅作防御)。
|
|
310
|
+
await turnStats.put(id, { ...turnStatsFrom(next), watermarkSeq: seq, watermarkTime: Number.isFinite(time) ? time : turnStatsFrom(current).watermarkTime ?? null })
|
|
277
311
|
broadcastBusy()
|
|
278
312
|
} catch (error) {
|
|
279
313
|
ctx.logger?.warn?.(`dsh-activity-pane: 回合统计登记失败(${id}): ${String(error)}`)
|