clementine-agent 1.0.23 → 1.0.24
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/agent/route-classifier.d.ts +1 -1
- package/dist/agent/route-classifier.js +15 -9
- package/dist/gateway/claim-tracker.js +28 -3
- package/dist/gateway/failure-diagnostics.js +4 -1
- package/dist/gateway/failure-monitor.js +6 -1
- package/dist/gateway/fix-verification.js +4 -1
- package/package.json +1 -1
|
@@ -43,7 +43,7 @@ export interface RouteDecision {
|
|
|
43
43
|
* - `slack:agent:*`, `slack:channel:*:{slug}:*`
|
|
44
44
|
* - `team:*` — inter-agent messages travel via team-bus, never route
|
|
45
45
|
*/
|
|
46
|
-
export declare function isRoutable(sessionKey: string,
|
|
46
|
+
export declare function isRoutable(sessionKey: string, _ownerAgentSlugs: Set<string>): boolean;
|
|
47
47
|
/**
|
|
48
48
|
* Classify a user message. Returns null if the call fails — caller
|
|
49
49
|
* should fall back to Clementine handling.
|
|
@@ -38,31 +38,37 @@ const logger = pino({ name: 'clementine.route-classifier' });
|
|
|
38
38
|
* - `slack:agent:*`, `slack:channel:*:{slug}:*`
|
|
39
39
|
* - `team:*` — inter-agent messages travel via team-bus, never route
|
|
40
40
|
*/
|
|
41
|
-
export function isRoutable(sessionKey,
|
|
41
|
+
export function isRoutable(sessionKey, _ownerAgentSlugs) {
|
|
42
42
|
if (!sessionKey)
|
|
43
43
|
return false;
|
|
44
44
|
const parts = sessionKey.split(':');
|
|
45
|
+
// Structural rule: any 5+ part channel key has an agent slug embedded
|
|
46
|
+
// (e.g. `discord:channel:{channelId}:{slug}:{userId}`). We reject this
|
|
47
|
+
// regardless of whether the slug appears in a passed-in roster — the
|
|
48
|
+
// ownerAgentSlugs list can be stale during agent-hire/rename events,
|
|
49
|
+
// and the key SHAPE is the safer source of truth.
|
|
50
|
+
//
|
|
51
|
+
// `_ownerAgentSlugs` is kept in the signature for future use but the
|
|
52
|
+
// current implementation is structure-only.
|
|
45
53
|
// Agent-bot DMs and member sessions are always agent-scoped
|
|
46
54
|
if (parts[0] === 'discord') {
|
|
47
55
|
const kind = parts[1];
|
|
48
56
|
if (kind === 'agent' || kind === 'member' || kind === 'member-dm')
|
|
49
57
|
return false;
|
|
50
|
-
// 5
|
|
51
|
-
if (kind === 'channel' && parts.length >= 5
|
|
58
|
+
// Any 5+ part channel key → agent-scoped, never route
|
|
59
|
+
if (kind === 'channel' && parts.length >= 5)
|
|
52
60
|
return false;
|
|
53
|
-
}
|
|
54
61
|
// discord:user:* and the 4-part discord:channel:{channelId}:{userId} pass
|
|
55
|
-
return kind === 'user' || kind === 'channel';
|
|
62
|
+
return kind === 'user' || (kind === 'channel' && parts.length === 4);
|
|
56
63
|
}
|
|
57
64
|
if (parts[0] === 'slack') {
|
|
58
65
|
const kind = parts[1];
|
|
59
66
|
if (kind === 'agent')
|
|
60
67
|
return false;
|
|
61
|
-
//
|
|
62
|
-
if (kind === 'channel' && parts.length >= 5
|
|
68
|
+
// Any 5+ part channel key → agent-scoped
|
|
69
|
+
if (kind === 'channel' && parts.length >= 5)
|
|
63
70
|
return false;
|
|
64
|
-
|
|
65
|
-
return kind === 'user' || kind === 'dm' || kind === 'channel';
|
|
71
|
+
return kind === 'user' || kind === 'dm' || (kind === 'channel' && parts.length === 4);
|
|
66
72
|
}
|
|
67
73
|
if (parts[0] === 'telegram')
|
|
68
74
|
return parts[1] === 'user' || /^\d+$/.test(parts[1] ?? '');
|
|
@@ -123,13 +123,24 @@ const PATTERNS = [
|
|
|
123
123
|
* Bounded to prevent memory growth — oldest entries are evicted.
|
|
124
124
|
*/
|
|
125
125
|
const MAX_PENDING_LLM = 20;
|
|
126
|
+
const PENDING_LLM_TTL_MS = 6 * 60 * 60 * 1000; // 6h — after that a claim is stale anyway
|
|
126
127
|
const pendingLLMExtraction = [];
|
|
128
|
+
function pruneExpiredPending(now = Date.now()) {
|
|
129
|
+
while (pendingLLMExtraction.length > 0) {
|
|
130
|
+
const oldest = pendingLLMExtraction[0];
|
|
131
|
+
if (now - oldest.queuedAt <= PENDING_LLM_TTL_MS)
|
|
132
|
+
break;
|
|
133
|
+
pendingLLMExtraction.shift();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
127
136
|
function enqueueForLLM(text, sessionKey, agentSlug) {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
pruneExpiredPending(now);
|
|
128
139
|
// De-dup by text hash within the queue — don't re-enqueue the same DM.
|
|
129
140
|
const hash = sha1(text);
|
|
130
|
-
if (pendingLLMExtraction.some(e =>
|
|
141
|
+
if (pendingLLMExtraction.some(e => e.hash === hash))
|
|
131
142
|
return;
|
|
132
|
-
pendingLLMExtraction.push({ text, sessionKey, agentSlug, queuedAt:
|
|
143
|
+
pendingLLMExtraction.push({ text, hash, sessionKey, agentSlug, queuedAt: now });
|
|
133
144
|
while (pendingLLMExtraction.length > MAX_PENDING_LLM)
|
|
134
145
|
pendingLLMExtraction.shift();
|
|
135
146
|
}
|
|
@@ -208,11 +219,16 @@ export function extractClaims(text, sessionKey, agentSlug) {
|
|
|
208
219
|
* the next sweep.
|
|
209
220
|
*/
|
|
210
221
|
export async function drainLLMFallback(gateway, maxPerSweep = 3) {
|
|
222
|
+
pruneExpiredPending();
|
|
211
223
|
let drained = 0;
|
|
212
|
-
|
|
224
|
+
// Peek — don't remove yet. We only splice on successful processing so a
|
|
225
|
+
// transient LLM failure doesn't silently drop the candidate.
|
|
226
|
+
const batch = pendingLLMExtraction.slice(0, Math.min(maxPerSweep, pendingLLMExtraction.length));
|
|
227
|
+
const toRemove = new Set();
|
|
213
228
|
for (const item of batch) {
|
|
214
229
|
try {
|
|
215
230
|
const claims = await llmExtractClaims(item.text, gateway);
|
|
231
|
+
toRemove.add(item.hash); // success (or "no claims" — not worth re-trying)
|
|
216
232
|
if (claims.length === 0)
|
|
217
233
|
continue;
|
|
218
234
|
const toRecord = claims.map(c => ({
|
|
@@ -229,9 +245,18 @@ export async function drainLLMFallback(gateway, maxPerSweep = 3) {
|
|
|
229
245
|
drained += claims.length;
|
|
230
246
|
}
|
|
231
247
|
catch (err) {
|
|
248
|
+
// Don't add to toRemove — leave in queue for next sweep. TTL eventually
|
|
249
|
+
// evicts permanently-failing entries.
|
|
232
250
|
logger.debug({ err }, 'LLM fallback extraction failed for one DM');
|
|
233
251
|
}
|
|
234
252
|
}
|
|
253
|
+
// Remove successfully-processed entries in one pass
|
|
254
|
+
if (toRemove.size > 0) {
|
|
255
|
+
for (let i = pendingLLMExtraction.length - 1; i >= 0; i--) {
|
|
256
|
+
if (toRemove.has(pendingLLMExtraction[i].hash))
|
|
257
|
+
pendingLLMExtraction.splice(i, 1);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
235
260
|
return drained;
|
|
236
261
|
}
|
|
237
262
|
async function llmExtractClaims(text, gateway) {
|
|
@@ -52,7 +52,10 @@ function loadCache() {
|
|
|
52
52
|
function saveCache(cache) {
|
|
53
53
|
try {
|
|
54
54
|
mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
55
|
-
|
|
55
|
+
const tmp = CACHE_FILE + '.tmp';
|
|
56
|
+
writeFileSync(tmp, JSON.stringify(cache, null, 2));
|
|
57
|
+
const { renameSync } = require('node:fs');
|
|
58
|
+
renameSync(tmp, CACHE_FILE);
|
|
56
59
|
}
|
|
57
60
|
catch (err) {
|
|
58
61
|
logger.warn({ err }, 'Failed to persist diagnostic cache');
|
|
@@ -50,7 +50,12 @@ function loadState() {
|
|
|
50
50
|
function saveState(state) {
|
|
51
51
|
try {
|
|
52
52
|
mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
53
|
-
|
|
53
|
+
// Atomic write — write to temp file then rename. Prevents partial
|
|
54
|
+
// writes from corrupting the state if the process is killed mid-write.
|
|
55
|
+
const tmp = STATE_FILE + '.tmp';
|
|
56
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
57
|
+
const { renameSync } = require('node:fs');
|
|
58
|
+
renameSync(tmp, STATE_FILE);
|
|
54
59
|
}
|
|
55
60
|
catch (err) {
|
|
56
61
|
logger.warn({ err }, 'Failed to persist failure-monitor state');
|
|
@@ -29,7 +29,10 @@ function loadState() {
|
|
|
29
29
|
function saveState(state) {
|
|
30
30
|
try {
|
|
31
31
|
mkdirSync(path.dirname(STATE_FILE), { recursive: true });
|
|
32
|
-
|
|
32
|
+
const tmp = STATE_FILE + '.tmp';
|
|
33
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
34
|
+
const { renameSync } = require('node:fs');
|
|
35
|
+
renameSync(tmp, STATE_FILE);
|
|
33
36
|
}
|
|
34
37
|
catch (err) {
|
|
35
38
|
logger.warn({ err }, 'Failed to persist fix-verification state');
|