@adhdev/daemon-core 0.9.82-rc.461 → 0.9.82-rc.463
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/commands/router-aggregate-status.d.ts +24 -0
- package/dist/commands/router-mesh-session-owner.d.ts +59 -0
- package/dist/commands/router.d.ts +7 -47
- package/dist/index.js +388 -222
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +388 -222
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-pending.d.ts +34 -2
- package/dist/mesh/mesh-events.d.ts +2 -2
- package/dist/mesh/mesh-reconcile-loop.d.ts +12 -0
- package/dist/providers/approval-utils.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +9 -0
- package/dist/providers/spec/types.d.ts +22 -0
- package/dist/repo-mesh-types.d.ts +31 -0
- package/package.json +3 -3
- package/src/commands/high-family/mesh-events.ts +14 -1
- package/src/commands/high-family/mesh-status.ts +19 -2
- package/src/commands/router-aggregate-status.ts +209 -0
- package/src/commands/router-mesh-session-owner.ts +114 -0
- package/src/commands/router.ts +27 -255
- package/src/mesh/mesh-events-pending.ts +99 -5
- package/src/mesh/mesh-events.ts +3 -0
- package/src/mesh/mesh-reconcile-loop.ts +66 -0
- package/src/providers/approval-utils.ts +1 -1
- package/src/providers/cli-provider-instance.ts +41 -12
- package/src/providers/native-history/hermes-cli-transcript.ts +54 -2
- package/src/providers/spec/native-history-executor.ts +73 -6
- package/src/providers/spec/types.ts +22 -0
- package/src/repo-mesh-types.ts +32 -0
|
@@ -27,7 +27,7 @@ import { shouldCollectTraceCategory } from '../logging/debug-config.js';
|
|
|
27
27
|
import { traceMeshEventStage, traceMeshEventDrop } from '../mesh/mesh-event-trace.js';
|
|
28
28
|
import type { ChatMessage } from '../types.js';
|
|
29
29
|
import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
|
|
30
|
-
import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOption, hasReliableApprovalAffirmative, looksLikeActiveApprovalPromptText } from './approval-utils.js';
|
|
30
|
+
import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOption, hasReliableApprovalAffirmative, looksLikeActiveApprovalPromptText, normalizeApprovalLabel } from './approval-utils.js';
|
|
31
31
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
32
32
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
33
33
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
@@ -1517,6 +1517,22 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1517
1517
|
return probe;
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
1520
|
+
/**
|
|
1521
|
+
* The spawned CLI's env overrides (e.g. the mesh coordinator points hermes
|
|
1522
|
+
* at a per-coordinator HERMES_HOME so its state.db lives in a tmpdir instead
|
|
1523
|
+
* of ~/.hermes). The native-history executor expands `${HERMES_HOME:-~/.hermes}`
|
|
1524
|
+
* from this map, so the completion gate MUST pass it through — otherwise the
|
|
1525
|
+
* gate reads ~/.hermes, finds no coordinator-session transcript, and
|
|
1526
|
+
* false-fires missing_final_assistant on every coordinator turn.
|
|
1527
|
+
*/
|
|
1528
|
+
private spawnedEnvOverrides(): Record<string, string> | undefined {
|
|
1529
|
+
const meta = typeof (this.adapter as any)?.getRuntimeMetadata === 'function'
|
|
1530
|
+
? (this.adapter as any).getRuntimeMetadata()
|
|
1531
|
+
: undefined;
|
|
1532
|
+
const env = meta && typeof meta === 'object' ? (meta as Record<string, unknown>).spawnedEnv : undefined;
|
|
1533
|
+
return env && typeof env === 'object' ? env as Record<string, string> : undefined;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1520
1536
|
private readExternalCompletionMessages(): unknown[] | null {
|
|
1521
1537
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1522
1538
|
if (!adapterOwnsMessagesElsewhere) return null;
|
|
@@ -1535,6 +1551,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1535
1551
|
historyBehavior: this.provider.historyBehavior,
|
|
1536
1552
|
scripts: this.provider.scripts as any,
|
|
1537
1553
|
sessionStartedAtMs: this.startedAt,
|
|
1554
|
+
envOverrides: this.spawnedEnvOverrides(),
|
|
1538
1555
|
forceRefresh: true,
|
|
1539
1556
|
});
|
|
1540
1557
|
if (restoredHistory.source !== 'provider-native') {
|
|
@@ -2317,19 +2334,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2317
2334
|
// kind gate). Surface the modal so the user decides; never pick blindly.
|
|
2318
2335
|
return autoApproveActive;
|
|
2319
2336
|
}
|
|
2320
|
-
// Modal *identity* signature — the question
|
|
2321
|
-
//
|
|
2322
|
-
// approvalEntrySeq on every fresh
|
|
2323
|
-
// modal→generating→modal flap (the question
|
|
2324
|
-
// captured frame while the button block stays)
|
|
2325
|
-
// again. Folding that seq into the settle signature
|
|
2326
|
-
// settle clock restart on every flap, so the modal never
|
|
2327
|
-
// long enough to fire — the gate was never satisfied.
|
|
2328
|
-
// the seq so
|
|
2337
|
+
// Modal *identity* signature — the question plus the STABLE affirmative
|
|
2338
|
+
// anchor only, NO volatile counters and NO raw button set. This is what
|
|
2339
|
+
// the settle gate tracks: the FSM bumps approvalEntrySeq on every fresh
|
|
2340
|
+
// waiting_approval entry, and a modal→generating→modal flap (the question
|
|
2341
|
+
// line scrolled out of the captured frame while the button block stays)
|
|
2342
|
+
// re-enters and bumps it again. Folding that seq into the settle signature
|
|
2343
|
+
// made the 600ms settle clock restart on every flap, so the modal never
|
|
2344
|
+
// stayed stable long enough to fire — the gate was never satisfied.
|
|
2345
|
+
// Identity excludes the seq so seq flap of the SAME modal keeps one clock.
|
|
2346
|
+
//
|
|
2347
|
+
// The raw button set is also excluded: on a TALL Write/Edit diff claude's
|
|
2348
|
+
// TUI repaints the button block 3↔5↔none between frames (buttons scroll in
|
|
2349
|
+
// and out of the captured region), which flipped both buttons.join('|') and
|
|
2350
|
+
// the positional buttonIndex every frame → signature flap → the settle
|
|
2351
|
+
// clock reset 4–9s and only mask-stalled episodes leaked to the coordinator
|
|
2352
|
+
// (AUTOAPPROVE-SETTLE-FLAP). The affirmative the auto-approve will actually
|
|
2353
|
+
// press is the invariant across those repaints, so we anchor on its
|
|
2354
|
+
// NORMALIZED label (numbers/bullets/punctuation stripped, so "1. Yes" and
|
|
2355
|
+
// "3. Yes" collapse to "yes"). Message + affirmative label uniquely
|
|
2356
|
+
// identifies the consent question without tracking the volatile button
|
|
2357
|
+
// positions.
|
|
2358
|
+
const affirmativeAnchor = normalizeApprovalLabel(buttonLabel);
|
|
2329
2359
|
const modalSignature = [
|
|
2330
2360
|
typeof modal?.message === 'string' ? modal.message.trim() : '',
|
|
2331
|
-
|
|
2332
|
-
buttonIndex,
|
|
2361
|
+
affirmativeAnchor,
|
|
2333
2362
|
].join('::');
|
|
2334
2363
|
// Busy-window re-entry guard still needs the seq: two DISTINCT
|
|
2335
2364
|
// back-to-back approvals can carry identical message/buttons (common
|
|
@@ -69,7 +69,59 @@ function openDb(): any | null {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Expand an anchor session id to every session id in its logical cluster.
|
|
74
|
+
*
|
|
75
|
+
* hermes ≥0.14 splits a SINGLE logical turn across several `sessions` rows
|
|
76
|
+
* linked by `parent_session_id` (a 0-message intermediate row is common), and
|
|
77
|
+
* the turn's final assistant message lands in a DIFFERENT row than the one the
|
|
78
|
+
* daemon pins. A bidirectional walk — up the parent chain to the cluster root,
|
|
79
|
+
* then down through every descendant — returns the complete set from ANY anchor
|
|
80
|
+
* (root, middle, or leaf), mirroring the declarative executor's
|
|
81
|
+
* `session_cluster_query`. Falls back to the anchor alone if the schema has no
|
|
82
|
+
* `parent_session_id` column (older hermes) or the walk fails.
|
|
83
|
+
*/
|
|
84
|
+
function resolveClusterSessionIds(db: any, anchorId: string): string[] {
|
|
85
|
+
if (!anchorId) return [];
|
|
86
|
+
try {
|
|
87
|
+
const rows: any[] = db.prepare(
|
|
88
|
+
`WITH RECURSIVE
|
|
89
|
+
up(id) AS (
|
|
90
|
+
SELECT id FROM sessions WHERE id = ?
|
|
91
|
+
UNION
|
|
92
|
+
SELECT s.parent_session_id FROM sessions s JOIN up ON s.id = up.id
|
|
93
|
+
WHERE s.parent_session_id IS NOT NULL
|
|
94
|
+
),
|
|
95
|
+
cluster(id) AS (
|
|
96
|
+
SELECT id FROM up
|
|
97
|
+
UNION
|
|
98
|
+
SELECT s.id FROM sessions s JOIN cluster ON s.parent_session_id = cluster.id
|
|
99
|
+
)
|
|
100
|
+
SELECT id FROM cluster`,
|
|
101
|
+
).all(anchorId);
|
|
102
|
+
const ids = new Set<string>([anchorId]);
|
|
103
|
+
for (const r of rows) {
|
|
104
|
+
if (r && r.id != null && String(r.id)) ids.add(String(r.id));
|
|
105
|
+
}
|
|
106
|
+
return Array.from(ids);
|
|
107
|
+
} catch {
|
|
108
|
+
// No parent_session_id column (older hermes) or a walk failure — the
|
|
109
|
+
// single anchor is still a valid (degenerate) cluster.
|
|
110
|
+
return [anchorId];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
72
114
|
function loadMessagesForSession(db: any, sessionId: string): NativeHistoryMessage[] {
|
|
115
|
+
// Read the WHOLE sub-session cluster, not just the pinned anchor. hermes
|
|
116
|
+
// ≥0.14 writes a turn's final assistant into a descendant sub-session row,
|
|
117
|
+
// so an anchor-only read misses it → read_chat shows zero assistant bubbles
|
|
118
|
+
// and the completion gate false-fires missing_final_assistant. Gather every
|
|
119
|
+
// cluster member (parent-chain walk) and merge; the SQL `ORDER BY timestamp`
|
|
120
|
+
// re-interleaves bubbles from different sub-sessions into true chronological
|
|
121
|
+
// order so the final assistant lands last.
|
|
122
|
+
const clusterIds = resolveClusterSessionIds(db, sessionId);
|
|
123
|
+
if (clusterIds.length === 0) return [];
|
|
124
|
+
const placeholders = clusterIds.map(() => '?').join(', ');
|
|
73
125
|
// Assistant turns whose finish_reason='tool_calls' persist an EMPTY
|
|
74
126
|
// `content` — their payload lives in the `tool_calls` column. Filtering on
|
|
75
127
|
// `content != ''` alone drops those rows, so a turn whose terminal message
|
|
@@ -79,10 +131,10 @@ function loadMessagesForSession(db: any, sessionId: string): NativeHistoryMessag
|
|
|
79
131
|
const rows: any[] = db.prepare(
|
|
80
132
|
`SELECT id, role, COALESCE(NULLIF(content, ''), tool_calls) AS content, timestamp
|
|
81
133
|
FROM messages
|
|
82
|
-
WHERE session_id
|
|
134
|
+
WHERE session_id IN (${placeholders})
|
|
83
135
|
AND ((content IS NOT NULL AND content != '') OR (tool_calls IS NOT NULL AND tool_calls != ''))
|
|
84
136
|
ORDER BY timestamp ASC, id ASC`,
|
|
85
|
-
).all(
|
|
137
|
+
).all(...clusterIds);
|
|
86
138
|
const out: NativeHistoryMessage[] = [];
|
|
87
139
|
for (const r of rows) {
|
|
88
140
|
const role = normalizeHermesRole(r.role);
|
|
@@ -279,12 +279,53 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
279
279
|
// this schema-agnostic and only rescues the mis-bound-id case: a genuine
|
|
280
280
|
// discovered pin (codex/claude use jsonl sources and never reach here;
|
|
281
281
|
// any real sqlite pin has rows) still short-circuits on its own rows.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
282
|
+
// Expand an anchor session id to every session id in its logical
|
|
283
|
+
// cluster. When the spec declares `session_cluster_query` the anchor is
|
|
284
|
+
// run through it (bound `?`) and each returned row's FIRST column is a
|
|
285
|
+
// cluster member id — typically a WITH RECURSIVE walk up to the cluster
|
|
286
|
+
// root and back down through all descendants, so passing a root, middle,
|
|
287
|
+
// or leaf anchor all resolve the same complete set. The anchor is always
|
|
288
|
+
// included even if the query omits it (defensive) so a spec with no
|
|
289
|
+
// cluster query, or a query that returns nothing, still reads the anchor
|
|
290
|
+
// itself. Absent query → just the anchor (single-session behaviour).
|
|
291
|
+
const resolveClusterIds = (anchorId: string): string[] => {
|
|
292
|
+
const ids = new Set<string>();
|
|
293
|
+
if (anchorId) ids.add(anchorId);
|
|
294
|
+
if (src.session_cluster_query && anchorId) {
|
|
295
|
+
try {
|
|
296
|
+
const rows: any[] = db.prepare(src.session_cluster_query).all(anchorId);
|
|
297
|
+
for (const row of rows) {
|
|
298
|
+
const idRaw = Object.values(row)[0];
|
|
299
|
+
if (idRaw != null && String(idRaw)) ids.add(String(idRaw));
|
|
300
|
+
}
|
|
301
|
+
} catch { /* fall back to anchor-only on a malformed cluster query */ }
|
|
302
|
+
}
|
|
303
|
+
return Array.from(ids);
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Read messages for an anchor's WHOLE cluster, merged and re-sorted by
|
|
307
|
+
// their mapped timestamp so bubbles from different sub-sessions interleave
|
|
308
|
+
// in true chronological order (the turn's final assistant — written into a
|
|
309
|
+
// descendant sub-session in the split-turn case — lands last). No per-session
|
|
310
|
+
// short-circuit: an anchor whose OWN row has zero messages (hermes writes a
|
|
311
|
+
// 0-message intermediate `sessions` row) still yields the cluster's rows,
|
|
312
|
+
// and the whole cluster is scanned rather than stopping at the first
|
|
313
|
+
// non-empty session. Returns null only when the ENTIRE cluster is empty,
|
|
314
|
+
// preserving the pin-validation contract below (a pin that resolves no rows
|
|
315
|
+
// anywhere is a mis-bound id and falls through to newest-session recovery).
|
|
316
|
+
const resolveMessagesFor = (anchorId: string): any[] | null => {
|
|
317
|
+
if (!anchorId) return null;
|
|
318
|
+
const clusterIds = resolveClusterIds(anchorId);
|
|
319
|
+
const merged: any[] = [];
|
|
320
|
+
for (const id of clusterIds) {
|
|
321
|
+
let rows: any[];
|
|
322
|
+
try { rows = db.prepare(src.message_query).all(id); }
|
|
323
|
+
catch { continue; }
|
|
324
|
+
if (rows && rows.length > 0) merged.push(...rows);
|
|
325
|
+
}
|
|
326
|
+
if (merged.length === 0) return null;
|
|
327
|
+
if (clusterIds.length > 1) sortRowsByMappedTimestamp(merged, src.message_map);
|
|
328
|
+
return merged;
|
|
288
329
|
};
|
|
289
330
|
|
|
290
331
|
const resolveNewestSessionId = (): string => {
|
|
@@ -362,6 +403,32 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
362
403
|
}
|
|
363
404
|
}
|
|
364
405
|
|
|
406
|
+
/**
|
|
407
|
+
* Stable-sort merged cluster rows by their mapped timestamp so bubbles read
|
|
408
|
+
* from different sub-sessions interleave in true chronological order. Uses the
|
|
409
|
+
* same `message_map.timestamp_ms` jsonpath + `parseTimestamp` heuristic the
|
|
410
|
+
* projection uses, so the sort key agrees with the receivedAt each row will be
|
|
411
|
+
* given. Rows with no resolvable timestamp keep their pre-sort relative order
|
|
412
|
+
* (stable), and equal timestamps preserve insertion order — both matter because
|
|
413
|
+
* a turn's terminal bubbles can share a sub-second timestamp.
|
|
414
|
+
*/
|
|
415
|
+
function sortRowsByMappedTimestamp(rows: any[], map: NativeHistoryMessageMap): void {
|
|
416
|
+
if (!map.timestamp_ms) return;
|
|
417
|
+
const keyed = rows.map((row, index) => {
|
|
418
|
+
const parsed = parseTimestamp(jsonPathGet(row, map.timestamp_ms as string));
|
|
419
|
+
return { row, index, ts: parsed == null ? Number.NaN : parsed };
|
|
420
|
+
});
|
|
421
|
+
keyed.sort((a, b) => {
|
|
422
|
+
const aHas = !Number.isNaN(a.ts);
|
|
423
|
+
const bHas = !Number.isNaN(b.ts);
|
|
424
|
+
if (aHas && bHas && a.ts !== b.ts) return a.ts - b.ts;
|
|
425
|
+
// Missing-timestamp rows and ties fall back to original insertion order
|
|
426
|
+
// so the sort stays stable.
|
|
427
|
+
return a.index - b.index;
|
|
428
|
+
});
|
|
429
|
+
for (let i = 0; i < keyed.length; i += 1) rows[i] = keyed[i].row;
|
|
430
|
+
}
|
|
431
|
+
|
|
365
432
|
// ────────────────────────────────────────────────────────────────────────────
|
|
366
433
|
// Path expansion + globbing
|
|
367
434
|
// ────────────────────────────────────────────────────────────────────────────
|
|
@@ -103,6 +103,28 @@ export interface NativeHistorySqliteSource {
|
|
|
103
103
|
path: string;
|
|
104
104
|
session_query: string;
|
|
105
105
|
message_query: string;
|
|
106
|
+
/**
|
|
107
|
+
* Optional sub-session cluster expansion. Some agents (hermes ≥0.14) split
|
|
108
|
+
* a SINGLE logical turn across several `sessions` rows linked by a parent
|
|
109
|
+
* pointer, and the turn's final assistant message lands in a DIFFERENT row
|
|
110
|
+
* than the one `session_query` / the daemon's pin resolves. Reading only
|
|
111
|
+
* the anchor session then surfaces zero (or stale) assistant bubbles even
|
|
112
|
+
* though the answer is physically present in a sibling/descendant row —
|
|
113
|
+
* `read_chat` returns no final assistant and the completion gate false-fires
|
|
114
|
+
* `missing_final_assistant`.
|
|
115
|
+
*
|
|
116
|
+
* When present, the executor treats the resolved session id as an ANCHOR
|
|
117
|
+
* and runs this query (bound `?` = anchor id) to expand it to every session
|
|
118
|
+
* id in the same logical cluster (typically a `WITH RECURSIVE` walk over the
|
|
119
|
+
* parent pointer, up to the root and back down through all descendants).
|
|
120
|
+
* `message_query` is then run once per cluster id and the rows merged and
|
|
121
|
+
* re-sorted by their mapped timestamp, so the turn's final assistant — in
|
|
122
|
+
* whichever sub-session it was written — is always included. Each returned
|
|
123
|
+
* row's first column is a cluster session id.
|
|
124
|
+
*
|
|
125
|
+
* Absent → single-session behaviour is unchanged (anchor session only).
|
|
126
|
+
*/
|
|
127
|
+
session_cluster_query?: string;
|
|
106
128
|
message_map: NativeHistoryMessageMap;
|
|
107
129
|
}
|
|
108
130
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -1020,6 +1020,38 @@ export interface RepoMeshStatus {
|
|
|
1020
1020
|
* Omitted when nothing was drained. Mirrors the MCP tool's meshProtocolMetrics.
|
|
1021
1021
|
*/
|
|
1022
1022
|
meshProtocolMetrics?: MeshProtocolMetrics;
|
|
1023
|
+
/**
|
|
1024
|
+
* T6 (B3c): live process-lifetime mesh-protocol-v2 enforce counters from THIS
|
|
1025
|
+
* daemon — the enforce flag state, drain-routing tallies (deliver / route-away /
|
|
1026
|
+
* dedup / quarantine), and the last-resort backstop fire counts (PHASE-4 synth,
|
|
1027
|
+
* acked-hold fast-track / death-deadline). Diagnostic-only and never cached (a
|
|
1028
|
+
* live snapshot). Under enforce, non-zero quarantine or backstop counts are the
|
|
1029
|
+
* rollout-health signal (target 0). Omitted when unavailable.
|
|
1030
|
+
*/
|
|
1031
|
+
meshProtocolV2Counters?: MeshProtocolV2Counters;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/** T6 (B3c) live v2 enforce/observability counters (see RepoMeshStatus.meshProtocolV2Counters). */
|
|
1035
|
+
export interface MeshProtocolV2Counters {
|
|
1036
|
+
/** True when MESH_PROTOCOL_V2_ENFORCE is active on this daemon. */
|
|
1037
|
+
enforce: boolean;
|
|
1038
|
+
/** Drain-path routing tallies (accept + enforce). Process-lifetime totals. */
|
|
1039
|
+
drain: {
|
|
1040
|
+
v2Delivered: number;
|
|
1041
|
+
v2RoutedAway: number;
|
|
1042
|
+
v2DedupSkipped: number;
|
|
1043
|
+
v2ValidationFailedAccepted: number;
|
|
1044
|
+
v2ReattributedToDrainer: number;
|
|
1045
|
+
v1BroadcastAccepted: number;
|
|
1046
|
+
v2ValidationFailedQuarantined: number;
|
|
1047
|
+
v1UnversionedQuarantined: number;
|
|
1048
|
+
};
|
|
1049
|
+
/** Last-resort backstop fire counts. Target 0 under a healthy v2 contract. */
|
|
1050
|
+
backstop: {
|
|
1051
|
+
phase4SynthesisFired: number;
|
|
1052
|
+
ackedHoldFastTrackFired: number;
|
|
1053
|
+
ackedHoldDeathDeadlineFired: number;
|
|
1054
|
+
};
|
|
1023
1055
|
}
|
|
1024
1056
|
|
|
1025
1057
|
/** One provider's version skew across mesh nodes (see RepoMeshStatus.providerVersionSkew). */
|