@adhdev/daemon-core 0.9.82-rc.441 → 0.9.82-rc.443
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/high-family/types.d.ts +4 -0
- package/dist/commands/router.d.ts +4 -0
- package/dist/index.js +249 -53
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +249 -53
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +15 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +15 -1
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +1 -0
- package/package.json +2 -2
- package/src/commands/chat-commands-read.ts +41 -0
- package/src/commands/high-family/mesh-events.ts +11 -2
- package/src/commands/high-family/mesh-status.ts +102 -9
- package/src/commands/high-family/types.ts +5 -1
- package/src/commands/router.ts +19 -2
- package/src/mesh/mesh-node-identity.ts +106 -18
- package/src/mesh/mesh-reconcile-loop.ts +22 -1
- package/src/providers/chat-message-normalization.ts +1 -1
- package/src/providers/cli-provider-instance.ts +95 -8
- package/src/providers/spec/native-history-executor.ts +52 -21
|
@@ -29,7 +29,7 @@ import { formatAutoApprovalMessage, pickApprovalButton, hasNegativeApprovalOptio
|
|
|
29
29
|
import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
|
|
30
30
|
import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
|
|
31
31
|
import { normalizeProviderSessionId } from './provider-session-id.js';
|
|
32
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter } from './chat-message-normalization.js';
|
|
32
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter, readChatMessageTimestampMs } from './chat-message-normalization.js';
|
|
33
33
|
import { workingDirBasename } from './working-dir.js';
|
|
34
34
|
import { ManualAttendanceTracker } from './manual-attendance.js';
|
|
35
35
|
|
|
@@ -75,6 +75,16 @@ type CompletedDebouncePending = {
|
|
|
75
75
|
// debounce that flushes before the producing turn's final assistant bubble lands
|
|
76
76
|
// in the native transcript never echoes the PRIOR task's last bubble.
|
|
77
77
|
turnStartedAt?: number;
|
|
78
|
+
// FALSE-IDLE continuity: the busyEpoch value at the instant this pending was armed.
|
|
79
|
+
// The flush guard requires this.busyEpoch to still equal this — proving no busy
|
|
80
|
+
// phase (generating/waiting_approval) opened since arming. A momentary busy→idle
|
|
81
|
+
// blip in an inter-approval valley bumps busyEpoch, so a completion armed before
|
|
82
|
+
// the blip is cancelled at flush instead of emitting a stale mid-turn summary.
|
|
83
|
+
busyEpochAtArm?: number;
|
|
84
|
+
// FALSE-IDLE continuity: the adapter's raw PTY lastOutputAt at arm time. New PTY
|
|
85
|
+
// output after arming means the session was not continuously idle through the
|
|
86
|
+
// settle window (the agent kept printing), so the completion is cancelled.
|
|
87
|
+
lastOutputAtArm?: number;
|
|
78
88
|
};
|
|
79
89
|
|
|
80
90
|
function isIdleStatus(value: unknown): boolean {
|
|
@@ -508,6 +518,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
508
518
|
// first sets it; the other becomes a no-op.
|
|
509
519
|
private agentReadyEmitted = false;
|
|
510
520
|
private generatingStartedAt: number = 0;
|
|
521
|
+
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
522
|
+
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
523
|
+
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
524
|
+
// — proving the session did not re-enter a busy phase (a momentary busy→idle blip
|
|
525
|
+
// in an inter-approval valley) between arming the debounce and flushing it. A
|
|
526
|
+
// single point-sample of status at flush time cannot see a generating phase that
|
|
527
|
+
// opened AND closed within the settle window; the epoch can. See
|
|
528
|
+
// flushCompletedDebounceIfFinalized.
|
|
529
|
+
private busyEpoch: number = 0;
|
|
511
530
|
// GENERATING-BOUNDARY (R4b): the per-turn taskId for which a startup-grace
|
|
512
531
|
// started+completed pair was already synthesized. Both fast-collapse callers
|
|
513
532
|
// (starting→idle transition AND the idle-stayed no-status-change poll) route
|
|
@@ -1411,7 +1430,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1411
1430
|
this.applyProviderResponse(parsed.payload, { phase: 'immediate' });
|
|
1412
1431
|
}
|
|
1413
1432
|
|
|
1414
|
-
private completionHasFinalAssistantMessage(messages: unknown): boolean {
|
|
1433
|
+
private completionHasFinalAssistantMessage(messages: unknown, turnStartedAt?: number): boolean {
|
|
1415
1434
|
const visibleMessages = (Array.isArray(messages) ? messages : [])
|
|
1416
1435
|
.filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
|
|
1417
1436
|
const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
|
|
@@ -1421,6 +1440,21 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1421
1440
|
// Guard: if the last assistant message looks like an active approval/input prompt,
|
|
1422
1441
|
// it is not a real completion — the session is still awaiting user input.
|
|
1423
1442
|
if (looksLikeActiveApprovalPromptText(content)) return false;
|
|
1443
|
+
// FALSE-IDLE turn-boundary evidence (Defect 1b): when a producing-turn start is
|
|
1444
|
+
// known, the final assistant bubble must POST-DATE it. A STALE mid-turn assistant
|
|
1445
|
+
// (predating this turn's start — e.g. the last bubble of a prior sub-turn observed
|
|
1446
|
+
// during an inter-approval valley) must NOT satisfy the finalization gate, or a
|
|
1447
|
+
// false-idle blip emits a completion carrying that stale summary. A bubble with no
|
|
1448
|
+
// parseable timestamp cannot be proven stale, so it is kept (fails open — behaviour
|
|
1449
|
+
// identical to before for providers/paths that carry no timestamps).
|
|
1450
|
+
if (typeof turnStartedAt === 'number' && Number.isFinite(turnStartedAt) && turnStartedAt > 0) {
|
|
1451
|
+
// readChatMessageTimestampMs mirrors the summary turn-scoping reader
|
|
1452
|
+
// (extractFinalSummaryFromMessagesAfter) — same seconds-vs-ms heuristic and
|
|
1453
|
+
// field precedence — so the present-check and the summary-scope agree on which
|
|
1454
|
+
// bubbles predate the turn.
|
|
1455
|
+
const ts = readChatMessageTimestampMs(lastVisible);
|
|
1456
|
+
if (typeof ts === 'number' && ts < turnStartedAt) return false;
|
|
1457
|
+
}
|
|
1424
1458
|
return true;
|
|
1425
1459
|
}
|
|
1426
1460
|
|
|
@@ -1485,8 +1519,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1485
1519
|
return restoredHistory.messages;
|
|
1486
1520
|
}
|
|
1487
1521
|
|
|
1488
|
-
private completionFinalAssistantEvidence(parsedMessages: unknown): CompletionFinalAssistantEvidence {
|
|
1489
|
-
if (this.completionHasFinalAssistantMessage(parsedMessages)) {
|
|
1522
|
+
private completionFinalAssistantEvidence(parsedMessages: unknown, turnStartedAt?: number): CompletionFinalAssistantEvidence {
|
|
1523
|
+
if (this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
|
|
1490
1524
|
return {
|
|
1491
1525
|
present: true,
|
|
1492
1526
|
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
@@ -1497,7 +1531,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1497
1531
|
const externalMessages = this.readExternalCompletionMessages();
|
|
1498
1532
|
if (externalMessages) {
|
|
1499
1533
|
return {
|
|
1500
|
-
present: this.completionHasFinalAssistantMessage(externalMessages),
|
|
1534
|
+
present: this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt),
|
|
1501
1535
|
messages: externalMessages,
|
|
1502
1536
|
source: 'external-native',
|
|
1503
1537
|
};
|
|
@@ -1528,10 +1562,17 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1528
1562
|
// at/after turnStartedAt yields '' in that race instead of the stale tail; the weak/empty
|
|
1529
1563
|
// summary is later upgraded by the mesh reconcile loop once the real bubble is written.
|
|
1530
1564
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1531
|
-
|
|
1532
|
-
|
|
1565
|
+
// FALSE-IDLE Defect 1b: turn-scope the PARSED screen fallback too. Without this a stale
|
|
1566
|
+
// mid-turn assistant (predating turnStartedAt) that the turn-boundary gate already
|
|
1567
|
+
// rejected as evidence could still leak into the finalSummary via this parsed fallback
|
|
1568
|
+
// when the external transcript's turn-scoped read is empty — freezing the very stale text
|
|
1569
|
+
// the gate rejected. extractFinalSummaryFromMessagesAfter drops bubbles before the turn
|
|
1570
|
+
// start; with no boundary known (turnStartedAt falsy) it is identical to the unscoped read.
|
|
1571
|
+
const parsedSummary = extractFinalSummaryFromMessagesAfter(
|
|
1572
|
+
(this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)
|
|
1533
1573
|
? (Array.isArray(parsedMessages) ? parsedMessages : [])
|
|
1534
1574
|
: []) as any,
|
|
1575
|
+
turnStartedAt,
|
|
1535
1576
|
);
|
|
1536
1577
|
if (adapterOwnsMessagesElsewhere) {
|
|
1537
1578
|
const externalMessages = this.readExternalCompletionMessages();
|
|
@@ -1669,7 +1710,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1669
1710
|
}
|
|
1670
1711
|
if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
|
|
1671
1712
|
const adapterOwnsMessagesElsewhere = (this.adapter as any)?.chatMessagesOwnedExternally === true;
|
|
1672
|
-
|
|
1713
|
+
// FALSE-IDLE turn-boundary evidence (Defect 1b): turn-scope the present-check so a
|
|
1714
|
+
// STALE mid-turn assistant (predating pending.turnStartedAt) cannot satisfy the
|
|
1715
|
+
// finalization gate. Only the confirming final-assistant bubble that POST-DATES this
|
|
1716
|
+
// turn's start counts as evidence the turn genuinely ended.
|
|
1717
|
+
const finalAssistantEvidence = this.completionFinalAssistantEvidence(parsed?.messages, pending.turnStartedAt);
|
|
1673
1718
|
const allowMissingAssistantTimeout = !!(this.settings.meshNodeFor || this.settings.meshActiveTaskId || this.settings.launchedByCoordinator);
|
|
1674
1719
|
LOG.debug('CLI', `[${this.type}] finalAssistantEvidence: present=${finalAssistantEvidence.present} source=${finalAssistantEvidence.source} adapterOwnsMessagesElsewhere=${adapterOwnsMessagesElsewhere} parsedStatus=${parsedStatus}`);
|
|
1675
1720
|
if (!finalAssistantEvidence.present) {
|
|
@@ -1844,6 +1889,32 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1844
1889
|
return;
|
|
1845
1890
|
}
|
|
1846
1891
|
|
|
1892
|
+
// FALSE-IDLE continuity guard (Defect 1a): the point-sample above only proves the
|
|
1893
|
+
// session is idle at THIS instant. A momentary busy→idle blip inside an inter-approval
|
|
1894
|
+
// valley (auto-approved tool turns) opens AND closes a generating phase entirely within
|
|
1895
|
+
// the settle window — so the single sample reads 'idle' even though the turn is still in
|
|
1896
|
+
// flight (it re-enters generating ~0.5s later). Require instead that the session stayed
|
|
1897
|
+
// CONTINUOUSLY idle since the debounce was armed: (1) no entry into a busy phase
|
|
1898
|
+
// (busyEpoch unchanged), and (2) no new raw PTY output (lastOutputAt did not advance).
|
|
1899
|
+
// Either signal ⇒ the idle was not continuous ⇒ cancel; the still-live turn re-arms its
|
|
1900
|
+
// own completion when it genuinely finishes. This only ever cancels (never emits more),
|
|
1901
|
+
// so shared behaviour for claude/codex/antigravity is strictly stricter, never looser.
|
|
1902
|
+
if (typeof pending.busyEpochAtArm === 'number' && this.busyEpoch !== pending.busyEpochAtArm) {
|
|
1903
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (busy re-entry during settle: epoch ${pending.busyEpochAtArm}→${this.busyEpoch})`);
|
|
1904
|
+
this.completedDebouncePending = null;
|
|
1905
|
+
this.completedDebounceTimer = null;
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
const latestOutputAt = typeof (latestStatus as any)?.lastOutputAt === 'number' ? (latestStatus as any).lastOutputAt as number : undefined;
|
|
1909
|
+
if (typeof pending.lastOutputAtArm === 'number'
|
|
1910
|
+
&& typeof latestOutputAt === 'number'
|
|
1911
|
+
&& latestOutputAt > pending.lastOutputAtArm) {
|
|
1912
|
+
LOG.info('CLI', `[${this.type}] cancelled pending completed (new PTY output during settle: ${pending.lastOutputAtArm}→${latestOutputAt})`);
|
|
1913
|
+
this.completedDebouncePending = null;
|
|
1914
|
+
this.completedDebounceTimer = null;
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1847
1918
|
const block = this.getCompletedFinalizationBlock(latestVisibleStatus, pending);
|
|
1848
1919
|
if (block) {
|
|
1849
1920
|
const blockReason = block.reason;
|
|
@@ -2357,6 +2428,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2357
2428
|
}
|
|
2358
2429
|
|
|
2359
2430
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
2431
|
+
// FALSE-IDLE continuity: entering a busy phase invalidates any
|
|
2432
|
+
// completedDebouncePending armed earlier in this settle window.
|
|
2433
|
+
this.busyEpoch++;
|
|
2360
2434
|
// Defer the generating_started event — if idle comes back within 3s,
|
|
2361
2435
|
// the whole started→completed pair was a false positive from PTY noise
|
|
2362
2436
|
if (this.generatingDebounceTimer) clearTimeout(this.generatingDebounceTimer);
|
|
@@ -2381,6 +2455,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2381
2455
|
this.completedDebouncePending = null;
|
|
2382
2456
|
|
|
2383
2457
|
if (!this.generatingStartedAt) this.generatingStartedAt = now;
|
|
2458
|
+
// FALSE-IDLE continuity: waiting_approval is a busy phase (the agent
|
|
2459
|
+
// resumes into it), so bump the epoch too — the completedDebouncePending
|
|
2460
|
+
// cancel above covers the currently-armed pending, and the epoch covers
|
|
2461
|
+
// a pending that re-arms and flushes across this same valley.
|
|
2462
|
+
this.busyEpoch++;
|
|
2384
2463
|
const modal = adapterStatus.activeModal;
|
|
2385
2464
|
LOG.info('CLI', `[${this.type}] approval modal: "${modal?.message?.slice(0, 80) ?? 'none'}"`);
|
|
2386
2465
|
// Include the FSM's approval entry seq, mirroring the auto-approve
|
|
@@ -2533,6 +2612,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2533
2612
|
const turnStartedAt = engineTurnStart || this.generatingStartedAt || 0;
|
|
2534
2613
|
return turnStartedAt ? { turnStartedAt } : {};
|
|
2535
2614
|
})()),
|
|
2615
|
+
// FALSE-IDLE continuity: snapshot the busy epoch + raw PTY output
|
|
2616
|
+
// clock at arm time so the flush guard can prove the session stayed
|
|
2617
|
+
// continuously idle (no busy re-entry, no new PTY output) through the
|
|
2618
|
+
// settle window rather than merely reading 'idle' once at flush.
|
|
2619
|
+
busyEpochAtArm: this.busyEpoch,
|
|
2620
|
+
...(typeof adapterStatus?.lastOutputAt === 'number' && Number.isFinite(adapterStatus.lastOutputAt)
|
|
2621
|
+
? { lastOutputAtArm: adapterStatus.lastOutputAt as number }
|
|
2622
|
+
: {}),
|
|
2536
2623
|
};
|
|
2537
2624
|
const ownsExternalHistory = !!(this.adapter as any)?.chatMessagesOwnedExternally;
|
|
2538
2625
|
// (FALSEIDLE-BGCHILD-a) Native-history providers flush immediately (the
|
|
@@ -264,22 +264,30 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
264
264
|
|
|
265
265
|
try {
|
|
266
266
|
const requested = input.providerSessionId || '';
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
267
|
+
// Resolve the session id the message query runs against. The `requested`
|
|
268
|
+
// pin path is tried first, but a pinned id that has NO rows in the store
|
|
269
|
+
// is not a real session — fall back to the newest-session `session_query`
|
|
270
|
+
// instead of returning empty. This is the hermes read_chat gap: hermes
|
|
271
|
+
// never surfaces its own provider session id to the daemon (the spec
|
|
272
|
+
// declares no session-id extraction and the adapter's screen-scrape is
|
|
273
|
+
// codex-only), so the read pipeline falls back to threading the mesh
|
|
274
|
+
// RUNTIME session id through as `providerSessionId`. That runtime id does
|
|
275
|
+
// not exist in ~/.hermes/state.db, so the old unconditional pin path ran
|
|
276
|
+
// `message_query WHERE session_id = '<runtime id>'` → 0 rows → null, and
|
|
277
|
+
// the answer (physically present under the real cli session) was never
|
|
278
|
+
// returned. Validating the pin by the spec's own `message_query` keeps
|
|
279
|
+
// this schema-agnostic and only rescues the mis-bound-id case: a genuine
|
|
280
|
+
// discovered pin (codex/claude use jsonl sources and never reach here;
|
|
281
|
+
// any real sqlite pin has rows) still short-circuits on its own rows.
|
|
282
|
+
const resolveMessagesFor = (sessionId: string): any[] | null => {
|
|
283
|
+
if (!sessionId) return null;
|
|
284
|
+
let rows: any[];
|
|
285
|
+
try { rows = db.prepare(src.message_query).all(sessionId); }
|
|
286
|
+
catch { return null; }
|
|
287
|
+
return rows && rows.length > 0 ? rows : null;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const resolveNewestSessionId = (): string => {
|
|
283
291
|
let sessionRow: any;
|
|
284
292
|
try {
|
|
285
293
|
// session_query may reference `?` to receive the session's
|
|
@@ -299,15 +307,38 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
|
|
|
299
307
|
} catch {
|
|
300
308
|
sessionRow = stmt.get();
|
|
301
309
|
}
|
|
302
|
-
} catch { return
|
|
303
|
-
if (!sessionRow) return
|
|
310
|
+
} catch { return ''; }
|
|
311
|
+
if (!sessionRow) return '';
|
|
304
312
|
// First column of the first row is the session id.
|
|
305
313
|
const sessionIdRaw = Object.values(sessionRow)[0];
|
|
306
|
-
|
|
314
|
+
return sessionIdRaw == null ? '' : String(sessionIdRaw);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
let sessionId: string;
|
|
318
|
+
let messageRows: any[] | null;
|
|
319
|
+
if (requested) {
|
|
320
|
+
// Pin path: read the requested session directly and skip the
|
|
321
|
+
// newest-wins `session_query`. hermes ≥0.14 spawns a fresh
|
|
322
|
+
// `sessions` row per internal sub-session, so an unpinned
|
|
323
|
+
// `ORDER BY started_at DESC LIMIT 1` pick drifts to a different id
|
|
324
|
+
// on every read (re-bind churn + reading completion evidence from
|
|
325
|
+
// the wrong session). A pin that resolves rows is authoritative.
|
|
326
|
+
messageRows = resolveMessagesFor(requested);
|
|
327
|
+
if (messageRows) {
|
|
328
|
+
sessionId = requested;
|
|
329
|
+
} else {
|
|
330
|
+
// The pinned id has no rows — it is not a real session in this
|
|
331
|
+
// store (the mis-bound mesh runtime-id case). Recover by letting
|
|
332
|
+
// the spec's own newest-session query self-resolve instead of
|
|
333
|
+
// returning empty.
|
|
334
|
+
sessionId = resolveNewestSessionId();
|
|
335
|
+
messageRows = resolveMessagesFor(sessionId);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
sessionId = resolveNewestSessionId();
|
|
339
|
+
messageRows = resolveMessagesFor(sessionId);
|
|
307
340
|
}
|
|
308
341
|
if (!sessionId) return null;
|
|
309
|
-
|
|
310
|
-
const messageRows: any[] = db.prepare(src.message_query).all(sessionId);
|
|
311
342
|
if (!messageRows || messageRows.length === 0) return null;
|
|
312
343
|
|
|
313
344
|
const mtime = safeMtimeMs(resolved);
|