@adhdev/daemon-core 0.9.82-rc.476 → 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.
@@ -314,24 +314,51 @@ function resolveAntigravityPath(
314
314
  }
315
315
 
316
316
  // (2) brain/<uuid>/.system_generated/logs/transcript.jsonl (legacy full source).
317
- // Only bind to a brain transcript that is NON-EMPTY: current antigravity
318
- // writes this file but leaves it 0 bytes (all real conversation data now
319
- // lives in the per-session .db), so an empty transcript here would
320
- // otherwise shadow the .db fallback below and return no messages. Skip
321
- // empty transcripts so an unbound read still reaches the .db. Exclude any
317
+ // Only bind to a brain transcript that is NON-EMPTY (antigravity may leave
318
+ // it 0 bytes when the real data lives in the per-session .db). Exclude any
322
319
  // brain conversation already claimed by a DIFFERENT live session.
320
+ //
321
+ // Selection MUST mirror pickUnboundConversationDb (step 3): when a spawn
322
+ // floor is known, a brain dir born at/after the floor is THIS session's own,
323
+ // and among those the OLDEST-created wins (the store created first after the
324
+ // session started). The previous newest-by-mtime sort silently mis-bound: in
325
+ // a MAGI panel every co-located antigravity session (coordinator + replicas)
326
+ // has a non-empty brain transcript, and the replica that finished its turn
327
+ // last has the newest mtime — so a coordinator's read grabbed the replica's
328
+ // transcript here, BEFORE step 3's floor-aware pick could run. That is the
329
+ // antigravity coordinator↔replica crosswire, and it lives in THIS step, not
330
+ // step 3. Keep newest-by-mtime only in the floor-less legacy path.
323
331
  const brainRoot = path.join(agyRoot, 'brain');
