@deepwatch/dsh-live 0.1.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/LICENSE +21 -0
- package/README.md +87 -0
- package/lib/capture.d.ts +155 -0
- package/lib/capture.js +301 -0
- package/lib/client/components.d.ts +55 -0
- package/lib/client/components.js +62 -0
- package/lib/client/index.d.ts +14 -0
- package/lib/client/index.js +22 -0
- package/lib/client/live-mode.d.ts +38 -0
- package/lib/client/live-mode.js +72 -0
- package/lib/client.js +951 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.ts +22 -0
- package/lib/index.js +22 -0
- package/lib/session.d.ts +216 -0
- package/lib/session.js +280 -0
- package/lib/sources-catalogue.d.ts +66 -0
- package/lib/sources-catalogue.js +110 -0
- package/lib/synthetic-source.d.ts +58 -0
- package/lib/synthetic-source.js +96 -0
- package/lib/triggers.d.ts +114 -0
- package/lib/triggers.js +138 -0
- package/package.json +97 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triggers: a rule that notices something, and cannot do anything about it.
|
|
3
|
+
*
|
|
4
|
+
* A live observation runs for a long time and the interesting second is
|
|
5
|
+
* usually not the one anybody is watching. So a trigger says "tell me when the
|
|
6
|
+
* deploy log says failed", or "pin the moment the stream drops", and the
|
|
7
|
+
* session keeps an eye out.
|
|
8
|
+
*
|
|
9
|
+
* The whole design question is what a trigger is allowed to *do*, and the
|
|
10
|
+
* answer here is deliberately narrow: it may pin a moment, raise a
|
|
11
|
+
* notification, or ask for a snapshot. It may not act. That is not a
|
|
12
|
+
* limitation waiting to be lifted — {@link TriggerEffect} has no member that
|
|
13
|
+
* touches the world, because a trigger fires on observed content, and a
|
|
14
|
+
* trigger that could act would be a page deciding to click something by
|
|
15
|
+
* putting the right words on screen.
|
|
16
|
+
*
|
|
17
|
+
* The second rule is a cooldown, which sounds like ergonomics and is not. A
|
|
18
|
+
* text trigger on a scrolling log fires on every line, and a thousand
|
|
19
|
+
* notifications is the same as none — except that it also buries the one that
|
|
20
|
+
* mattered.
|
|
21
|
+
*
|
|
22
|
+
* @module @deepwatch/dsh-live/triggers
|
|
23
|
+
*/
|
|
24
|
+
import type { LiveEvent, LiveEventKind, LiveSessionState } from './session.js';
|
|
25
|
+
/**
|
|
26
|
+
* What firing does.
|
|
27
|
+
*
|
|
28
|
+
* Three members, all of them observation. There is no `act`, and adding one
|
|
29
|
+
* would mean observed content could reach the operator loop without a person
|
|
30
|
+
* in between.
|
|
31
|
+
*/
|
|
32
|
+
export type TriggerEffect =
|
|
33
|
+
/** Keep the moment, so it survives the buffer bound. */
|
|
34
|
+
'pin'
|
|
35
|
+
/** Tell the person watching. */
|
|
36
|
+
| 'notify'
|
|
37
|
+
/** Ask for a fresh snapshot, for a trigger about continuity. */
|
|
38
|
+
| 'snapshot';
|
|
39
|
+
/** Every effect, so a UI can enumerate them and a test can check the set. */
|
|
40
|
+
export declare const TRIGGER_EFFECTS: readonly TriggerEffect[];
|
|
41
|
+
/** When a trigger fires. */
|
|
42
|
+
export type TriggerCondition =
|
|
43
|
+
/** Any event of this kind. */
|
|
44
|
+
{
|
|
45
|
+
readonly kind: 'event_kind';
|
|
46
|
+
readonly eventKind: LiveEventKind;
|
|
47
|
+
}
|
|
48
|
+
/** Observed text containing a phrase. */
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: 'text_contains';
|
|
51
|
+
readonly phrase: string;
|
|
52
|
+
readonly caseSensitive: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** A capture gap longer than a threshold. */
|
|
55
|
+
| {
|
|
56
|
+
readonly kind: 'gap_longer_than';
|
|
57
|
+
readonly ms: number;
|
|
58
|
+
}
|
|
59
|
+
/** Nothing observed for a while, which is itself information. */
|
|
60
|
+
| {
|
|
61
|
+
readonly kind: 'silence_for';
|
|
62
|
+
readonly ms: number;
|
|
63
|
+
};
|
|
64
|
+
/** One rule. */
|
|
65
|
+
export interface LiveTrigger {
|
|
66
|
+
readonly triggerId: string;
|
|
67
|
+
/** What the person called it. */
|
|
68
|
+
readonly label: string;
|
|
69
|
+
readonly when: TriggerCondition;
|
|
70
|
+
readonly effect: TriggerEffect;
|
|
71
|
+
/** How long after firing before it may fire again. */
|
|
72
|
+
readonly cooldownMs: number;
|
|
73
|
+
readonly enabled: boolean;
|
|
74
|
+
}
|
|
75
|
+
/** One firing. */
|
|
76
|
+
export interface TriggerFiring {
|
|
77
|
+
readonly triggerId: string;
|
|
78
|
+
readonly effect: TriggerEffect;
|
|
79
|
+
/** The event that caused it, when one did. */
|
|
80
|
+
readonly eventSeq: number | null;
|
|
81
|
+
readonly atMs: number;
|
|
82
|
+
/** One line for the notification. Presentation only. */
|
|
83
|
+
readonly reason: string;
|
|
84
|
+
}
|
|
85
|
+
/** When each trigger last fired, so a cooldown can be applied. */
|
|
86
|
+
export type TriggerCooldowns = ReadonlyMap<string, number>;
|
|
87
|
+
/**
|
|
88
|
+
* Evaluate triggers against newly arrived events.
|
|
89
|
+
*
|
|
90
|
+
* Pure, and takes the cooldown map rather than holding one, so the same
|
|
91
|
+
* function serves the live session and a replay of it. A trigger evaluator
|
|
92
|
+
* with internal state would produce different firings on replay than it did
|
|
93
|
+
* live, which would make a pinned moment unreproducible.
|
|
94
|
+
*/
|
|
95
|
+
export declare function evaluateTriggers(state: LiveSessionState, triggers: readonly LiveTrigger[], newEvents: readonly LiveEvent[], nowMs: number, cooldowns?: TriggerCooldowns): {
|
|
96
|
+
readonly firings: readonly TriggerFiring[];
|
|
97
|
+
readonly cooldowns: TriggerCooldowns;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Whether a firing may cause anything outside the session.
|
|
101
|
+
*
|
|
102
|
+
* Always false. Present as a function rather than as a comment so a caller
|
|
103
|
+
* that is about to route a firing somewhere has something to check, and so a
|
|
104
|
+
* test can assert the answer for every effect the type allows.
|
|
105
|
+
*/
|
|
106
|
+
export declare function mayAct(_firing: TriggerFiring): false;
|
|
107
|
+
/**
|
|
108
|
+
* One line describing what a trigger will do, for the panel that creates it.
|
|
109
|
+
*
|
|
110
|
+
* Says the effect in the words of what it does rather than its name, because
|
|
111
|
+
* "pin" is a word somebody could read as "act on".
|
|
112
|
+
*/
|
|
113
|
+
export declare function describeTrigger(trigger: LiveTrigger): string;
|
|
114
|
+
//# sourceMappingURL=triggers.d.ts.map
|
package/lib/triggers.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Triggers: a rule that notices something, and cannot do anything about it.
|
|
3
|
+
*
|
|
4
|
+
* A live observation runs for a long time and the interesting second is
|
|
5
|
+
* usually not the one anybody is watching. So a trigger says "tell me when the
|
|
6
|
+
* deploy log says failed", or "pin the moment the stream drops", and the
|
|
7
|
+
* session keeps an eye out.
|
|
8
|
+
*
|
|
9
|
+
* The whole design question is what a trigger is allowed to *do*, and the
|
|
10
|
+
* answer here is deliberately narrow: it may pin a moment, raise a
|
|
11
|
+
* notification, or ask for a snapshot. It may not act. That is not a
|
|
12
|
+
* limitation waiting to be lifted — {@link TriggerEffect} has no member that
|
|
13
|
+
* touches the world, because a trigger fires on observed content, and a
|
|
14
|
+
* trigger that could act would be a page deciding to click something by
|
|
15
|
+
* putting the right words on screen.
|
|
16
|
+
*
|
|
17
|
+
* The second rule is a cooldown, which sounds like ergonomics and is not. A
|
|
18
|
+
* text trigger on a scrolling log fires on every line, and a thousand
|
|
19
|
+
* notifications is the same as none — except that it also buries the one that
|
|
20
|
+
* mattered.
|
|
21
|
+
*
|
|
22
|
+
* @module @deepwatch/dsh-live/triggers
|
|
23
|
+
*/
|
|
24
|
+
/** Every effect, so a UI can enumerate them and a test can check the set. */
|
|
25
|
+
export const TRIGGER_EFFECTS = ['pin', 'notify', 'snapshot'];
|
|
26
|
+
/** Whether one event satisfies one condition. */
|
|
27
|
+
function matches(condition, event) {
|
|
28
|
+
switch (condition.kind) {
|
|
29
|
+
case 'event_kind':
|
|
30
|
+
return event.kind === condition.eventKind
|
|
31
|
+
? { hit: true, reason: `a ${condition.eventKind} event` }
|
|
32
|
+
: { hit: false, reason: '' };
|
|
33
|
+
case 'text_contains': {
|
|
34
|
+
const haystack = condition.caseSensitive ? event.text : event.text.toLowerCase();
|
|
35
|
+
const needle = condition.caseSensitive ? condition.phrase : condition.phrase.toLowerCase();
|
|
36
|
+
return haystack.includes(needle)
|
|
37
|
+
? { hit: true, reason: `observed text contains "${condition.phrase}"` }
|
|
38
|
+
: { hit: false, reason: '' };
|
|
39
|
+
}
|
|
40
|
+
case 'gap_longer_than': {
|
|
41
|
+
if (event.kind !== 'gap' || event.range === null)
|
|
42
|
+
return { hit: false, reason: '' };
|
|
43
|
+
const length = event.range.endMs - event.range.startMs;
|
|
44
|
+
return length > condition.ms
|
|
45
|
+
? { hit: true, reason: `a ${String(length)}ms capture gap` }
|
|
46
|
+
: { hit: false, reason: '' };
|
|
47
|
+
}
|
|
48
|
+
case 'silence_for':
|
|
49
|
+
// Silence is not a property of an event. Handled separately below.
|
|
50
|
+
return { hit: false, reason: '' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Evaluate triggers against newly arrived events.
|
|
55
|
+
*
|
|
56
|
+
* Pure, and takes the cooldown map rather than holding one, so the same
|
|
57
|
+
* function serves the live session and a replay of it. A trigger evaluator
|
|
58
|
+
* with internal state would produce different firings on replay than it did
|
|
59
|
+
* live, which would make a pinned moment unreproducible.
|
|
60
|
+
*/
|
|
61
|
+
export function evaluateTriggers(state, triggers, newEvents, nowMs, cooldowns = new Map()) {
|
|
62
|
+
const firings = [];
|
|
63
|
+
const updated = new Map(cooldowns);
|
|
64
|
+
/** Whether a trigger is allowed to fire right now. */
|
|
65
|
+
const ready = (trigger, atMs) => {
|
|
66
|
+
if (!trigger.enabled)
|
|
67
|
+
return false;
|
|
68
|
+
const last = updated.get(trigger.triggerId);
|
|
69
|
+
return last === undefined || atMs - last >= trigger.cooldownMs;
|
|
70
|
+
};
|
|
71
|
+
for (const trigger of triggers) {
|
|
72
|
+
if (trigger.when.kind === 'silence_for') {
|
|
73
|
+
// A silence trigger is about the absence of events, so it is evaluated
|
|
74
|
+
// against the clock rather than against a batch.
|
|
75
|
+
const newest = state.events[state.events.length - 1];
|
|
76
|
+
const since = newest === undefined ? nowMs - state.startedAtMs : nowMs - newest.at;
|
|
77
|
+
if (newEvents.length === 0 && since >= trigger.when.ms && ready(trigger, nowMs)) {
|
|
78
|
+
updated.set(trigger.triggerId, nowMs);
|
|
79
|
+
firings.push({
|
|
80
|
+
triggerId: trigger.triggerId,
|
|
81
|
+
effect: trigger.effect,
|
|
82
|
+
eventSeq: null,
|
|
83
|
+
atMs: nowMs,
|
|
84
|
+
reason: `nothing observed for ${String(since)}ms`,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
for (const event of newEvents) {
|
|
90
|
+
if (!ready(trigger, event.at))
|
|
91
|
+
continue;
|
|
92
|
+
const result = matches(trigger.when, event);
|
|
93
|
+
if (!result.hit)
|
|
94
|
+
continue;
|
|
95
|
+
updated.set(trigger.triggerId, event.at);
|
|
96
|
+
firings.push({
|
|
97
|
+
triggerId: trigger.triggerId,
|
|
98
|
+
effect: trigger.effect,
|
|
99
|
+
eventSeq: event.seq,
|
|
100
|
+
atMs: event.at,
|
|
101
|
+
reason: result.reason,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { firings, cooldowns: updated };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Whether a firing may cause anything outside the session.
|
|
109
|
+
*
|
|
110
|
+
* Always false. Present as a function rather than as a comment so a caller
|
|
111
|
+
* that is about to route a firing somewhere has something to check, and so a
|
|
112
|
+
* test can assert the answer for every effect the type allows.
|
|
113
|
+
*/
|
|
114
|
+
export function mayAct(_firing) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* One line describing what a trigger will do, for the panel that creates it.
|
|
119
|
+
*
|
|
120
|
+
* Says the effect in the words of what it does rather than its name, because
|
|
121
|
+
* "pin" is a word somebody could read as "act on".
|
|
122
|
+
*/
|
|
123
|
+
export function describeTrigger(trigger) {
|
|
124
|
+
const when = trigger.when.kind === 'event_kind'
|
|
125
|
+
? `on any ${trigger.when.eventKind} event`
|
|
126
|
+
: trigger.when.kind === 'text_contains'
|
|
127
|
+
? `when observed text contains "${trigger.when.phrase}"`
|
|
128
|
+
: trigger.when.kind === 'gap_longer_than'
|
|
129
|
+
? `when capture drops for more than ${String(trigger.when.ms)}ms`
|
|
130
|
+
: `when nothing is observed for ${String(trigger.when.ms)}ms`;
|
|
131
|
+
const does = trigger.effect === 'pin'
|
|
132
|
+
? 'keep the moment'
|
|
133
|
+
: trigger.effect === 'notify'
|
|
134
|
+
? 'tell you'
|
|
135
|
+
: 'ask for a fresh snapshot';
|
|
136
|
+
return `${when}, ${does}. A trigger never acts on what it sees.`;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=triggers.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@deepwatch/dsh-live",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Live mode — cursors, gaps, clocks, reconnect and a bounded buffer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Sayed Allam",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/oxbshw/watch-skill.git",
|
|
11
|
+
"directory": "workspace/packages/watch/live"
|
|
12
|
+
},
|
|
13
|
+
"homepage": "https://github.com/oxbshw/watch-skill/tree/main/workspace#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/oxbshw/watch-skill/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"deepwatch",
|
|
19
|
+
"deepseek-harness",
|
|
20
|
+
"watch-skill",
|
|
21
|
+
"live",
|
|
22
|
+
"capture"
|
|
23
|
+
],
|
|
24
|
+
"dsh": {
|
|
25
|
+
"client": {
|
|
26
|
+
"inject": [
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
28
|
+
],
|
|
29
|
+
"platform": "web"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"main": "lib/index.js",
|
|
33
|
+
"types": "lib/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./lib/index.d.ts",
|
|
37
|
+
"default": "./lib/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./capture": {
|
|
40
|
+
"types": "./lib/capture.d.ts",
|
|
41
|
+
"default": "./lib/capture.js"
|
|
42
|
+
},
|
|
43
|
+
"./client": {
|
|
44
|
+
"types": "./lib/client/index.d.ts",
|
|
45
|
+
"default": "./lib/client.js"
|
|
46
|
+
},
|
|
47
|
+
"./components": {
|
|
48
|
+
"types": "./lib/client/components.d.ts",
|
|
49
|
+
"default": "./lib/client/components.js"
|
|
50
|
+
},
|
|
51
|
+
"./live-mode": {
|
|
52
|
+
"types": "./lib/client/live-mode.d.ts",
|
|
53
|
+
"default": "./lib/client/live-mode.js"
|
|
54
|
+
},
|
|
55
|
+
"./package.json": "./package.json",
|
|
56
|
+
"./sources-catalogue": {
|
|
57
|
+
"types": "./lib/sources-catalogue.d.ts",
|
|
58
|
+
"default": "./lib/sources-catalogue.js"
|
|
59
|
+
},
|
|
60
|
+
"./synthetic-source": {
|
|
61
|
+
"types": "./lib/synthetic-source.d.ts",
|
|
62
|
+
"default": "./lib/synthetic-source.js"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"lib/index.js",
|
|
67
|
+
"lib/client.js",
|
|
68
|
+
"lib/client.js.map",
|
|
69
|
+
"lib/**/*.js",
|
|
70
|
+
"lib/**/*.d.ts"
|
|
71
|
+
],
|
|
72
|
+
"sideEffects": false,
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
75
|
+
},
|
|
76
|
+
"publishConfig": {
|
|
77
|
+
"access": "public"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@deepwatch/dsh-client-brand": "^0.1.0",
|
|
81
|
+
"@deepwatch/dsh-contracts": "^0.1.0",
|
|
82
|
+
"@deepwatch/dsh-workspace": "^0.1.0"
|
|
83
|
+
},
|
|
84
|
+
"peerDependencies": {
|
|
85
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
86
|
+
"react": "^18.2.0"
|
|
87
|
+
},
|
|
88
|
+
"devDependencies": {
|
|
89
|
+
"@deepseek-ai/cordis": "4.0.2",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
|
|
91
|
+
"@types/react": "~18.3.1",
|
|
92
|
+
"react": "^18.2.0"
|
|
93
|
+
},
|
|
94
|
+
"scripts": {
|
|
95
|
+
"bundle": "tsdown"
|
|
96
|
+
}
|
|
97
|
+
}
|