@yeaft/webchat-agent 1.0.76 → 1.0.78
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/connection/buffer.js +2 -6
- package/connection/message-router.js +1 -64
- package/conversation.js +4 -32
- package/history.js +2 -2
- package/index.js +0 -6
- package/package.json +1 -1
- package/providers/claude-code.js +1 -1
- package/yeaft/attachments.js +2 -2
- package/yeaft/config.js +1 -1
- package/yeaft/conversation/persist.js +458 -112
- package/yeaft/conversation/search.js +51 -15
- package/yeaft/session.js +27 -18
- package/yeaft/web-bridge.js +27 -15
- package/crew/builtin-actions.js +0 -154
- package/crew/context-loader.js +0 -171
- package/crew/control.js +0 -444
- package/crew/human-interaction.js +0 -195
- package/crew/persistence.js +0 -295
- package/crew/role-management.js +0 -182
- package/crew/role-output.js +0 -461
- package/crew/role-query.js +0 -406
- package/crew/role-states.js +0 -180
- package/crew/routing-fallback.js +0 -64
- package/crew/routing-metrics.js +0 -215
- package/crew/routing.js +0 -951
- package/crew/session.js +0 -648
- package/crew/shared-dir.js +0 -266
- package/crew/task-files.js +0 -554
- package/crew/ui-messages.js +0 -274
- package/crew/worktree.js +0 -130
- package/crew-i18n.js +0 -579
- package/crew.js +0 -48
package/crew/routing-metrics.js
DELETED
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* task-330b — Routing metrics counter (Final Spec §B).
|
|
3
|
-
*
|
|
4
|
-
* Centralised observability for crew routing fallbacks. Five canonical
|
|
5
|
-
* reasons that any fallback path MUST pass to `recordRoutingEvent`:
|
|
6
|
-
*
|
|
7
|
-
* - missing-route : turn ended with no parseable ROUTE block
|
|
8
|
-
* - parse-fail : ROUTE block found but parse returned null/invalid
|
|
9
|
-
* - self-route : route.to resolves to the sender (rejected by §A)
|
|
10
|
-
* - state-stopped : message arrived while session was stopped/paused
|
|
11
|
-
* and was diverted/dropped
|
|
12
|
-
* - fallback-forward : auto-forward path engaged (non-PM → PM safety net)
|
|
13
|
-
*
|
|
14
|
-
* Counters are kept in-memory keyed by `${sessionId}::${reason}` and flushed
|
|
15
|
-
* to `${sharedDir}/context/routing-metrics.json` periodically (default 30s)
|
|
16
|
-
* AND on demand via `flushRoutingMetricsNow(session)`. The on-disk format:
|
|
17
|
-
*
|
|
18
|
-
* {
|
|
19
|
-
* "schemaVersion": 1,
|
|
20
|
-
* "lastFlushedAt": <ms>,
|
|
21
|
-
* "counts": {
|
|
22
|
-
* "missing-route": 4,
|
|
23
|
-
* "parse-fail": 1,
|
|
24
|
-
* "self-route": 0,
|
|
25
|
-
* "state-stopped": 2,
|
|
26
|
-
* "fallback-forward": 4
|
|
27
|
-
* },
|
|
28
|
-
* "recent": [
|
|
29
|
-
* { ts, reason, fromRole, toRole?, taskId?, note? },
|
|
30
|
-
* ... // bounded ring buffer (50)
|
|
31
|
-
* ]
|
|
32
|
-
* }
|
|
33
|
-
*
|
|
34
|
-
* Red lines (§330b):
|
|
35
|
-
* - Pure observer; never mutates routing decisions.
|
|
36
|
-
* - Never throws; failures degrade to console.warn so callers can rely on
|
|
37
|
-
* `recordRoutingEvent()` being safe inside hot paths.
|
|
38
|
-
*
|
|
39
|
-
* Red lines (§330a — shared with this PR):
|
|
40
|
-
* - No engine state-machine touch.
|
|
41
|
-
* - PM-self-loop is the responsibility of §A; §B only records the metric
|
|
42
|
-
* when §A rejects.
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
import { promises as fs } from 'fs';
|
|
46
|
-
import { join } from 'path';
|
|
47
|
-
|
|
48
|
-
export const ROUTING_REASONS = Object.freeze([
|
|
49
|
-
'missing-route',
|
|
50
|
-
'parse-fail',
|
|
51
|
-
'self-route',
|
|
52
|
-
'state-stopped',
|
|
53
|
-
'fallback-forward',
|
|
54
|
-
]);
|
|
55
|
-
|
|
56
|
-
const REASON_SET = new Set(ROUTING_REASONS);
|
|
57
|
-
const RECENT_RING_SIZE = 50;
|
|
58
|
-
const FLUSH_INTERVAL_MS = 30_000;
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* In-process state — ONE bag per process. Keyed by sessionId so multiple
|
|
62
|
-
* crew sessions running in the same agent each keep their own counts.
|
|
63
|
-
*
|
|
64
|
-
* Shape: Map<sessionId, {
|
|
65
|
-
* sharedDir: string,
|
|
66
|
-
* counts: Record<reason, number>,
|
|
67
|
-
* recent: Array<{ ts, reason, fromRole, toRole?, taskId?, note? }>,
|
|
68
|
-
* dirty: boolean,
|
|
69
|
-
* flushTimer: NodeJS.Timeout | null,
|
|
70
|
-
* }>
|
|
71
|
-
*/
|
|
72
|
-
const _state = new Map();
|
|
73
|
-
|
|
74
|
-
function _zeroCounts() {
|
|
75
|
-
const c = {};
|
|
76
|
-
for (const r of ROUTING_REASONS) c[r] = 0;
|
|
77
|
-
return c;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function _getOrInit(session) {
|
|
81
|
-
const sid = session?.id;
|
|
82
|
-
if (!sid) return null;
|
|
83
|
-
let bag = _state.get(sid);
|
|
84
|
-
if (!bag) {
|
|
85
|
-
bag = {
|
|
86
|
-
sharedDir: session.sharedDir || null,
|
|
87
|
-
counts: _zeroCounts(),
|
|
88
|
-
recent: [],
|
|
89
|
-
dirty: false,
|
|
90
|
-
flushTimer: null,
|
|
91
|
-
};
|
|
92
|
-
_state.set(sid, bag);
|
|
93
|
-
}
|
|
94
|
-
// sharedDir may not be available at session creation — keep latest.
|
|
95
|
-
if (session.sharedDir) bag.sharedDir = session.sharedDir;
|
|
96
|
-
return bag;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Record a routing fallback event.
|
|
101
|
-
*
|
|
102
|
-
* @param {object} session — crew session (must have .id; .sharedDir for flush)
|
|
103
|
-
* @param {string} reason — one of ROUTING_REASONS
|
|
104
|
-
* @param {object} [meta]
|
|
105
|
-
* @param {string} [meta.fromRole]
|
|
106
|
-
* @param {string} [meta.toRole]
|
|
107
|
-
* @param {string} [meta.taskId]
|
|
108
|
-
* @param {string} [meta.note]
|
|
109
|
-
* @returns {boolean} true if recorded; false if invalid input
|
|
110
|
-
*/
|
|
111
|
-
export function recordRoutingEvent(session, reason, meta = {}) {
|
|
112
|
-
if (!session || !session.id) return false;
|
|
113
|
-
if (!REASON_SET.has(reason)) {
|
|
114
|
-
console.warn(`[routing-metrics] Unknown reason: ${reason} (allowed: ${ROUTING_REASONS.join(', ')})`);
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
const bag = _getOrInit(session);
|
|
118
|
-
if (!bag) return false;
|
|
119
|
-
|
|
120
|
-
bag.counts[reason] = (bag.counts[reason] || 0) + 1;
|
|
121
|
-
bag.recent.push({
|
|
122
|
-
ts: Date.now(),
|
|
123
|
-
reason,
|
|
124
|
-
fromRole: meta.fromRole || null,
|
|
125
|
-
toRole: meta.toRole || null,
|
|
126
|
-
taskId: meta.taskId || null,
|
|
127
|
-
note: meta.note || null,
|
|
128
|
-
});
|
|
129
|
-
// Bound the ring.
|
|
130
|
-
if (bag.recent.length > RECENT_RING_SIZE) {
|
|
131
|
-
bag.recent.splice(0, bag.recent.length - RECENT_RING_SIZE);
|
|
132
|
-
}
|
|
133
|
-
bag.dirty = true;
|
|
134
|
-
_ensureTimer(session.id, bag);
|
|
135
|
-
return true;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function _ensureTimer(sessionId, bag) {
|
|
139
|
-
if (bag.flushTimer) return;
|
|
140
|
-
bag.flushTimer = setTimeout(() => {
|
|
141
|
-
bag.flushTimer = null;
|
|
142
|
-
_flush(sessionId, bag).catch((e) =>
|
|
143
|
-
console.warn(`[routing-metrics] periodic flush failed for ${sessionId}: ${e.message}`),
|
|
144
|
-
);
|
|
145
|
-
}, FLUSH_INTERVAL_MS);
|
|
146
|
-
// Don't keep the event loop alive solely for metrics flush.
|
|
147
|
-
if (typeof bag.flushTimer.unref === 'function') bag.flushTimer.unref();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function _flush(sessionId, bag) {
|
|
151
|
-
if (!bag.dirty) return;
|
|
152
|
-
if (!bag.sharedDir) return; // can't flush without target dir
|
|
153
|
-
const dir = join(bag.sharedDir, 'context');
|
|
154
|
-
const file = join(dir, 'routing-metrics.json');
|
|
155
|
-
const payload = {
|
|
156
|
-
schemaVersion: 1,
|
|
157
|
-
lastFlushedAt: Date.now(),
|
|
158
|
-
counts: { ...bag.counts },
|
|
159
|
-
recent: bag.recent.slice(),
|
|
160
|
-
};
|
|
161
|
-
try {
|
|
162
|
-
await fs.mkdir(dir, { recursive: true });
|
|
163
|
-
// Write-then-rename for atomicity (single-line file is small; tolerate
|
|
164
|
-
// platform quirks).
|
|
165
|
-
const tmp = `${file}.tmp`;
|
|
166
|
-
await fs.writeFile(tmp, JSON.stringify(payload, null, 2), 'utf8');
|
|
167
|
-
await fs.rename(tmp, file);
|
|
168
|
-
bag.dirty = false;
|
|
169
|
-
} catch (e) {
|
|
170
|
-
console.warn(`[routing-metrics] flush write failed: ${e.message}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Force a synchronous-ish flush (still returns a Promise). Useful from
|
|
176
|
-
* shutdown paths or tests.
|
|
177
|
-
*/
|
|
178
|
-
export async function flushRoutingMetricsNow(session) {
|
|
179
|
-
const bag = _state.get(session?.id);
|
|
180
|
-
if (!bag) return;
|
|
181
|
-
if (bag.flushTimer) {
|
|
182
|
-
clearTimeout(bag.flushTimer);
|
|
183
|
-
bag.flushTimer = null;
|
|
184
|
-
}
|
|
185
|
-
await _flush(session.id, bag);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Read current counts (test/inspection only; non-mutating snapshot).
|
|
190
|
-
* @returns {{ counts: Record<string, number>, recent: Array<object> } | null}
|
|
191
|
-
*/
|
|
192
|
-
export function getRoutingMetricsSnapshot(session) {
|
|
193
|
-
const bag = _state.get(session?.id);
|
|
194
|
-
if (!bag) return null;
|
|
195
|
-
return {
|
|
196
|
-
counts: { ...bag.counts },
|
|
197
|
-
recent: bag.recent.slice(),
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Reset (test-only).
|
|
203
|
-
*/
|
|
204
|
-
export function _resetRoutingMetricsForTest(sessionId) {
|
|
205
|
-
if (sessionId) {
|
|
206
|
-
const bag = _state.get(sessionId);
|
|
207
|
-
if (bag?.flushTimer) clearTimeout(bag.flushTimer);
|
|
208
|
-
_state.delete(sessionId);
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
for (const [, bag] of _state) {
|
|
212
|
-
if (bag.flushTimer) clearTimeout(bag.flushTimer);
|
|
213
|
-
}
|
|
214
|
-
_state.clear();
|
|
215
|
-
}
|