324
332
  if (fs.existsSync(brainRoot)) {
325
333
  const cutoff = spawnAwareCutoff(sessionStartedAtMs);
326
- const entries = fs.readdirSync(brainRoot, { withFileTypes: true })
334
+ const nonEmptyBrain = (uuid: string, p: string): string | null => {
335
+ const t = path.join(p, '.system_generated', 'logs', 'transcript.jsonl');
336
+ return (fs.existsSync(t) && safeSize(t) > 0) ? t : null;
337
+ };
338
+ const all = fs.readdirSync(brainRoot, { withFileTypes: true })
327
339
  .filter(e => e.isDirectory() && isUuidLikeSessionId(e.name))
328
340
  .filter(e => !isAntigravityConversationClaimedByOther(e.name, owner))
329
- .map(e => ({ uuid: e.name, p: path.join(brainRoot, e.name), mtime: safeMtime(path.join(brainRoot, e.name)) }))
330
- .filter(e => e.mtime >= cutoff)
331
- .sort((a, b) => b.mtime - a.mtime);
332
- for (const e of entries) {
333
- const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
334
- if (fs.existsSync(t) && safeSize(t) > 0) {
341
+ .map(e => {
342
+ const p = path.join(brainRoot, e.name);
343
+ return { uuid: e.name, p, mtime: safeMtime(p), birth: safeBirthtime(p) };
344
+ })
345
+ .filter(e => e.mtime >= cutoff);
346
+ let ordered: Array<{ uuid: string; p: string }> = [];
347
+ if (sessionStartedAtMs > 0) {
348
+ // Floor branch: this session's own = born at/after (floor - grace),
349
+ // oldest-birth first. Mirrors pickUnboundConversationDb exactly so the
350
+ // two steps can never resolve DIFFERENT conversations for one session.
351
+ const floor = sessionStartedAtMs - AGY_SPAWN_CLAIM_GRACE_MS;
352
+ ordered = all
353
+ .filter(e => (e.birth > 0 ? e.birth : e.mtime) >= floor)
354
+ .sort((a, b) => (a.birth || a.mtime) - (b.birth || b.mtime));
355
+ } else {
356
+ // Floor-less legacy/unpinned discovery: newest-by-mtime (single-session).
357
+ ordered = [...all].sort((a, b) => b.mtime - a.mtime);
358
+ }
359
+ for (const e of ordered) {
360
+ const t = nonEmptyBrain(e.uuid, e.p);
361
+ if (t) {
335
362
  if (owner) claimAntigravityConversation(e.uuid, owner);
336
363
  return t;
337
364
  }
@@ -14,6 +14,19 @@ export interface SessionRuntimeTarget {
14
14
  /** Wall clock at register time. native-history readers use it as a
15
15
  * cutoff so a fresh session can't show records from a prior one. */
16
16
  spawnedAtMs?: number;
17
+ /**
18
+ * Authoritative provider-native conversation id for this session (SSOT).
19
+ * For providers that expose a session id on the CLI (codex/claude/hermes)
20
+ * this equals that id. For antigravity — which takes no --session-id — this
21
+ * is the on-disk conversations/<uuid>.db basename, discovered by the
22
+ * native-history dispatcher and written back here via setProviderSessionId
23
+ * the first time it resolves. Every downstream reader (read_chat, the
24
+ * completion probe, the dashboard) should prefer this over re-deriving the
25
+ * conversation by spawn-floor/mtime heuristics — that re-derivation is the
26
+ * source of the antigravity conversation crosswire/theft class. Empty until
27
+ * the first successful native read binds it.
28
+ */
29
+ providerSessionId?: string;
17
30
  }
18
31
 
19
32
  export class SessionRegistry {
@@ -23,7 +36,15 @@ export class SessionRegistry {
23
36
  private readonly byParentSessionId = new Map<string, Set<string>>();
24
37
 
25
38
  register(target: SessionRuntimeTarget): void {
39
+ // Preserve an already-resolved conversation binding across a
40
+ // re-register (attach-restore, meta refresh): the caller rarely knows
41
+ // the antigravity conv uuid at register time, so a plain replace would
42
+ // drop the SSOT binding and force a fresh (crosswire-prone) re-resolve.
43
+ const priorProviderSessionId = this.bySessionId.get(target.sessionId)?.providerSessionId;
26
44
  this.unregister(target.sessionId);
45
+ if (priorProviderSessionId && !target.providerSessionId) {
46
+ target = { ...target, providerSessionId: priorProviderSessionId };
47
+ }
27
48
  this.bySessionId.set(target.sessionId, target);
28
49
  if (target.cdpManagerKey) this.addIndex(this.byManagerKey, target.cdpManagerKey, target.sessionId);
29
50
  if (target.instanceKey) this.addIndex(this.byInstanceKey, target.instanceKey, target.sessionId);
@@ -35,6 +56,23 @@ export class SessionRegistry {
35
56
  return this.bySessionId.get(sessionId);
36
57
  }
37
58
 
59
+ /**
60
+ * Record the authoritative provider-native conversation id for a session
61
+ * (SSOT). Idempotent; a no-op when the session is unknown or the value is
62
+ * empty or unchanged. Never overwrites a known binding with an empty one.
63
+ * Returns whether the stored value changed.
64
+ */
65
+ setProviderSessionId(sessionId: string | undefined | null, providerSessionId: string | undefined | null): boolean {
66
+ const sid = typeof sessionId === 'string' ? sessionId.trim() : '';
67
+ const value = typeof providerSessionId === 'string' ? providerSessionId.trim() : '';
68
+ if (!sid || !value) return false;
69
+ const target = this.bySessionId.get(sid);
70
+ if (!target) return false;
71
+ if (target.providerSessionId === value) return false;
72
+ target.providerSessionId = value;
73
+ return true;
74
+ }
75
+
38
76
  unregister(sessionId: string | undefined | null): void {
39
77
  if (!sessionId) return;
40
78
  const target = this.bySessionId.get(sessionId);