@adhdev/daemon-core 0.9.82-rc.440 → 0.9.82-rc.442

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.440",
3
+ "version": "0.9.82-rc.442",
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",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.440",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.442",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -50,6 +50,34 @@ const HOT_TAIL_MIN_LIMIT = 60;
50
50
  // and routes solely through canonicalHistory.contractVersion +
51
51
  // isNativeSourceCanonicalHistory().
52
52
  const CLI_NATIVE_TRANSCRIPT_PROVIDERS = new Set(['codex-cli', 'claude-cli', 'hermes-cli', 'antigravity-cli']);
53
+
54
+ // Last successfully-bound provider-native session id, keyed by the mesh session
55
+ // id (targetSessionId) the read was scoped to. The live pin lives on the
56
+ // CliProviderInstance and is torn down when the turn ends; a *post-turn* read
57
+ // then finds historySessionId empty AND canBindFromLiveSession=false (no live
58
+ // spawnedAtMs), so readCliProviderNativeHistory would fail closed with
59
+ // native_history_workspace_only_lookup_unsafe and surface providerSessionId=null
60
+ // + zero rows even though the transcript is physically present in state.db.
61
+ // Persisting the last resolved id here lets that later read reuse the known pin
62
+ // and run the native query normally instead of fail-closing. Refreshed on every
63
+ // successful bind; never lets an empty id clear a known pin. Keyed by mesh
64
+ // session id so pins never alias across distinct sessions sharing a workspace.
65
+ const lastBoundProviderSessionIdByMeshSession = new Map<string, string>();
66
+
67
+ function recordBoundProviderSessionId(meshSessionId: string | undefined, providerSessionId: string | undefined): void {
68
+ const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
69
+ const value = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
70
+ if (!key || !value) return;
71
+ lastBoundProviderSessionIdByMeshSession.set(key, value);
72
+ }
73
+
74
+ function getBoundProviderSessionIdPin(meshSessionId: string | undefined): string | undefined {
75
+ const key = typeof meshSessionId === 'string' ? meshSessionId.trim() : '';
76
+ if (!key) return undefined;
77
+ const pinned = lastBoundProviderSessionIdByMeshSession.get(key);
78
+ return pinned && pinned.trim() ? pinned.trim() : undefined;
79
+ }
80
+
53
81
  const warnedLegacyNativeAllowlistHits = new Set<string>();
