@adhdev/daemon-core 0.9.82-rc.475 → 0.9.82-rc.476
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 +73 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +73 -11
- package/dist/index.mjs.map +1 -1
- package/dist/shared-types.d.ts +12 -0
- package/package.json +3 -3
- package/src/commands/chat-commands-read.ts +72 -3
- package/src/commands/chat-commands.ts +1 -1
- package/src/config/state-store.ts +55 -0
- package/src/providers/native-history/dispatcher.ts +53 -2
- package/src/shared-types.ts +12 -0
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.476",
|
|
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.476",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.476",
|
|
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,69 @@ 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;
|
|
77
|
+
|
|
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
|
+
}
|
|
66
93
|
|
|
67
94
|
function recordBoundProviderSessionId(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;
|
|
71
98
|
lastBoundProviderSessionIdByMeshSession.set(key, value);
|
|
99
|
+
// Always attempt the disk mirror — recordPersistedProviderSessionPin is itself a
|
|
100
|
+
// no-op when the ON-DISK value already matches, so it does not rewrite state.json
|
|
101
|
+
// on steady re-reads, yet it still lands a pin the in-memory map already holds but
|
|
102
|
+
// disk lost (a prior write clobbered by another state-store writer, or a restart
|
|
103
|
+
// whose hydration ran before this bind). Gating on the in-memory previous value
|
|
104
|
+
// let the in-memory and on-disk pin diverge permanently, defeating the persistence.
|
|
105
|
+
try { recordPersistedProviderSessionPin(key, value); } catch { /* best-effort disk mirror */ }
|
|
72
106
|
}
|
|
73
107
|
|
|
74
108
|
function getBoundProviderSessionIdPin(meshSessionId: string | undefined): string | undefined {
|
|
75
109
|
const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
|
|
76
110
|
if (!key) return undefined;
|
|
111
|
+
hydratePersistedProviderSessionPinsOnce();
|
|
77
112
|
const pinned = lastBoundProviderSessionIdByMeshSession.get(key);
|
|
78
113
|
return pinned && pinned.trim() ? pinned.trim() : undefined;
|
|
79
114
|
}
|
|
80
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Test-only: clear the in-memory read-pin map and re-arm cold-start hydration so
|
|
118
|
+
* each test starts from a clean pin state. The on-disk mirror is isolated per
|
|
119
|
+
* test process via ADHDEV_CONFIG_DIR (test/helpers/setup-env.ts); this resets the
|
|
120
|
+
* module-level cache that would otherwise leak a pin across tests sharing the
|
|
121
|
+
* worker. Not part of the runtime contract.
|
|
122
|
+
*/
|
|
123
|
+
export function __resetProviderSessionPinsForTest(): void {
|
|
124
|
+
lastBoundProviderSessionIdByMeshSession.clear();
|
|
125
|
+
persistedProviderSessionPinsHydrated = false;
|
|
126
|
+
try { clearPersistedProviderSessionPins(); } catch { /* best-effort */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
81
129
|
const warnedLegacyNativeAllowlistHits = new Set<string>();
|
|
82
130
|
function warnLegacyNativeAllowlistHit(providerType: string): void {
|
|
83
131
|
if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
|
|
@@ -1768,10 +1816,31 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1768
1816
|
let nativeHistory: any | null = null;
|
|
1769
1817
|
let nativeHistoryError: unknown | undefined;
|
|
1770
1818
|
if (supportsNative) {
|
|
1819
|
+
// Runtime-fallback → pin substitution: nativeHistoryReadSessionId is
|
|
1820
|
+
// the bare runtime/session id when no explicit provider handle was
|
|
1821
|
+
// supplied and none was parsed (antigravity takes no --session-id, so
|
|
1822
|
+
// its this.providerSessionId stays empty and getHistorySessionId falls
|
|
1823
|
+
// back to targetSessionId). That runtime id is not the on-disk
|
|
1824
|
+
// conversations/<uuid>.db name, so a native read keyed on it can never
|
|
1825
|
+
// exact-bind and falls to the recency heuristic — which drops an idle
|
|
1826
|
+
// (or restored, spawnedAtMs=0) session's own store. Prefer a pin (a
|
|
1827
|
+
// real conversation id a prior read resolved for THIS session, now also
|
|
1828
|
+
// persisted across restart) over the runtime id, else drop the runtime
|
|
1829
|
+
// id so readCliProviderNativeHistory's pin / workspace-latest paths can
|
|
1830
|
+
// engage. Mirrors the handleChatHistory path's established handling.
|
|
1831
|
+
const pinnedProviderSessionIdForRead = getBoundProviderSessionIdPin(targetSessionId);
|
|
1832
|
+
const nativeReadSessionIdIsRuntimeFallback = Boolean(
|
|
1833
|
+
targetSessionId
|
|
1834
|
+
&& nativeHistoryReadSessionId === targetSessionId
|
|
1835
|
+
&& !getExplicitHistorySessionId(args),
|
|
1836
|
+
);
|
|
1837
|
+
const effectiveNativeReadSessionId = nativeReadSessionIdIsRuntimeFallback
|
|
1838
|
+
? (pinnedProviderSessionIdForRead || undefined)
|
|
1839
|
+
: nativeHistoryReadSessionId;
|
|
1771
1840
|
try {
|
|
1772
1841
|
nativeHistory = readCliProviderNativeHistory(agentStr, {
|
|
1773
1842
|
canonicalHistory: provider?.nativeHistory,
|
|
1774
|
-
historySessionId:
|
|
1843
|
+
historySessionId: effectiveNativeReadSessionId,
|
|
1775
1844
|
workspace,
|
|
1776
1845
|
offset: 0,
|
|
1777
1846
|
limit: nativeHistoryLimit,
|
|
@@ -1784,11 +1853,11 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1784
1853
|
// Stable per-session identity for antigravity's conversation-claim
|
|
1785
1854
|
// owner token (== session registry sessionId == instance instanceId).
|
|
1786
1855
|
instanceId: typeof args?.targetSessionId === 'string' ? args.targetSessionId : undefined,
|
|
1787
|
-
pinnedProviderSessionId:
|
|
1856
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForRead,
|
|
1788
1857
|
// Last-resort only when no pin was ever recorded for this
|
|
1789
1858
|
// session; the downstream workspace-overlap safety gate
|
|
1790
1859
|
// still filters an aliased session out.
|
|
1791
|
-
allowWorkspaceLatestFallback: !
|
|
1860
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForRead,
|
|
1792
1861
|
});
|
|
1793
1862
|
// Refresh the per-mesh-session pin whenever a native read
|
|
1794
1863
|
// resolves a concrete provider-native session id. A later
|
|
@@ -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
|
+
}
|
|
@@ -87,6 +87,23 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// For antigravity, the authoritative conversation id is the on-disk uuid
|
|
91
|
+
// embedded in the resolved path (conversations/<uuid>.db or
|
|
92
|
+
// brain/<uuid>/…/transcript.jsonl), NOT the ADHDev session id the caller
|
|
93
|
+
// threaded in. Surface that uuid as providerSessionId whenever the reader
|
|
94
|
+
// did not already return a distinct one, so the read_chat layer can pin
|
|
95
|
+
// the real conversation and (post-restart) exact-bind straight to it
|
|
96
|
+
// instead of re-running the mtime/recency heuristic that drops an idle
|
|
97
|
+
// store (ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP). Other providers keep the
|
|
98
|
+
// reader's value verbatim.
|
|
99
|
+
let resolvedProviderSessionId = session.providerSessionId;
|
|
100
|
+
if (reader === 'antigravity-cli') {
|
|
101
|
+
const onDiskUuid = extractAntigravityConversationUuid(session.sourcePath || sourcePath);
|
|
102
|
+
if (onDiskUuid && (!resolvedProviderSessionId || resolvedProviderSessionId === sessionId)) {
|
|
103
|
+
resolvedProviderSessionId = onDiskUuid;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
return {
|
|
91
108
|
messages: session.messages.map((m: any) => ({
|
|
92
109
|
role: normalizeRole(m.role),
|
|
@@ -95,7 +112,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
95
112
|
kind: typeof m.kind === 'string' ? m.kind : 'standard',
|
|
96
113
|
workspace: typeof m.workspace === 'string' ? m.workspace : workspace || undefined,
|
|
97
114
|
})),
|
|
98
|
-
providerSessionId:
|
|
115
|
+
providerSessionId: resolvedProviderSessionId,
|
|
99
116
|
sourcePath: session.sourcePath,
|
|
100
117
|
sourceMtimeMs: session.sourceMtimeMs,
|
|
101
118
|
nativeHistoryCoverage: (session as any).nativeHistoryCoverage || 'full',
|
|
@@ -239,6 +256,27 @@ function resolveRealPath(value: string): string {
|
|
|
239
256
|
try { return fs.realpathSync(value); } catch { return value; }
|
|
240
257
|
}
|
|
241
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Pull the antigravity conversation uuid out of a resolved source path. Both
|
|
261
|
+
* on-disk layouts embed it: conversations/<uuid>.db and
|
|
262
|
+
* brain/<uuid>/.system_generated/logs/transcript*.jsonl (and the legacy
|
|
263
|
+
* conversations/<uuid>.pb). Returns the uuid when a segment matches the
|
|
264
|
+
* canonical form, else ''.
|
|
265
|
+
*/
|
|
266
|
+
function extractAntigravityConversationUuid(sourcePath: string): string {
|
|
267
|
+
if (!sourcePath) return '';
|
|
268
|
+
const segments = sourcePath.split(/[\\/]/);
|
|
269
|
+
// conversations/<uuid>.db|.pb — the basename minus extension.
|
|
270
|
+
const base = segments[segments.length - 1] || '';
|
|
271
|
+
const baseMatch = /^([0-9a-f-]+)\.(?:db|pb)$/i.exec(base);
|
|
272
|
+
if (baseMatch && isUuidLikeSessionId(baseMatch[1])) return baseMatch[1];
|
|
273
|
+
// brain/<uuid>/… — the first uuid-like path segment.
|
|
274
|
+
for (const seg of segments) {
|
|
275
|
+
if (isUuidLikeSessionId(seg)) return seg;
|
|
276
|
+
}
|
|
277
|
+
return '';
|
|
278
|
+
}
|
|
279
|
+
|
|
242
280
|
/**
|
|
243
281
|
* The daemon may stamp a session's spawn time a hair before the CLI child
|
|
244
282
|
* actually creates its conversation .db, so treat a store born within this
|
|
@@ -341,6 +379,19 @@ function pickUnboundConversationDb(
|
|
|
341
379
|
let entries: fs.Dirent[] = [];
|
|
342
380
|
try { entries = fs.readdirSync(convRoot, { withFileTypes: true }); } catch { return null; }
|
|
343
381
|
|
|
382
|
+
// A known spawn floor already pins a candidate to THIS session by birth time
|
|
383
|
+
// (a store created at/after the session spawned is its own). Once that floor
|
|
384
|
+
// is available, the recency window is not just unnecessary but harmful: an
|
|
385
|
+
// antigravity session that has sat idle longer than RECENT_WINDOW_MS still
|
|
386
|
+
// owns its conversation .db, but the recency cutoff would drop it from the
|
|
387
|
+
// candidate set, collapsing the read to native_history_empty and forcing the
|
|
388
|
+
// dashboard onto the PTY parse (user echo only, assistant tail lost —
|
|
389
|
+
// ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP, most visible right after a daemon
|
|
390
|
+
// restart clears the in-memory read pin). So only apply the recency cutoff in
|
|
391
|
+
// the floor-less (legacy/unpinned) discovery path, where it is the sole guard
|
|
392
|
+
// against binding an unrelated old store. When a floor is known the birth-time
|
|
393
|
+
// filter below is the authoritative, idle-agnostic owner check.
|
|
394
|
+
const applyRecencyCutoff = !(sessionFloorMs > 0);
|
|
344
395
|
const recencyCutoff = Date.now() - RECENT_WINDOW_MS;
|
|
345
396
|
const candidates: Array<{ path: string; uuid: string; mtime: number; birth: number }> = [];
|
|
346
397
|
for (const entry of entries) {
|
|
@@ -352,7 +403,7 @@ function pickUnboundConversationDb(
|
|
|
352
403
|
if (isAntigravityConversationClaimedByOther(uuid, owner)) continue;
|
|
353
404
|
const p = path.join(convRoot, entry.name);
|
|
354
405
|
const mtime = safeMtime(p);
|
|
355
|
-
if (mtime < recencyCutoff) continue;
|
|
406
|
+
if (applyRecencyCutoff && mtime < recencyCutoff) continue;
|
|
356
407
|
candidates.push({ path: p, uuid, mtime, birth: safeBirthtime(p) });
|
|
357
408
|
}
|
|
358
409
|
if (candidates.length === 0) return null;
|
package/src/shared-types.ts
CHANGED
|
@@ -298,6 +298,18 @@ export interface DaemonMetadataUpdate {
|
|
|
298
298
|
userName?: string;
|
|
299
299
|
seq: number;
|
|
300
300
|
timestamp: number;
|
|
301
|
+
/**
|
|
302
|
+
* Per-mesh state-change revision counters (meshId → monotonically increasing
|
|
303
|
+
* integer), bumped whenever the daemon's mesh graph/queue/mission state for that
|
|
304
|
+
* mesh changes (onMeshStateChange). Lets the dashboard replace its client-side
|
|
305
|
+
* mesh_status polling with an event-driven background refresh: when the revision
|
|
306
|
+
* for the mesh it is viewing advances, it re-fetches the aggregate mesh_status
|
|
307
|
+
* (SWR, keeping the current graph on screen). This is a lightweight nudge — the
|
|
308
|
+
* full aggregate snapshot is fetched on demand, not embedded here, so the
|
|
309
|
+
* daemon.metadata payload stays small. Optional/absent for daemons/builds that
|
|
310
|
+
* don't emit it (the client then keeps its polling fallback).
|
|
311
|
+
*/
|
|
312
|
+
meshStateRevisions?: Record<string, number>;
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
export interface TopicUpdateEnvelopeMap {
|