@adhdev/daemon-core 0.9.82-rc.466 → 0.9.82-rc.468
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/index.js +188 -147
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +195 -154
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-effect-format.d.ts +30 -0
- package/dist/providers/cli-provider-instance-types.d.ts +45 -0
- package/dist/providers/cli-provider-instance.d.ts +12 -6
- package/dist/providers/cli-provider-transcript-merge.d.ts +7 -0
- package/package.json +3 -3
- package/src/mesh/mesh-events-pending.ts +2 -0
- package/src/mesh/mesh-reconcile-loop.ts +32 -5
- package/src/mesh/mesh-runtime-store.ts +46 -1
- package/src/providers/cli-provider-effect-format.ts +53 -0
- package/src/providers/cli-provider-instance-types.ts +131 -0
- package/src/providers/cli-provider-instance.ts +87 -303
- package/src/providers/cli-provider-transcript-merge.ts +114 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export declare function getEffectDedupKey(effect: {
|
|
2
|
+
id?: string;
|
|
3
|
+
type: string;
|
|
4
|
+
message?: {
|
|
5
|
+
content?: unknown;
|
|
6
|
+
};
|
|
7
|
+
toast?: {
|
|
8
|
+
message?: string;
|
|
9
|
+
};
|
|
10
|
+
notification?: {
|
|
11
|
+
title?: string;
|
|
12
|
+
body?: string;
|
|
13
|
+
};
|
|
14
|
+
}): string;
|
|
15
|
+
export declare function getPersistedEffectContent(effect: {
|
|
16
|
+
type: string;
|
|
17
|
+
message?: {
|
|
18
|
+
content?: unknown;
|
|
19
|
+
};
|
|
20
|
+
toast?: {
|
|
21
|
+
message?: string;
|
|
22
|
+
};
|
|
23
|
+
notification?: {
|
|
24
|
+
title?: string;
|
|
25
|
+
body?: string;
|
|
26
|
+
bubbleContent?: unknown;
|
|
27
|
+
};
|
|
28
|
+
}): string | null;
|
|
29
|
+
export declare function formatApprovalRequestMessage(modalMessage?: string, buttons?: string[]): string;
|
|
30
|
+
export declare function formatMarkerTimestamp(timestamp: number): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const STATUS_HYDRATION_TAIL_LIMIT = 200;
|
|
2
|
+
export type CompletedDebouncePending = {
|
|
3
|
+
chatTitle: string;
|
|
4
|
+
duration: number;
|
|
5
|
+
timestamp: number;
|
|
6
|
+
firstObservedAt: number;
|
|
7
|
+
previousStatus: string;
|
|
8
|
+
loggedBlockReason?: string;
|
|
9
|
+
loggedTranscriptProbe?: boolean;
|
|
10
|
+
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
11
|
+
taskId?: string;
|
|
12
|
+
turnStartedAt?: number;
|
|
13
|
+
busyEpochAtArm?: number;
|
|
14
|
+
lastOutputAtArm?: number;
|
|
15
|
+
};
|
|
16
|
+
export type CompletedFinalizationBlock = {
|
|
17
|
+
reason: string;
|
|
18
|
+
terminal?: boolean;
|
|
19
|
+
allowTimeout?: boolean;
|
|
20
|
+
holdForTranscript?: boolean;
|
|
21
|
+
};
|
|
22
|
+
export type CompletionFinalAssistantEvidence = {
|
|
23
|
+
present: boolean;
|
|
24
|
+
messages: unknown[];
|
|
25
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
26
|
+
};
|
|
27
|
+
export type ExternalTranscriptProbe = {
|
|
28
|
+
readAt: number;
|
|
29
|
+
msgCount: number;
|
|
30
|
+
lastRole: string | null;
|
|
31
|
+
lastKind: string | null;
|
|
32
|
+
contentLen: number;
|
|
33
|
+
sourcePath: string | null;
|
|
34
|
+
sourceMtimeMs: number | null;
|
|
35
|
+
mtimeAgeMs: number | null;
|
|
36
|
+
};
|
|
37
|
+
export declare const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
38
|
+
export declare const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30000;
|
|
39
|
+
export declare const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 4000;
|
|
40
|
+
export declare const USER_INPUT_ACK_DEDUP_WINDOW_MS = 60000;
|
|
41
|
+
export declare const STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS = 12000;
|
|
42
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
43
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
44
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
45
|
+
export declare const TERMINAL_MESH_EVENTS: Set<string>;
|
|
@@ -278,7 +278,6 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
278
278
|
private lastExternalCompletionProbe;
|
|
279
279
|
private enforceFreshSessionLaunchIfNeeded;
|
|
280
280
|
private completionHasFinalAssistantMessage;
|
|
281
|
-
private buildExternalTranscriptProbe;
|
|
282
281
|
private recordPendingTranscriptProbe;
|
|
283
282
|
/**
|
|
284
283
|
* The spawned CLI's env overrides (e.g. the mesh coordinator points hermes
|
|
@@ -312,6 +311,18 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
312
311
|
* genuine resolution frees the gate promptly.
|
|
313
312
|
*/
|
|
314
313
|
private autoApproveContinuityWindowMs;
|
|
314
|
+
/**
|
|
315
|
+
* The settle-gate identity signature for a raw activeModal, or null when the
|
|
316
|
+
* modal is NOT a concrete auto-approvable consent prompt (no captured buttons,
|
|
317
|
+
* a picker/confirm kind, or no reliable affirmative+decline anchor). Mirrors the
|
|
318
|
+
* gates the auto-approve fire path applies before computing modalSignature, so
|
|
319
|
+
* the mask-stall nudge can ask the SAME question the settle gate is tracking —
|
|
320
|
+
* "is THIS frame's modal the identity the settle clock is accruing against?" —
|
|
321
|
+
* without duplicating the button-pick logic. The signature is message +
|
|
322
|
+
* normalized affirmative label only (no volatile counters/button set), matching
|
|
323
|
+
* the fire path exactly (AUTOAPPROVE-SETTLE-FLAP).
|
|
324
|
+
*/
|
|
325
|
+
private approvableModalSignature;
|
|
315
326
|
private isAutonomousMeshSession;
|
|
316
327
|
/**
|
|
317
328
|
* ARCH-REFACTOR R1: the taskId to attribute the CURRENTLY-completing turn to.
|
|
@@ -378,8 +389,6 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
378
389
|
private pushEvent;
|
|
379
390
|
private flushEvents;
|
|
380
391
|
private applyProviderResponse;
|
|
381
|
-
private getEffectDedupKey;
|
|
382
|
-
private getPersistedEffectContent;
|
|
383
392
|
getAdapter(): ProviderCliAdapter;
|
|
384
393
|
get cliType(): string;
|
|
385
394
|
get cliName(): string;
|
|
@@ -422,13 +431,10 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
422
431
|
private maybeEmitStalledApprovalNudge;
|
|
423
432
|
private recordAutoApproval;
|
|
424
433
|
recordApprovalSelection(buttonText: string): void;
|
|
425
|
-
private formatMarkerTimestamp;
|
|
426
434
|
private maybeAppendRuntimeRecoveryMessage;
|
|
427
435
|
private appendRuntimeSystemMessage;
|
|
428
436
|
private appendRuntimeMessage;
|
|
429
437
|
mergeRuntimeChatMessages(parsedMessages: ChatMessage[]): ChatMessage[];
|
|
430
|
-
private mergeConversationMessages;
|
|
431
|
-
private formatApprovalRequestMessage;
|
|
432
438
|
private promoteProviderSessionId;
|
|
433
439
|
private shouldHydrateExistingProviderHistory;
|
|
434
440
|
private shouldSuppressFreshLaunchStartupReplay;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ChatMessage } from '../types.js';
|
|
2
|
+
import type { ExternalTranscriptProbe } from './cli-provider-instance-types.js';
|
|
3
|
+
export declare function mergeConversationMessages(runtimeMessages: Array<{
|
|
4
|
+
key: string;
|
|
5
|
+
message: ChatMessage;
|
|
6
|
+
}>, parsedMessages: any[]): ChatMessage[];
|
|
7
|
+
export declare function buildExternalTranscriptProbe(messages: unknown[], sourcePath?: string, sourceMtimeMs?: number): ExternalTranscriptProbe;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.468",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
51
|
-
"@adhdev/session-host-core": "0.9.82-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "0.9.82-rc.468",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.468",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -551,6 +551,8 @@ export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent)
|
|
|
551
551
|
// wasDirectDeliveredToCoordinator) was removed when spontaneous PTY direct-inject was retired.
|
|
552
552
|
// Delivery is now queue-drain-only: an event is consumed by exactly one drainer via the atomic
|
|
553
553
|
// SQLite drained=1 marking, so there is no PTY-vs-poll double-delivery left to dedup against.
|
|
554
|
+
// The dormant mesh_direct_delivered_events table that backed it was dropped in
|
|
555
|
+
// MeshRuntimeStore.migrateMeshIsolationColumns (DROP TABLE IF EXISTS, migration step 5).
|
|
554
556
|
|
|
555
557
|
export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
556
558
|
const fingerprint = buildPendingEventFingerprint(event);
|
|
@@ -1531,7 +1531,11 @@ async function pullRemoteNodeQueues(
|
|
|
1531
1531
|
? candidateDaemonIds.map(id => ({ meshId, coordinatorDaemonId: id }))
|
|
1532
1532
|
: [{ meshId }];
|
|
1533
1533
|
|
|
1534
|
-
|
|
1534
|
+
// Parallelize across nodes: a single connected-but-slow node must not serially
|
|
1535
|
+
// block the other nodes for the rest of the tick. Each node callback is fully
|
|
1536
|
+
// self-contained (local/candidate skip, peer-connected pre-check, per-candidate
|
|
1537
|
+
// pulls, extract→re-inject) and best-effort — allSettled swallows per-node errors.
|
|
1538
|
+
await Promise.allSettled(mesh.nodes.map(async (node) => {
|
|
1535
1539
|
const nodeDaemonId = readNonEmptyString(node.daemonId);
|
|
1536
1540
|
// Skip nodes without a daemon, and nodes on THIS daemon (their events are
|
|
1537
1541
|
// already in the local queue drained in PHASE 2). "This daemon" is matched
|
|
@@ -1539,9 +1543,22 @@ async function pullRemoteNodeQueues(
|
|
|
1539
1543
|
// localDaemonId — a self node can be registered under the config-form daemonId
|
|
1540
1544
|
// (`daemon_<machineId>`) which would NOT equal bare localDaemonId, and pulling
|
|
1541
1545
|
// from ourselves over P2P is both wasteful and a self-dispatch hazard.
|
|
1542
|
-
if (!nodeDaemonId)
|
|
1543
|
-
if (daemonIdsEquivalent(nodeDaemonId, localDaemonId))
|
|
1544
|
-
if (daemonIdListIncludes(candidateDaemonIds, nodeDaemonId))
|
|
1546
|
+
if (!nodeDaemonId) return;
|
|
1547
|
+
if (daemonIdsEquivalent(nodeDaemonId, localDaemonId)) return;
|
|
1548
|
+
if (daemonIdListIncludes(candidateDaemonIds, nodeDaemonId)) return;
|
|
1549
|
+
|
|
1550
|
+
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a)): a degraded peer whose
|
|
1551
|
+
// DataChannel is not open would sink this pull into peer.connectQueue and stall
|
|
1552
|
+
// until CONNECT_TIMEOUT_MS (90s), formerly freezing the whole serial loop and
|
|
1553
|
+
// delaying completion-event recovery from healthy nodes. Skip such a node THIS
|
|
1554
|
+
// tick and retry next tick — LOSSLESS: an unconnected peer has not drained
|
|
1555
|
+
// anything (drained=0 preserved), so its events are recovered whole on the next
|
|
1556
|
+
// successful tick. Skip = delay, never loss.
|
|
1557
|
+
// • snapshot present and state !== 'connected' → skip (continue next tick).
|
|
1558
|
+
// • snapshot null/undefined (getter unwired, e.g. standalone) → DO NOT skip;
|
|
1559
|
+
// fall through to the legacy path so this stays regression-free.
|
|
1560
|
+
const peerSnapshot = components.getMeshPeerConnectionStatus?.(nodeDaemonId);
|
|
1561
|
+
if (peerSnapshot && String(peerSnapshot.state) !== 'connected') return;
|
|
1545
1562
|
|
|
1546
1563
|
for (const pendingEventArgs of pulls) {
|
|
1547
1564
|
let events: unknown;
|
|
@@ -1560,7 +1577,7 @@ async function pullRemoteNodeQueues(
|
|
|
1560
1577
|
} catch { /* best-effort re-inject */ }
|
|
1561
1578
|
}
|
|
1562
1579
|
}
|
|
1563
|
-
}
|
|
1580
|
+
}));
|
|
1564
1581
|
}
|
|
1565
1582
|
|
|
1566
1583
|
// Pull the read_chat payload out of whatever envelope the transport returned.
|
|
@@ -1989,6 +2006,16 @@ async function collectLiveNodesWithSessions(
|
|
|
1989
2006
|
const isLocalNode = !nodeDaemonId
|
|
1990
2007
|
|| daemonIdListIncludes(selfIds, nodeDaemonId)
|
|
1991
2008
|
|| daemonIdsEquivalent(nodeDaemonId, localDaemonId);
|
|
2009
|
+
// Peer-connected pre-check (EVENT-DELIVERY-DELAY fix(a)): mirror pullRemoteNodeQueues.
|
|
2010
|
+
// Without this the 90s connect-deadline block re-enters via this Promise.all —
|
|
2011
|
+
// a degraded remote's get_status_metadata sinks into peer.connectQueue and stalls
|
|
2012
|
+
// the whole prune probe. Only call the remote when the peer is 'connected'; an
|
|
2013
|
+
// unconnected peer is left undecorated (empty session list), same as unreachable.
|
|
2014
|
+
// Getter unwired (null/undefined) → do NOT skip, fall through (regression-free).
|
|
2015
|
+
if (!isLocalNode) {
|
|
2016
|
+
const peerSnapshot = components.getMeshPeerConnectionStatus?.(nodeDaemonId);
|
|
2017
|
+
if (peerSnapshot && String(peerSnapshot.state) !== 'connected') return node;
|
|
2018
|
+
}
|
|
1992
2019
|
let statusResult: unknown;
|
|
1993
2020
|
try {
|
|
1994
2021
|
if (isLocalNode) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from 'fs';
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { LOG } from '../logging/logger.js';
|
|
4
4
|
import { loadBetterSqlite3 } from '../system/load-better-sqlite3.js';
|
|
5
|
+
import { getConfigDir } from '../config/config.js';
|
|
5
6
|
import { getLedgerDir } from './mesh-ledger.js';
|
|
6
7
|
import { nodeSatisfiesRequiredTags, isTaskReadonly, taskDependenciesSatisfied } from './mesh-work-queue.js';
|
|
7
8
|
import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, sessionIdsEquivalent } from '@adhdev/mesh-shared';
|
|
@@ -39,10 +40,43 @@ function legacyQueuePath(meshId: string): string {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
let loggedMigrationFailure = false;
|
|
43
|
+
let loggedStrayCleanup = false;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* MESH-COMPLEXITY-AUDIT Part 8-1: one-shot hygiene for a stray root mesh-runtime.db.
|
|
47
|
+
*
|
|
48
|
+
* The store lives at `~/.adhdev/mesh-ledger/mesh-runtime.db` (getLedgerDir()). An older
|
|
49
|
+
* build path could create a 0-byte `mesh-runtime.db` directly under `~/.adhdev/` — a
|
|
50
|
+
* dead file that is never opened or read (the canonical path is the only one used) but
|
|
51
|
+
* lingers. Remove it if and only if it is provably that stray: (a) exists, (b) is NOT the
|
|
52
|
+
* canonical store path, and (c) is empty (0 bytes). The size gate is the safety belt — we
|
|
53
|
+
* never unlink a non-empty file, so a real DB that somehow landed here is left untouched
|
|
54
|
+
* and surfaces as data rather than being silently deleted. Best-effort: any error is
|
|
55
|
+
* swallowed (with one diagnostic warn), never blocking store init.
|
|
56
|
+
*/
|
|
57
|
+
function cleanupStrayRootRuntimeDb(canonicalPath: string): void {
|
|
58
|
+
try {
|
|
59
|
+
const strayPath = join(getConfigDir(), 'mesh-runtime.db');
|
|
60
|
+
if (strayPath === canonicalPath) return; // canonical dir IS the config dir — never touch
|
|
61
|
+
if (!existsSync(strayPath)) return;
|
|
62
|
+
if (statSync(strayPath).size !== 0) return; // non-empty → not the known 0-byte stray; leave it
|
|
63
|
+
unlinkSync(strayPath);
|
|
64
|
+
if (!loggedStrayCleanup) {
|
|
65
|
+
loggedStrayCleanup = true;
|
|
66
|
+
LOG.info('MeshRuntimeStore', `Removed stray 0-byte root mesh-runtime.db at ${strayPath}`);
|
|
67
|
+
}
|
|
68
|
+
} catch (err: any) {
|
|
69
|
+
if (!loggedStrayCleanup) {
|
|
70
|
+
loggedStrayCleanup = true;
|
|
71
|
+
LOG.warn('MeshRuntimeStore', `Stray root mesh-runtime.db cleanup failed (ignored): ${err?.message || err}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
42
75
|
|
|
43
76
|
function meshRuntimeStorePath(): string {
|
|
44
77
|
const dir = getLedgerDir();
|
|
45
78
|
const nextPath = join(dir, 'mesh-runtime.db');
|
|
79
|
+
cleanupStrayRootRuntimeDb(nextPath);
|
|
46
80
|
if (existsSync(nextPath)) return nextPath;
|
|
47
81
|
|
|
48
82
|
const legacyPath = join(dir, 'beads.db');
|
|
@@ -459,6 +493,17 @@ export class MeshRuntimeStore {
|
|
|
459
493
|
ON mesh_pending_events(mesh_id, event_id)
|
|
460
494
|
WHERE event_id IS NOT NULL
|
|
461
495
|
`);
|
|
496
|
+
|
|
497
|
+
// 5. MESH-COMPLEXITY-AUDIT Part 8-1: drop the legacy mesh_direct_delivered_events
|
|
498
|
+
// table. It backed the retired R3 "direct-delivered" dedup marker
|
|
499
|
+
// (markMeshCoordinatorEventDirectDelivered / wasDirectDeliveredToCoordinator,
|
|
500
|
+
// removed when spontaneous PTY direct-inject was retired — see the NOTE in
|
|
501
|
+
// mesh-events-pending.ts). No live code CREATEs, reads, or writes it anymore,
|
|
502
|
+
// so this is a pure runtime-residue cleanup with no behavior change: a store
|
|
503
|
+
// that never had the table just no-ops (IF EXISTS), an old install carrying
|
|
504
|
+
// the dormant table has it removed once. Idempotent — DROP TABLE IF EXISTS is
|
|
505
|
+
// a no-op on every subsequent boot.
|
|
506
|
+
this.db.exec(`DROP TABLE IF EXISTS mesh_direct_delivered_events`);
|
|
462
507
|
} catch (err: any) {
|
|
463
508
|
// Best-effort: a failed isolation migration must not brick the store. The
|
|
464
509
|
// CREATE-TABLE definitions above already carry the new schema for fresh DBs;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Provider-effect dedup and message-formatting helpers extracted from
|
|
2
|
+
// cli-provider-instance.ts. Pure move — no behavior change. None of these used
|
|
3
|
+
// any instance state; they were private methods invoked only within the class.
|
|
4
|
+
|
|
5
|
+
export function getEffectDedupKey(effect: { id?: string; type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string } }): string {
|
|
6
|
+
if (effect.id) return `provider_effect:${effect.id}`;
|
|
7
|
+
if (effect.type === 'message') {
|
|
8
|
+
const content = typeof effect.message?.content === 'string'
|
|
9
|
+
? effect.message.content
|
|
10
|
+
: JSON.stringify(effect.message?.content || '');
|
|
11
|
+
return `provider_effect:message:${content}`;
|
|
12
|
+
}
|
|
13
|
+
if (effect.type === 'notification') {
|
|
14
|
+
return `provider_effect:notification:${effect.notification?.title || ''}:${effect.notification?.body || ''}`;
|
|
15
|
+
}
|
|
16
|
+
return `provider_effect:toast:${effect.toast?.message || ''}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getPersistedEffectContent(effect: { type: string; message?: { content?: unknown }; toast?: { message?: string }; notification?: { title?: string; body?: string; bubbleContent?: unknown } }): string | null {
|
|
20
|
+
if (effect.type === 'message') {
|
|
21
|
+
return typeof effect.message?.content === 'string'
|
|
22
|
+
? effect.message.content
|
|
23
|
+
: JSON.stringify(effect.message?.content || '');
|
|
24
|
+
}
|
|
25
|
+
if (effect.type === 'toast') {
|
|
26
|
+
return effect.toast?.message || null;
|
|
27
|
+
}
|
|
28
|
+
if (effect.type === 'notification') {
|
|
29
|
+
if (typeof effect.notification?.bubbleContent === 'string') return effect.notification.bubbleContent;
|
|
30
|
+
if (typeof effect.notification?.title === 'string' && effect.notification.title.trim()) {
|
|
31
|
+
return `${effect.notification.title}\n${effect.notification.body || ''}`.trim();
|
|
32
|
+
}
|
|
33
|
+
return effect.notification?.body || null;
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function formatApprovalRequestMessage(modalMessage?: string, buttons?: string[]): string {
|
|
39
|
+
const lines = ['Approval requested'];
|
|
40
|
+
const cleanMessage = String(modalMessage || '').trim();
|
|
41
|
+
if (cleanMessage) lines.push(cleanMessage);
|
|
42
|
+
const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean);
|
|
43
|
+
if (labels.length > 0) {
|
|
44
|
+
lines.push(labels.map((label) => `[${label}]`).join(' '));
|
|
45
|
+
}
|
|
46
|
+
return lines.join('\n');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatMarkerTimestamp(timestamp: number): string {
|
|
50
|
+
const date = new Date(timestamp);
|
|
51
|
+
const pad = (value: number) => String(value).padStart(2, '0');
|
|
52
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Module-level types and constants extracted from cli-provider-instance.ts.
|
|
2
|
+
// Pure move — no behavior change. These were module-private in the original
|
|
3
|
+
// file; they are re-imported by cli-provider-instance.ts and used identically.
|
|
4
|
+
|
|
5
|
+
// Status snapshots only ever surface the newest messages: the cloud 'live'
|
|
6
|
+
// profile drops chat messages entirely (loaded lazily via read_chat on
|
|
7
|
+
// subscribe) and the 'full' profile caps activeChat.messages to the last 60
|
|
8
|
+
// (see status/normalize.ts). Unread/completion markers walk only the tail.
|
|
9
|
+
// So getState()'s saved-history hydration — which runs once per resume/manual
|
|
10
|
+
// CLI session on every status report — must read only a bounded tail, not the
|
|
11
|
+
// entire transcript. A full MAX_SAFE_INTEGER read here makes the initial
|
|
12
|
+
// status report O(transcript) × N(sessions), which is the real cold first-
|
|
13
|
+
// connection bottleneck on chat-heavy machines. The window comfortably exceeds
|
|
14
|
+
// the 60-message snapshot cap so dedup/collapse at the boundary stays stable.
|
|
15
|
+
export const STATUS_HYDRATION_TAIL_LIMIT = 200;
|
|
16
|
+
|
|
17
|
+
export type CompletedDebouncePending = {
|
|
18
|
+
chatTitle: string;
|
|
19
|
+
duration: number;
|
|
20
|
+
timestamp: number;
|
|
21
|
+
firstObservedAt: number;
|
|
22
|
+
previousStatus: string;
|
|
23
|
+
loggedBlockReason?: string;
|
|
24
|
+
loggedTranscriptProbe?: boolean;
|
|
25
|
+
transcriptProbeHistory?: ExternalTranscriptProbe[];
|
|
26
|
+
// ARCH-REFACTOR R1: the taskId of the turn that produced this (debounced) completion,
|
|
27
|
+
// captured SYNCHRONOUSLY at the generating→idle transition. The actual completion
|
|
28
|
+
// event is emitted later by the debounce flush, by which point a follow-up task may
|
|
29
|
+
// already have started its own turn and overwritten engine.currentTurnTaskId — so the
|
|
30
|
+
// id must be snapshotted here, not re-read at flush time.
|
|
31
|
+
taskId?: string;
|
|
32
|
+
// NOTIF Defect-B: the wall-clock start of the turn that produced this (debounced)
|
|
33
|
+
// completion, snapshotted SYNCHRONOUSLY at the generating→idle transition (same
|
|
34
|
+
// reason as taskId — a follow-up turn moves engine.currentTurnStartedAt). The
|
|
35
|
+
// completion's finalSummary is turn-scoped to bubbles at/after this instant so a
|
|
36
|
+
// debounce that flushes before the producing turn's final assistant bubble lands
|
|
37
|
+
// in the native transcript never echoes the PRIOR task's last bubble.
|
|
38
|
+
turnStartedAt?: number;
|
|
39
|
+
// FALSE-IDLE continuity: the busyEpoch value at the instant this pending was armed.
|
|
40
|
+
// The flush guard requires this.busyEpoch to still equal this — proving no busy
|
|
41
|
+
// phase (generating/waiting_approval) opened since arming. A momentary busy→idle
|
|
42
|
+
// blip in an inter-approval valley bumps busyEpoch, so a completion armed before
|
|
43
|
+
// the blip is cancelled at flush instead of emitting a stale mid-turn summary.
|
|
44
|
+
busyEpochAtArm?: number;
|
|
45
|
+
// FALSE-IDLE continuity: the adapter's raw PTY lastOutputAt at arm time. New PTY
|
|
46
|
+
// output after arming means the session was not continuously idle through the
|
|
47
|
+
// settle window (the agent kept printing), so the completion is cancelled.
|
|
48
|
+
lastOutputAtArm?: number;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type CompletedFinalizationBlock = {
|
|
52
|
+
reason: string;
|
|
53
|
+
terminal?: boolean;
|
|
54
|
+
allowTimeout?: boolean;
|
|
55
|
+
// (SETTLE-VALLEY) When set, suppress the CANON-C decoupled-immediate emit for this
|
|
56
|
+
// missing_final_assistant block and HOLD (retry up to COMPLETED_FINALIZATION_MAX_WAIT_MS)
|
|
57
|
+
// until the native transcript's final assistant turn arrives (block clears → genuine emit)
|
|
58
|
+
// or the worker resumes (resume guard cancels). Set only for the inter-approval idle valley
|
|
59
|
+
// of a native-history mesh worker, where an immediate weak emit would freeze a truncated
|
|
60
|
+
// preamble summary (evidenceLevel=insufficient) into the append-only ledger before the
|
|
61
|
+
// worker's next approval turn resumes. Independent of valley length.
|
|
62
|
+
holdForTranscript?: boolean;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type CompletionFinalAssistantEvidence = {
|
|
66
|
+
present: boolean;
|
|
67
|
+
messages: unknown[];
|
|
68
|
+
source: 'parsed' | 'external-native' | 'unavailable';
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ExternalTranscriptProbe = {
|
|
72
|
+
readAt: number;
|
|
73
|
+
msgCount: number;
|
|
74
|
+
lastRole: string | null;
|
|
75
|
+
lastKind: string | null;
|
|
76
|
+
contentLen: number;
|
|
77
|
+
sourcePath: string | null;
|
|
78
|
+
sourceMtimeMs: number | null;
|
|
79
|
+
mtimeAgeMs: number | null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
83
|
+
export const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
|
|
84
|
+
// (FALSEIDLE-BGCHILD-a) Minimum generating→idle settle window for native-history mesh worker
|
|
85
|
+
// sessions. Native-history providers (e.g. claude-cli) normally flush the completion with
|
|
86
|
+
// flushDelay=0 — the transcript is authoritative, so there is no reason to wait. But a worker
|
|
87
|
+
// turn that spawns a BACKGROUND child (e.g. `npm test &`, a backgrounded Bash tool) can paint
|
|
88
|
+
// a burst of child output, fall quiet, and have the screen parser read a PRIOR/intermediate
|
|
89
|
+
// standard assistant as if the turn were done — firing a false idle while the agent is in fact
|
|
90
|
+
// still generating (e.g. mid-commit). With flushDelay=0 there is no window for the resume guard
|
|
91
|
+
// in flushCompletedDebounceIfFinalized (latestVisibleStatus !== 'idle' → cancel) to observe the
|
|
92
|
+
// agent picking the turn back up. A short non-zero settle window restores that resume guard for
|
|
93
|
+
// mesh workers without delaying genuinely-finished turns beyond this bound. Scoped to mesh
|
|
94
|
+
// worker sessions so interactive native-history sessions keep the immediate flush.
|
|
95
|
+
// 4000ms (was 1500): live measurement showed the completion event can fire 1.6–3s
|
|
96
|
+
// BEFORE the worker's final-assistant turn lands in the transcript on a natural
|
|
97
|
+
// generating→idle completion (no approval modal), freezing a prior intermediate
|
|
98
|
+
// bubble as finalSummary (evidenceLevel=insufficient). The 68a3c324 waiting_approval
|
|
99
|
+
// hold only covers the approval-resolved valley; widening this settle window to 4000ms
|
|
100
|
+
// covers that race AND the ~3s waiting_approval valley within the settle bound.
|
|
101
|
+
export const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 4000;
|
|
102
|
+
// TASKBUBBLE-DUP: window during which an identical user-input ack (same trimmed
|
|
103
|
+
// content on the same instance) is treated as a redelivery of one dispatch and
|
|
104
|
+
// suppressed from the chat transcript. Matches the coordinator-side
|
|
105
|
+
// DUPLICATE_DISPATCH_WINDOW_MS (mesh-tools) so the daemon's bubble-level guard
|
|
106
|
+
// covers the same retry horizon as the MCP-level dispatch dedup.
|
|
107
|
+
export const USER_INPUT_ACK_DEDUP_WINDOW_MS = 60_000;
|
|
108
|
+
// GENERATING-BOUNDARY (R4c): window — measured from the startup-grace COLLAPSE moment
|
|
109
|
+
// (startupGraceCollapseAt), not boot — inside which a "first turn that ran+completed
|
|
110
|
+
// without the FSM ever observing a 'generating' frame" is attributed to the startup-grace
|
|
111
|
+
// collapse and synthesized (reason 'startup_grace_idle_turn_collapse').
|
|
112
|
+
// The spec's startup-grace exit is elapsed_ms=8000, so the FSM spends ~8s in 'starting'
|
|
113
|
+
// before collapsing to idle; a turn can be dispatched a few seconds AFTER that collapse
|
|
114
|
+
// (the live R4b miss: collapse at boot+8s, dispatch at boot+12.4s — already past a 12s
|
|
115
|
+
// boot-anchored window before the turn even started). Anchoring on the collapse moment
|
|
116
|
+
// covers dispatch-delay; R4d additionally anchors on the turn-START moment
|
|
117
|
+
// (engine.currentTurnStartedAt) so a non-trivial turn-DURATION cannot push the completion
|
|
118
|
+
// past a now-anchored window (the live rc.405 Probe2 miss). The strong discriminator is
|
|
119
|
+
// generatingStartedAt===0 (generating was never observed) AND a started-but-finished turn
|
|
120
|
+
// — the window only keeps the synthesized reason honest and scopes the synthesis to the
|
|
121
|
+
// boot collapse, so a much-later unobservably-fast turn is not mislabelled a startup collapse.
|
|
122
|
+
export const STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS = 12_000;
|
|
123
|
+
|
|
124
|
+
/** Events that signal a dispatched mesh task has reached a terminal state.
|
|
125
|
+
* Detach the mesh assignment after emitting one of these so the worker's
|
|
126
|
+
* next unrelated turn doesn't impersonate another completion. */
|
|
127
|
+
export const TERMINAL_MESH_EVENTS = new Set([
|
|
128
|
+
'agent:generating_completed',
|
|
129
|
+
'agent:stopped',
|
|
130
|
+
'agent:ready',
|
|
131
|
+
]);
|