54
82
  function warnLegacyNativeAllowlistHit(providerType: string): void {
55
83
  if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
@@ -1055,13 +1083,49 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1055
1083
  excludeInProgressTurn?: boolean;
1056
1084
  sessionStartedAtMs?: number;
1057
1085
  envOverrides?: Record<string, string>;
1086
+ // Last provider-native session id previously bound for this mesh session
1087
+ // (see lastBoundProviderSessionIdByMeshSession). When historySessionId is
1088
+ // empty and no live session can be bound (post-turn read), reuse this pin so
1089
+ // the native query runs against the known session instead of fail-closing.
1090
+ pinnedProviderSessionId?: string;
1091
+ // Opt-in last-resort: when there is no caller session id, no live binding,
1092
+ // and NO pin was ever recorded, allow a workspace-scoped read (newest
1093
+ // session in state.db with rows for this workspace). Strictly behind the pin
1094
+ // — it never fires when pinnedProviderSessionId is set — and still subject to
1095
+ // the downstream hasSafeNativeHistoryMapping workspace-overlap gate. Only the
1096
+ // read_chat path opts in, and only with a concrete workspace.
1097
+ allowWorkspaceLatestFallback?: boolean;
1058
1098
  }): ReturnType<typeof readProviderChatHistory> & { lookup: 'session' | 'workspace' } {
1059
1099
  const canBindFromLiveSession = !args.historySessionId
1060
1100
  && typeof args.sessionStartedAtMs === 'number'
1061
1101
  && args.sessionStartedAtMs > 0
1062
1102
  && typeof args.workspace === 'string'
1063
1103
  && args.workspace.trim().length > 0;
1064
- if (!args.historySessionId && !canBindFromLiveSession) {
1104
+ const pinnedProviderSessionId = typeof args.pinnedProviderSessionId === 'string'
1105
+ ? args.pinnedProviderSessionId.trim()
1106
+ : '';
1107
+ // Pin reuse (PRIMARY): a later read whose live binding is gone
1108
+ // (historySessionId empty, no live spawnedAtMs) can still resolve to the
1109
+ // session it was last bound to. Read THAT session directly by threading the
1110
+ // pin through as historySessionId — same code path an explicit session read
1111
+ // takes — instead of fail-closing. Never overrides a caller-supplied
1112
+ // historySessionId; only kicks in when there is none.
1113
+ const effectiveHistorySessionId = args.historySessionId || (!canBindFromLiveSession ? pinnedProviderSessionId : '');
1114
+ // Last-resort workspace-latest (b): only when nothing above resolved a
1115
+ // session id AND no pin exists AND the caller opted in with a workspace.
1116
+ // Strictly behind pin reuse — pinnedProviderSessionId being set disables it.
1117
+ const workspaceLatestFallback = !effectiveHistorySessionId
1118
+ && !canBindFromLiveSession
1119
+ && !pinnedProviderSessionId
1120
+ && args.allowWorkspaceLatestFallback === true
1121
+ && typeof args.workspace === 'string'
1122
+ && args.workspace.trim().length > 0;
1123
+ if (!effectiveHistorySessionId && !canBindFromLiveSession && !workspaceLatestFallback) {
1124
+ // No caller session id, no live binding, no known pin, no opted-in
1125
+ // workspace-latest. This is the genuinely-unresolvable case — a
1126
+ // workspace-only lookup here could alias a concurrent session sharing the
1127
+ // cwd, so fail closed as before. The pin/live/workspace-latest paths are
1128
+ // all checked AHEAD of this so a resolvable session is never dropped here.
1065
1129
  return {
1066
1130
  messages: [],
1067
1131
  hasMore: false,
@@ -1072,7 +1136,7 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1072
1136
  }
1073
1137
  const sessionHistory = readProviderChatHistory(agentStr, {
1074
1138
  canonicalHistory: args.canonicalHistory,
1075
- historySessionId: args.historySessionId,
1139
+ historySessionId: effectiveHistorySessionId || undefined,
1076
1140
  workspace: args.workspace,
1077
1141
  offset: args.offset,
1078
1142
  limit: args.limit,
@@ -1087,10 +1151,11 @@ function readCliProviderNativeHistory(agentStr: string, args: {
1087
1151
  ? (sessionHistory as any).providerSessionId.trim()
1088
1152
  : '';
1089
1153
  // A fresh live session can be bound without a provider id when the native
1090
- // reader matched both cwd and session_meta.timestamp to spawnedAtMs.
1154
+ // reader matched both cwd and session_meta.timestamp to spawnedAtMs. A
1155
+ // pin-bound read is always session-scoped (we passed an explicit id).
1091
1156
  return {
1092
1157
  ...(sessionHistory as any),
1093
- lookup: args.historySessionId || (canBindFromLiveSession && boundProviderSessionId)
1158
+ lookup: effectiveHistorySessionId || (canBindFromLiveSession && boundProviderSessionId)
1094
1159
  ? 'session'
1095
1160
  : 'workspace',
1096
1161
  };
@@ -1476,6 +1541,7 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1476
1541
  scripts: provider?.scripts as any,
1477
1542
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1478
1543
  envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1544
+ pinnedProviderSessionId: getBoundProviderSessionIdPin(args?.targetSessionId),
1479
1545
  })
1480
1546
  : readProviderChatHistory(agentStr, {
1481
1547
  canonicalHistory: provider?.nativeHistory,
@@ -1495,6 +1561,9 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
1495
1561
  const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
1496
1562
  ? (result as any).providerSessionId
1497
1563
  : readHistorySessionIdFromMessages(messages) || historySessionId;
1564
+ if (typeof (result as any)?.providerSessionId === 'string' && (result as any).providerSessionId.trim()) {
1565
+ recordBoundProviderSessionId(args?.targetSessionId, (result as any).providerSessionId.trim());
1566
+ }
1498
1567
  const safeMapping = hasSafeNativeHistoryMapping({
1499
1568
  historySessionId: lookup === 'workspace' ? undefined : historySessionId,
1500
1569
  providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
@@ -1702,8 +1771,24 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
1702
1771
  scripts: provider?.scripts as any,
1703
1772
  excludeInProgressTurn: returnedStatus === 'waiting_approval',
1704
1773
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
1705
- envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1774
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
1775
+ pinnedProviderSessionId: getBoundProviderSessionIdPin(targetSessionId),
1776
+ // Last-resort only when no pin was ever recorded for this
1777
+ // session; the downstream workspace-overlap safety gate
1778
+ // still filters an aliased session out.
1779
+ allowWorkspaceLatestFallback: !getBoundProviderSessionIdPin(targetSessionId),
1706
1780
  });
1781
+ // Refresh the per-mesh-session pin whenever a native read
1782
+ // resolves a concrete provider-native session id. A later
1783
+ // post-turn read (live binding gone) can then reuse it
1784
+ // instead of fail-closing. Only a non-empty resolved id
1785
+ // updates the pin; an empty result never clears a known one.
1786
+ const resolvedProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
1787
+ ? nativeHistory.providerSessionId.trim()
1788
+ : '';
1789
+ if (resolvedProviderSessionId) {
1790
+ recordBoundProviderSessionId(targetSessionId, resolvedProviderSessionId);
1791
+ }
1707
1792
  } catch (error: any) {
1708
1793
  nativeHistoryError = error;
1709
1794
  nativeHistory = null;
@@ -2026,10 +2111,32 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2026
2111
  const intendedWorkspace = argsWorkspace;
2027
2112
  const supportsNative = supportsCliNativeTranscript(agentStr, provider)
2028
2113
  && isNativeSourceCanonicalHistory(provider?.nativeHistory);
2114
+ // Post-turn read (no live adapter): getHistorySessionId falls back to
2115
+ // the daemon runtime session id when no provider-native id was ever
2116
+ // registered. That runtime id is NOT a real provider session, so a
2117
+ // native read keyed on it resolves nothing (providerSessionId=null,
2118
+ // zero rows) even though the transcript is present in state.db. When
2119
+ // this is that runtime fallback (historySessionId === targetSid and no
2120
+ // explicit id was passed) and we hold a pin from an earlier bound
2121
+ // read, prefer the pin so the query hits the real session.
2122
+ const pinnedProviderSessionIdForHistory = getBoundProviderSessionIdPin(targetSid);
2123
+ const historySessionIdIsRuntimeFallback = Boolean(
2124
+ targetSid
2125
+ && historySessionId === targetSid
2126
+ && !getExplicitHistorySessionId(args),
2127
+ );
2128
+ // When this is the runtime fallback (not a real provider id): prefer
2129
+ // the pin if we have one, else drop the runtime id entirely so the
2130
+ // pin-reuse / workspace-latest logic inside readCliProviderNativeHistory
2131
+ // can engage (passing the runtime id as historySessionId would pin the
2132
+ // query to a non-existent session and never reach those paths).
2133
+ const effectiveHistorySessionIdForRead = historySessionIdIsRuntimeFallback
2134
+ ? (pinnedProviderSessionIdForHistory || undefined)
2135
+ : historySessionId;
2029
2136
  const history = supportsNative
2030
2137
  ? readCliProviderNativeHistory(agentStr, {
2031
2138
  canonicalHistory: provider?.nativeHistory,
2032
- historySessionId,
2139
+ historySessionId: effectiveHistorySessionIdForRead,
2033
2140
  workspace,
2034
2141
  offset: 0,
2035
2142
  limit: historyLimit,
@@ -2037,7 +2144,11 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2037
2144
  historyBehavior: provider?.historyBehavior,
2038
2145
  scripts: provider?.scripts as any,
2039
2146
  sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
2040
- envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2147
+ envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
2148
+ pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
2149
+ // Last-resort only when no pin was ever recorded AND the
2150
+ // runtime fallback did not resolve a real provider session.
2151
+ allowWorkspaceLatestFallback: !pinnedProviderSessionIdForHistory && historySessionIdIsRuntimeFallback,
2041
2152
  })
2042
2153
  : readProviderChatHistory(agentStr, {
2043
2154
  canonicalHistory: provider?.nativeHistory,
@@ -2055,19 +2166,28 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
2055
2166
  : [];
2056
2167
  const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
2057
2168
  ? (history as any).providerSessionId
2058
- : readHistorySessionIdFromMessages(historyMessages) || historySessionId;
2169
+ : readHistorySessionIdFromMessages(historyMessages) || effectiveHistorySessionIdForRead;
2170
+ // Refresh the pin whenever this path resolves a real provider id.
2171
+ if (typeof (history as any)?.providerSessionId === 'string' && (history as any).providerSessionId.trim()) {
2172
+ recordBoundProviderSessionId(targetSid, (history as any).providerSessionId.trim());
2173
+ }
2174
+ // Use the id we actually read with (pin / real provider id), NOT the
2175
+ // raw runtime-fallback historySessionId — otherwise the mapping guard
2176
+ // compares the stamped messages' real id against the runtime id and
2177
+ // fails closed, undoing the pin reuse.
2178
+ const mappingSessionId = effectiveHistorySessionIdForRead;
2059
2179
  const safeMapping = supportsNative
2060
2180
  ? hasSafeNativeHistoryMapping({
2061
- historySessionId: lookup === 'workspace' ? undefined : historySessionId,
2181
+ historySessionId: lookup === 'workspace' ? undefined : mappingSessionId,
2062
2182
  providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
2063
2183
  workspace,
2064
2184
  nativeMessages: historyMessages,
2065
2185
  })
2066
2186
  : false;
2067
2187
  const trustedExactNativeIdentity = lookup !== 'workspace'
2068
- && Boolean(historySessionId)
2188
+ && Boolean(mappingSessionId)
2069
2189
  && Boolean(historyProviderSessionId)
2070
- && historySessionId === historyProviderSessionId;
2190
+ && mappingSessionId === historyProviderSessionId;
2071
2191
 
2072
2192
  const machineSessionKey = String(
2073
2193
  args?.targetSessionId
@@ -264,22 +264,30 @@ function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput
264
264
 
265
265
  try {
266
266
  const requested = input.providerSessionId || '';
267
- let sessionId: string;
268
- if (requested) {
269
- // Session pin: the caller already bound this instance to a
270
- // specific provider session, so read THAT session directly and
271
- // skip the newest-wins `session_query` entirely. hermes ≥0.14
272
- // spawns a fresh `sessions` row per internal sub-session, so the
273
- // `ORDER BY started_at DESC LIMIT 1` pick drifts to a different
274
- // id on every read. Left unpinned that churns the bound session
275
- // (each re-bind re-hydrates unbounded history daemon
276
- // saturation) and reads completion evidence from the wrong
277
- // session (turn never finalizes). Binding straight to the
278
- // requested id fixes both. Existence is validated below by the
279
- // spec's own `message_query` returning rows for this id, so we
280
- // don't hardcode any schema here.
281
- sessionId = requested;
282
- } else {
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 null; }
303
- if (!sessionRow) return null;
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
- sessionId = sessionIdRaw == null ? '' : String(sessionIdRaw);
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);