@adhdev/daemon-core 0.9.82-rc.475 → 0.9.82-rc.477
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/chat-commands-read.d.ts +8 -0
- package/dist/commands/chat-commands.d.ts +1 -1
- package/dist/config/state-store.d.ts +30 -0
- package/dist/index.js +231 -34
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +231 -34
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-event-forwarding.d.ts +8 -0
- package/dist/providers/cli-provider-instance.d.ts +19 -0
- package/dist/providers/native-history/antigravity-claim-registry.d.ts +17 -6
- package/dist/sessions/registry.d.ts +20 -0
- package/dist/shared-types.d.ts +12 -0
- package/package.json +3 -3
- package/src/commands/chat-commands-read.ts +114 -13
- package/src/commands/chat-commands.ts +1 -1
- package/src/config/state-store.ts +55 -0
- package/src/mesh/mesh-event-forwarding.ts +38 -0
- package/src/providers/cli-provider-instance.ts +148 -5
- package/src/providers/native-history/antigravity-claim-registry.ts +19 -12
- package/src/providers/native-history/dispatcher.ts +92 -14
- package/src/sessions/registry.ts +38 -0
- package/src/shared-types.ts +12 -0
|
@@ -11,6 +11,14 @@ export declare function handleMeshForwardEvent(components: DaemonComponents, pay
|
|
|
11
11
|
} | {
|
|
12
12
|
success: boolean;
|
|
13
13
|
forwarded: number;
|
|
14
|
+
suppressed: boolean;
|
|
15
|
+
autoApprovingWorkerApproval: boolean;
|
|
16
|
+
error?: undefined;
|
|
17
|
+
} | {
|
|
18
|
+
success: boolean;
|
|
19
|
+
forwarded: number;
|
|
20
|
+
suppressed?: undefined;
|
|
21
|
+
autoApprovingWorkerApproval?: undefined;
|
|
14
22
|
error?: undefined;
|
|
15
23
|
} | {
|
|
16
24
|
success: boolean;
|
|
@@ -308,6 +308,18 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
308
308
|
private completedDebounceTimer;
|
|
309
309
|
private completedDebouncePending;
|
|
310
310
|
private lastExternalCompletionProbe;
|
|
311
|
+
/**
|
|
312
|
+
* The final assistant summary of the last completed turn, cached at
|
|
313
|
+
* completion-emit time. For a native-source provider (antigravity) whose
|
|
314
|
+
* assistant answer lives only in native-history — never in the PTY parse that
|
|
315
|
+
* feeds activeChat.messages — the dashboard's preview / lastMessageRole /
|
|
316
|
+
* completionMarker would otherwise never see the answer and show the session
|
|
317
|
+
* stuck on the user prompt. getState() appends this cached assistant bubble to
|
|
318
|
+
* the status messages when the PTY tail has none, so those fields reflect the
|
|
319
|
+
* real last answer with ZERO per-tick native reads (the native read already ran
|
|
320
|
+
* once at completion). Reset on the next turn's start.
|
|
321
|
+
*/
|
|
322
|
+
private lastCompletionSummary;
|
|
311
323
|
private enforceFreshSessionLaunchIfNeeded;
|
|
312
324
|
private completionHasFinalAssistantMessage;
|
|
313
325
|
private recordPendingTranscriptProbe;
|
|
@@ -321,6 +333,13 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
321
333
|
*/
|
|
322
334
|
private spawnedEnvOverrides;
|
|
323
335
|
private readExternalCompletionMessages;
|
|
336
|
+
/**
|
|
337
|
+
* The content of the LAST visible assistant bubble in a message list, or ''
|
|
338
|
+
* when the tail is not an assistant reply. Skips trailing system/tool/activity
|
|
339
|
+
* bubbles; stops (returns '') at the first user/human message. Used only for
|
|
340
|
+
* the dashboard tail-repair cache — a display value, not a completion decision.
|
|
341
|
+
*/
|
|
342
|
+
private lastVisibleAssistantSummary;
|
|
324
343
|
private completionFinalAssistantEvidence;
|
|
325
344
|
private completionFinalSummary;
|
|
326
345
|
private buildCompletedFinalizationDiagnostic;
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/** A claim older than this with no refresh is reclaimable (owner presumed dead). */
|
|
2
2
|
export declare const CLAIM_STALE_MS: number;
|
|
3
3
|
/**
|
|
4
|
-
* Derive the per-session owner token
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* Derive the per-session owner token, keyed ONLY on the stable instanceId
|
|
5
|
+
* (== the session registry sessionId == the read path's targetSessionId). Both
|
|
6
|
+
* the dispatcher (read side) and the provider instance (claim/release side) pass
|
|
7
|
+
* this same id, so their tokens always agree and the claim isolation holds.
|
|
8
|
+
*
|
|
9
|
+
* Returns '' when no instanceId is available — the caller then skips claiming
|
|
10
|
+
* (the exclusion checks still run against existing claims). This is the SSOT
|
|
11
|
+
* rule: there is exactly ONE token form. The removed legacy fallback derived a
|
|
12
|
+
* `spawn:<workspace>:<sessionStartedAtMs>` token from the spawn timestamp when
|
|
13
|
+
* the instanceId was missing; because one session's spawn time is sampled
|
|
14
|
+
* independently at three sites (instance startedAt, adapter spawnedAtMs, registry
|
|
15
|
+
* spawnedAtMs) those never matched, so the SAME session's instance-side and
|
|
16
|
+
* read-side tokens silently diverged and the claim mutual-exclusion collapsed
|
|
17
|
+
* (the antigravity conversation crosswire). An empty token (skip-claim) is
|
|
18
|
+
* strictly safer than a token that disagrees with the same session's other
|
|
19
|
+
* token. workspace/sessionStartedAtMs are kept in the signature for call-site
|
|
20
|
+
* compatibility but no longer affect the token.
|
|
10
21
|
*/
|
|
11
22
|
export declare function antigravityOwnerToken(workspace: string, sessionStartedAtMs: number, instanceId?: string): string;
|
|
12
23
|
/**
|
|
@@ -13,6 +13,19 @@ export interface SessionRuntimeTarget {
|
|
|
13
13
|
/** Wall clock at register time. native-history readers use it as a
|
|
14
14
|
* cutoff so a fresh session can't show records from a prior one. */
|
|
15
15
|
spawnedAtMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* Authoritative provider-native conversation id for this session (SSOT).
|
|
18
|
+
* For providers that expose a session id on the CLI (codex/claude/hermes)
|
|
19
|
+
* this equals that id. For antigravity — which takes no --session-id — this
|
|
20
|
+
* is the on-disk conversations/<uuid>.db basename, discovered by the
|
|
21
|
+
* native-history dispatcher and written back here via setProviderSessionId
|
|
22
|
+
* the first time it resolves. Every downstream reader (read_chat, the
|
|
23
|
+
* completion probe, the dashboard) should prefer this over re-deriving the
|
|
24
|
+
* conversation by spawn-floor/mtime heuristics — that re-derivation is the
|
|
25
|
+
* source of the antigravity conversation crosswire/theft class. Empty until
|
|
26
|
+
* the first successful native read binds it.
|
|
27
|
+
*/
|
|
28
|
+
providerSessionId?: string;
|
|
16
29
|
}
|
|
17
30
|
export declare class SessionRegistry {
|
|
18
31
|
private readonly bySessionId;
|
|
@@ -21,6 +34,13 @@ export declare class SessionRegistry {
|
|
|
21
34
|
private readonly byParentSessionId;
|
|
22
35
|
register(target: SessionRuntimeTarget): void;
|
|
23
36
|
get(sessionId: string | undefined | null): SessionRuntimeTarget | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Record the authoritative provider-native conversation id for a session
|
|
39
|
+
* (SSOT). Idempotent; a no-op when the session is unknown or the value is
|
|
40
|
+
* empty or unchanged. Never overwrites a known binding with an empty one.
|
|
41
|
+
* Returns whether the stored value changed.
|
|
42
|
+
*/
|
|
43
|
+
setProviderSessionId(sessionId: string | undefined | null, providerSessionId: string | undefined | null): boolean;
|
|
24
44
|
unregister(sessionId: string | undefined | null): void;
|
|
25
45
|
unregisterByManagerKey(managerKey: string): void;
|
|
26
46
|
unregisterByInstanceKey(instanceKey: string): void;
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -183,6 +183,18 @@ export interface DaemonMetadataUpdate {
|
|
|
183
183
|
userName?: string;
|
|
184
184
|
seq: number;
|
|
185
185
|
timestamp: number;
|
|
186
|
+
/**
|
|
187
|
+
* Per-mesh state-change revision counters (meshId → monotonically increasing
|
|
188
|
+
* integer), bumped whenever the daemon's mesh graph/queue/mission state for that
|
|
189
|
+
* mesh changes (onMeshStateChange). Lets the dashboard replace its client-side
|
|
190
|
+
* mesh_status polling with an event-driven background refresh: when the revision
|
|
191
|
+
* for the mesh it is viewing advances, it re-fetches the aggregate mesh_status
|
|
192
|
+
* (SWR, keeping the current graph on screen). This is a lightweight nudge — the
|
|
193
|
+
* full aggregate snapshot is fetched on demand, not embedded here, so the
|
|
194
|
+
* daemon.metadata payload stays small. Optional/absent for daemons/builds that
|
|
195
|
+
* don't emit it (the client then keeps its polling fallback).
|
|
196
|
+
*/
|
|
197
|
+
meshStateRevisions?: Record<string, number>;
|
|
186
198
|
}
|
|
187
199
|
export interface TopicUpdateEnvelopeMap {
|
|
188
200
|
'session.chat_tail': SessionChatTailUpdate;
|
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.477",
|
|
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.477",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.477",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -9,6 +9,7 @@ import type { CliAdapter } from '../cli-adapter-types.js';
|
|
|
9
9
|
import { flattenContent, type ProviderModule, type ProviderScripts } from '../providers/contracts.js';
|
|
10
10
|
import { validateReadChatResultPayload } from '../providers/read-chat-contract.js';
|
|
11
11
|
import { isNativeSourceCanonicalHistory, readChatHistory, readProviderChatHistory } from '../config/chat-history.js';
|
|
12
|
+
import { clearPersistedProviderSessionPins, loadPersistedProviderSessionPins, recordPersistedProviderSessionPin } from '../config/state-store.js';
|
|
12
13
|
import { getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
13
14
|
import { LOG } from '../logging/logger.js';
|
|
14
15
|
import { recordDebugTrace } from '../logging/debug-trace.js';
|
|
@@ -62,22 +63,77 @@ const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'her
|
|
|
62
63
|
// and run the native query normally instead of fail-closing. Refreshed on every
|
|
63
64
|
// successful bind; never lets an empty id clear a known pin. Keyed by mesh
|
|
64
65
|
// session id so pins never alias across distinct sessions sharing a workspace.
|
|
66
|
+
//
|
|
67
|
+
// The map is ALSO mirrored to disk (state.json sessionProviderSessionPins) so a
|
|
68
|
+
// pin survives a daemon restart. Without that, an attach-restored antigravity
|
|
69
|
+
// session (spawnedAtMs=0, so no live spawn floor) that has sat idle past the
|
|
70
|
+
// native reader's recency window can no longer resolve its own conversation .db
|
|
71
|
+
// after the daemon comes back — read_chat falls to the PTY parse and the
|
|
72
|
+
// dashboard shows the user prompt with the assistant tail missing
|
|
73
|
+
// (ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP). The in-memory map stays the hot path;
|
|
74
|
+
// disk is the cold-start hydration source, read lazily on the first miss.
|
|
65
75
|
const lastBoundProviderSessionIdByMeshSession = new Map<string, string>();
|
|
76
|
+
let persistedProviderSessionPinsHydrated = false;
|
|
66
77
|
|
|
67
|
-
function
|
|
78
|
+
function hydratePersistedProviderSessionPinsOnce(): void {
|
|
79
|
+
if (persistedProviderSessionPinsHydrated) return;
|
|
80
|
+
persistedProviderSessionPinsHydrated = true;
|
|
81
|
+
try {
|
|
82
|
+
for (const [key, value] of Object.entries(loadPersistedProviderSessionPins())) {
|
|
83
|
+
// Never let a stale persisted value clobber a fresher in-memory bind
|
|
84
|
+
// recorded earlier this process lifetime.
|
|
85
|
+
if (!lastBoundProviderSessionIdByMeshSession.has(key)) {
|
|
86
|
+
lastBoundProviderSessionIdByMeshSession.set(key, value);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// Best-effort: a missing/corrupt state file just means no cold-start pins.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function recordBoundProviderSessionId(h: CommandHelpers, meshSessionId: string | undefined, providerSessionId: string | undefined): void {
|
|
68
95
|
const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
|
|
69
96
|
const value = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
|
|
70
97
|
if (!key || !value) return;
|
|
98
|
+
// SSOT: the session registry entry (keyed by sessionId == instanceId) is the
|
|
99
|
+
// authoritative sessionId → conversation-uuid record. Writing it here — the
|
|
100
|
+
// moment a native read resolves the real conversation id — makes
|
|
101
|
+
// getHistorySessionId return it directly on every subsequent read, so the
|
|
102
|
+
// conversation is exact-bound instead of re-resolved by the spawn-floor/mtime
|
|
103
|
+
// heuristic (the crosswire/theft source). The pin below stays as the durable
|
|
104
|
+
// cross-restart mirror (the registry is in-memory and cleared on restart).
|
|
105
|
+
try { h.ctx?.sessionRegistry?.setProviderSessionId?.(key, value); } catch { /* best-effort SSOT write-back */ }
|
|
71
106
|
lastBoundProviderSessionIdByMeshSession.set(key, value);
|
|
107
|
+
// Always attempt the disk mirror — recordPersistedProviderSessionPin is itself a
|
|
108
|
+
// no-op when the ON-DISK value already matches, so it does not rewrite state.json
|
|
109
|
+
// on steady re-reads, yet it still lands a pin the in-memory map already holds but
|
|
110
|
+
// disk lost (a prior write clobbered by another state-store writer, or a restart
|
|
111
|
+
// whose hydration ran before this bind). Gating on the in-memory previous value
|
|
112
|
+
// let the in-memory and on-disk pin diverge permanently, defeating the persistence.
|
|
113
|
+
try { recordPersistedProviderSessionPin(key, value); } catch { /* best-effort disk mirror */ }
|
|
72
114
|
}
|
|
73
115
|
|
|
74
116
|
function getBoundProviderSessionIdPin(meshSessionId: string | undefined): string | undefined {
|
|
75
117
|
const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
|
|
76
118
|
if (!key) return undefined;
|
|
119
|
+
hydratePersistedProviderSessionPinsOnce();
|
|
77
120
|
const pinned = lastBoundProviderSessionIdByMeshSession.get(key);
|
|
78
121
|
return pinned && pinned.trim() ? pinned.trim() : undefined;
|
|
79
122
|
}
|
|
80
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Test-only: clear the in-memory read-pin map and re-arm cold-start hydration so
|
|
126
|
+
* each test starts from a clean pin state. The on-disk mirror is isolated per
|
|
127
|
+
* test process via ADHDEV_CONFIG_DIR (test/helpers/setup-env.ts); this resets the
|
|
128
|
+
* module-level cache that would otherwise leak a pin across tests sharing the
|
|
129
|
+
* worker. Not part of the runtime contract.
|
|
130
|
+
*/
|
|
131
|
+
export function __resetProviderSessionPinsForTest(): void {
|
|
132
|
+
lastBoundProviderSessionIdByMeshSession.clear();
|
|
133
|
+
persistedProviderSessionPinsHydrated = false;
|
|
134
|
+
try { clearPersistedProviderSessionPins(); } catch { /* best-effort */ }
|
|
135
|
+
}
|
|
136
|
+
|
|
81
137
|
const warnedLegacyNativeAllowlistHits = new Set<string>();
|
|
82
138
|
function warnLegacyNativeAllowlistHit(providerType: string): void {
|
|
83
139
|
if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
|
|
@@ -1036,6 +1092,28 @@ function hasSafeNativeHistoryMapping(args: {
|
|
|
1036
1092
|
// other. historySessionId (the provider-native session key) is required to
|
|
1037
1093
|
// establish ownership. hasSafeNativeHistoryMapping() enforces the same
|
|
1038
1094
|
// invariant after the read; both guards must hold for native history to be used.
|
|
1095
|
+
/**
|
|
1096
|
+
* The session id a native-history read should be scoped to: the explicit
|
|
1097
|
+
* targetSessionId when the caller named one (reading a specific/worker
|
|
1098
|
+
* session), otherwise the current live session (a self / dashboard read where
|
|
1099
|
+
* the current session IS the one being read). getTargetedCliAdapter already
|
|
1100
|
+
* uses this same fallback to resolve the adapter; the native-history floor and
|
|
1101
|
+
* claim-owner token must use it too. Without the fallback, a self-read arrives
|
|
1102
|
+
* with no targetSessionId → the floor collapses to undefined (→0) and the
|
|
1103
|
+
* antigravity claim-owner token collapses to '' → pickUnboundConversationDb
|
|
1104
|
+
* drops out of its spawn-floor branch into newest-by-mtime and binds whichever
|
|
1105
|
+
* conversation .db was written most recently. For an antigravity MAGI
|
|
1106
|
+
* coordinator that is exactly a co-located replica's .db (the replica finished
|
|
1107
|
+
* its turn last), so the coordinator's read cross-wires onto the replica's
|
|
1108
|
+
* conversation instead of its own (ANTIGRAVITY coordinator↔replica crosswire).
|
|
1109
|
+
*/
|
|
1110
|
+
function effectiveReadSessionId(h: CommandHelpers, targetSessionId: string | undefined): string {
|
|
1111
|
+
const explicit = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
1112
|
+
if (explicit) return explicit;
|
|
1113
|
+
const current = (h.currentSession as any)?.sessionId;
|
|
1114
|
+
return typeof current === 'string' ? current.trim() : '';
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1039
1117
|
/**
|
|
1040
1118
|
* Pull the session's spawnedAtMs out of the registry. Native-history
|
|
1041
1119
|
* file pickers use it as a "files older than this can't be from this
|
|
@@ -1043,10 +1121,12 @@ function hasSafeNativeHistoryMapping(args: {
|
|
|
1043
1121
|
* previous session's transcript whenever its file happened to be the
|
|
1044
1122
|
* newest match. Returns undefined when the session isn't registered
|
|
1045
1123
|
* (e.g. read_chat before the live session was wired up) — the executor
|
|
1046
|
-
* treats undefined as "no floor".
|
|
1124
|
+
* treats undefined as "no floor". Resolves the effective session id
|
|
1125
|
+
* (targetSessionId or the current live session) so a self-read still gets its
|
|
1126
|
+
* real spawn floor rather than 0.
|
|
1047
1127
|
*/
|
|
1048
1128
|
function sessionStartedAtMsFromRegistry(h: CommandHelpers, targetSessionId: string | undefined): number | undefined {
|
|
1049
|
-
const sid =
|
|
1129
|
+
const sid = effectiveReadSessionId(h, targetSessionId);
|
|
1050
1130
|
if (!sid) return undefined;
|
|
1051
1131
|
const target = h.ctx?.sessionRegistry?.get?.(sid);
|
|
1052
1132
|
return typeof target?.spawnedAtMs === 'number' ? target.spawnedAtMs : undefined;
|
|
@@ -1549,7 +1629,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
1549
1629
|
scripts: provider?.scripts as any,
|
|
1550
1630
|
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
1551
1631
|
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1552
|
-
instanceId:
|
|
1632
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
1553
1633
|
pinnedProviderSessionId: getBoundProviderSessionIdPin(args?.targetSessionId),
|
|
1554
1634
|
})
|
|
1555
1635
|
: readProviderChatHistory(agentStr, {
|
|
@@ -1571,7 +1651,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
1571
1651
|
? (result as any).providerSessionId
|
|
1572
1652
|
: readHistorySessionIdFromMessages(messages) || historySessionId;
|
|
1573
1653
|
if (typeof (result as any)?.providerSessionId === 'string' && (result as any).providerSessionId.trim()) {
|
|
1574
|
-
recordBoundProviderSessionId(args?.targetSessionId, (result as any).providerSessionId.trim());
|
|
1654
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, args?.targetSessionId), (result as any).providerSessionId.trim());
|
|
1575
1655
|
}
|
|
1576
1656
|
const safeMapping = hasSafeNativeHistoryMapping({
|
|
1577
1657
|
historySessionId: lookup === 'workspace' ? undefined : historySessionId,
|
|
@@ -1768,10 +1848,31 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1768
1848
|
let nativeHistory: any | null = null;
|
|
1769
1849
|
let nativeHistoryError: unknown | undefined;
|
|
1770
1850
|
if (supportsNative) {
|
|
1851
|
+
// Runtime-fallback → pin substitution: nativeHistoryReadSessionId is
|
|
1852
|
+
// the bare runtime/session id when no explicit provider handle was
|
|
1853
|
+
// supplied and none was parsed (antigravity takes no --session-id, so
|
|
1854
|
+
// its this.providerSessionId stays empty and getHistorySessionId falls
|
|
1855
|
+
// back to targetSessionId). That runtime id is not the on-disk
|
|
1856
|
+
// conversations/<uuid>.db name, so a native read keyed on it can never
|
|
1857
|
+
// exact-bind and falls to the recency heuristic — which drops an idle
|
|
1858
|
+
// (or restored, spawnedAtMs=0) session's own store. Prefer a pin (a
|
|
1859
|
+
// real conversation id a prior read resolved for THIS session, now also
|
|
1860
|
+
// persisted across restart) over the runtime id, else drop the runtime
|
|
1861
|
+
// id so readCliProviderNativeHistory's pin / workspace-latest paths can
|
|
1862
|
+
// engage. Mirrors the handleChatHistory path's established handling.
|
|
1863
|
+
const pinnedProviderSessionIdForRead = getBoundProviderSessionIdPin(targetSessionId);
|
|
1864
|
+
const nativeReadSessionIdIsRuntimeFallback = Boolean(
|
|
1865
|
+
targetSessionId
|
|
1866
|
+
&& nativeHistoryReadSessionId === targetSessionId
|
|
1867
|
+
&& !getExplicitHistorySessionId(args),
|
|
1868
|
+
);
|
|
1869
|
+
const effectiveNativeReadSessionId = nativeReadSessionIdIsRuntimeFallback
|
|
1870
|
+
? (pinnedProviderSessionIdForRead || undefined)
|
|
1871
|
+
: nativeHistoryReadSessionId;
|
|
1771
1872
|
try {
|
|
1772
1873
|
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
1773
1874
|
canonicalHistory: provider?.nativeHistory,
|
|
1774
|
-
historySessionId:
|
|
1875
|
+
historySessionId: effectiveNativeReadSessionId,
|
|
1775
1876
|
workspace,
|
|
1776
1877
|
offset: 0,
|
|
1777
1878
|
limit: nativeHistoryLimit,
|
|
@@ -1783,12 +1884,12 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1783
1884
|
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1784
1885
|
// Stable per-session identity for antigravity's conversation-claim
|
|
1785
1886
|
// owner token (== session registry sessionId == instance instanceId).
|
|
1786
|
-
instanceId:
|
|
1787
|
-
pinnedProviderSessionId:
|
|
1887
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
1888
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForRead,
|
|
1788
1889
|
// Last-resort only when no pin was ever recorded for this
|
|
1789
1890
|
// session; the downstream workspace-overlap safety gate
|
|
1790
1891
|
// still filters an aliased session out.
|
|
1791
|
-
allowWorkspaceLatestFallback: !
|
|
1892
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForRead,
|
|
1792
1893
|
});
|
|
1793
1894
|
// Refresh the per-mesh-session pin whenever a native read
|
|
1794
1895
|
// resolves a concrete provider-native session id. A later
|
|
@@ -1799,7 +1900,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1799
1900
|
? nativeHistory.providerSessionId.trim()
|
|
1800
1901
|
: '';
|
|
1801
1902
|
if (resolvedProviderSessionId) {
|
|
1802
|
-
recordBoundProviderSessionId(targetSessionId, resolvedProviderSessionId);
|
|
1903
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSessionId), resolvedProviderSessionId);
|
|
1803
1904
|
}
|
|
1804
1905
|
} catch (error: any) {
|
|
1805
1906
|
nativeHistoryError = error;
|
|
@@ -1859,7 +1960,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1859
1960
|
excludeInProgressTurn: returnedStatus === 'waiting_approval',
|
|
1860
1961
|
sessionStartedAtMs,
|
|
1861
1962
|
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1862
|
-
instanceId:
|
|
1963
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
1863
1964
|
});
|
|
1864
1965
|
nativeHistoryError = undefined;
|
|
1865
1966
|
nativeMessages = nativeHistory && Array.isArray(nativeHistory.messages)
|
|
@@ -2158,7 +2259,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
2158
2259
|
scripts: provider?.scripts as any,
|
|
2159
2260
|
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
2160
2261
|
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
2161
|
-
instanceId:
|
|
2262
|
+
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
2162
2263
|
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
2163
2264
|
// Last-resort only when no pin was ever recorded AND the
|
|
2164
2265
|
// runtime fallback did not resolve a real provider session.
|
|
@@ -2183,7 +2284,7 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
2183
2284
|
: readHistorySessionIdFromMessages(historyMessages) || effectiveHistorySessionIdForRead;
|
|
2184
2285
|
// Refresh the pin whenever this path resolves a real provider id.
|
|
2185
2286
|
if (typeof (history as any)?.providerSessionId === 'string' && (history as any).providerSessionId.trim()) {
|
|
2186
|
-
recordBoundProviderSessionId(targetSid, (history as any).providerSessionId.trim());
|
|
2287
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSid), (history as any).providerSessionId.trim());
|
|
2187
2288
|
}
|
|
2188
2289
|
// Use the id we actually read with (pin / real provider id), NOT the
|
|
2189
2290
|
// raw runtime-fallback historySessionId — otherwise the mapping guard
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
export { READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS, buildSendInputSignature } from './chat-commands-shared.js';
|
|
12
12
|
export { evaluateReadChatNodeWorkspaceScope } from './chat-commands-scope.js';
|
|
13
13
|
export { sanitizeDebugBundleValue, handleGetChatDebugBundle } from './chat-commands-debug-bundle.js';
|
|
14
|
-
export { handleChatHistory, handleReadChat } from './chat-commands-read.js';
|
|
14
|
+
export { handleChatHistory, handleReadChat, __resetProviderSessionPinsForTest } from './chat-commands-read.js';
|
|
15
15
|
export {
|
|
16
16
|
handleSendChat,
|
|
17
17
|
handleListChats,
|
|
@@ -26,6 +26,17 @@ export interface DaemonState {
|
|
|
26
26
|
sessionNotificationDismissals: Record<string, string>;
|
|
27
27
|
/** Current notification unread override ids keyed by stable session target */
|
|
28
28
|
sessionNotificationUnreadOverrides: Record<string, string>;
|
|
29
|
+
/**
|
|
30
|
+
* Resolved provider-native conversation id for a live session, keyed by the
|
|
31
|
+
* ADHDev/mesh session id. Persisted so it survives a daemon restart: a
|
|
32
|
+
* provider whose on-disk store is keyed by an internally-generated id it
|
|
33
|
+
* never exposes on the CLI (antigravity — no --session-id) can then exact-bind
|
|
34
|
+
* its conversation .db after restart instead of re-running the mtime/recency
|
|
35
|
+
* heuristic, which drops the store once idle and collapses read_chat to the
|
|
36
|
+
* PTY parse (user echo only, assistant tail lost). Mirrors the in-memory read
|
|
37
|
+
* pin (chat-commands-read lastBoundProviderSessionIdByMeshSession).
|
|
38
|
+
*/
|
|
39
|
+
sessionProviderSessionPins: Record<string, string>;
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
const DEFAULT_STATE: DaemonState = {
|
|
@@ -35,6 +46,7 @@ const DEFAULT_STATE: DaemonState = {
|
|
|
35
46
|
sessionReadMarkers: {},
|
|
36
47
|
sessionNotificationDismissals: {},
|
|
37
48
|
sessionNotificationUnreadOverrides: {},
|
|
49
|
+
sessionProviderSessionPins: {},
|
|
38
50
|
};
|
|
39
51
|
|
|
40
52
|
function isPlainObject(value: unknown): value is Record<string, any> {
|
|
@@ -77,6 +89,10 @@ function normalizeState(raw: unknown): DaemonState {
|
|
|
77
89
|
Object.entries(isPlainObject(parsed.sessionNotificationUnreadOverrides) ? parsed.sessionNotificationUnreadOverrides : {})
|
|
78
90
|
.filter(([, value]) => typeof value === 'string' && value.length > 0)
|
|
79
91
|
);
|
|
92
|
+
const sessionProviderSessionPins = Object.fromEntries(
|
|
93
|
+
Object.entries(isPlainObject(parsed.sessionProviderSessionPins) ? parsed.sessionProviderSessionPins : {})
|
|
94
|
+
.filter(([key, value]) => typeof key === 'string' && key.length > 0 && typeof value === 'string' && value.length > 0)
|
|
95
|
+
);
|
|
80
96
|
|
|
81
97
|
return {
|
|
82
98
|
recentActivity,
|
|
@@ -85,6 +101,7 @@ function normalizeState(raw: unknown): DaemonState {
|
|
|
85
101
|
sessionReadMarkers,
|
|
86
102
|
sessionNotificationDismissals,
|
|
87
103
|
sessionNotificationUnreadOverrides,
|
|
104
|
+
sessionProviderSessionPins,
|
|
88
105
|
};
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -121,3 +138,41 @@ export function saveState(state: DaemonState): void {
|
|
|
121
138
|
export function resetState(): void {
|
|
122
139
|
saveState({ ...DEFAULT_STATE });
|
|
123
140
|
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Load the full persisted session→provider-conversation pin map (sessionId →
|
|
144
|
+
* provider-native conversation id). Survives daemon restart. Empty object when
|
|
145
|
+
* none recorded or the state file is unreadable.
|
|
146
|
+
*/
|
|
147
|
+
export function loadPersistedProviderSessionPins(): Record<string, string> {
|
|
148
|
+
return { ...loadState().sessionProviderSessionPins };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Persist one session→provider-conversation pin. Load-mutate-save against the
|
|
153
|
+
* on-disk state so it survives a daemon restart; a no-op when the value already
|
|
154
|
+
* matches (avoids rewriting state.json on every read). Never clears a pin with an
|
|
155
|
+
* empty value.
|
|
156
|
+
*/
|
|
157
|
+
export function recordPersistedProviderSessionPin(sessionId: string, providerSessionId: string): void {
|
|
158
|
+
const key = typeof sessionId === 'string' ? sessionId.trim() : '';
|
|
159
|
+
const value = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
|
|
160
|
+
if (!key || !value) return;
|
|
161
|
+
const state = loadState();
|
|
162
|
+
if (state.sessionProviderSessionPins[key] === value) return;
|
|
163
|
+
saveState({
|
|
164
|
+
...state,
|
|
165
|
+
sessionProviderSessionPins: { ...state.sessionProviderSessionPins, [key]: value },
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Test-only: drop all persisted session→provider-conversation pins from disk.
|
|
171
|
+
* Used by tests that assert a clean "no pin" state so a sibling test's write to
|
|
172
|
+
* the shared per-process ADHDEV_CONFIG_DIR does not leak in.
|
|
173
|
+
*/
|
|
174
|
+
export function clearPersistedProviderSessionPins(): void {
|
|
175
|
+
const state = loadState();
|
|
176
|
+
if (Object.keys(state.sessionProviderSessionPins).length === 0) return;
|
|
177
|
+
saveState({ ...state, sessionProviderSessionPins: {} });
|
|
178
|
+
}
|
|
@@ -688,6 +688,31 @@ function evaluateMeshEventSuppression(
|
|
|
688
688
|
return null;
|
|
689
689
|
}
|
|
690
690
|
|
|
691
|
+
/**
|
|
692
|
+
* True when the worker session that emitted this event has auto-approve enabled
|
|
693
|
+
* (a MAGI/delegated worker is launched with autoApprove:true). Such a worker
|
|
694
|
+
* resolves its own approval modals locally, so an agent:waiting_approval event
|
|
695
|
+
* from it is transient noise for the coordinator: forwarding it injects a
|
|
696
|
+
* "[System] … is waiting for approval, use mesh_approve" turn that the
|
|
697
|
+
* coordinator cannot usefully act on (the modal is already auto-resolving) and,
|
|
698
|
+
* mid-MAGI-collect, HIJACKS the coordinator's synthesis turn — the observed
|
|
699
|
+
* failure where a replica's repeated auto-approvals drowned out the completion
|
|
700
|
+
* events and the coordinator answered about approvals instead of the RCA. Only
|
|
701
|
+
* suppress when we can positively confirm the source worker auto-approves; a
|
|
702
|
+
* worker that genuinely needs a human/coordinator approval (autoApprove off)
|
|
703
|
+
* still forwards so the coordinator is told.
|
|
704
|
+
*/
|
|
705
|
+
function sourceWorkerAutoApproves(components: DaemonComponents, sessionId: string): boolean {
|
|
706
|
+
if (!sessionId) return false;
|
|
707
|
+
try {
|
|
708
|
+
const state = components.instanceManager?.getInstance?.(sessionId)?.getState?.();
|
|
709
|
+
const settings = (state?.settings as Record<string, unknown>) || {};
|
|
710
|
+
return settings.autoApprove === true;
|
|
711
|
+
} catch {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
691
716
|
function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
692
717
|
meshId: string;
|
|
693
718
|
sourceInstanceId?: string;
|
|
@@ -805,6 +830,19 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
805
830
|
}
|
|
806
831
|
|
|
807
832
|
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
833
|
+
|
|
834
|
+
// Auto-approving worker: never forward its approval prompts to the coordinator.
|
|
835
|
+
// The daemon resolves the modal locally, so the "[System] … waiting for
|
|
836
|
+
// approval" injection is pure noise that hijacks the coordinator's turn — the
|
|
837
|
+
// observed MAGI failure where a replica's repeated auto-approvals flooded the
|
|
838
|
+
// coordinator and derailed its final synthesis. A worker without auto-approve
|
|
839
|
+
// (genuinely blocked on a human/coordinator decision) still forwards.
|
|
840
|
+
if (args.event === 'agent:waiting_approval' && sourceWorkerAutoApproves(components, eventSessionId)) {
|
|
841
|
+
LOG.info('MeshEvents', `Suppressed agent:waiting_approval for auto-approving worker session ${eventSessionId || '(unknown)'} (mesh ${args.meshId}) — modal is resolved locally, coordinator not notified`);
|
|
842
|
+
traceMeshEventDrop('waiting_approval_auto_approving_worker', traceCtx);
|
|
843
|
+
return { success: true, forwarded: 0, suppressed: true, autoApprovingWorkerApproval: true };
|
|
844
|
+
}
|
|
845
|
+
|
|
808
846
|
// Coordinator-side dedup/suppression gate (extracted, behavior-preserving). A non-null
|
|
809
847
|
// outcome either short-circuits with a forwarded result or signals a no-progress→completion
|
|
810
848
|
// reconciliation that we re-inject; null lets the event fall through to the ledger machinery.
|