@agentdeck/shared 0.2.0 → 1.0.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/dist/adapter.d.ts +3 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +24 -0
- package/dist/adapter.js.map +1 -1
- package/dist/command-builders.d.ts +8 -2
- package/dist/command-builders.d.ts.map +1 -1
- package/dist/command-builders.js +20 -2
- package/dist/command-builders.js.map +1 -1
- package/dist/connection-status.d.ts +42 -0
- package/dist/connection-status.d.ts.map +1 -0
- package/dist/connection-status.js +52 -0
- package/dist/connection-status.js.map +1 -0
- package/dist/creature-layout.d.ts +18 -0
- package/dist/creature-layout.d.ts.map +1 -0
- package/dist/creature-layout.js +131 -0
- package/dist/creature-layout.js.map +1 -0
- package/dist/d200h-layout.d.ts +146 -0
- package/dist/d200h-layout.d.ts.map +1 -0
- package/dist/d200h-layout.js +808 -0
- package/dist/d200h-layout.js.map +1 -0
- package/dist/design-tokens.d.ts +261 -0
- package/dist/design-tokens.d.ts.map +1 -0
- package/dist/design-tokens.js +157 -0
- package/dist/design-tokens.js.map +1 -0
- package/dist/eval-schema.d.ts +32 -2
- package/dist/eval-schema.d.ts.map +1 -1
- package/dist/eval-schema.js.map +1 -1
- package/dist/format-utils.d.ts +38 -0
- package/dist/format-utils.d.ts.map +1 -1
- package/dist/format-utils.js +70 -0
- package/dist/format-utils.js.map +1 -1
- package/dist/gateway-protocol.d.ts +1 -1
- package/dist/gateway-protocol.js +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/llm-settings.d.ts +3 -3
- package/dist/llm-settings.d.ts.map +1 -1
- package/dist/llm-settings.js +3 -3
- package/dist/llm-settings.js.map +1 -1
- package/dist/net-utils.d.ts +2 -1
- package/dist/net-utils.d.ts.map +1 -1
- package/dist/net-utils.js +100 -3
- package/dist/net-utils.js.map +1 -1
- package/dist/pricing.d.ts +45 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +93 -0
- package/dist/pricing.js.map +1 -0
- package/dist/protocol.d.ts +220 -5
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +11 -1
- package/dist/protocol.js.map +1 -1
- package/dist/sample.d.ts +152 -0
- package/dist/sample.d.ts.map +1 -0
- package/dist/sample.js +22 -0
- package/dist/sample.js.map +1 -0
- package/dist/session-utils.d.ts +82 -2
- package/dist/session-utils.d.ts.map +1 -1
- package/dist/session-utils.js +205 -10
- package/dist/session-utils.js.map +1 -1
- package/dist/state-colors.d.ts.map +1 -1
- package/dist/state-colors.js +2 -0
- package/dist/state-colors.js.map +1 -1
- package/dist/states.d.ts +1 -0
- package/dist/states.d.ts.map +1 -1
- package/dist/states.js +6 -1
- package/dist/states.js.map +1 -1
- package/dist/svg-renderers/agent-logos.d.ts +20 -12
- package/dist/svg-renderers/agent-logos.d.ts.map +1 -1
- package/dist/svg-renderers/agent-logos.js +154 -46
- package/dist/svg-renderers/agent-logos.js.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.d.ts +71 -6
- package/dist/svg-renderers/session-slot-renderer.d.ts.map +1 -1
- package/dist/svg-renderers/session-slot-renderer.js +429 -209
- package/dist/svg-renderers/session-slot-renderer.js.map +1 -1
- package/dist/svg-renderers/text-utils.d.ts +14 -0
- package/dist/svg-renderers/text-utils.d.ts.map +1 -1
- package/dist/svg-renderers/text-utils.js +34 -0
- package/dist/svg-renderers/text-utils.js.map +1 -1
- package/dist/telemetry-envelope.d.ts +1 -1
- package/dist/telemetry-envelope.d.ts.map +1 -1
- package/dist/terrarium-rules.d.ts +46 -0
- package/dist/terrarium-rules.d.ts.map +1 -0
- package/dist/terrarium-rules.js +63 -0
- package/dist/terrarium-rules.js.map +1 -0
- package/dist/timeline-icons.d.ts +80 -0
- package/dist/timeline-icons.d.ts.map +1 -0
- package/dist/timeline-icons.js +174 -0
- package/dist/timeline-icons.js.map +1 -0
- package/dist/timeline-label.d.ts +38 -0
- package/dist/timeline-label.d.ts.map +1 -0
- package/dist/timeline-label.js +95 -0
- package/dist/timeline-label.js.map +1 -0
- package/dist/timeline-markdown.d.ts +80 -0
- package/dist/timeline-markdown.d.ts.map +1 -0
- package/dist/timeline-markdown.js +221 -0
- package/dist/timeline-markdown.js.map +1 -0
- package/dist/timeline-summarizer.d.ts +24 -0
- package/dist/timeline-summarizer.d.ts.map +1 -1
- package/dist/timeline-summarizer.js +55 -13
- package/dist/timeline-summarizer.js.map +1 -1
- package/dist/timeline-task-display.d.ts +66 -0
- package/dist/timeline-task-display.d.ts.map +1 -0
- package/dist/timeline-task-display.js +94 -0
- package/dist/timeline-task-display.js.map +1 -0
- package/dist/timeline.d.ts +101 -2
- package/dist/timeline.d.ts.map +1 -1
- package/dist/timeline.js +249 -11
- package/dist/timeline.js.map +1 -1
- package/dist/trmnl-layout.d.ts.map +1 -0
- package/dist/trmnl-layout.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timeline-task-display.ts — the SINGLE source of truth for how task
|
|
3
|
+
* hierarchy rows (`task_start` / `task_end`) are RENDERED.
|
|
4
|
+
*
|
|
5
|
+
* Contract (2026-07-19, "one row per task"):
|
|
6
|
+
* - `task_end` is a DATA-ONLY closure record. It stays on the wire and in
|
|
7
|
+
* every store — it stops the in-flight spinner (`isInFlightTask`), is the
|
|
8
|
+
* judge-result upsert vehicle (merge by type='task_end' + taskId), and is
|
|
9
|
+
* what the orphan reaper synthesizes — but renderers NEVER show it as a
|
|
10
|
+
* standalone row. Before this contract the visible timeline was dominated
|
|
11
|
+
* by reaper-synthesized "Interrupted · ~Xh" rows (no judge ever runs for
|
|
12
|
+
* those), while the judged closures were suppressed as internal
|
|
13
|
+
* boundaries: the noise showed and the signal hid.
|
|
14
|
+
* - A task renders as at most ONE row: its `task_start` header, which FOLDS
|
|
15
|
+
* IN the matching closure's fields — closure label (`task_end.raw`, e.g.
|
|
16
|
+
* "Session end · 2 turns · 6m 5s"), judge summary as the title when the
|
|
17
|
+
* header's own title is a bare "Task N", and the score/outcome badge.
|
|
18
|
+
* - Header visibility: meaningful title OR eval payload (own or closure).
|
|
19
|
+
* `_empty` category always hides. Bare unjudged tasks — including every
|
|
20
|
+
* interrupted reaper closure — render nothing; the timeline stays an
|
|
21
|
+
* activity log of actual turns.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors (update in the same commit):
|
|
24
|
+
* - Apple `timelineShouldShowTaskMarker` / `timelineTaskClosure` /
|
|
25
|
+
* `timelineTaskHeaderDisplay` (apple/.../UI/Monitor/TimelineStripView.swift)
|
|
26
|
+
* - Android `shouldShowTaskMarker` / `taskClosure` / `taskHeaderDisplay`
|
|
27
|
+
* (android/.../state/TimelineDisplay.kt)
|
|
28
|
+
* - ESP32 boards exclude task rows from milestone/ticker selection
|
|
29
|
+
* (esp32/src/ui/eink/eink_display.cpp, esp32/src/ui/widgets/hud_bar.cpp)
|
|
30
|
+
* — no closure fold there; glance surfaces show turn rows only.
|
|
31
|
+
*/
|
|
32
|
+
import type { TimelineEntry } from './timeline.js';
|
|
33
|
+
/** Fields a task header needs from its row or its closure. */
|
|
34
|
+
type TaskRowLike = Pick<TimelineEntry, 'type' | 'taskId' | 'raw' | 'ts' | 'endedAt' | 'taskScore' | 'taskOutcome' | 'taskCategory' | 'taskSummary' | 'boundarySignal'>;
|
|
35
|
+
/** False for empty titles and the auto-minted "Task N" / "작업 N" labels —
|
|
36
|
+
* those carry no information a reader can act on. */
|
|
37
|
+
export declare function timelineIsMeaningfulTaskTitle(raw: string | undefined | null): boolean;
|
|
38
|
+
/** The matching `task_end` closure record for a `task_start` header, if it
|
|
39
|
+
* has arrived among `siblings`. Undefined for non-headers and open tasks. */
|
|
40
|
+
export declare function timelineTaskClosure<T extends Pick<TimelineEntry, 'type' | 'taskId'>>(entry: Pick<TimelineEntry, 'type' | 'taskId'>, siblings: ReadonlyArray<T>): T | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Canonical render predicate for task hierarchy rows. Non-task rows pass
|
|
43
|
+
* through (`true`); callers keep their own rules for those.
|
|
44
|
+
*/
|
|
45
|
+
export declare function timelineShouldRenderTaskRow(entry: TaskRowLike, siblings: ReadonlyArray<TaskRowLike>): boolean;
|
|
46
|
+
export interface TaskHeaderDisplay {
|
|
47
|
+
/** Header title: the row's own title when meaningful, else the judge's
|
|
48
|
+
* one-line summary from the closure, else the raw title as-is. */
|
|
49
|
+
title: string;
|
|
50
|
+
/** Closure label to render as a trailing chip ("Session end · 2 turns ·
|
|
51
|
+
* 6m 5s"). Undefined while the task is open. */
|
|
52
|
+
closureText?: string;
|
|
53
|
+
/** True once the matching `task_end` exists (spinner should be static). */
|
|
54
|
+
closed: boolean;
|
|
55
|
+
/** Badge inputs — closure fields win over the header's own (the judge
|
|
56
|
+
* upserts onto the closure record). */
|
|
57
|
+
taskScore?: number;
|
|
58
|
+
taskOutcome?: string;
|
|
59
|
+
/** Epoch ms the task closed at, for pending→unscored badge timing. */
|
|
60
|
+
closedAtMs?: number;
|
|
61
|
+
}
|
|
62
|
+
/** Decompose a `task_start` header + its closure into the displayed pieces.
|
|
63
|
+
* Callers must have already passed `timelineShouldRenderTaskRow`. */
|
|
64
|
+
export declare function timelineTaskHeaderDisplay(entry: TaskRowLike, siblings: ReadonlyArray<TaskRowLike>): TaskHeaderDisplay;
|
|
65
|
+
export {};
|
|
66
|
+
//# sourceMappingURL=timeline-task-display.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline-task-display.d.ts","sourceRoot":"","sources":["../src/timeline-task-display.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,8DAA8D;AAC9D,KAAK,WAAW,GAAG,IAAI,CACrB,aAAa,EACb,MAAM,GAAG,QAAQ,GAAG,KAAK,GAAG,IAAI,GAAG,SAAS,GAC5C,WAAW,GAAG,aAAa,GAAG,cAAc,GAAG,aAAa,GAAG,gBAAgB,CAChF,CAAC;AAIF;sDACsD;AACtD,wBAAgB,6BAA6B,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAIrF;AAED;8EAC8E;AAC9E,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,EAClF,KAAK,EAAE,IAAI,CAAC,aAAa,EAAE,MAAM,GAAG,QAAQ,CAAC,EAC7C,QAAQ,EAAE,aAAa,CAAC,CAAC,CAAC,GACzB,CAAC,GAAG,SAAS,CAGf;AAUD;;;GAGG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,GACnC,OAAO,CAQT;AAED,MAAM,WAAW,iBAAiB;IAChC;uEACmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd;qDACiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,MAAM,EAAE,OAAO,CAAC;IAChB;4CACwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;sEACsE;AACtE,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,GACnC,iBAAiB,CAiBnB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* timeline-task-display.ts — the SINGLE source of truth for how task
|
|
3
|
+
* hierarchy rows (`task_start` / `task_end`) are RENDERED.
|
|
4
|
+
*
|
|
5
|
+
* Contract (2026-07-19, "one row per task"):
|
|
6
|
+
* - `task_end` is a DATA-ONLY closure record. It stays on the wire and in
|
|
7
|
+
* every store — it stops the in-flight spinner (`isInFlightTask`), is the
|
|
8
|
+
* judge-result upsert vehicle (merge by type='task_end' + taskId), and is
|
|
9
|
+
* what the orphan reaper synthesizes — but renderers NEVER show it as a
|
|
10
|
+
* standalone row. Before this contract the visible timeline was dominated
|
|
11
|
+
* by reaper-synthesized "Interrupted · ~Xh" rows (no judge ever runs for
|
|
12
|
+
* those), while the judged closures were suppressed as internal
|
|
13
|
+
* boundaries: the noise showed and the signal hid.
|
|
14
|
+
* - A task renders as at most ONE row: its `task_start` header, which FOLDS
|
|
15
|
+
* IN the matching closure's fields — closure label (`task_end.raw`, e.g.
|
|
16
|
+
* "Session end · 2 turns · 6m 5s"), judge summary as the title when the
|
|
17
|
+
* header's own title is a bare "Task N", and the score/outcome badge.
|
|
18
|
+
* - Header visibility: meaningful title OR eval payload (own or closure).
|
|
19
|
+
* `_empty` category always hides. Bare unjudged tasks — including every
|
|
20
|
+
* interrupted reaper closure — render nothing; the timeline stays an
|
|
21
|
+
* activity log of actual turns.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors (update in the same commit):
|
|
24
|
+
* - Apple `timelineShouldShowTaskMarker` / `timelineTaskClosure` /
|
|
25
|
+
* `timelineTaskHeaderDisplay` (apple/.../UI/Monitor/TimelineStripView.swift)
|
|
26
|
+
* - Android `shouldShowTaskMarker` / `taskClosure` / `taskHeaderDisplay`
|
|
27
|
+
* (android/.../state/TimelineDisplay.kt)
|
|
28
|
+
* - ESP32 boards exclude task rows from milestone/ticker selection
|
|
29
|
+
* (esp32/src/ui/eink/eink_display.cpp, esp32/src/ui/widgets/hud_bar.cpp)
|
|
30
|
+
* — no closure fold there; glance surfaces show turn rows only.
|
|
31
|
+
*/
|
|
32
|
+
const TASK_NUMBER_TITLES = [/^task\s+\d+$/i, /^작업\s*\d+$/];
|
|
33
|
+
/** False for empty titles and the auto-minted "Task N" / "작업 N" labels —
|
|
34
|
+
* those carry no information a reader can act on. */
|
|
35
|
+
export function timelineIsMeaningfulTaskTitle(raw) {
|
|
36
|
+
const title = (raw ?? '').trim();
|
|
37
|
+
if (!title)
|
|
38
|
+
return false;
|
|
39
|
+
return !TASK_NUMBER_TITLES.some((rx) => rx.test(title));
|
|
40
|
+
}
|
|
41
|
+
/** The matching `task_end` closure record for a `task_start` header, if it
|
|
42
|
+
* has arrived among `siblings`. Undefined for non-headers and open tasks. */
|
|
43
|
+
export function timelineTaskClosure(entry, siblings) {
|
|
44
|
+
if (entry.type !== 'task_start' || !entry.taskId)
|
|
45
|
+
return undefined;
|
|
46
|
+
return siblings.find((s) => s.type === 'task_end' && s.taskId === entry.taskId);
|
|
47
|
+
}
|
|
48
|
+
function hasEvalPayload(e) {
|
|
49
|
+
if (!e)
|
|
50
|
+
return false;
|
|
51
|
+
return e.taskScore != null ||
|
|
52
|
+
!!e.taskOutcome?.trim() ||
|
|
53
|
+
(!!e.taskCategory?.trim() && e.taskCategory !== '_empty') ||
|
|
54
|
+
!!e.taskSummary?.trim();
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Canonical render predicate for task hierarchy rows. Non-task rows pass
|
|
58
|
+
* through (`true`); callers keep their own rules for those.
|
|
59
|
+
*/
|
|
60
|
+
export function timelineShouldRenderTaskRow(entry, siblings) {
|
|
61
|
+
if (entry.type === 'task_end')
|
|
62
|
+
return false;
|
|
63
|
+
if (entry.type !== 'task_start')
|
|
64
|
+
return true;
|
|
65
|
+
if (entry.taskCategory === '_empty')
|
|
66
|
+
return false;
|
|
67
|
+
const closure = timelineTaskClosure(entry, siblings);
|
|
68
|
+
if (closure?.taskCategory === '_empty')
|
|
69
|
+
return false;
|
|
70
|
+
if (timelineIsMeaningfulTaskTitle(entry.raw))
|
|
71
|
+
return true;
|
|
72
|
+
return hasEvalPayload(entry) || hasEvalPayload(closure);
|
|
73
|
+
}
|
|
74
|
+
/** Decompose a `task_start` header + its closure into the displayed pieces.
|
|
75
|
+
* Callers must have already passed `timelineShouldRenderTaskRow`. */
|
|
76
|
+
export function timelineTaskHeaderDisplay(entry, siblings) {
|
|
77
|
+
const closure = timelineTaskClosure(entry, siblings);
|
|
78
|
+
const ownTitle = (entry.raw ?? '').trim();
|
|
79
|
+
const summary = (closure?.taskSummary ?? entry.taskSummary ?? '').trim();
|
|
80
|
+
const title = timelineIsMeaningfulTaskTitle(ownTitle) ? ownTitle : (summary || ownTitle);
|
|
81
|
+
const closureText = closure?.raw?.trim() || undefined;
|
|
82
|
+
const score = closure?.taskScore ?? entry.taskScore;
|
|
83
|
+
const outcome = (closure?.taskOutcome ?? entry.taskOutcome)?.trim() || undefined;
|
|
84
|
+
const closedAtMs = closure ? (closure.endedAt ?? closure.ts) : undefined;
|
|
85
|
+
return {
|
|
86
|
+
title,
|
|
87
|
+
...(closureText ? { closureText } : {}),
|
|
88
|
+
closed: !!closure,
|
|
89
|
+
...(score != null ? { taskScore: score } : {}),
|
|
90
|
+
...(outcome ? { taskOutcome: outcome } : {}),
|
|
91
|
+
...(closedAtMs != null ? { closedAtMs } : {}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=timeline-task-display.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline-task-display.js","sourceRoot":"","sources":["../src/timeline-task-display.ts"],"names":[],"mappings":"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"}
|
package/dist/timeline.d.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Shared timeline types and log parser for OpenClaw mode.
|
|
3
3
|
* Used by both bridge (BridgeLogStream) and plugin (LogStream).
|
|
4
4
|
*/
|
|
5
|
-
export type TimelineEntryType = 'tool_request' | 'tool_resolved' | 'chat_start' | 'chat_end' | 'chat_response' | 'error' | 'scheduled' | 'user_action' | 'model_call' | 'model_response' | 'memory_recall' | 'tool_exec' | 'eval_result';
|
|
5
|
+
export type TimelineEntryType = 'tool_request' | 'tool_resolved' | 'chat_start' | 'chat_end' | 'chat_response' | 'error' | 'scheduled' | 'user_action' | 'model_call' | 'model_response' | 'memory_recall' | 'tool_exec' | 'eval_result' | 'task_start' | 'task_end' | 'task_milestone';
|
|
6
|
+
import type { TaskBoundarySignal } from './eval-schema.js';
|
|
7
|
+
export type { TaskBoundarySignal } from './eval-schema.js';
|
|
6
8
|
export interface TimelineEntry {
|
|
7
9
|
ts: number;
|
|
8
10
|
type: TimelineEntryType;
|
|
@@ -11,12 +13,71 @@ export interface TimelineEntry {
|
|
|
11
13
|
approvalId?: string;
|
|
12
14
|
status?: 'pending' | 'approved' | 'denied';
|
|
13
15
|
agentType?: string;
|
|
16
|
+
projectName?: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
runId?: string;
|
|
19
|
+
startedAt?: number;
|
|
20
|
+
endedAt?: number;
|
|
14
21
|
repeatCount?: number;
|
|
15
22
|
automated?: boolean;
|
|
23
|
+
/** APME task id. Set on task_start/task_end and on every turn entry inside the task scope. */
|
|
24
|
+
taskId?: string;
|
|
25
|
+
/** Only on task_end. Why the task closed. */
|
|
26
|
+
boundarySignal?: TaskBoundarySignal;
|
|
27
|
+
/**
|
|
28
|
+
* Per-task evaluation result, attached when the task_judge resolves
|
|
29
|
+
* (always AFTER the initial task_end emit — clients upsert the existing
|
|
30
|
+
* row by (type='task_end', taskId) and merge these four fields).
|
|
31
|
+
*
|
|
32
|
+
* - `taskScore` : 0..1 composite (matches `tasks.composite_score`)
|
|
33
|
+
* - `taskOutcome` : terminal outcome string from `bridge/src/apme/outcome.ts`
|
|
34
|
+
* (`'committed' | 'abandoned' | 'iterated' | 'ab_winner' | 'ab_loser'
|
|
35
|
+
* | 'interrupted' | 'exploratory' | 'pending'`). UIs collapse into
|
|
36
|
+
* three visual classes — success / fail / partial / pending — and pick
|
|
37
|
+
* the badge color via design-system status tokens.
|
|
38
|
+
* - `taskCategory`: task_rollup category ('general' | 'conversation' | …)
|
|
39
|
+
* - `taskSummary` : one-line judge summary
|
|
40
|
+
*/
|
|
41
|
+
taskScore?: number;
|
|
42
|
+
taskOutcome?: string;
|
|
43
|
+
taskCategory?: string;
|
|
44
|
+
taskSummary?: string;
|
|
45
|
+
/**
|
|
46
|
+
* How the row's `raw` summary was produced. Lets clients decide whether
|
|
47
|
+
* the detail pane is worth showing — when `'none'`, detail is just the
|
|
48
|
+
* unfiltered response that the heuristic couldn't summarize, and showing
|
|
49
|
+
* it duplicates content rather than adding value.
|
|
50
|
+
* - `'llm'` : LLM-summarized (clean, short, distinct from detail)
|
|
51
|
+
* - `'heuristic'`: topic-hint extracted from response or prompt
|
|
52
|
+
* - `'none'` : last-resort fallback (literal "Completed", bare tool name, etc.)
|
|
53
|
+
* - `'progress'`: non-terminal assistant status update (work still running)
|
|
54
|
+
*/
|
|
55
|
+
summaryKind?: 'llm' | 'heuristic' | 'none' | 'progress';
|
|
16
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Prepare a chat-response body for storage as `entry.detail` while keeping
|
|
59
|
+
* markdown intact. Use this on chat_response / chat_end emit paths so the
|
|
60
|
+
* client renderer can apply heading / bullet / table / inline span styles —
|
|
61
|
+
* `cleanDetailText` would have stripped all of that before it ever reached
|
|
62
|
+
* the dashboard.
|
|
63
|
+
*
|
|
64
|
+
* Filters and caps:
|
|
65
|
+
* - System JSON blobs (`{"connectionId": ...}` etc.) → empty
|
|
66
|
+
* - Error JSON (`{"error": ...}`) → the error string
|
|
67
|
+
* - Other JSON object → compact stringified form (200 char cap)
|
|
68
|
+
* - Otherwise → original text with 3+ blank lines collapsed to 2, trimmed
|
|
69
|
+
*
|
|
70
|
+
* Markdown markers (`**`, `##`, `|`, `` ` ``, `[text](url)`, etc.) are
|
|
71
|
+
* preserved verbatim so the client `parseTimelineMarkdown` /
|
|
72
|
+
* `parseInlineSpans` can do their job.
|
|
73
|
+
*/
|
|
74
|
+
export declare function prepareMarkdownDetail(text: string): string;
|
|
17
75
|
/**
|
|
18
76
|
* Clean detail text — strip markdown artifacts, JSON blobs, and system noise.
|
|
19
|
-
* Applied to timeline entry `detail` fields before storage
|
|
77
|
+
* Applied to timeline entry `detail` fields before storage on **non-chat**
|
|
78
|
+
* paths (tool errors, system log lines). Chat response detail should go
|
|
79
|
+
* through `prepareMarkdownDetail` instead so the dashboard can render the
|
|
80
|
+
* markdown.
|
|
20
81
|
*/
|
|
21
82
|
export declare function cleanDetailText(text: string): string;
|
|
22
83
|
/**
|
|
@@ -29,6 +90,44 @@ export declare function cleanRawText(text: string): string;
|
|
|
29
90
|
* Applied to both raw and detail fields.
|
|
30
91
|
*/
|
|
31
92
|
export declare function cleanNopMarkers(text: string): string;
|
|
93
|
+
export declare function isAssistantProgressUpdate(text?: string): boolean;
|
|
94
|
+
export declare function detailHasRealSignal(detail?: string): boolean;
|
|
95
|
+
export declare function isOpenClawPlaceholderRaw(raw: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* A user prompt that invokes a slash command ("/merge", "/session-end args").
|
|
98
|
+
*
|
|
99
|
+
* Two wire shapes reach the daemons' UserPromptSubmit paths:
|
|
100
|
+
* 1. Plain text — the user's literal keystrokes: "/merge --squash"
|
|
101
|
+
* 2. Claude Code's command XML envelope (what the transcript records and
|
|
102
|
+
* some hook versions forward):
|
|
103
|
+
* <command-message>merge</command-message>
|
|
104
|
+
* <command-name>/merge</command-name>
|
|
105
|
+
* <command-args>--squash</command-args>
|
|
106
|
+
*
|
|
107
|
+
* `parseSlashCommandPrompt` recognizes both and returns the normalized
|
|
108
|
+
* invocation, so emit paths can store a clean "/merge --squash" row and
|
|
109
|
+
* renderers can style command turns distinctly (terminal glyph instead of a
|
|
110
|
+
* chat bubble). Returns null for ordinary prompts — including absolute paths
|
|
111
|
+
* ("/Users/x/file" never matches because path segments contain further "/").
|
|
112
|
+
*
|
|
113
|
+
* Mirror: TimelineStripView.swift `timelineSlashCommand` must match.
|
|
114
|
+
*/
|
|
115
|
+
export interface SlashCommandInvocation {
|
|
116
|
+
/** Command including the leading slash, e.g. "/merge". */
|
|
117
|
+
command: string;
|
|
118
|
+
/** Argument string after the command, if any. */
|
|
119
|
+
args?: string;
|
|
120
|
+
}
|
|
121
|
+
export declare function parseSlashCommandPrompt(text?: string): SlashCommandInvocation | null;
|
|
122
|
+
/** Normalize a prompt for timeline display: command XML envelopes collapse to
|
|
123
|
+
* their "/name args" form; everything else passes through unchanged. */
|
|
124
|
+
export declare function normalizeCommandPrompt(text: string): string;
|
|
125
|
+
export declare function isTaskNotificationChatStart(entry: TimelineEntry): boolean;
|
|
126
|
+
export declare function isOpenClawLowSignalResponse(entry: TimelineEntry): boolean;
|
|
127
|
+
export declare function isOpenClawCronPrompt(text?: string): boolean;
|
|
128
|
+
export declare function summarizeOpenClawCronPrompt(text?: string): string;
|
|
129
|
+
export declare function shouldDropLowSignalTimelineEntry(entry: TimelineEntry): boolean;
|
|
130
|
+
export declare function normalizeTimelineEntryForStorage(entry: TimelineEntry): TimelineEntry | null;
|
|
32
131
|
/**
|
|
33
132
|
* Extract the semantic core of a timeline entry for dedup comparison.
|
|
34
133
|
* For chat_end: strip duration/tool suffix (everything after first ' · ').
|
package/dist/timeline.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,iBAAiB,GACzB,cAAc,GAAG,eAAe,GAAG,YAAY,GAAG,UAAU,GAC5D,eAAe,GAAG,OAAO,GAAG,WAAW,GAAG,aAAa,GACvD,YAAY,GAAG,gBAAgB,GAAG,eAAe,GAAG,WAAW,GAC/D,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,MAAM,iBAAiB,GACzB,cAAc,GAAG,eAAe,GAAG,YAAY,GAAG,UAAU,GAC5D,eAAe,GAAG,OAAO,GAAG,WAAW,GAAG,aAAa,GACvD,YAAY,GAAG,gBAAgB,GAAG,eAAe,GAAG,WAAW,GAC/D,aAAa,GAGb,YAAY,GAAG,UAAU,GAIzB,gBAAgB,CAAC;AAIrB,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,iBAAiB,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8FAA8F;IAC9F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,kBAAkB,CAAC;IACpC;;;;;;;;;;;;;OAaG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,KAAK,GAAG,WAAW,GAAG,MAAM,GAAG,UAAU,CAAC;CACzD;AAuDD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgB1D;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CA8CpD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CASjD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMpD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAqBhE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAQ5D;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAG7D;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,WAAW,sBAAsB;IACrC,0DAA0D;IAC1D,OAAO,EAAE,MAAM,CAAC;IAChB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,uBAAuB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,sBAAsB,GAAG,IAAI,CAuBpF;AAED;yEACyE;AACzE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAK3D;AAED,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAKzE;AAQD,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAuBzE;AAED,wBAAgB,oBAAoB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAE3D;AAED,wBAAgB,2BAA2B,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAYjE;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAkC9E;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,aAAa,GAAG,aAAa,GAAG,IAAI,CAoB3F;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAMhF;AA2DD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,aAAa,EACpB,aAAa,EAAE,SAAS,aAAa,EAAE,EACvC,QAAQ,SAAY,GACnB,MAAM,CAmCR;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,aAAa,CAAA;CAAE,CAAC;AAE5C,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,SAAS,aAAa,EAAE,GAChC,iBAAiB,CA+CnB;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,aAAa,GAAG,IAAI,CAkMhE"}
|
package/dist/timeline.js
CHANGED
|
@@ -56,9 +56,49 @@ function extractReadableMessage(message) {
|
|
|
56
56
|
cleaned = cleaned.slice(0, 497) + '...';
|
|
57
57
|
return cleaned || message.slice(0, 500);
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Prepare a chat-response body for storage as `entry.detail` while keeping
|
|
61
|
+
* markdown intact. Use this on chat_response / chat_end emit paths so the
|
|
62
|
+
* client renderer can apply heading / bullet / table / inline span styles —
|
|
63
|
+
* `cleanDetailText` would have stripped all of that before it ever reached
|
|
64
|
+
* the dashboard.
|
|
65
|
+
*
|
|
66
|
+
* Filters and caps:
|
|
67
|
+
* - System JSON blobs (`{"connectionId": ...}` etc.) → empty
|
|
68
|
+
* - Error JSON (`{"error": ...}`) → the error string
|
|
69
|
+
* - Other JSON object → compact stringified form (200 char cap)
|
|
70
|
+
* - Otherwise → original text with 3+ blank lines collapsed to 2, trimmed
|
|
71
|
+
*
|
|
72
|
+
* Markdown markers (`**`, `##`, `|`, `` ` ``, `[text](url)`, etc.) are
|
|
73
|
+
* preserved verbatim so the client `parseTimelineMarkdown` /
|
|
74
|
+
* `parseInlineSpans` can do their job.
|
|
75
|
+
*/
|
|
76
|
+
export function prepareMarkdownDetail(text) {
|
|
77
|
+
if (!text)
|
|
78
|
+
return text;
|
|
79
|
+
if (typeof text !== 'string')
|
|
80
|
+
return '';
|
|
81
|
+
const trimmed = text.trim();
|
|
82
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(trimmed);
|
|
85
|
+
if (parsed.connectionId || parsed.stateVersion || parsed.seq)
|
|
86
|
+
return '';
|
|
87
|
+
if (parsed.error)
|
|
88
|
+
return String(parsed.error);
|
|
89
|
+
return JSON.stringify(parsed).slice(0, 200);
|
|
90
|
+
}
|
|
91
|
+
catch { /* not valid JSON, fall through */ }
|
|
92
|
+
}
|
|
93
|
+
// Preserve markdown — only collapse runs of blank lines + trim ends.
|
|
94
|
+
return text.replace(/\n{3,}/g, '\n\n').trim();
|
|
95
|
+
}
|
|
59
96
|
/**
|
|
60
97
|
* Clean detail text — strip markdown artifacts, JSON blobs, and system noise.
|
|
61
|
-
* Applied to timeline entry `detail` fields before storage
|
|
98
|
+
* Applied to timeline entry `detail` fields before storage on **non-chat**
|
|
99
|
+
* paths (tool errors, system log lines). Chat response detail should go
|
|
100
|
+
* through `prepareMarkdownDetail` instead so the dashboard can render the
|
|
101
|
+
* markdown.
|
|
62
102
|
*/
|
|
63
103
|
export function cleanDetailText(text) {
|
|
64
104
|
if (!text)
|
|
@@ -129,6 +169,189 @@ export function cleanNopMarkers(text) {
|
|
|
129
169
|
.replace(/\n{3,}/g, '\n\n')
|
|
130
170
|
.trim();
|
|
131
171
|
}
|
|
172
|
+
export function isAssistantProgressUpdate(text) {
|
|
173
|
+
if (!text)
|
|
174
|
+
return false;
|
|
175
|
+
const trimmed = text.trim();
|
|
176
|
+
if (!trimmed)
|
|
177
|
+
return false;
|
|
178
|
+
const head = trimmed.slice(0, 800);
|
|
179
|
+
const lower = head.toLowerCase();
|
|
180
|
+
const explicitProgress = /\b(still|currently|continues? to|is|are)\s+(running|building|installing|executing|processing|waiting)\b/.test(lower) ||
|
|
181
|
+
/\b(still running|still building|build is running|is still running|are still running)\b/.test(lower) ||
|
|
182
|
+
/\b(waiting for|wait until|once (?:the )?.*(?:finishes|completes|arrives)|continue once|will continue once|i.ll continue once)\b/.test(lower) ||
|
|
183
|
+
/\b(no interim lines|buffers? output until completion|tail buffers output)\b/.test(lower) ||
|
|
184
|
+
/(아직|계속)\s*(실행|진행|빌드|설치)\s*중/.test(head) ||
|
|
185
|
+
/(완료|끝나|도착)면\s*(계속|이어)/.test(head) ||
|
|
186
|
+
/(기다리는 중|대기 중)/.test(head);
|
|
187
|
+
if (!explicitProgress)
|
|
188
|
+
return false;
|
|
189
|
+
const startsAsFinal = /^(done|completed|complete|fixed|merged|verified|all done)\b/i.test(trimmed) ||
|
|
190
|
+
/^(완료|수정 완료|검증 완료|반영 완료|머지 완료)/.test(trimmed);
|
|
191
|
+
return !startsAsFinal;
|
|
192
|
+
}
|
|
193
|
+
export function detailHasRealSignal(detail) {
|
|
194
|
+
if (!detail)
|
|
195
|
+
return false;
|
|
196
|
+
for (const line of detail.split(/\r?\n/)) {
|
|
197
|
+
const trimmed = line.trim();
|
|
198
|
+
if (!trimmed)
|
|
199
|
+
continue;
|
|
200
|
+
if (!trimmed.toLowerCase().startsWith('status:'))
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
export function isOpenClawPlaceholderRaw(raw) {
|
|
206
|
+
const normalized = raw.trim().toLowerCase();
|
|
207
|
+
return normalized === 'tool' || normalized.startsWith('tool \u00b7 ');
|
|
208
|
+
}
|
|
209
|
+
export function parseSlashCommandPrompt(text) {
|
|
210
|
+
if (!text)
|
|
211
|
+
return null;
|
|
212
|
+
const trimmed = text.trim();
|
|
213
|
+
if (!trimmed)
|
|
214
|
+
return null;
|
|
215
|
+
// Shape 2: command XML envelope.
|
|
216
|
+
if (trimmed.includes('<command-name>')) {
|
|
217
|
+
const name = /<command-name>\s*(\/?[A-Za-z][\w:-]*)\s*<\/command-name>/.exec(trimmed)?.[1];
|
|
218
|
+
if (!name)
|
|
219
|
+
return null;
|
|
220
|
+
const args = /<command-args>([\s\S]*?)<\/command-args>/.exec(trimmed)?.[1]?.trim();
|
|
221
|
+
return {
|
|
222
|
+
command: name.startsWith('/') ? name : `/${name}`,
|
|
223
|
+
...(args ? { args } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// Shape 1: plain "/command [args...]" — the command token must be the whole
|
|
227
|
+
// first word (a "/" inside the token means it's a filesystem path, not a
|
|
228
|
+
// command).
|
|
229
|
+
const m = /^\/([A-Za-z][\w:-]*)(?:\s+([\s\S]+))?$/.exec(trimmed);
|
|
230
|
+
if (!m)
|
|
231
|
+
return null;
|
|
232
|
+
const args = m[2]?.trim();
|
|
233
|
+
return { command: `/${m[1]}`, ...(args ? { args } : {}) };
|
|
234
|
+
}
|
|
235
|
+
/** Normalize a prompt for timeline display: command XML envelopes collapse to
|
|
236
|
+
* their "/name args" form; everything else passes through unchanged. */
|
|
237
|
+
export function normalizeCommandPrompt(text) {
|
|
238
|
+
if (!text.includes('<command-name>'))
|
|
239
|
+
return text;
|
|
240
|
+
const cmd = parseSlashCommandPrompt(text);
|
|
241
|
+
if (!cmd)
|
|
242
|
+
return text;
|
|
243
|
+
return cmd.args ? `${cmd.command} ${cmd.args}` : cmd.command;
|
|
244
|
+
}
|
|
245
|
+
export function isTaskNotificationChatStart(entry) {
|
|
246
|
+
if (entry.type !== 'chat_start')
|
|
247
|
+
return false;
|
|
248
|
+
const raw = entry.raw.trim().toLowerCase();
|
|
249
|
+
const detail = entry.detail?.trim().toLowerCase() ?? '';
|
|
250
|
+
return raw.startsWith('<task-notification>') || detail.startsWith('<task-notification>');
|
|
251
|
+
}
|
|
252
|
+
function hasOpenClawNotificationFailureSignal(text) {
|
|
253
|
+
return /\b(line|notification|userid|target id|target issue)\b/i.test(text) &&
|
|
254
|
+
/\b(fail(?:ed|ure)?|missing|unconfigured|notified|needed|pending)\b/i.test(text) ||
|
|
255
|
+
/(LINE|알림|userId|사용자 ID|대상 ID).*(실패|미등록|미설정|구성되지|필요|대기)/i.test(text);
|
|
256
|
+
}
|
|
257
|
+
export function isOpenClawLowSignalResponse(entry) {
|
|
258
|
+
if (entry.agentType !== 'openclaw')
|
|
259
|
+
return false;
|
|
260
|
+
const isResponse = entry.type === 'chat_response' || entry.type === 'model_response';
|
|
261
|
+
const isAutomatedStart = entry.type === 'chat_start' && entry.automated === true;
|
|
262
|
+
if (!isResponse && !isAutomatedStart)
|
|
263
|
+
return false;
|
|
264
|
+
const text = `${entry.raw}\n${entry.detail ?? ''}`.trim();
|
|
265
|
+
if (!text)
|
|
266
|
+
return false;
|
|
267
|
+
if (hasOpenClawNotificationFailureSignal(text))
|
|
268
|
+
return false;
|
|
269
|
+
const lower = text.toLowerCase();
|
|
270
|
+
const hasNoReply = /\bno_reply\b/.test(lower);
|
|
271
|
+
const looksLikePolling = /\bstill translating\b/.test(lower) ||
|
|
272
|
+
/\btranslation still in progress\b/.test(lower) ||
|
|
273
|
+
/\bnot all .*?(terminal|published|failed|complete|completed)\b/.test(lower) ||
|
|
274
|
+
/\b(in progress|still active|no action needed|nothing to notify yet)\b/.test(lower) ||
|
|
275
|
+
/\bcron job (?:stays|retained|active)\b/.test(lower) ||
|
|
276
|
+
/\bpipeline still active\b/.test(lower) ||
|
|
277
|
+
/(아직|여전히|계속).*(번역|진행)\s*중/.test(text) ||
|
|
278
|
+
/알릴 필요 없음|수행할 작업이 없음|대기합니다/.test(text);
|
|
279
|
+
if (isAutomatedStart)
|
|
280
|
+
return looksLikePolling;
|
|
281
|
+
return looksLikePolling && (hasNoReply || /no action needed|nothing to notify yet|알릴 필요 없음/.test(lower));
|
|
282
|
+
}
|
|
283
|
+
export function isOpenClawCronPrompt(text) {
|
|
284
|
+
return !!text && text.trimStart().startsWith('[cron:');
|
|
285
|
+
}
|
|
286
|
+
export function summarizeOpenClawCronPrompt(text) {
|
|
287
|
+
if (!text)
|
|
288
|
+
return '자동 작업';
|
|
289
|
+
const trimmed = text.trimStart();
|
|
290
|
+
const m = trimmed.match(/^\[cron:[^\s\]]+\s+([^\]]+)\]/);
|
|
291
|
+
if (!m)
|
|
292
|
+
return '자동 작업';
|
|
293
|
+
const job = m[1]
|
|
294
|
+
.replace(/[-_]+/g, ' ')
|
|
295
|
+
.replace(/\s+/g, ' ')
|
|
296
|
+
.trim();
|
|
297
|
+
if (!job)
|
|
298
|
+
return '자동 작업';
|
|
299
|
+
const capped = job.length > 64 ? `${job.slice(0, 61)}...` : job;
|
|
300
|
+
return `자동 작업 \u00b7 ${capped}`;
|
|
301
|
+
}
|
|
302
|
+
export function shouldDropLowSignalTimelineEntry(entry) {
|
|
303
|
+
if (isTaskNotificationChatStart(entry))
|
|
304
|
+
return true;
|
|
305
|
+
if (isOpenClawLowSignalResponse(entry))
|
|
306
|
+
return true;
|
|
307
|
+
const lowSignalTypes = new Set(['tool_exec', 'tool_request', 'tool_resolved']);
|
|
308
|
+
if (!lowSignalTypes.has(entry.type))
|
|
309
|
+
return false;
|
|
310
|
+
// Observed-agent tool hooks are extremely high volume (Bash/MCP/read/
|
|
311
|
+
// todowrite start+complete for every internal action). Keep them available
|
|
312
|
+
// to APME ingestion, but do not persist them into the bounded user-facing
|
|
313
|
+
// timeline buffer — otherwise a tool-heavy observed turn drowns its own
|
|
314
|
+
// prompt/response rows (OpenCode had no suppression and flooded the strip
|
|
315
|
+
// with one row per tool while Claude/Codex read clean). Antigravity is
|
|
316
|
+
// included forward-compat: `classifyObservedHookEvent` already accepts
|
|
317
|
+
// `antigravity_*` hooks, so if an AGY observer producer lands its tool rows
|
|
318
|
+
// must not flood the strip either.
|
|
319
|
+
if ((entry.agentType === 'codex-cli' ||
|
|
320
|
+
entry.agentType === 'codex-app' ||
|
|
321
|
+
entry.agentType === 'opencode' ||
|
|
322
|
+
entry.agentType === 'antigravity') &&
|
|
323
|
+
entry.type === 'tool_exec') {
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
if (detailHasRealSignal(entry.detail))
|
|
327
|
+
return false;
|
|
328
|
+
const raw = entry.raw.trim().toLowerCase();
|
|
329
|
+
if ((entry.agentType === 'codex-cli' || entry.agentType === 'codex-app') && entry.sessionId === 'codex:otel-active') {
|
|
330
|
+
return new Set(['tool', 'tool completed', 'unknown', 'unknown completed', 'exec', 'exec completed']).has(raw);
|
|
331
|
+
}
|
|
332
|
+
if (entry.agentType === 'openclaw') {
|
|
333
|
+
return isOpenClawPlaceholderRaw(raw);
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
export function normalizeTimelineEntryForStorage(entry) {
|
|
338
|
+
if (shouldDropLowSignalTimelineEntry(entry))
|
|
339
|
+
return null;
|
|
340
|
+
if (entry.agentType === 'openclaw' &&
|
|
341
|
+
entry.type === 'model_call' &&
|
|
342
|
+
(entry.automated || isOpenClawCronPrompt(entry.raw) || isOpenClawCronPrompt(entry.detail)) &&
|
|
343
|
+
(isOpenClawCronPrompt(entry.raw) || isOpenClawCronPrompt(entry.detail))) {
|
|
344
|
+
const source = isOpenClawCronPrompt(entry.raw) ? entry.raw : entry.detail;
|
|
345
|
+
return {
|
|
346
|
+
...entry,
|
|
347
|
+
raw: summarizeOpenClawCronPrompt(source),
|
|
348
|
+
detail: undefined,
|
|
349
|
+
automated: true,
|
|
350
|
+
summaryKind: entry.summaryKind ?? 'heuristic',
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
return entry;
|
|
354
|
+
}
|
|
132
355
|
/**
|
|
133
356
|
* Extract the semantic core of a timeline entry for dedup comparison.
|
|
134
357
|
* For chat_end: strip duration/tool suffix (everything after first ' · ').
|
|
@@ -213,6 +436,14 @@ export function isRepetitiveEntry(entry, recentEntries, windowMs = 3_600_000) {
|
|
|
213
436
|
break;
|
|
214
437
|
if (e.type !== entry.type)
|
|
215
438
|
continue;
|
|
439
|
+
// Never merge across sessions. Turn rows are structural (a chat_end pairs
|
|
440
|
+
// with ITS session's chat_start); merging session B's "Interrupted"/
|
|
441
|
+
// "Completed" close into session A's row moves the close out of B — and
|
|
442
|
+
// the merge path reassigns sessionId, stealing A's close too. Only applies
|
|
443
|
+
// when both sides carry a sessionId (legacy unattributed rows keep the
|
|
444
|
+
// old behavior).
|
|
445
|
+
if (entry.sessionId && e.sessionId && entry.sessionId !== e.sessionId)
|
|
446
|
+
continue;
|
|
216
447
|
// Automated entries: content-agnostic dedup (any two automated chats collapse)
|
|
217
448
|
if (entry.automated && e.automated)
|
|
218
449
|
return i;
|
|
@@ -228,6 +459,13 @@ export function deduplicateEntry(entry, entries) {
|
|
|
228
459
|
entry = { ...entry, raw: cleanNopMarkers(cleanRawText(entry.raw)) };
|
|
229
460
|
if (entry.detail)
|
|
230
461
|
entry = { ...entry, detail: cleanNopMarkers(entry.detail) };
|
|
462
|
+
// Task hierarchy entries bypass dedup — they're hierarchy markers keyed by
|
|
463
|
+
// taskId, not content. Two adjacent task_starts are legitimately distinct,
|
|
464
|
+
// and two `task_milestone` rows carrying identical raw ("Todos done") but
|
|
465
|
+
// different taskIds within the 8s exact-dedup window must not collapse.
|
|
466
|
+
if (entry.type === 'task_start' || entry.type === 'task_end' || entry.type === 'task_milestone') {
|
|
467
|
+
return { action: 'add', entry };
|
|
468
|
+
}
|
|
231
469
|
// 2. Exact dedup: skip if same type + raw within 8 seconds
|
|
232
470
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
233
471
|
const e = entries[i];
|
|
@@ -236,7 +474,14 @@ export function deduplicateEntry(entry, entries) {
|
|
|
236
474
|
if (e.type === entry.type && e.raw === entry.raw)
|
|
237
475
|
return { action: 'skip' };
|
|
238
476
|
}
|
|
239
|
-
// 3. Repetitive entry dedup (1h window)
|
|
477
|
+
// 3. Repetitive entry dedup (1h window). Interrupted turn-closes are exempt:
|
|
478
|
+
// they are structural pair-closers for a specific turn (their semantic core
|
|
479
|
+
// collapses to just "Interrupted", so two interrupted turns in the window
|
|
480
|
+
// would merge — silently reopening the earlier turn's spinner). The exact
|
|
481
|
+
// 8s dedup above still absorbs duplicate emits of the same close.
|
|
482
|
+
if (entry.type === 'chat_end' && /^Interrupted\b/.test(entry.raw)) {
|
|
483
|
+
return { action: 'add', entry };
|
|
484
|
+
}
|
|
240
485
|
const repIdx = isRepetitiveEntry(entry, entries);
|
|
241
486
|
if (repIdx >= 0) {
|
|
242
487
|
let removeChatStartIndex;
|
|
@@ -442,15 +687,8 @@ export function parseLogLine(json) {
|
|
|
442
687
|
if (/\b(inference|model|llm)\b.*\b(start|request|call|complet|done|response|finish)\b/i.test(message)) {
|
|
443
688
|
return null;
|
|
444
689
|
}
|
|
445
|
-
//
|
|
446
|
-
|
|
447
|
-
return { ts, type: 'memory_recall', raw: extractReadableMessage(message) };
|
|
448
|
-
}
|
|
449
|
-
// Tool/exec patterns in message text
|
|
450
|
-
if (/\b(tool|exec|execute|command)\b/i.test(message)) {
|
|
451
|
-
return { ts, type: 'tool_exec', raw: extractReadableMessage(message) };
|
|
452
|
-
}
|
|
453
|
-
// Unrecognized — skip
|
|
690
|
+
// Anything else — skip. Real chat/tool/error activity flows through the
|
|
691
|
+
// OpenClaw Gateway adapter's RPC events, not heuristic log parsing.
|
|
454
692
|
return null;
|
|
455
693
|
}
|
|
456
694
|
//# sourceMappingURL=timeline.js.map
|