@adhdev/daemon-core 0.9.82-rc.479 → 0.9.82-rc.480
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/cli-adapters/cli-state-engine.d.ts +20 -0
- package/dist/commands/chat-commands-read.d.ts +8 -0
- package/dist/commands/chat-commands.d.ts +1 -1
- package/dist/commands/cli-manager.d.ts +26 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +211 -52
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +210 -52
- package/dist/index.mjs.map +1 -1
- package/dist/providers/native-history/dispatcher.d.ts +13 -0
- package/dist/status/chat-tail-hot-sessions.d.ts +40 -0
- package/package.json +3 -3
- package/src/cli-adapters/cli-state-engine.ts +36 -0
- package/src/cli-adapters/provider-cli-adapter.ts +40 -0
- package/src/commands/chat-commands-read.ts +193 -29
- package/src/commands/chat-commands.ts +1 -1
- package/src/commands/cli-manager.d.ts +2 -0
- package/src/commands/cli-manager.ts +72 -9
- package/src/commands/low-family/session-host.ts +8 -0
- package/src/config/chat-history.ts +8 -0
- package/src/index.ts +1 -0
- package/src/providers/native-history/dispatcher.ts +48 -12
- package/src/status/chat-tail-hot-sessions.ts +117 -2
|
@@ -27,5 +27,18 @@ export interface NativeHistoryResult {
|
|
|
27
27
|
sourcePath: string;
|
|
28
28
|
sourceMtimeMs: number;
|
|
29
29
|
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
30
|
+
/**
|
|
31
|
+
* True only when the resolved conversation is confirmed to belong to THIS
|
|
32
|
+
* reading session by the owner-token identity (an exact uuid bind, or a
|
|
33
|
+
* spawn-floor/birth-time pick born after this session started). False for a
|
|
34
|
+
* bare recency/newest-by-mtime pick made with no spawn floor — that pick may
|
|
35
|
+
* alias a co-located concurrent session (the antigravity coordinator↔replica
|
|
36
|
+
* crosswire), so its uuid must NEVER be recorded as a pin nor trusted to
|
|
37
|
+
* satisfy the same-pass safe-mapping identity check. Read-path callers gate
|
|
38
|
+
* the workspace-latest pin + first-read trust on this flag. Non-antigravity
|
|
39
|
+
* readers leave it undefined (their existing exact-file resolution is
|
|
40
|
+
* unaffected by this signal).
|
|
41
|
+
*/
|
|
42
|
+
ownerConfirmed?: boolean;
|
|
30
43
|
}
|
|
31
44
|
export declare function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeHistoryInput) => NativeHistoryResult | null;
|
|
@@ -16,7 +16,47 @@ export declare function classifyHotChatSessionsForSubscriptionFlush(sessions: Ho
|
|
|
16
16
|
recentMessageGraceMs?: number;
|
|
17
17
|
activeStatuses?: ReadonlySet<string>;
|
|
18
18
|
activeSessionIds?: ReadonlySet<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Per-session `lastMessageAt` of the most recent completion tail that has
|
|
21
|
+
* already been flushed to subscribers. A completed-but-unseen session is
|
|
22
|
+
* kept hot for delivery REGARDLESS of the 8s recency timer, but only until
|
|
23
|
+
* its current tail has been delivered once — bounding the guaranteed
|
|
24
|
+
* delivery so a slow-finalizing turn is not re-pushed every tick forever.
|
|
25
|
+
* A newer `lastMessageAt` (fresh turn) re-arms delivery.
|
|
26
|
+
*/
|
|
27
|
+
deliveredCompletionTailAt?: ReadonlyMap<string, number>;
|
|
28
|
+
/**
|
|
29
|
+
* (D8) PER-SUBSCRIPTION ACK gate — the authoritative guaranteed-delivery
|
|
30
|
+
* signal. Session ids for which at least one currently-subscribed ws has
|
|
31
|
+
* NOT yet been sent the session's finalized tail (its per-ws
|
|
32
|
+
* `lastDeliveredSignature` is empty / does not match the session's current
|
|
33
|
+
* tail). The daemon computes this from its live ws-subscription map; the
|
|
34
|
+
* classifier keeps every such session hot-for-delivery INDEPENDENT of the
|
|
35
|
+
* seen/unread badge, so a session whose unread badge cleared (or a fresh
|
|
36
|
+
* browser that re-subscribed after the single completion flush already
|
|
37
|
+
* fired) still gets its assistant-final tail delivered. Bounded because a
|
|
38
|
+
* subscriber's `lastDeliveredSignature` converges to the current tail the
|
|
39
|
+
* moment it is actually sent, dropping the session out of this set. When
|
|
40
|
+
* provided, this set is the guaranteed-delivery driver and the
|
|
41
|
+
* `deliveredCompletionTailAt` recency-watermark path is bypassed for these
|
|
42
|
+
* sessions (it stays only as the legacy opt-in for callers that don't pass
|
|
43
|
+
* a per-subscription set).
|
|
44
|
+
*/
|
|
45
|
+
underDeliveredSessionIds?: ReadonlySet<string>;
|
|
19
46
|
}): {
|
|
20
47
|
active: Set<string>;
|
|
21
48
|
finalizing: Set<string>;
|
|
49
|
+
guaranteedDelivery: Set<string>;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Detect sessions that just transitioned from an active/generating state into
|
|
53
|
+
* a settled/completed-but-unseen state, so their finalized completion tail can
|
|
54
|
+
* be flushed exactly once regardless of the recency window. Pure: the caller
|
|
55
|
+
* owns the `previousStatus` map and updates it from the returned `nextStatus`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function detectNewlySettledCompletedSessions(sessions: HotChatSessionLike[], previousStatus: ReadonlyMap<string, string>, options?: {
|
|
58
|
+
activeStatuses?: ReadonlySet<string>;
|
|
59
|
+
}): {
|
|
60
|
+
settled: Set<string>;
|
|
61
|
+
nextStatus: Map<string, string>;
|
|
22
62
|
};
|
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.480",
|
|
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.480",
|
|
51
|
+
"@adhdev/session-host-core": "0.9.82-rc.480",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -458,6 +458,42 @@ export class CliStateEngine {
|
|
|
458
458
|
this.idleFinishCandidate = null;
|
|
459
459
|
}
|
|
460
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Poll-driven static-idle confirm (D4). A hosted CLI session (e.g. a fresh
|
|
463
|
+
* antigravity coordinator) whose boot banner drove the FSM into 'generating'
|
|
464
|
+
* can then sit at a STATIC ready prompt emitting no further PTY output. Every
|
|
465
|
+
* output-driven busy→idle re-eval (handleOutput/resolveStartupState/settle)
|
|
466
|
+
* is starved because there is no new output, and the startup-settle loop has
|
|
467
|
+
* hard-stopped past spawnAt+10s — so currentStatus stays frozen at generating
|
|
468
|
+
* and the dashboard disables Send. This is the ONE path that can release that
|
|
469
|
+
* wedge from the read-only status poll.
|
|
470
|
+
*
|
|
471
|
+
* Safety: this must NEVER flip a real generating turn to idle. The gate is
|
|
472
|
+
* done by the caller (getStatus) reusing resolveStartupState's proven
|
|
473
|
+
* predicates: no recent PTY output for a grace window, runDetectStatus of the
|
|
474
|
+
* current screen === 'idle', and no active/parsed modal. Here we add the
|
|
475
|
+
* final structural guard: there must be NO active turn scope. A live user
|
|
476
|
+
* turn always carries a currentTurnScope (set in onTurnStarted), so this only
|
|
477
|
+
* releases the boot-banner wedge and the post-turn static-idle case, both of
|
|
478
|
+
* which have already had their scope nulled. Returns true when it transitioned.
|
|
479
|
+
*/
|
|
480
|
+
confirmPollStaticIdle(reason: string): boolean {
|
|
481
|
+
if (this.currentStatus !== 'generating') return false;
|
|
482
|
+
if (this.currentTurnScope || this.activeModal) return false;
|
|
483
|
+
this.clearAllTimers();
|
|
484
|
+
this.clearIdleFinishCandidate(reason);
|
|
485
|
+
this.isWaitingForResponse = false;
|
|
486
|
+
this.responseSettleIgnoreUntil = 0;
|
|
487
|
+
this.submitRetryUsed = false;
|
|
488
|
+
this.submitRetryPromptSnippet = '';
|
|
489
|
+
this.finishRetryCount = 0;
|
|
490
|
+
this.currentTurnScope = null;
|
|
491
|
+
this.activeModal = null;
|
|
492
|
+
this.setStatus('idle', reason);
|
|
493
|
+
this.recordTrace('poll_static_idle_confirmed', { reason });
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
|
|
461
497
|
hasActionableApproval(startupModal?: { message: string; buttons: string[] } | null): boolean {
|
|
462
498
|
return !!(startupModal ?? this.activeModal);
|
|
463
499
|
}
|
|
@@ -926,6 +926,46 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
926
926
|
const startupDetectedStatus = allowParse && this.startupParseGate && !startupModal
|
|
927
927
|
? this.runDetectStatus(this.recentOutputBuffer || this.terminalScreen.getText())
|
|
928
928
|
: null;
|
|
929
|
+
// (D4) Poll-driven static-idle confirm. A hosted CLI session whose boot
|
|
930
|
+
// banner drove the FSM to 'generating' can then sit at a STATIC ready
|
|
931
|
+
// prompt emitting NO further PTY output — every output-driven busy→idle
|
|
932
|
+
// re-eval is starved and the startup-settle loop has hard-stopped past
|
|
933
|
+
// spawnAt+10s, so currentStatus stays frozen at generating and the
|
|
934
|
+
// dashboard disables Send. This read-path poll is the only place that can
|
|
935
|
+
// release the wedge. Gate PRECISELY (reuse resolveStartupState's proven
|
|
936
|
+
// predicates so a real generating turn is NEVER mis-flipped):
|
|
937
|
+
// (a) no recent output for >= statusActivityHold (the 2000ms stable
|
|
938
|
+
// window resolveStartupState uses),
|
|
939
|
+
// (b) runDetectStatus(current screen) === 'idle' (same detector),
|
|
940
|
+
// (c) no active/parsed modal — an approval/choice screen is never
|
|
941
|
+
// flipped to idle.
|
|
942
|
+
// A real generating turn keeps producing output (fresh
|
|
943
|
+
// lastNonEmptyOutputAt) and/or shows 'esc to cancel' (detects busy) → it
|
|
944
|
+
// fails (a) or (b). confirmPollStaticIdle adds the final structural guard
|
|
945
|
+
// (currentStatus==='generating' AND no currentTurnScope/activeModal), so
|
|
946
|
+
// this only releases the boot-banner wedge and the post-turn static-idle
|
|
947
|
+
// case. Direct-spawn sessions already settle idle in the startup window
|
|
948
|
+
// → this is a no-op for them.
|
|
949
|
+
if (
|
|
950
|
+
allowParse
|
|
951
|
+
&& this.engine.currentStatus === 'generating'
|
|
952
|
+
&& !this.engine.currentTurnScope
|
|
953
|
+
&& !this.engine.activeModal
|
|
954
|
+
) {
|
|
955
|
+
const now = Date.now();
|
|
956
|
+
const quietForMs = this.lastNonEmptyOutputAt
|
|
957
|
+
? (now - this.lastNonEmptyOutputAt)
|
|
958
|
+
: Number.MAX_SAFE_INTEGER;
|
|
959
|
+
if (quietForMs >= this.getStatusActivityHoldMs()) {
|
|
960
|
+
const screenText = this.terminalScreen.getText();
|
|
961
|
+
const pollDetect = this.runDetectStatus(screenText || this.recentOutputBuffer);
|
|
962
|
+
const pollModal = this.runParseApproval(screenText)
|
|
963
|
+
|| this.runParseApproval(this.recentOutputBuffer);
|
|
964
|
+
if (pollDetect === 'idle' && !pollModal) {
|
|
965
|
+
this.engine.confirmPollStaticIdle('poll_static_idle');
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
929
969
|
let effectiveStatus = this.projectEffectiveStatus(startupModal);
|
|
930
970
|
let effectiveModal = startupModal || this.engine.activeModal;
|
|
931
971
|
// (fix) When we have no captured modal yet, take one more live attempt
|
|
@@ -134,6 +134,17 @@ export function __resetProviderSessionPinsForTest(): void {
|
|
|
134
134
|
try { clearPersistedProviderSessionPins(); } catch { /* best-effort */ }
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Test-only: read the in-memory read-pin (the mesh-session → conversation-uuid
|
|
139
|
+
* bind recorded by recordBoundProviderSessionId and mirrored to state.json
|
|
140
|
+
* sessionProviderSessionPins). Lets the antigravity-coordinator-pin tests assert
|
|
141
|
+
* that an owner-confirmed workspace-latest read recorded the pin — and that a
|
|
142
|
+
* non-owner-confirmed read did NOT. Not part of the runtime contract.
|
|
143
|
+
*/
|
|
144
|
+
export function __getProviderSessionPinForTest(meshSessionId: string): string | undefined {
|
|
145
|
+
return getBoundProviderSessionIdPin(meshSessionId);
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
const warnedLegacyNativeAllowlistHits = new Set<string>();
|
|
138
149
|
function warnLegacyNativeAllowlistHit(providerType: string): void {
|
|
139
150
|
if (warnedLegacyNativeAllowlistHits.has(providerType)) return;
|
|
@@ -155,6 +166,35 @@ function getExplicitHistorySessionId(args: any): string | undefined {
|
|
|
155
166
|
|
|
156
167
|
return undefined;
|
|
157
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A native-history session id is a "runtime fallback" — the daemon's own
|
|
172
|
+
* ADHDev session id (targetSessionId) standing in for a real provider-native
|
|
173
|
+
* conversation uuid — when it exactly equals targetSessionId. For an
|
|
174
|
+
* antigravity coordinator (agy takes no --session-id, so its providerSessionId
|
|
175
|
+
* never surfaces to the web), getConversationHistorySessionId falls back to the
|
|
176
|
+
* ADHDev sessionId, and the browser then sends that runtime id back as
|
|
177
|
+
* args.historySessionId. That id is NOT the on-disk conversations/<uuid>.db
|
|
178
|
+
* name (e.g. targetSessionId 28c530af vs stamped conv uuid 07f6ed3e), so a
|
|
179
|
+
* native read keyed on it can never exact-bind — it fail-closes to pty-parser
|
|
180
|
+
* (user-echo only) AND bypasses the owner-confirmed pin/live-bind resolution
|
|
181
|
+
* (which only runs when historySessionId is empty). Detect it whether it
|
|
182
|
+
* arrived EXPLICITLY (args.historySessionId === targetSessionId, the browser's
|
|
183
|
+
* poisoned read) OR only via getHistorySessionId's internal fallback (empty
|
|
184
|
+
* args), and in both cases treat historySessionId as ABSENT so the owner-
|
|
185
|
+
* confirmed native resolution engages and returns [user, assistant, ...].
|
|
186
|
+
* A REAL, DISTINCT provider conv uuid (≠ targetSessionId) is never a runtime
|
|
187
|
+
* fallback and must still exact-bind as before.
|
|
188
|
+
*/
|
|
189
|
+
function isRuntimeFallbackHistorySessionId(
|
|
190
|
+
candidateHistorySessionId: string | undefined,
|
|
191
|
+
targetSessionId: string | undefined,
|
|
192
|
+
): boolean {
|
|
193
|
+
const target = typeof targetSessionId === 'string' ? targetSessionId.trim() : '';
|
|
194
|
+
if (!target) return false;
|
|
195
|
+
const candidate = typeof candidateHistorySessionId === 'string' ? candidateHistorySessionId.trim() : '';
|
|
196
|
+
return candidate === target;
|
|
197
|
+
}
|
|
158
198
|
function getHistorySessionId(h: CommandHelpers, args: any): string | undefined {
|
|
159
199
|
const explicit = getExplicitHistorySessionId(args);
|
|
160
200
|
if (explicit) return explicit;
|
|
@@ -1612,15 +1652,34 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
1612
1652
|
: typeof (h.currentSession as any)?.workspace === 'string'
|
|
1613
1653
|
? (h.currentSession as any).workspace
|
|
1614
1654
|
: undefined;
|
|
1655
|
+
// Same runtime-fallback poison guard as the subscribe / history-only
|
|
1656
|
+
// paths: getHistorySessionId falls back to targetSessionId (the ADHDev
|
|
1657
|
+
// id) for an agy coordinator, and the browser may also send that id back
|
|
1658
|
+
// explicitly. Reading native history keyed on it can never exact-bind
|
|
1659
|
+
// (it is not the on-disk conv uuid). Drop it here too so the pin /
|
|
1660
|
+
// workspace-latest / owner-confirmed resolution engages instead of
|
|
1661
|
+
// fail-closing to pty-parser. A real DISTINCT provider uuid is preserved.
|
|
1662
|
+
const targetSidForHistory = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1663
|
+
const explicitHistorySessionIdForHistory = getExplicitHistorySessionId(args);
|
|
1664
|
+
const historySessionIdIsRuntimeFallback = Boolean(
|
|
1665
|
+
targetSidForHistory
|
|
1666
|
+
&& isRuntimeFallbackHistorySessionId(historySessionId, targetSidForHistory)
|
|
1667
|
+
&& (!explicitHistorySessionIdForHistory
|
|
1668
|
+
|| isRuntimeFallbackHistorySessionId(explicitHistorySessionIdForHistory, targetSidForHistory)),
|
|
1669
|
+
);
|
|
1670
|
+
const pinnedProviderSessionIdForHistory = getBoundProviderSessionIdPin(args?.targetSessionId);
|
|
1671
|
+
const effectiveHistorySessionId = historySessionIdIsRuntimeFallback
|
|
1672
|
+
? (pinnedProviderSessionIdForHistory || undefined)
|
|
1673
|
+
: historySessionId;
|
|
1615
1674
|
const exactNativeHistoryScope = Boolean(
|
|
1616
1675
|
(typeof args?.targetSessionId === 'string' && args.targetSessionId.trim())
|
|
1617
|
-
|| (typeof args?.historySessionId === 'string' && args.historySessionId.trim())
|
|
1676
|
+
|| (typeof args?.historySessionId === 'string' && args.historySessionId.trim() && !historySessionIdIsRuntimeFallback)
|
|
1618
1677
|
|| (typeof args?.providerSessionId === 'string' && args.providerSessionId.trim())
|
|
1619
1678
|
);
|
|
1620
1679
|
const result = supportsCliNativeTranscript(agentStr, provider) && isNativeSourceCanonicalHistory(provider?.nativeHistory)
|
|
1621
1680
|
? readCliProviderNativeHistory(agentStr, {
|
|
1622
1681
|
canonicalHistory: provider?.nativeHistory,
|
|
1623
|
-
historySessionId,
|
|
1682
|
+
historySessionId: effectiveHistorySessionId,
|
|
1624
1683
|
workspace,
|
|
1625
1684
|
offset: offset || 0,
|
|
1626
1685
|
limit: limit || 30,
|
|
@@ -1630,7 +1689,8 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
1630
1689
|
sessionStartedAtMs: sessionStartedAtMsFromRegistry(h, args?.targetSessionId),
|
|
1631
1690
|
envOverrides: sessionSpawnEnvFromAdapter(h, args?.targetSessionId),
|
|
1632
1691
|
instanceId: effectiveReadSessionId(h, args?.targetSessionId) || undefined,
|
|
1633
|
-
pinnedProviderSessionId:
|
|
1692
|
+
pinnedProviderSessionId: pinnedProviderSessionIdForHistory,
|
|
1693
|
+
allowWorkspaceLatestFallback: !pinnedProviderSessionIdForHistory && historySessionIdIsRuntimeFallback,
|
|
1634
1694
|
})
|
|
1635
1695
|
: readProviderChatHistory(agentStr, {
|
|
1636
1696
|
canonicalHistory: provider?.nativeHistory,
|
|
@@ -1649,13 +1709,26 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
1649
1709
|
: [];
|
|
1650
1710
|
const historyProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
|
|
1651
1711
|
? (result as any).providerSessionId
|
|
1652
|
-
: readHistorySessionIdFromMessages(messages) ||
|
|
1653
|
-
|
|
1654
|
-
|
|
1712
|
+
: readHistorySessionIdFromMessages(messages) || effectiveHistorySessionId;
|
|
1713
|
+
// Mirror of the subscribe path (see handleReadChat): an antigravity
|
|
1714
|
+
// workspace-latest read still surfaces the on-disk uuid, but that uuid is
|
|
1715
|
+
// only safe to persist / trust when it was OWNER-token-confirmed as this
|
|
1716
|
+
// session's own — a bare recency pick could be a co-located replica's
|
|
1717
|
+
// conversation. Gate the pin and the same-pass identity on ownerConfirmed.
|
|
1718
|
+
const resolvedProviderSessionId = typeof (result as any)?.providerSessionId === 'string'
|
|
1719
|
+
? (result as any).providerSessionId.trim()
|
|
1720
|
+
: '';
|
|
1721
|
+
const resultLookupIsWorkspace = lookup === 'workspace';
|
|
1722
|
+
const resultOwnerConfirmed = (result as any)?.ownerConfirmed === true;
|
|
1723
|
+
const ownerConfirmedUuid = resultOwnerConfirmed && typeof historyProviderSessionId === 'string' && historyProviderSessionId.trim()
|
|
1724
|
+
? historyProviderSessionId.trim()
|
|
1725
|
+
: '';
|
|
1726
|
+
if (resolvedProviderSessionId && (!resultLookupIsWorkspace || resultOwnerConfirmed)) {
|
|
1727
|
+
recordBoundProviderSessionId(h, effectiveReadSessionId(h, args?.targetSessionId), resolvedProviderSessionId);
|
|
1655
1728
|
}
|
|
1656
1729
|
const safeMapping = hasSafeNativeHistoryMapping({
|
|
1657
|
-
historySessionId: lookup === 'workspace' ? undefined :
|
|
1658
|
-
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
1730
|
+
historySessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : effectiveHistorySessionId),
|
|
1731
|
+
providerSessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId),
|
|
1659
1732
|
workspace,
|
|
1660
1733
|
nativeMessages: messages,
|
|
1661
1734
|
});
|
|
@@ -1861,10 +1934,18 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1861
1934
|
// id so readCliProviderNativeHistory's pin / workspace-latest paths can
|
|
1862
1935
|
// engage. Mirrors the handleChatHistory path's established handling.
|
|
1863
1936
|
const pinnedProviderSessionIdForRead = getBoundProviderSessionIdPin(targetSessionId);
|
|
1937
|
+
// Runtime fallback whether the runtime id was reached via
|
|
1938
|
+
// getHistorySessionId's internal fallback (empty args) OR the
|
|
1939
|
+
// browser explicitly sent historySessionId === targetSessionId
|
|
1940
|
+
// (the poisoned agy-coordinator read). Both must drop the
|
|
1941
|
+
// runtime id so pin / live-bind resolution engages; only a real
|
|
1942
|
+
// DISTINCT provider uuid stays as an exact-bind id.
|
|
1943
|
+
const explicitHistorySessionIdForRead = getExplicitHistorySessionId(args);
|
|
1864
1944
|
const nativeReadSessionIdIsRuntimeFallback = Boolean(
|
|
1865
1945
|
targetSessionId
|
|
1866
|
-
&& nativeHistoryReadSessionId
|
|
1867
|
-
&& !
|
|
1946
|
+
&& isRuntimeFallbackHistorySessionId(nativeHistoryReadSessionId, targetSessionId)
|
|
1947
|
+
&& (!explicitHistorySessionIdForRead
|
|
1948
|
+
|| isRuntimeFallbackHistorySessionId(explicitHistorySessionIdForRead, targetSessionId)),
|
|
1868
1949
|
);
|
|
1869
1950
|
const effectiveNativeReadSessionId = nativeReadSessionIdIsRuntimeFallback
|
|
1870
1951
|
? (pinnedProviderSessionIdForRead || undefined)
|
|
@@ -1899,7 +1980,23 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1899
1980
|
const resolvedProviderSessionId = typeof nativeHistory?.providerSessionId === 'string'
|
|
1900
1981
|
? nativeHistory.providerSessionId.trim()
|
|
1901
1982
|
: '';
|
|
1902
|
-
|
|
1983
|
+
// Pin gating for the antigravity workspace-latest branch: a
|
|
1984
|
+
// coordinator session has no pin (agy takes no --session-id) and
|
|
1985
|
+
// spawnedAtMs=0 after attach-restore, so the read resolves via the
|
|
1986
|
+
// workspace-latest fallback (lookup === 'workspace') rather than an
|
|
1987
|
+
// exact bind. The dispatcher STILL surfaces the on-disk conversation
|
|
1988
|
+
// uuid there — but that uuid is only safe to persist as a pin when it
|
|
1989
|
+
// was OWNER-token-confirmed (exact uuid bind, or a spawn-floor/birth
|
|
1990
|
+
// pick). A bare recency/newest-by-mtime pick (ownerConfirmed=false)
|
|
1991
|
+
// could be a co-located replica's conversation, so recording it would
|
|
1992
|
+
// hard-wire the coordinator↔replica crosswire permanently — never pin
|
|
1993
|
+
// that. Exact-bind / session-scoped reads (lookup === 'session') are
|
|
1994
|
+
// already owner-scoped by construction, so keep pinning them as before.
|
|
1995
|
+
const resolvedLookupIsWorkspace = (nativeHistory as any)?.lookup === 'workspace';
|
|
1996
|
+
const nativeOwnerConfirmed = (nativeHistory as any)?.ownerConfirmed === true;
|
|
1997
|
+
const mayPinResolvedProviderSessionId = resolvedProviderSessionId
|
|
1998
|
+
&& (!resolvedLookupIsWorkspace || nativeOwnerConfirmed);
|
|
1999
|
+
if (mayPinResolvedProviderSessionId) {
|
|
1903
2000
|
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSessionId), resolvedProviderSessionId);
|
|
1904
2001
|
}
|
|
1905
2002
|
} catch (error: any) {
|
|
@@ -1918,20 +2015,44 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
1918
2015
|
? nativeHistory.providerSessionId
|
|
1919
2016
|
: readHistorySessionIdFromMessages(nativeMessages) || nativeHistoryReadSessionId || historySessionId;
|
|
1920
2017
|
let lookup = nativeHistory?.lookup === 'workspace' ? 'workspace' : 'session';
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
2018
|
+
// Owner-confirmed uuid for THIS read (antigravity): the dispatcher
|
|
2019
|
+
// resolved a conversation and confirmed it is this session's own via the
|
|
2020
|
+
// owner token (exact uuid bind, or a spawn-floor/birth pick) — NOT a bare
|
|
2021
|
+
// recency pick. When present it is the authoritative conversation
|
|
2022
|
+
// identity for the same-pass safe-mapping check below, even on a
|
|
2023
|
+
// workspace-latest (lookup === 'workspace') read where the coordinator
|
|
2024
|
+
// has no pin. A coordinator session hits this path (agy takes no
|
|
2025
|
+
// --session-id, spawnedAtMs=0 after attach-restore); without it the
|
|
2026
|
+
// safe-mapping check saw undefined identity → workspace-overlap branch →
|
|
2027
|
+
// the PTY snapshot has only the user echo → fail-closed → regress to
|
|
2028
|
+
// pty-parser (user-echo only). Trusting the owner-confirmed uuid lets the
|
|
2029
|
+
// assistant answer reach the dashboard on the FIRST read.
|
|
2030
|
+
const ownerConfirmedUuid = adapter.cliType === 'antigravity-cli'
|
|
2031
|
+
&& (nativeHistory as any)?.ownerConfirmed === true
|
|
2032
|
+
&& typeof historyProviderSessionId === 'string'
|
|
2033
|
+
&& historyProviderSessionId.trim()
|
|
2034
|
+
? historyProviderSessionId.trim()
|
|
2035
|
+
: '';
|
|
2036
|
+
let nativeHistorySessionForMapping = ownerConfirmedUuid
|
|
2037
|
+
? ownerConfirmedUuid
|
|
2038
|
+
: adapter.cliType === 'antigravity-cli'
|
|
2039
|
+
&& historyProviderSessionId
|
|
2040
|
+
&& nativeHistoryReadSessionId
|
|
2041
|
+
&& historyProviderSessionId !== nativeHistoryReadSessionId
|
|
2042
|
+
? undefined
|
|
2043
|
+
: nativeHistoryReadSessionId;
|
|
2044
|
+
// For an owner-confirmed uuid, feed the uuid as the explicit session
|
|
2045
|
+
// identity to the safe-mapping check even on a workspace-latest read so
|
|
2046
|
+
// the session-branch identity test runs uuid-to-uuid (messages carry the
|
|
2047
|
+
// uuid as historySessionId) and trusts the assistant in this same pass.
|
|
1927
2048
|
let safeMapping = supportsNative && nativeHistory
|
|
1928
2049
|
? hasSafeNativeHistoryMapping({
|
|
1929
|
-
historySessionId: lookup === 'workspace' ? undefined : nativeHistorySessionForMapping,
|
|
1930
|
-
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId,
|
|
2050
|
+
historySessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : nativeHistorySessionForMapping),
|
|
2051
|
+
providerSessionId: ownerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId || providerSessionId),
|
|
1931
2052
|
workspace,
|
|
1932
2053
|
nativeMessages,
|
|
1933
2054
|
ptyMessages: returnedMessages,
|
|
1934
|
-
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope,
|
|
2055
|
+
requireWorkspaceContentOverlap: lookup === 'workspace' && !exactNativeHistoryScope && !ownerConfirmedUuid,
|
|
1935
2056
|
})
|
|
1936
2057
|
: false;
|
|
1937
2058
|
if (skipLiveNativeHistoryWithoutProviderSession && (!safeMapping || returnedMessages.length === 0)) {
|
|
@@ -2234,10 +2355,19 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
2234
2355
|
// explicit id was passed) and we hold a pin from an earlier bound
|
|
2235
2356
|
// read, prefer the pin so the query hits the real session.
|
|
2236
2357
|
const pinnedProviderSessionIdForHistory = getBoundProviderSessionIdPin(targetSid);
|
|
2358
|
+
// Runtime fallback whether historySessionId reached targetSid via
|
|
2359
|
+
// getHistorySessionId's internal fallback (empty args) OR the browser
|
|
2360
|
+
// explicitly sent historySessionId === targetSid (the poisoned
|
|
2361
|
+
// agy-coordinator subscription / D8 refreshAuthoritativeTail read).
|
|
2362
|
+
// In both cases the runtime id is NOT a real provider conv uuid, so
|
|
2363
|
+
// drop it and let pin / workspace-latest / owner-confirmed resolution
|
|
2364
|
+
// run. A real DISTINCT provider uuid still exact-binds unchanged.
|
|
2365
|
+
const explicitHistorySessionId = getExplicitHistorySessionId(args);
|
|
2237
2366
|
const historySessionIdIsRuntimeFallback = Boolean(
|
|
2238
2367
|
targetSid
|
|
2239
|
-
&& historySessionId
|
|
2240
|
-
&& !
|
|
2368
|
+
&& isRuntimeFallbackHistorySessionId(historySessionId, targetSid)
|
|
2369
|
+
&& (!explicitHistorySessionId
|
|
2370
|
+
|| isRuntimeFallbackHistorySessionId(explicitHistorySessionId, targetSid)),
|
|
2241
2371
|
);
|
|
2242
2372
|
// When this is the runtime fallback (not a real provider id): prefer
|
|
2243
2373
|
// the pin if we have one, else drop the runtime id entirely so the
|
|
@@ -2282,24 +2412,58 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
2282
2412
|
const historyProviderSessionId = typeof (history as any)?.providerSessionId === 'string'
|
|
2283
2413
|
? (history as any).providerSessionId
|
|
2284
2414
|
: readHistorySessionIdFromMessages(historyMessages) || effectiveHistorySessionIdForRead;
|
|
2285
|
-
//
|
|
2286
|
-
|
|
2415
|
+
// Antigravity coordinator root fix (history-only path — the post-turn
|
|
2416
|
+
// read a coordinator actually hits: no live adapter, no pin, agy takes no
|
|
2417
|
+
// --session-id so spawnedAtMs is 0 after attach-restore → the read resolves
|
|
2418
|
+
// via the workspace-latest fallback, lookup === 'workspace'). The dispatcher
|
|
2419
|
+
// STILL surfaces the on-disk conversation uuid there, and flags whether it
|
|
2420
|
+
// was OWNER-token-confirmed as this session's own (an exact/birth pick) vs a
|
|
2421
|
+
// bare recency pick that could be a co-located replica's conversation.
|
|
2422
|
+
// • Pin the uuid on a workspace-latest read ONLY when owner-confirmed —
|
|
2423
|
+
// recording a replica's uuid would hard-wire the coordinator↔replica
|
|
2424
|
+
// crosswire permanently. Exact/session-scoped reads pin as before.
|
|
2425
|
+
// • Feed the owner-confirmed uuid as the explicit identity to the
|
|
2426
|
+
// safe-mapping check even on a workspace-latest read so the identity
|
|
2427
|
+
// test runs uuid-to-uuid and trusts the assistant on this FIRST read
|
|
2428
|
+
// (else it saw undefined identity → workspace-overlap branch → the PTY
|
|
2429
|
+
// snapshot has only the user echo → fail-closed → regress to pty-parser).
|
|
2430
|
+
const historyLookupIsWorkspace = lookup === 'workspace';
|
|
2431
|
+
const historyOwnerConfirmed = agentStr === 'antigravity-cli' && (history as any)?.ownerConfirmed === true;
|
|
2432
|
+
const historyOwnerConfirmedUuid = historyOwnerConfirmed
|
|
2433
|
+
&& typeof historyProviderSessionId === 'string' && historyProviderSessionId.trim()
|
|
2434
|
+
? historyProviderSessionId.trim()
|
|
2435
|
+
: '';
|
|
2436
|
+
// Refresh the pin whenever this path resolves a real provider id — but for
|
|
2437
|
+
// a workspace-latest antigravity read, only when the uuid is owner-confirmed.
|
|
2438
|
+
if (typeof (history as any)?.providerSessionId === 'string'
|
|
2439
|
+
&& (history as any).providerSessionId.trim()
|
|
2440
|
+
&& (!historyLookupIsWorkspace || !agentStr || agentStr !== 'antigravity-cli' || historyOwnerConfirmed)) {
|
|
2287
2441
|
recordBoundProviderSessionId(h, effectiveReadSessionId(h, targetSid), (history as any).providerSessionId.trim());
|
|
2288
2442
|
}
|
|
2289
2443
|
// Use the id we actually read with (pin / real provider id), NOT the
|
|
2290
2444
|
// raw runtime-fallback historySessionId — otherwise the mapping guard
|
|
2291
2445
|
// compares the stamped messages' real id against the runtime id and
|
|
2292
2446
|
// fails closed, undoing the pin reuse.
|
|
2293
|
-
const mappingSessionId = effectiveHistorySessionIdForRead;
|
|
2294
|
-
|
|
2447
|
+
const mappingSessionId = historyOwnerConfirmedUuid || effectiveHistorySessionIdForRead;
|
|
2448
|
+
// Fail closed for an antigravity workspace-latest read whose uuid was NOT
|
|
2449
|
+
// owner-confirmed: it is a bare recency/newest-by-mtime pick that could be
|
|
2450
|
+
// a co-located concurrent session's (replica's) conversation. Without an
|
|
2451
|
+
// owner-token confirmation we cannot prove ownership, so refuse it rather
|
|
2452
|
+
// than surface a sibling's transcript (the coordinator↔replica crosswire
|
|
2453
|
+
// guard). This is the same fail-closed default the design study protects —
|
|
2454
|
+
// only an owner-confirmed uuid escapes it above.
|
|
2455
|
+
const antigravityWorkspaceLatestUnconfirmed = agentStr === 'antigravity-cli'
|
|
2456
|
+
&& historyLookupIsWorkspace
|
|
2457
|
+
&& !historyOwnerConfirmedUuid;
|
|
2458
|
+
const safeMapping = supportsNative && !antigravityWorkspaceLatestUnconfirmed
|
|
2295
2459
|
? hasSafeNativeHistoryMapping({
|
|
2296
|
-
historySessionId: lookup === 'workspace' ? undefined : mappingSessionId,
|
|
2297
|
-
providerSessionId: lookup === 'workspace' ? undefined : historyProviderSessionId,
|
|
2460
|
+
historySessionId: historyOwnerConfirmedUuid || (lookup === 'workspace' ? undefined : mappingSessionId),
|
|
2461
|
+
providerSessionId: historyOwnerConfirmedUuid || (lookup === 'workspace' ? undefined : historyProviderSessionId),
|
|
2298
2462
|
workspace,
|
|
2299
2463
|
nativeMessages: historyMessages,
|
|
2300
2464
|
})
|
|
2301
2465
|
: false;
|
|
2302
|
-
const trustedExactNativeIdentity = lookup !== 'workspace'
|
|
2466
|
+
const trustedExactNativeIdentity = (lookup !== 'workspace' || Boolean(historyOwnerConfirmedUuid))
|
|
2303
2467
|
&& Boolean(mappingSessionId)
|
|
2304
2468
|
&& Boolean(historyProviderSessionId)
|
|
2305
2469
|
&& mappingSessionId === historyProviderSessionId;
|
|
@@ -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, __resetProviderSessionPinsForTest } from './chat-commands-read.js';
|
|
14
|
+
export { handleChatHistory, handleReadChat, __resetProviderSessionPinsForTest, __getProviderSessionPinForTest } from './chat-commands-read.js';
|
|
15
15
|
export {
|
|
16
16
|
handleSendChat,
|
|
17
17
|
handleListChats,
|
|
@@ -49,6 +49,8 @@ export interface HostedCliRuntimeDescriptor {
|
|
|
49
49
|
workspace: string;
|
|
50
50
|
cliArgs?: string[];
|
|
51
51
|
providerSessionId?: string;
|
|
52
|
+
managedBy?: string;
|
|
53
|
+
startedAtMs?: number;
|
|
52
54
|
}
|
|
53
55
|
export declare function supportsExplicitSessionResume(resume?: ProviderResumeCapability): boolean;
|
|
54
56
|
export declare class DaemonCliManager {
|
|
@@ -233,6 +233,14 @@ export interface HostedCliRuntimeDescriptor {
|
|
|
233
233
|
cliArgs?: string[];
|
|
234
234
|
providerSessionId?: string;
|
|
235
235
|
managedBy?: string;
|
|
236
|
+
/**
|
|
237
|
+
* Real spawn time (ms epoch) of the underlying session-host runtime — a PAST
|
|
238
|
+
* timestamp recorded when the runtime first started. Threaded through so an
|
|
239
|
+
* attach can restore the native-history session-floor to the runtime's actual
|
|
240
|
+
* birth instead of collapsing spawnedAtMs to 0. Undefined when unrecoverable
|
|
241
|
+
* (genuine post-restart-unknown), in which case the caller keeps the 0 fallback.
|
|
242
|
+
*/
|
|
243
|
+
startedAtMs?: number;
|
|
236
244
|
}
|
|
237
245
|
|
|
238
246
|
type CliPresentationInstance = ProviderInstance & {
|
|
@@ -294,6 +302,33 @@ function hasCliArg(args: string[], flag: string): boolean {
|
|
|
294
302
|
return args.some((arg) => arg === flag || arg.startsWith(`${flag}=`));
|
|
295
303
|
}
|
|
296
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Decide the session-registry spawnedAtMs (the native-history session-floor) for a
|
|
307
|
+
* newly registered CLI instance.
|
|
308
|
+
*
|
|
309
|
+
* - Fresh launch (attachExisting=false): now (nowMs). A real live spawn floor
|
|
310
|
+
* isolates a fresh session's own store and holds prior-session leak protection.
|
|
311
|
+
* - Attach WITH a recoverable record startedAt (a PAST timestamp): that startedAt.
|
|
312
|
+
* Restoring a hosted runtime (coordinator / MAGI replica / hermes / claude /
|
|
313
|
+
* codex) after a daemon restart, the real spawn time is in the past. Using it
|
|
314
|
+
* restores each session's per-session birth-floor so co-located antigravity
|
|
315
|
+
* runtimes resolve their OWN conversation (ownerConfirmed) instead of the
|
|
316
|
+
* floor-less newest-by-mtime path that let a replica claim the coordinator's conv.
|
|
317
|
+
* - Attach with NO recoverable startedAt: 0 — disables the floor for this session
|
|
318
|
+
* (recent_window_ms still bounds the look-back). NEVER use nowMs here: nowMs is in
|
|
319
|
+
* the FUTURE relative to existing transcripts and would push the floor past every
|
|
320
|
+
* transcript file, losing them (the ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP regression).
|
|
321
|
+
*/
|
|
322
|
+
export function resolveHostedSpawnedAtMs(
|
|
323
|
+
attachExisting: boolean,
|
|
324
|
+
attachStartedAtMs: number | undefined,
|
|
325
|
+
nowMs: number,
|
|
326
|
+
): number {
|
|
327
|
+
if (!attachExisting) return nowMs;
|
|
328
|
+
if (typeof attachStartedAtMs === 'number' && attachStartedAtMs > 0) return attachStartedAtMs;
|
|
329
|
+
return 0;
|
|
330
|
+
}
|
|
331
|
+
|
|
297
332
|
function hasConfigOverride(args: string[], key: string): boolean {
|
|
298
333
|
for (let index = 0; index < args.length; index += 1) {
|
|
299
334
|
const arg = args[index];
|
|
@@ -673,6 +708,13 @@ export class DaemonCliManager {
|
|
|
673
708
|
providerSessionId?: string;
|
|
674
709
|
launchMode?: CliLaunchMode;
|
|
675
710
|
extraEnv?: Record<string, string>;
|
|
711
|
+
/**
|
|
712
|
+
* On an attach (attachExisting=true), the real spawn time (ms epoch) of the
|
|
713
|
+
* session-host runtime being restored — a PAST timestamp. Used to restore the
|
|
714
|
+
* native-history session-floor to the runtime's actual birth instead of 0.
|
|
715
|
+
* See the spawnedAtMs computation below. Ignored for fresh launches.
|
|
716
|
+
*/
|
|
717
|
+
attachStartedAtMs?: number;
|
|
676
718
|
onProviderSessionResolved?: (info: {
|
|
677
719
|
instanceId: string;
|
|
678
720
|
providerType: string;
|
|
@@ -734,15 +776,30 @@ export class DaemonCliManager {
|
|
|
734
776
|
workspace: resolvedDir,
|
|
735
777
|
// attachExisting === true means we're restoring an already-spawned
|
|
736
778
|
// hosted runtime after a daemon restart, not starting a fresh PTY.
|
|
737
|
-
//
|
|
738
|
-
//
|
|
739
|
-
//
|
|
740
|
-
// every existing transcript file, so the
|
|
741
|
-
// would return null even though the transcript
|
|
742
|
-
//
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
779
|
+
//
|
|
780
|
+
// NEVER use Date.now() for the attach case: the real spawn time is in
|
|
781
|
+
// the PAST, and pinning the floor to now would push the native-history
|
|
782
|
+
// session-floor cutoff past every existing transcript file, so the
|
|
783
|
+
// agy/hermes/claude reader would return null even though the transcript
|
|
784
|
+
// on disk is fresh (the ANTIGRAVITY-FINAL-MESSAGE-TAIL-GAP regression).
|
|
785
|
+
//
|
|
786
|
+
// But collapsing to 0 for EVERY attach is also wrong: with the mesh
|
|
787
|
+
// coordinator + MAGI replicas all running as hosted runtimes sharing one
|
|
788
|
+
// workspace and attached with attachExisting=true, spawnedAtMs=0 disables
|
|
789
|
+
// the per-session native-history birth-floor for all of them. Without a
|
|
790
|
+
// floor, resolveAntigravityPath takes the floor-less newest-by-mtime
|
|
791
|
+
// branch (ownerConfirmed:false) and a replica's read can claim the
|
|
792
|
+
// coordinator's OWN conversation, which then reads as claimedByOther —
|
|
793
|
+
// regressing the coordinator chat to the pty-parser (user-only) path.
|
|
794
|
+
//
|
|
795
|
+
// So when the session-host record's REAL startedAt (a PAST timestamp) is
|
|
796
|
+
// recoverable, use it: the floor lands at the runtime's actual birth, the
|
|
797
|
+
// transcript is still found, AND each session's floor isolates its own
|
|
798
|
+
// conversation. Fall back to 0 ONLY when startedAt is unrecoverable (the
|
|
799
|
+
// genuine post-restart-unknown case) — that preserves the tail-gap
|
|
800
|
+
// protection. Fresh launches still get Date.now() so prior-session leak
|
|
801
|
+
// protection holds.
|
|
802
|
+
spawnedAtMs: resolveHostedSpawnedAtMs(attachExisting, options?.attachStartedAtMs, Date.now()),
|
|
746
803
|
});
|
|
747
804
|
} catch (spawnErr: any) {
|
|
748
805
|
LOG.error('CLI', `[${cliType}] Spawn failed: ${spawnErr?.message}`);
|
|
@@ -1241,6 +1298,12 @@ export class DaemonCliManager {
|
|
|
1241
1298
|
{
|
|
1242
1299
|
providerSessionId: sessionBinding.providerSessionId,
|
|
1243
1300
|
launchMode: 'manual',
|
|
1301
|
+
// Thread the runtime's REAL past spawn time so the attach restores
|
|
1302
|
+
// the per-session native-history birth-floor instead of collapsing
|
|
1303
|
+
// to spawnedAtMs:0 (which disabled the antigravity per-session floor
|
|
1304
|
+
// and let MAGI replicas claim the coordinator's own conversation).
|
|
1305
|
+
// Undefined → registerCliInstance keeps the 0 fallback.
|
|
1306
|
+
attachStartedAtMs: record.startedAtMs,
|
|
1244
1307
|
},
|
|
1245
1308
|
);
|
|
1246
1309
|
restoredBindings.add(bindingKey);
|