reflectt-node 0.1.20 → 0.1.22
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/README.md +64 -0
- package/dist/canvas-interactive.d.ts +53 -0
- package/dist/canvas-interactive.d.ts.map +1 -1
- package/dist/canvas-interactive.js +194 -0
- package/dist/canvas-interactive.js.map +1 -1
- package/dist/canvas-push.d.ts +2 -0
- package/dist/canvas-push.d.ts.map +1 -1
- package/dist/canvas-push.js +89 -0
- package/dist/canvas-push.js.map +1 -1
- package/dist/channels.d.ts +1 -1
- package/dist/intervention-template.d.ts +67 -0
- package/dist/intervention-template.d.ts.map +1 -0
- package/dist/intervention-template.js +290 -0
- package/dist/intervention-template.js.map +1 -0
- package/dist/request-tracker.d.ts +1 -0
- package/dist/request-tracker.d.ts.map +1 -1
- package/dist/request-tracker.js +16 -4
- package/dist/request-tracker.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +157 -4
- package/dist/server.js.map +1 -1
- package/dist/stall-detector.d.ts +109 -0
- package/dist/stall-detector.d.ts.map +1 -0
- package/dist/stall-detector.js +279 -0
- package/dist/stall-detector.js.map +1 -0
- package/package.json +1 -1
- package/public/docs.md +9 -0
- package/dist/agent-config.test.d.ts +0 -2
- package/dist/agent-config.test.d.ts.map +0 -1
- package/dist/agent-config.test.js +0 -91
- package/dist/agent-config.test.js.map +0 -1
- package/dist/agent-exec-guardrail.test.d.ts +0 -2
- package/dist/agent-exec-guardrail.test.d.ts.map +0 -1
- package/dist/agent-exec-guardrail.test.js +0 -55
- package/dist/agent-exec-guardrail.test.js.map +0 -1
- package/dist/agent-memories.test.d.ts +0 -2
- package/dist/agent-memories.test.d.ts.map +0 -1
- package/dist/agent-memories.test.js +0 -327
- package/dist/agent-memories.test.js.map +0 -1
- package/dist/agent-messaging.test.d.ts +0 -2
- package/dist/agent-messaging.test.d.ts.map +0 -1
- package/dist/agent-messaging.test.js +0 -105
- package/dist/agent-messaging.test.js.map +0 -1
- package/dist/agent-runs.test.d.ts +0 -2
- package/dist/agent-runs.test.d.ts.map +0 -1
- package/dist/agent-runs.test.js +0 -386
- package/dist/agent-runs.test.js.map +0 -1
- package/dist/api.test.d.ts +0 -2
- package/dist/api.test.d.ts.map +0 -1
- package/dist/api.test.js +0 -99
- package/dist/api.test.js.map +0 -1
- package/dist/approval-queue.test.d.ts +0 -2
- package/dist/approval-queue.test.d.ts.map +0 -1
- package/dist/approval-queue.test.js +0 -118
- package/dist/approval-queue.test.js.map +0 -1
- package/dist/artifact-store.test.d.ts +0 -2
- package/dist/artifact-store.test.d.ts.map +0 -1
- package/dist/artifact-store.test.js +0 -119
- package/dist/artifact-store.test.js.map +0 -1
- package/dist/canvas-input.test.d.ts +0 -2
- package/dist/canvas-input.test.d.ts.map +0 -1
- package/dist/canvas-input.test.js +0 -96
- package/dist/canvas-input.test.js.map +0 -1
- package/dist/canvas-render.test.d.ts +0 -2
- package/dist/canvas-render.test.d.ts.map +0 -1
- package/dist/canvas-render.test.js +0 -95
- package/dist/canvas-render.test.js.map +0 -1
- package/dist/e2e-loop-proof.test.d.ts +0 -2
- package/dist/e2e-loop-proof.test.d.ts.map +0 -1
- package/dist/e2e-loop-proof.test.js +0 -114
- package/dist/e2e-loop-proof.test.js.map +0 -1
- package/dist/email-sms-send.test.d.ts +0 -2
- package/dist/email-sms-send.test.d.ts.map +0 -1
- package/dist/email-sms-send.test.js +0 -96
- package/dist/email-sms-send.test.js.map +0 -1
- package/dist/handoff-state.test.d.ts +0 -2
- package/dist/handoff-state.test.d.ts.map +0 -1
- package/dist/handoff-state.test.js +0 -102
- package/dist/handoff-state.test.js.map +0 -1
- package/dist/routing-enforcement.test.d.ts +0 -2
- package/dist/routing-enforcement.test.d.ts.map +0 -1
- package/dist/routing-enforcement.test.js +0 -62
- package/dist/routing-enforcement.test.js.map +0 -1
- package/dist/run-retention.test.d.ts +0 -2
- package/dist/run-retention.test.d.ts.map +0 -1
- package/dist/run-retention.test.js +0 -57
- package/dist/run-retention.test.js.map +0 -1
- package/dist/run-stream.test.d.ts +0 -2
- package/dist/run-stream.test.d.ts.map +0 -1
- package/dist/run-stream.test.js +0 -70
- package/dist/run-stream.test.js.map +0 -1
- package/dist/webhook-storage.test.d.ts +0 -2
- package/dist/webhook-storage.test.d.ts.map +0 -1
- package/dist/webhook-storage.test.js +0 -86
- package/dist/webhook-storage.test.js.map +0 -1
- package/dist/workflow-canvas-state.test.d.ts +0 -2
- package/dist/workflow-canvas-state.test.d.ts.map +0 -1
- package/dist/workflow-canvas-state.test.js +0 -53
- package/dist/workflow-canvas-state.test.js.map +0 -1
- package/dist/workflow-templates.test.d.ts +0 -2
- package/dist/workflow-templates.test.d.ts.map +0 -1
- package/dist/workflow-templates.test.js +0 -76
- package/dist/workflow-templates.test.js.map +0 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StallDetector — event-driven stall detection
|
|
3
|
+
*
|
|
4
|
+
* Detects when users go inactive during key lifecycle moments:
|
|
5
|
+
* - New user stall: 4 min inactivity post-first-action
|
|
6
|
+
* - In-session stall: 6 min inactivity post-agent-response
|
|
7
|
+
* - Setup stall: 5 min onboarding inactivity
|
|
8
|
+
*
|
|
9
|
+
* Emits stall events that the InterventionTemplateEngine can consume.
|
|
10
|
+
*/
|
|
11
|
+
export type StallType = 'new_user_stall' | 'in_session_stall' | 'setup_stall' | 'task_stalled' | 'review_pending' | 'handoff_waiting' | 'approval_pending';
|
|
12
|
+
export type SessionPhase = 'new_user' | 'in_session' | 'setup';
|
|
13
|
+
export interface StallContext {
|
|
14
|
+
userId: string;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
lastAction?: string;
|
|
17
|
+
lastAgent?: string;
|
|
18
|
+
lastActionAt?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface StallEvent {
|
|
21
|
+
stallId: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
sessionId: string;
|
|
24
|
+
stallType: StallType;
|
|
25
|
+
context: StallContext;
|
|
26
|
+
timestamp: number;
|
|
27
|
+
thresholdMinutes: number;
|
|
28
|
+
inactivityMinutes: number;
|
|
29
|
+
}
|
|
30
|
+
export interface StallThresholds {
|
|
31
|
+
newUserStallMinutes: number;
|
|
32
|
+
inSessionStallMinutes: number;
|
|
33
|
+
setupStallMinutes: number;
|
|
34
|
+
}
|
|
35
|
+
export interface StallDetectorConfig {
|
|
36
|
+
thresholds: StallThresholds;
|
|
37
|
+
enabled: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const DEFAULT_THRESHOLDS: StallThresholds;
|
|
40
|
+
export declare const DEFAULT_CONFIG: StallDetectorConfig;
|
|
41
|
+
type StallEventHandler = (event: StallEvent) => void;
|
|
42
|
+
export declare function onStallEvent(handler: StallEventHandler): () => void;
|
|
43
|
+
/**
|
|
44
|
+
* Emit a workflow stall event (task_stalled, review_pending, etc.)
|
|
45
|
+
* Call this from task lifecycle events — no session tracking needed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function emitWorkflowStall(userId: string, stallType: 'task_stalled' | 'review_pending' | 'handoff_waiting' | 'approval_pending', context?: {
|
|
48
|
+
lastAction?: string;
|
|
49
|
+
lastAgent?: string;
|
|
50
|
+
lastActionAt?: number;
|
|
51
|
+
}): void;
|
|
52
|
+
interface UserSessionState {
|
|
53
|
+
userId: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
phase: SessionPhase;
|
|
56
|
+
lastActionAt: number;
|
|
57
|
+
lastAgentResponseAt?: number;
|
|
58
|
+
firstActionAt?: number;
|
|
59
|
+
stallFired: Set<string>;
|
|
60
|
+
context?: StallContext;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Record a user action (message sent, button clicked, etc.)
|
|
64
|
+
*/
|
|
65
|
+
export declare function recordUserAction(userId: string, sessionId: string, action: string, timestamp?: number): void;
|
|
66
|
+
/**
|
|
67
|
+
* Record an agent response
|
|
68
|
+
*/
|
|
69
|
+
export declare function recordAgentResponse(userId: string, sessionId: string, agentName: string, timestamp?: number): void;
|
|
70
|
+
/**
|
|
71
|
+
* Check all sessions for stalls and emit events
|
|
72
|
+
*/
|
|
73
|
+
export declare function checkForStalls(config?: StallDetectorConfig, now?: number): StallEvent[];
|
|
74
|
+
/**
|
|
75
|
+
* Transition a session's phase
|
|
76
|
+
*/
|
|
77
|
+
export declare function transitionSessionPhase(userId: string, sessionId: string, newPhase: SessionPhase): void;
|
|
78
|
+
/**
|
|
79
|
+
* Clear a session (user completed the flow)
|
|
80
|
+
*/
|
|
81
|
+
export declare function clearSession(userId: string, sessionId: string): void;
|
|
82
|
+
/**
|
|
83
|
+
* Get current state for a session
|
|
84
|
+
*/
|
|
85
|
+
export declare function getSessionState(userId: string, sessionId: string): UserSessionState | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* Get all active sessions
|
|
88
|
+
*/
|
|
89
|
+
export declare function getActiveSessions(): UserSessionState[];
|
|
90
|
+
/**
|
|
91
|
+
* Cleanup old sessions (call periodically)
|
|
92
|
+
*/
|
|
93
|
+
export declare function cleanupStaleSessions(maxAgeMs?: number): number;
|
|
94
|
+
export declare function _resetStallDetectorState(): void;
|
|
95
|
+
export declare class StallDetector {
|
|
96
|
+
private config;
|
|
97
|
+
constructor(config?: Partial<StallDetectorConfig>);
|
|
98
|
+
getAllStates(): UserSessionState[];
|
|
99
|
+
getState(userId: string): UserSessionState | undefined;
|
|
100
|
+
recordActivity(userId: string, options?: {
|
|
101
|
+
phase?: SessionPhase;
|
|
102
|
+
sessionId?: string;
|
|
103
|
+
}): void;
|
|
104
|
+
recordAgentResponse(userId: string, agentId: string): void;
|
|
105
|
+
start(): void;
|
|
106
|
+
}
|
|
107
|
+
export declare function getStallDetector(): StallDetector;
|
|
108
|
+
export {};
|
|
109
|
+
//# sourceMappingURL=stall-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-detector.d.ts","sourceRoot":"","sources":["../src/stall-detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* StallDetector — event-driven stall detection
|
|
4
|
+
*
|
|
5
|
+
* Detects when users go inactive during key lifecycle moments:
|
|
6
|
+
* - New user stall: 4 min inactivity post-first-action
|
|
7
|
+
* - In-session stall: 6 min inactivity post-agent-response
|
|
8
|
+
* - Setup stall: 5 min onboarding inactivity
|
|
9
|
+
*
|
|
10
|
+
* Emits stall events that the InterventionTemplateEngine can consume.
|
|
11
|
+
*/
|
|
12
|
+
export const DEFAULT_THRESHOLDS = {
|
|
13
|
+
newUserStallMinutes: 4,
|
|
14
|
+
inSessionStallMinutes: 6,
|
|
15
|
+
setupStallMinutes: 5,
|
|
16
|
+
};
|
|
17
|
+
export const DEFAULT_CONFIG = {
|
|
18
|
+
thresholds: DEFAULT_THRESHOLDS,
|
|
19
|
+
enabled: false, // Defaults to false until validated
|
|
20
|
+
};
|
|
21
|
+
const stallHandlers = new Set();
|
|
22
|
+
export function onStallEvent(handler) {
|
|
23
|
+
stallHandlers.add(handler);
|
|
24
|
+
return () => stallHandlers.delete(handler);
|
|
25
|
+
}
|
|
26
|
+
function emitStallEvent(event) {
|
|
27
|
+
for (const handler of stallHandlers) {
|
|
28
|
+
try {
|
|
29
|
+
handler(event);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
console.error('[StallDetector] Handler error:', err);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Emit a workflow stall event (task_stalled, review_pending, etc.)
|
|
38
|
+
* Call this from task lifecycle events — no session tracking needed.
|
|
39
|
+
*/
|
|
40
|
+
export function emitWorkflowStall(userId, stallType, context = {}) {
|
|
41
|
+
const event = {
|
|
42
|
+
stallId: `workflow-${userId}-${stallType}-${Date.now()}`,
|
|
43
|
+
userId,
|
|
44
|
+
sessionId: `workflow:${stallType}`,
|
|
45
|
+
stallType,
|
|
46
|
+
context: { userId, sessionId: `workflow:${stallType}`, ...context },
|
|
47
|
+
timestamp: Date.now(),
|
|
48
|
+
thresholdMinutes: 5, // default, overridden by caller
|
|
49
|
+
inactivityMinutes: 0,
|
|
50
|
+
};
|
|
51
|
+
emitStallEvent(event);
|
|
52
|
+
}
|
|
53
|
+
const sessionStates = new Map(); // Key: `${userId}:${sessionId}`
|
|
54
|
+
// Track last check time to avoid duplicate stalls
|
|
55
|
+
const lastCheckTimes = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Record a user action (message sent, button clicked, etc.)
|
|
58
|
+
*/
|
|
59
|
+
export function recordUserAction(userId, sessionId, action, timestamp = Date.now()) {
|
|
60
|
+
const key = `${userId}:${sessionId}`;
|
|
61
|
+
let state = sessionStates.get(key);
|
|
62
|
+
if (!state) {
|
|
63
|
+
state = {
|
|
64
|
+
userId,
|
|
65
|
+
sessionId,
|
|
66
|
+
phase: 'new_user',
|
|
67
|
+
lastActionAt: timestamp,
|
|
68
|
+
stallFired: new Set(),
|
|
69
|
+
};
|
|
70
|
+
sessionStates.set(key, state);
|
|
71
|
+
}
|
|
72
|
+
state.lastActionAt = timestamp;
|
|
73
|
+
if (!state.firstActionAt) {
|
|
74
|
+
state.firstActionAt = timestamp;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Record an agent response
|
|
79
|
+
*/
|
|
80
|
+
export function recordAgentResponse(userId, sessionId, agentName, timestamp = Date.now()) {
|
|
81
|
+
const key = `${userId}:${sessionId}`;
|
|
82
|
+
let state = sessionStates.get(key);
|
|
83
|
+
if (!state) {
|
|
84
|
+
// Agent responded but we have no user state - create it
|
|
85
|
+
state = {
|
|
86
|
+
userId,
|
|
87
|
+
sessionId,
|
|
88
|
+
phase: 'setup',
|
|
89
|
+
lastActionAt: timestamp,
|
|
90
|
+
stallFired: new Set(),
|
|
91
|
+
};
|
|
92
|
+
sessionStates.set(key, state);
|
|
93
|
+
}
|
|
94
|
+
state.lastAgentResponseAt = timestamp;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check all sessions for stalls and emit events
|
|
98
|
+
*/
|
|
99
|
+
export function checkForStalls(config = DEFAULT_CONFIG, now = Date.now()) {
|
|
100
|
+
if (!config.enabled)
|
|
101
|
+
return [];
|
|
102
|
+
// now injected for testability (defaults to Date.now())
|
|
103
|
+
const emitted = [];
|
|
104
|
+
for (const [key, state] of sessionStates) {
|
|
105
|
+
// Skip if stall already fired for this session
|
|
106
|
+
if (state.stallFired.size > 0)
|
|
107
|
+
continue;
|
|
108
|
+
// Skip if checked recently (within 30 seconds)
|
|
109
|
+
const lastCheck = lastCheckTimes.get(key) ?? 0;
|
|
110
|
+
if (now - lastCheck < 30_000)
|
|
111
|
+
continue;
|
|
112
|
+
lastCheckTimes.set(key, now);
|
|
113
|
+
let thresholdMinutes;
|
|
114
|
+
let stallType;
|
|
115
|
+
let inactivityMs;
|
|
116
|
+
if (state.phase === 'new_user') {
|
|
117
|
+
thresholdMinutes = config.thresholds.newUserStallMinutes;
|
|
118
|
+
stallType = 'new_user_stall';
|
|
119
|
+
inactivityMs = state.lastAgentResponseAt
|
|
120
|
+
? now - state.lastAgentResponseAt
|
|
121
|
+
: now - state.lastActionAt;
|
|
122
|
+
}
|
|
123
|
+
else if (state.phase === 'in_session') {
|
|
124
|
+
thresholdMinutes = config.thresholds.inSessionStallMinutes;
|
|
125
|
+
stallType = 'in_session_stall';
|
|
126
|
+
inactivityMs = state.lastAgentResponseAt
|
|
127
|
+
? now - state.lastAgentResponseAt
|
|
128
|
+
: now - state.lastActionAt;
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
thresholdMinutes = config.thresholds.setupStallMinutes;
|
|
132
|
+
stallType = 'setup_stall';
|
|
133
|
+
inactivityMs = now - state.lastActionAt;
|
|
134
|
+
}
|
|
135
|
+
const thresholdMs = thresholdMinutes * 60_000;
|
|
136
|
+
if (inactivityMs >= thresholdMs) {
|
|
137
|
+
// Stall detected!
|
|
138
|
+
state.stallFired.add(stallType);
|
|
139
|
+
const event = {
|
|
140
|
+
stallId: `stall-${key}-${now}`,
|
|
141
|
+
userId: state.userId,
|
|
142
|
+
sessionId: state.sessionId,
|
|
143
|
+
stallType,
|
|
144
|
+
context: {
|
|
145
|
+
userId: state.userId,
|
|
146
|
+
sessionId: state.sessionId,
|
|
147
|
+
lastAction: 'user_action',
|
|
148
|
+
lastAgent: 'agent',
|
|
149
|
+
lastActionAt: state.lastActionAt,
|
|
150
|
+
},
|
|
151
|
+
timestamp: now,
|
|
152
|
+
thresholdMinutes,
|
|
153
|
+
inactivityMinutes: Math.round(inactivityMs / 60_000),
|
|
154
|
+
};
|
|
155
|
+
emitStallEvent(event);
|
|
156
|
+
emitted.push(event);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return emitted;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Transition a session's phase
|
|
163
|
+
*/
|
|
164
|
+
export function transitionSessionPhase(userId, sessionId, newPhase) {
|
|
165
|
+
const key = `${userId}:${sessionId}`;
|
|
166
|
+
const state = sessionStates.get(key);
|
|
167
|
+
if (state) {
|
|
168
|
+
state.phase = newPhase;
|
|
169
|
+
// Reset stall fired when transitioning phases
|
|
170
|
+
state.stallFired.clear();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Clear a session (user completed the flow)
|
|
175
|
+
*/
|
|
176
|
+
export function clearSession(userId, sessionId) {
|
|
177
|
+
const key = `${userId}:${sessionId}`;
|
|
178
|
+
sessionStates.delete(key);
|
|
179
|
+
lastCheckTimes.delete(key);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Get current state for a session
|
|
183
|
+
*/
|
|
184
|
+
export function getSessionState(userId, sessionId) {
|
|
185
|
+
return sessionStates.get(`${userId}:${sessionId}`);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Get all active sessions
|
|
189
|
+
*/
|
|
190
|
+
export function getActiveSessions() {
|
|
191
|
+
return [...sessionStates.values()];
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Cleanup old sessions (call periodically)
|
|
195
|
+
*/
|
|
196
|
+
export function cleanupStaleSessions(maxAgeMs = 30 * 60 * 1000) {
|
|
197
|
+
const now = Date.now();
|
|
198
|
+
let cleaned = 0;
|
|
199
|
+
for (const [key, state] of sessionStates) {
|
|
200
|
+
const lastActivity = state.lastAgentResponseAt ?? state.lastActionAt;
|
|
201
|
+
if (now - lastActivity > maxAgeMs) {
|
|
202
|
+
sessionStates.delete(key);
|
|
203
|
+
lastCheckTimes.delete(key);
|
|
204
|
+
cleaned++;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return cleaned;
|
|
208
|
+
}
|
|
209
|
+
// For testing: reset all state
|
|
210
|
+
export function _resetStallDetectorState() {
|
|
211
|
+
sessionStates.clear();
|
|
212
|
+
lastCheckTimes.clear();
|
|
213
|
+
}
|
|
214
|
+
// ── StallDetector class (for test compatibility) ─────────────────────────────────
|
|
215
|
+
let _stallDetectorInstance = null;
|
|
216
|
+
export class StallDetector {
|
|
217
|
+
config;
|
|
218
|
+
constructor(config = {}) {
|
|
219
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
220
|
+
}
|
|
221
|
+
getAllStates() {
|
|
222
|
+
return [...sessionStates.values()];
|
|
223
|
+
}
|
|
224
|
+
getState(userId) {
|
|
225
|
+
for (const state of sessionStates.values()) {
|
|
226
|
+
if (state.userId === userId)
|
|
227
|
+
return state;
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
recordActivity(userId, options) {
|
|
232
|
+
const now = Date.now();
|
|
233
|
+
let state = this.getState(userId);
|
|
234
|
+
const sessionId = options?.sessionId ?? `session-${userId}`;
|
|
235
|
+
const key = `${userId}:${sessionId}`;
|
|
236
|
+
if (!state) {
|
|
237
|
+
state = { userId, sessionId, phase: options?.phase ?? 'new_user', lastActionAt: now, stallFired: new Set() };
|
|
238
|
+
sessionStates.set(key, state);
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
if (options?.phase)
|
|
242
|
+
state.phase = options.phase;
|
|
243
|
+
state.lastActionAt = now;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
recordAgentResponse(userId, agentId) {
|
|
247
|
+
const now = Date.now();
|
|
248
|
+
for (const state of sessionStates.values()) {
|
|
249
|
+
if (state.userId === userId) {
|
|
250
|
+
state.lastAgentResponseAt = now;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
start() {
|
|
256
|
+
// Periodic stall check — run every 60s when enabled
|
|
257
|
+
if (this.config.enabled)
|
|
258
|
+
return; // already running
|
|
259
|
+
this.config.enabled = true;
|
|
260
|
+
setInterval(() => {
|
|
261
|
+
if (!this.config.enabled)
|
|
262
|
+
return;
|
|
263
|
+
try {
|
|
264
|
+
checkForStalls(this.config);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
console.error('[StallDetector] Check error:', err);
|
|
268
|
+
}
|
|
269
|
+
}, 60_000);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// Singleton accessor
|
|
273
|
+
export function getStallDetector() {
|
|
274
|
+
if (!_stallDetectorInstance) {
|
|
275
|
+
_stallDetectorInstance = new StallDetector();
|
|
276
|
+
}
|
|
277
|
+
return _stallDetectorInstance;
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=stall-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stall-detector.js","sourceRoot":"","sources":["../src/stall-detector.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/public/docs.md
CHANGED
|
@@ -141,6 +141,9 @@ Remote hosts (multi-host installs) phone-home via a lightweight heartbeat so the
|
|
|
141
141
|
| POST | `/health/mention-rescue/tick` | Trigger mention-rescue fallback |
|
|
142
142
|
| POST | `/health/working-contract/tick` | Evaluate working-contract enforcement: auto-requeue stale doing tasks (90m warning → 15m grace → auto todo) and fire alerts. |
|
|
143
143
|
| GET | `/health/working-contract/gate/:agent` | Dry-run claim gate check for an agent. Returns `{ allowed, reason }` — whether the agent can claim a new task given current WIP and contract status. |
|
|
144
|
+
| GET | `/stall-detector` | Stall detection status: enabled flag and all tracked session states. Returns `{ enabled, states: [{ userId, phase, context, stallFired[] }] }`. |
|
|
145
|
+
| POST | `/stall-detector/config` | Update stall detector config. Body: `{ enabled?: boolean, thresholds?: { newUserStallMinutes?, inSessionStallMinutes?, setupStallMinutes? } }`. Returns `{ success, config }`. |
|
|
146
|
+
| POST | `/stall-detector/test` | Record test activity for a userId. Body: `{ userId }`. Returns `{ success, message }`. |
|
|
144
147
|
|
|
145
148
|
### Quick system-loop check
|
|
146
149
|
|
|
@@ -836,6 +839,7 @@ Autonomous work-continuity system. Monitors agent queue floors and auto-replenis
|
|
|
836
839
|
| GET | `/canvas/history` | Recent render history (?slot=&limit=) |
|
|
837
840
|
| GET | `/canvas/rejections` | Recent contract validation rejections |
|
|
838
841
|
| GET | `/canvas/stream` | SSE stream of canvas render events |
|
|
842
|
+
| GET | `/canvas/viewers` | Returns `{ viewers: number }` — count of open SSE connections to `/canvas/stream`. Used by reflectt-cloud /live page to show real-time viewer count. |
|
|
839
843
|
| GET | `/execution-health` | Execution sweeper status: validating queue violations, SLA breaches, escalation tracking. |
|
|
840
844
|
| POST | `/pr-event` | PR state webhook. Body: `{ taskId, prState: "merged"|"closed", prUrl? }`. Auto-updates task artifacts on merge, auto-blocks on close. |
|
|
841
845
|
| GET | `/pr-automerge/status` | PR auto-merge attempt log: recent merge/close attempts with summary counts (attempted, success, failed, skipped, auto-close, close-gate-fail). |
|
|
@@ -1167,10 +1171,15 @@ Auth-gated endpoints for managing a reflectt-node instance remotely. Provide `RE
|
|
|
1167
1171
|
| POST | `/canvas/takeover/release` | **Release takeover** — agent gives back the screen. Body: `{ agentId, transition?: 'fade'\|'slide'\|'instant' }`. Only the owning agent can release. Emits `canvas_takeover` with `action: 'release'`. |
|
|
1168
1172
|
| GET | `/canvas/takeover` | **Takeover state** — returns current takeover status: `{ active, agentId?, id?, title?, startedAt?, expiresAt?, remainingMs? }`. |
|
|
1169
1173
|
| GET | `/canvas/render/stream` | **Reality Mixer SSE stream** — subscribe to receive real-time medium commands from agents. New subscribers get last 20 commands for catch-up (event type `replay`). Live commands arrive as `data` events. Shape: `{ id, ts, agentId, cmd: { type, ...fields } }`. |
|
|
1174
|
+
| GET | `/canvas/capability` | **Get all agent capabilities** — returns registered capabilities for all agents. Returns `{ capabilities: AgentCapabilities[] }`. |
|
|
1175
|
+
| POST | `/canvas/capability` | **Register agent capabilities** — register or update capabilities. Body: `{ agentId, agentName?, capabilities: Capability[] }`. Broadcasts `capability_setup` SSE event to `/canvas/render/stream` subscribers. Returns `{ ok, agentId, count }`. |
|
|
1176
|
+
| GET | `/audio/:id` | **Serve cached TTS audio** — serves MP3 audio cached from Kokoro TTS. Audio cached for 30 minutes by SHA256(text + voice) key. Returns 404 if not found or expired. |
|
|
1177
|
+
| POST | `/canvas/speak` | **Generate TTS audio + emit voice_output SSE** — generates speech via Kokoro TTS (with ElevenLabs fallback), caches result, emits `voice_queued` immediately + `voice_output` with audio URL to all `/canvas/render/stream` subscribers. Body: `{ text, agentId, agentName? }`. Returns `{ ok, voiceId, estimatedMs }` immediately. `voice_output` SSE fires when audio is ready (~90s cold start). Voice map: link/safari→af_sarah, kai/kotlin/pixel/echo/harmony→af_nicole, rhythm→af_james, bookkeeper/sage→bf_emma. |
|
|
1170
1178
|
| GET | `/canvas/activity-stream` | **Canvas activity SSE** — replays last 20 canvas events on connect (event: `backfill`, includes `_staggerMs` hint for animated replay at 50ms intervals), then streams live events (event: `activity`). Event types: canvas_message, canvas_render, canvas_expression, canvas_burst. `backfill_done` event signals replay complete. Canvas feels alive from frame 1. |
|
|
1171
1179
|
| GET | `/canvas/attention` | **Single highest-priority attention item** for the canvas viewer. Query: `?viewer=human` (default). Returns `{ item: { source, priority, title, detail?, taskId?, prUrl?, agentId?, actionLabel, actionType, notificationId? } \| null, pendingCount: number }`. Priority order: critical/high notifications → validating tasks needing review → blocked tasks → remaining notifications. |
|
|
1172
1180
|
| GET | `/canvas/pulse` | SSE stream emitting a heartbeat tick every 2s. Also emits real-time named events: `canvas_burst` (dramatic state transitions), `canvas_spark` (agent arcs), `canvas_milestone` (task_complete/pr_merged — the room exhaling), `canvas_message` (query response cards), `canvas_push` (agent proactive emissions). Connect once, animate forever. Tick shape: `{ t, agents: [{ id, state, urgency, activeSpeaker, color, age }], team: { rhythm, tension, ambientPulse, dominantColor } }`. |
|
|
1173
1181
|
| POST | `/canvas/push` | **Proactive canvas** — agent self-initiates a canvas event without a human query. Types: `utterance` (text floats from orb, max 60 chars, default TTL 4000ms), `work_released` (release pulse when work ships, intensity 0–1), `handoff` (arc between agents when work moves, requires `toAgentId`), `canvas_response` (agent responds to a canvas query with a structured card — requires `card` object with `type` field, optional `query` string; also emits `canvas_message` for living-canvas rendering), `rich` (arbitrary visual content — agents paint the canvas with markdown, code, images, SVG, or HTML; requires `content` object with any of: `markdown`, `code`, `language`, `image` (URL), `svg`, `html`, `title`; optional `position: {x,y}`, `layer: foreground\|midground\|background`, `size: {w,h}`, `ttl` up to 120s default 30s). All types emit `canvas_push` on the pulse SSE stream. Body: `{ type, agentId, text?, ttl?, intensity?, toAgentId?, taskTitle?, card?, query?, content?, position?, layer?, size? }`. |
|
|
1182
|
+
| POST | `/canvas/welcome` | **First-wow auto-welcome** — triggers an immediate greeting when a visitor loads the canvas. Selects a random active agent, creates a welcome task assigned to them, and emits a canvas push greeting. Agents have personalized greetings. Also emits `canvas_first_action` activation event. Returns `{ success, agentId, greeting, taskId }`. |
|
|
1174
1183
|
| POST | `/canvas/artifact` | **Proof artifact stream** — emit a proof card that drifts through the canvas. Types: `commit`, `pr`, `test`, `run`, `approval`. Also fires automatically on task completion (approval type) and PR merge (pr type) via `/canvas/victory`. Payload: `{ type, agentId, title (max 80 chars), url?, taskId? }`. Emits `canvas_artifact` event on pulse SSE. |
|
|
1175
1184
|
| POST | `/canvas/victory` | **The Victory** — whole team acknowledges a PR merge. Fires `canvas_expression { _victory: true }` (gold flash + celebration + resolve sound) then a `_victoryWave` per active agent staggered 350ms apart. Body: `{ prUrl, agentId, prTitle?, prNumber?, intensity? }`. Returns: `{ success, prNumber, intensity, wave: [{ agentId, delay }] }`. |
|
|
1176
1185
|
| GET | `/canvas/flow-score` | **Team flow metric** — real-time 0–1 composite score. Factors: active agents (30%), state distribution (35%), expression velocity last 5m (25%), time of day (10%). Labels: surge/flow/grinding/quiet/idle. <50ms. Returns: `{ score, label, factors, activeAgents, expressionsLast5m }`. |
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-config.test.d.ts","sourceRoot":"","sources":["../src/agent-config.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
// Test the cost cap logic in isolation
|
|
5
|
-
function checkCostCap(config, dailySpend, monthlySpend) {
|
|
6
|
-
if (!config)
|
|
7
|
-
return { allowed: true, action: 'allow' };
|
|
8
|
-
let action = 'allow';
|
|
9
|
-
if (config.costCapDaily !== null) {
|
|
10
|
-
const remaining = config.costCapDaily - dailySpend;
|
|
11
|
-
if (remaining <= 0)
|
|
12
|
-
action = 'deny';
|
|
13
|
-
else if (remaining < config.costCapDaily * 0.1)
|
|
14
|
-
action = 'downgrade';
|
|
15
|
-
else if (remaining < config.costCapDaily * 0.2)
|
|
16
|
-
action = 'warn';
|
|
17
|
-
}
|
|
18
|
-
if (config.costCapMonthly !== null) {
|
|
19
|
-
const remaining = config.costCapMonthly - monthlySpend;
|
|
20
|
-
if (remaining <= 0)
|
|
21
|
-
action = 'deny';
|
|
22
|
-
else if (remaining < config.costCapMonthly * 0.1 && action !== 'deny')
|
|
23
|
-
action = 'downgrade';
|
|
24
|
-
else if (remaining < config.costCapMonthly * 0.2 && action === 'allow')
|
|
25
|
-
action = 'warn';
|
|
26
|
-
}
|
|
27
|
-
return { allowed: action !== 'deny', action };
|
|
28
|
-
}
|
|
29
|
-
describe('agent config cost enforcement', () => {
|
|
30
|
-
it('allows when no config exists', () => {
|
|
31
|
-
const r = checkCostCap(null, 5, 50);
|
|
32
|
-
assert.equal(r.allowed, true);
|
|
33
|
-
assert.equal(r.action, 'allow');
|
|
34
|
-
});
|
|
35
|
-
it('allows when no caps are set', () => {
|
|
36
|
-
const r = checkCostCap({ costCapDaily: null, costCapMonthly: null, model: null, fallbackModel: null }, 5, 50);
|
|
37
|
-
assert.equal(r.allowed, true);
|
|
38
|
-
assert.equal(r.action, 'allow');
|
|
39
|
-
});
|
|
40
|
-
it('allows when well under daily cap', () => {
|
|
41
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: null, model: 'opus', fallbackModel: 'sonnet' }, 3, 0);
|
|
42
|
-
assert.equal(r.allowed, true);
|
|
43
|
-
assert.equal(r.action, 'allow');
|
|
44
|
-
});
|
|
45
|
-
it('warns at 80% daily spend', () => {
|
|
46
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: null, model: 'opus', fallbackModel: 'sonnet' }, 8.5, 0);
|
|
47
|
-
assert.equal(r.allowed, true);
|
|
48
|
-
assert.equal(r.action, 'warn');
|
|
49
|
-
});
|
|
50
|
-
it('downgrades at 90% daily spend', () => {
|
|
51
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: null, model: 'opus', fallbackModel: 'sonnet' }, 9.5, 0);
|
|
52
|
-
assert.equal(r.allowed, true);
|
|
53
|
-
assert.equal(r.action, 'downgrade');
|
|
54
|
-
});
|
|
55
|
-
it('denies at 100% daily spend', () => {
|
|
56
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: null, model: 'opus', fallbackModel: 'sonnet' }, 10, 0);
|
|
57
|
-
assert.equal(r.allowed, false);
|
|
58
|
-
assert.equal(r.action, 'deny');
|
|
59
|
-
});
|
|
60
|
-
it('denies when over daily cap', () => {
|
|
61
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: null, model: 'opus', fallbackModel: 'sonnet' }, 12, 0);
|
|
62
|
-
assert.equal(r.allowed, false);
|
|
63
|
-
assert.equal(r.action, 'deny');
|
|
64
|
-
});
|
|
65
|
-
it('monthly cap denies overriding daily allow', () => {
|
|
66
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: 100, model: null, fallbackModel: null }, 3, 100);
|
|
67
|
-
assert.equal(r.allowed, false);
|
|
68
|
-
assert.equal(r.action, 'deny');
|
|
69
|
-
});
|
|
70
|
-
it('monthly warn when daily is fine', () => {
|
|
71
|
-
const r = checkCostCap({ costCapDaily: null, costCapMonthly: 100, model: null, fallbackModel: null }, 0, 85);
|
|
72
|
-
assert.equal(r.allowed, true);
|
|
73
|
-
assert.equal(r.action, 'warn');
|
|
74
|
-
});
|
|
75
|
-
it('monthly downgrade at 90%', () => {
|
|
76
|
-
const r = checkCostCap({ costCapDaily: null, costCapMonthly: 100, model: null, fallbackModel: null }, 0, 95);
|
|
77
|
-
assert.equal(r.allowed, true);
|
|
78
|
-
assert.equal(r.action, 'downgrade');
|
|
79
|
-
});
|
|
80
|
-
it('both caps — tighter one wins', () => {
|
|
81
|
-
// Daily is at 95% (downgrade), monthly is fine
|
|
82
|
-
const r = checkCostCap({ costCapDaily: 10, costCapMonthly: 1000, model: 'opus', fallbackModel: 'sonnet' }, 9.5, 50);
|
|
83
|
-
assert.equal(r.action, 'downgrade');
|
|
84
|
-
});
|
|
85
|
-
it('zero cap always denies', () => {
|
|
86
|
-
const r = checkCostCap({ costCapDaily: 0, costCapMonthly: null, model: null, fallbackModel: null }, 0, 0);
|
|
87
|
-
assert.equal(r.allowed, false);
|
|
88
|
-
assert.equal(r.action, 'deny');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
//# sourceMappingURL=agent-config.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-config.test.js","sourceRoot":"","sources":["../src/agent-config.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-exec-guardrail.test.d.ts","sourceRoot":"","sources":["../src/agent-exec-guardrail.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
import { describe, it } from 'node:test';
|
|
3
|
-
import assert from 'node:assert/strict';
|
|
4
|
-
import { checkActionAllowed, requiresApprovalGate } from './agent-exec-guardrail.js';
|
|
5
|
-
describe('checkActionAllowed', () => {
|
|
6
|
-
it('allows github_issue_create with github.com domain', () => {
|
|
7
|
-
const result = checkActionAllowed('github_issue_create', 'https://github.com/owner/repo');
|
|
8
|
-
assert.equal(result.allowed, true);
|
|
9
|
-
assert.equal(result.reason, undefined);
|
|
10
|
-
});
|
|
11
|
-
it('allows github_issue_create with no target', () => {
|
|
12
|
-
const result = checkActionAllowed('github_issue_create');
|
|
13
|
-
assert.equal(result.allowed, true);
|
|
14
|
-
});
|
|
15
|
-
it('denies unknown action with reason', () => {
|
|
16
|
-
// @ts-expect-error testing unknown kind
|
|
17
|
-
const result = checkActionAllowed('unknown_action');
|
|
18
|
-
assert.equal(result.allowed, false);
|
|
19
|
-
assert.ok(result.reason?.includes('unknown_action'));
|
|
20
|
-
assert.ok(result.reason?.includes('approved action list'));
|
|
21
|
-
});
|
|
22
|
-
it('denies out-of-scope domain with reason', () => {
|
|
23
|
-
const result = checkActionAllowed('github_issue_create', 'https://evil.com/owner/repo');
|
|
24
|
-
assert.equal(result.allowed, false);
|
|
25
|
-
assert.ok(result.reason?.includes('evil.com'));
|
|
26
|
-
assert.ok(result.reason?.includes('approved domain list'));
|
|
27
|
-
});
|
|
28
|
-
it('allows subdomain of github.com', () => {
|
|
29
|
-
const result = checkActionAllowed('github_issue_create', 'https://api.github.com/repos/owner/repo/issues');
|
|
30
|
-
assert.equal(result.allowed, true);
|
|
31
|
-
});
|
|
32
|
-
it('does not match partial domain (githubx.com)', () => {
|
|
33
|
-
const result = checkActionAllowed('github_issue_create', 'https://githubx.com/owner/repo');
|
|
34
|
-
assert.equal(result.allowed, false);
|
|
35
|
-
assert.ok(result.reason?.includes('githubx.com'));
|
|
36
|
-
});
|
|
37
|
-
it('ignores non-URL target strings (no hostname to check)', () => {
|
|
38
|
-
// If target is not a URL, URL parsing returns null and no domain check is done
|
|
39
|
-
const result = checkActionAllowed('github_issue_create', 'owner/repo');
|
|
40
|
-
assert.equal(result.allowed, true);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
describe('requiresApprovalGate', () => {
|
|
44
|
-
it('returns true for github_issue_create', () => {
|
|
45
|
-
assert.equal(requiresApprovalGate('github_issue_create'), true);
|
|
46
|
-
});
|
|
47
|
-
it('returns true for all v1 actions (always requires human approval)', () => {
|
|
48
|
-
// All v1 actions are irreversible — gate is always on
|
|
49
|
-
const kinds = ['github_issue_create'];
|
|
50
|
-
for (const kind of kinds) {
|
|
51
|
-
assert.equal(requiresApprovalGate(kind), true);
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
});
|
|
55
|
-
//# sourceMappingURL=agent-exec-guardrail.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-exec-guardrail.test.js","sourceRoot":"","sources":["../src/agent-exec-guardrail.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"agent-memories.test.d.ts","sourceRoot":"","sources":["../src/agent-memories.test.ts"],"names":[],"mappings":""}
|