@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
|
@@ -33,6 +33,14 @@ function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor |
|
|
|
33
33
|
providerSessionId: typeof record.meta?.providerSessionId === 'string'
|
|
34
34
|
? String(record.meta.providerSessionId)
|
|
35
35
|
: undefined,
|
|
36
|
+
// Real spawn time (PAST timestamp) of the underlying runtime — startedAt is
|
|
37
|
+
// stamped on markStarted; fall back to createdAt (record creation). Threaded
|
|
38
|
+
// through so an attach restores the native-history session-floor to the
|
|
39
|
+
// runtime's actual birth instead of collapsing spawnedAtMs to 0 (which broke
|
|
40
|
+
// the antigravity per-session birth-floor for co-located MAGI runtimes).
|
|
41
|
+
startedAtMs: typeof record.startedAt === 'number' && record.startedAt > 0
|
|
42
|
+
? record.startedAt
|
|
43
|
+
: (typeof record.createdAt === 'number' && record.createdAt > 0 ? record.createdAt : undefined),
|
|
36
44
|
};
|
|
37
45
|
}
|
|
38
46
|
|
|
@@ -1742,6 +1742,11 @@ type ProviderNativeHistoryReadResult = {
|
|
|
1742
1742
|
nativeHistoryCoverage?: string;
|
|
1743
1743
|
partialReason?: string;
|
|
1744
1744
|
unavailableReason?: string;
|
|
1745
|
+
// Antigravity: whether the resolved conversation was owner-token-confirmed as
|
|
1746
|
+
// this session's own (vs a bare recency pick). Threaded to the read-path so
|
|
1747
|
+
// the workspace-latest pin + first-read safe-mapping trust only fire for a
|
|
1748
|
+
// confirmed uuid. See dispatcher NativeHistoryResult.ownerConfirmed.
|
|
1749
|
+
ownerConfirmed?: boolean;
|
|
1745
1750
|
};
|
|
1746
1751
|
|
|
1747
1752
|
function getNativeHistoryScriptName(canonicalHistory: ProviderCanonicalHistoryConfig | undefined, key: 'readSession' | 'listSessions'): string {
|
|
@@ -1860,6 +1865,7 @@ function callProviderNativeHistoryRead(
|
|
|
1860
1865
|
nativeHistoryCoverage: typeof (result as any).nativeHistoryCoverage === 'string' ? (result as any).nativeHistoryCoverage.trim() : undefined,
|
|
1861
1866
|
partialReason: typeof (result as any).partialReason === 'string' ? (result as any).partialReason.trim() : undefined,
|
|
1862
1867
|
unavailableReason: typeof (result as any).unavailableReason === 'string' ? (result as any).unavailableReason.trim() : undefined,
|
|
1868
|
+
ownerConfirmed: typeof (result as any).ownerConfirmed === 'boolean' ? (result as any).ownerConfirmed : undefined,
|
|
1863
1869
|
};
|
|
1864
1870
|
}
|
|
1865
1871
|
|
|
@@ -1957,6 +1963,7 @@ export function readProviderChatHistory(
|
|
|
1957
1963
|
nativeHistoryCoverage?: string;
|
|
1958
1964
|
partialReason?: string;
|
|
1959
1965
|
unavailableReason?: string;
|
|
1966
|
+
ownerConfirmed?: boolean;
|
|
1960
1967
|
} {
|
|
1961
1968
|
if (isNativeSourceCanonicalHistory(options.canonicalHistory) && (options.historySessionId || options.workspace)) {
|
|
1962
1969
|
const nativeResult = buildNativeHistoryReadResult(agentType, options.canonicalHistory, options.scripts, options.historySessionId, options.workspace, options.excludeInProgressTurn, options.sessionStartedAtMs, options.envOverrides, options.forceRefresh, options.instanceId);
|
|
@@ -1971,6 +1978,7 @@ export function readProviderChatHistory(
|
|
|
1971
1978
|
nativeHistoryCoverage: nativeResult.nativeHistoryCoverage,
|
|
1972
1979
|
partialReason: nativeResult.partialReason,
|
|
1973
1980
|
unavailableReason: nativeResult.unavailableReason,
|
|
1981
|
+
ownerConfirmed: nativeResult.ownerConfirmed,
|
|
1974
1982
|
};
|
|
1975
1983
|
}
|
|
1976
1984
|
return {
|
package/src/index.ts
CHANGED
|
@@ -344,6 +344,7 @@ export { getHostMemorySnapshot } from './system/host-memory.js';
|
|
|
344
344
|
export type { HostMemorySnapshot } from './system/host-memory.js';
|
|
345
345
|
export {
|
|
346
346
|
classifyHotChatSessionsForSubscriptionFlush,
|
|
347
|
+
detectNewlySettledCompletedSessions,
|
|
347
348
|
DEFAULT_ACTIVE_CHAT_POLL_STATUSES,
|
|
348
349
|
DEFAULT_CHAT_TAIL_RECENT_MESSAGE_GRACE_MS,
|
|
349
350
|
} from './status/chat-tail-hot-sessions.js';
|
|
@@ -50,6 +50,19 @@ export interface NativeHistoryResult {
|
|
|
50
50
|
sourcePath: string;
|
|
51
51
|
sourceMtimeMs: number;
|
|
52
52
|
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
53
|
+
/**
|
|
54
|
+
* True only when the resolved conversation is confirmed to belong to THIS
|
|
55
|
+
* reading session by the owner-token identity (an exact uuid bind, or a
|
|
56
|
+
* spawn-floor/birth-time pick born after this session started). False for a
|
|
57
|
+
* bare recency/newest-by-mtime pick made with no spawn floor — that pick may
|
|
58
|
+
* alias a co-located concurrent session (the antigravity coordinator↔replica
|
|
59
|
+
* crosswire), so its uuid must NEVER be recorded as a pin nor trusted to
|
|
60
|
+
* satisfy the same-pass safe-mapping identity check. Read-path callers gate
|
|
61
|
+
* the workspace-latest pin + first-read trust on this flag. Non-antigravity
|
|
62
|
+
* readers leave it undefined (their existing exact-file resolution is
|
|
63
|
+
* unaffected by this signal).
|
|
64
|
+
*/
|
|
65
|
+
ownerConfirmed?: boolean;
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeHistoryInput) => NativeHistoryResult | null {
|
|
@@ -74,8 +87,13 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
74
87
|
: typeof input.args?.instanceId === 'string'
|
|
75
88
|
? (input.args.instanceId as string)
|
|
76
89
|
: '';
|
|
77
|
-
const
|
|
90
|
+
const resolved = resolveSourcePath(reader, workspace, sessionId, sessionStartedAtMs, instanceId);
|
|
91
|
+
const sourcePath = resolved?.path || null;
|
|
78
92
|
if (!sourcePath) return null;
|
|
93
|
+
// Owner-confirmation only meaningful for antigravity (see resolveAntigravityPath).
|
|
94
|
+
// For other readers the file was resolved by exact per-session key already;
|
|
95
|
+
// leave the flag undefined so the read-path callers treat them as before.
|
|
96
|
+
const ownerConfirmed = reader === 'antigravity-cli' ? resolved?.ownerConfirmed === true : undefined;
|
|
79
97
|
if (input.forceRefresh === true || input.args?.forceRefresh === true) {
|
|
80
98
|
try { fs.statSync(sourcePath); } catch { /* best-effort metadata refresh */ }
|
|
81
99
|
}
|
|
@@ -116,6 +134,7 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
116
134
|
sourcePath: session.sourcePath,
|
|
117
135
|
sourceMtimeMs: session.sourceMtimeMs,
|
|
118
136
|
nativeHistoryCoverage: (session as any).nativeHistoryCoverage || 'full',
|
|
137
|
+
ownerConfirmed,
|
|
119
138
|
};
|
|
120
139
|
};
|
|
121
140
|
}
|
|
@@ -124,12 +143,19 @@ export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeH
|
|
|
124
143
|
// Per-provider path resolution
|
|
125
144
|
// ────────────────────────────────────────────────────────────────────────────
|
|
126
145
|
|
|
127
|
-
|
|
146
|
+
interface ResolvedSource {
|
|
147
|
+
path: string;
|
|
148
|
+
/** Antigravity only: whether the resolution was owner-token-confirmed (see
|
|
149
|
+
* NativeHistoryResult.ownerConfirmed / resolveAntigravityPath). */
|
|
150
|
+
ownerConfirmed?: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string, sessionStartedAtMs: number, instanceId: string): ResolvedSource | null {
|
|
128
154
|
switch (reader) {
|
|
129
|
-
case 'claude-cli':
|
|
130
|
-
case 'codex-cli':
|
|
155
|
+
case 'claude-cli': { const p = resolveClaudePath(workspace, sessionId); return p ? { path: p } : null; }
|
|
156
|
+
case 'codex-cli': { const p = resolveCodexPath(workspace, sessionId, sessionStartedAtMs); return p ? { path: p } : null; }
|
|
131
157
|
case 'antigravity-cli': return resolveAntigravityPath(workspace, sessionId, sessionStartedAtMs, instanceId);
|
|
132
|
-
case 'hermes-cli':
|
|
158
|
+
case 'hermes-cli': { const p = resolveHermesPath(workspace, sessionId); return p ? { path: p } : null; }
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
|
@@ -291,7 +317,7 @@ function resolveAntigravityPath(
|
|
|
291
317
|
sessionId: string,
|
|
292
318
|
sessionStartedAtMs: number,
|
|
293
319
|
instanceId: string,
|
|
294
|
-
):
|
|
320
|
+
): ResolvedSource | null {
|
|
295
321
|
const agyRoot = path.join(os.homedir(), '.gemini', 'antigravity-cli');
|
|
296
322
|
// Owner token identifies THIS reading session. The provider instance derives
|
|
297
323
|
// the identical token (workspace + startedAt, or instanceId) so it can
|
|
@@ -309,7 +335,9 @@ function resolveAntigravityPath(
|
|
|
309
335
|
const dbPath = path.join(agyRoot, 'conversations', `${sessionId}.db`);
|
|
310
336
|
if (fs.existsSync(dbPath)) {
|
|
311
337
|
if (owner) claimAntigravityConversation(sessionId, owner);
|
|
312
|
-
|
|
338
|
+
// Exact uuid bind — the caller named this conversation, so it is
|
|
339
|
+
// authoritatively this session's own. Owner-confirmed.
|
|
340
|
+
return { path: dbPath, ownerConfirmed: true };
|
|
313
341
|
}
|
|
314
342
|
}
|
|
315
343
|
|
|
@@ -344,6 +372,10 @@ function resolveAntigravityPath(
|
|
|
344
372
|
})
|
|
345
373
|
.filter(e => e.mtime >= cutoff);
|
|
346
374
|
let ordered: Array<{ uuid: string; p: string }> = [];
|
|
375
|
+
// Floor branch (sessionStartedAtMs > 0) is birth-confirmed as this
|
|
376
|
+
// session's own store → owner-confirmed. Floor-less newest-by-mtime is a
|
|
377
|
+
// bare recency pick that can alias a co-located session → NOT confirmed.
|
|
378
|
+
const brainOwnerConfirmed = sessionStartedAtMs > 0;
|
|
347
379
|
if (sessionStartedAtMs > 0) {
|
|
348
380
|
// Floor branch: this session's own = born at/after (floor - grace),
|
|
349
381
|
// oldest-birth first. Mirrors pickUnboundConversationDb exactly so the
|
|
@@ -360,7 +392,7 @@ function resolveAntigravityPath(
|
|
|
360
392
|
const t = nonEmptyBrain(e.uuid, e.p);
|
|
361
393
|
if (t) {
|
|
362
394
|
if (owner) claimAntigravityConversation(e.uuid, owner);
|
|
363
|
-
return t;
|
|
395
|
+
return { path: t, ownerConfirmed: brainOwnerConfirmed };
|
|
364
396
|
}
|
|
365
397
|
}
|
|
366
398
|
}
|
|
@@ -372,7 +404,7 @@ function resolveAntigravityPath(
|
|
|
372
404
|
const picked = pickUnboundConversationDb(convRoot, sessionStartedAtMs, owner);
|
|
373
405
|
if (picked) {
|
|
374
406
|
if (owner) claimAntigravityConversation(picked.uuid, owner);
|
|
375
|
-
return picked.path;
|
|
407
|
+
return { path: picked.path, ownerConfirmed: picked.ownerConfirmed };
|
|
376
408
|
}
|
|
377
409
|
|
|
378
410
|
return null;
|
|
@@ -402,7 +434,7 @@ function pickUnboundConversationDb(
|
|
|
402
434
|
convRoot: string,
|
|
403
435
|
sessionFloorMs: number,
|
|
404
436
|
owner: string,
|
|
405
|
-
): { path: string; uuid: string } | null {
|
|
437
|
+
): { path: string; uuid: string; ownerConfirmed: boolean } | null {
|
|
406
438
|
let entries: fs.Dirent[] = [];
|
|
407
439
|
try { entries = fs.readdirSync(convRoot, { withFileTypes: true }); } catch { return null; }
|
|
408
440
|
|
|
@@ -442,11 +474,15 @@ function pickUnboundConversationDb(
|
|
|
442
474
|
// session — do NOT bind to it. Wait for our own store on the next read.
|
|
443
475
|
if (own.length === 0) return null;
|
|
444
476
|
own.sort((a, b) => (a.birth || a.mtime) - (b.birth || b.mtime));
|
|
445
|
-
|
|
477
|
+
// Birth-time confirmed as this session's own store → owner-confirmed.
|
|
478
|
+
return { path: own[0].path, uuid: own[0].uuid, ownerConfirmed: true };
|
|
446
479
|
}
|
|
447
480
|
|
|
481
|
+
// Floor-less newest-by-mtime: a bare recency pick with no per-session
|
|
482
|
+
// guarantee — it can alias a co-located concurrent session, so it is NOT
|
|
483
|
+
// owner-confirmed and must not be pinned/trusted by the read-path callers.
|
|
448
484
|
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
449
|
-
return { path: candidates[0].path, uuid: candidates[0].uuid };
|
|
485
|
+
return { path: candidates[0].path, uuid: candidates[0].uuid, ownerConfirmed: false };
|
|
450
486
|
}
|
|
451
487
|
|
|
452
488
|
/**
|
|
@@ -60,8 +60,35 @@ export function classifyHotChatSessionsForSubscriptionFlush(
|
|
|
60
60
|
recentMessageGraceMs?: number;
|
|
61
61
|
activeStatuses?: ReadonlySet<string>;
|
|
62
62
|
activeSessionIds?: ReadonlySet<string>;
|
|
63
|
+
/**
|
|
64
|
+
* Per-session `lastMessageAt` of the most recent completion tail that has
|
|
65
|
+
* already been flushed to subscribers. A completed-but-unseen session is
|
|
66
|
+
* kept hot for delivery REGARDLESS of the 8s recency timer, but only until
|
|
67
|
+
* its current tail has been delivered once — bounding the guaranteed
|
|
68
|
+
* delivery so a slow-finalizing turn is not re-pushed every tick forever.
|
|
69
|
+
* A newer `lastMessageAt` (fresh turn) re-arms delivery.
|
|
70
|
+
*/
|
|
71
|
+
deliveredCompletionTailAt?: ReadonlyMap<string, number>;
|
|
72
|
+
/**
|
|
73
|
+
* (D8) PER-SUBSCRIPTION ACK gate — the authoritative guaranteed-delivery
|
|
74
|
+
* signal. Session ids for which at least one currently-subscribed ws has
|
|
75
|
+
* NOT yet been sent the session's finalized tail (its per-ws
|
|
76
|
+
* `lastDeliveredSignature` is empty / does not match the session's current
|
|
77
|
+
* tail). The daemon computes this from its live ws-subscription map; the
|
|
78
|
+
* classifier keeps every such session hot-for-delivery INDEPENDENT of the
|
|
79
|
+
* seen/unread badge, so a session whose unread badge cleared (or a fresh
|
|
80
|
+
* browser that re-subscribed after the single completion flush already
|
|
81
|
+
* fired) still gets its assistant-final tail delivered. Bounded because a
|
|
82
|
+
* subscriber's `lastDeliveredSignature` converges to the current tail the
|
|
83
|
+
* moment it is actually sent, dropping the session out of this set. When
|
|
84
|
+
* provided, this set is the guaranteed-delivery driver and the
|
|
85
|
+
* `deliveredCompletionTailAt` recency-watermark path is bypassed for these
|
|
86
|
+
* sessions (it stays only as the legacy opt-in for callers that don't pass
|
|
87
|
+
* a per-subscription set).
|
|
88
|
+
*/
|
|
89
|
+
underDeliveredSessionIds?: ReadonlySet<string>;
|
|
63
90
|
} = {},
|
|
64
|
-
): { active: Set<string>; finalizing: Set<string> } {
|
|
91
|
+
): { active: Set<string>; finalizing: Set<string>; guaranteedDelivery: Set<string> } {
|
|
65
92
|
const now = options.now ?? Date.now();
|
|
66
93
|
const recentMessageGraceMs = Math.max(
|
|
67
94
|
0,
|
|
@@ -71,8 +98,19 @@ export function classifyHotChatSessionsForSubscriptionFlush(
|
|
|
71
98
|
);
|
|
72
99
|
const activeStatuses = options.activeStatuses ?? DEFAULT_ACTIVE_CHAT_POLL_STATUSES;
|
|
73
100
|
const activeSessionIds = options.activeSessionIds ?? new Set<string>();
|
|
101
|
+
// The guaranteed-delivery path is opt-in: only callers that provide a
|
|
102
|
+
// delivered-watermark map (so they can BOUND re-pushes) participate. Callers
|
|
103
|
+
// without it keep the original recency-window-only behavior and never enter
|
|
104
|
+
// the completed-but-unseen keep-hot branch (which would otherwise thrash by
|
|
105
|
+
// re-pushing every tick with no delivered record to stop it).
|
|
106
|
+
const deliveredCompletionTailAt = options.deliveredCompletionTailAt ?? null;
|
|
107
|
+
const underDeliveredSessionIds = options.underDeliveredSessionIds ?? null;
|
|
74
108
|
const active = new Set<string>();
|
|
75
109
|
const excluded = new Set<string>();
|
|
110
|
+
// Sessions kept hot purely because they finalized late (outside the 8s
|
|
111
|
+
// window) and have not been delivered yet. The caller records these so the
|
|
112
|
+
// next classification knows their tail is now delivered.
|
|
113
|
+
const guaranteedDelivery = new Set<string>();
|
|
76
114
|
|
|
77
115
|
for (const session of sessions) {
|
|
78
116
|
const sessionId = typeof session?.id === 'string' ? session.id : '';
|
|
@@ -104,6 +142,49 @@ export function classifyHotChatSessionsForSubscriptionFlush(
|
|
|
104
142
|
|
|
105
143
|
if (activeStatuses.has(status) || shouldKeepRecentTailHot) {
|
|
106
144
|
active.add(sessionId);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// (D8) PER-SUBSCRIPTION ACK-BASED guaranteed delivery — the authoritative
|
|
149
|
+
// path. If ANY currently-subscribed ws for this session still lacks the
|
|
150
|
+
// finalized tail (empty / mismatched per-ws lastDeliveredSignature), keep
|
|
151
|
+
// the session hot-for-delivery REGARDLESS of the seen/unread badge. This is
|
|
152
|
+
// the fix for the agy coordinator stranded user-only: the single completion
|
|
153
|
+
// flush was marked "delivered" on FLUSH-FIRE (not browser-APPLIED), and
|
|
154
|
+
// re-delivery was gated on completedUnseen — so once the unread badge
|
|
155
|
+
// cleared, a subscriber that never actually applied the assistant tail (D6
|
|
156
|
+
// shrink-defer drop, or a fresh browser that re-subscribed after the flush
|
|
157
|
+
// instant) was permanently dropped and stayed user-only forever. Gating on
|
|
158
|
+
// "does a live subscriber still lack the tail" instead of the badge makes it
|
|
159
|
+
// re-arm for exactly those subscribers, and it self-bounds: the moment a
|
|
160
|
+
// subscriber is actually sent the tail its signature matches and it leaves
|
|
161
|
+
// this set (a same-tail re-flush is a cheap no-op via prepareSessionChatTailUpdate).
|
|
162
|
+
if (underDeliveredSessionIds && underDeliveredSessionIds.has(sessionId)) {
|
|
163
|
+
active.add(sessionId);
|
|
164
|
+
guaranteedDelivery.add(sessionId);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Legacy recency-watermark guaranteed-delivery path (opt-in via
|
|
169
|
+
// deliveredCompletionTailAt): a completed-but-unseen session whose native
|
|
170
|
+
// tail finalized AFTER the 8s window (common for multi-turn MAGI
|
|
171
|
+
// coordinators — the native-history tail lags the last PTY echo). Kept for
|
|
172
|
+
// callers that do not supply a per-subscription ACK set. When the
|
|
173
|
+
// per-subscription set is provided it is authoritative and this path is not
|
|
174
|
+
// consulted (the `continue` above already handled under-delivered sessions;
|
|
175
|
+
// a session NOT in the set is genuinely delivered, so it must not be revived
|
|
176
|
+
// here on the stale badge alone).
|
|
177
|
+
const completedUnseen = unread || inboxBucket === 'task_complete';
|
|
178
|
+
if (!underDeliveredSessionIds && deliveredCompletionTailAt && completedUnseen) {
|
|
179
|
+
const delivered = deliveredCompletionTailAt.get(sessionId) ?? 0;
|
|
180
|
+
// Not yet delivered for this turn (no record, or a newer tail arrived
|
|
181
|
+
// since the last delivery). lastMessageAt===0 (unknown ts) still counts
|
|
182
|
+
// as undelivered so the tail is not silently dropped.
|
|
183
|
+
const alreadyDelivered = delivered > 0 && lastMessageAt > 0 && delivered >= lastMessageAt;
|
|
184
|
+
if (!alreadyDelivered) {
|
|
185
|
+
active.add(sessionId);
|
|
186
|
+
guaranteedDelivery.add(sessionId);
|
|
187
|
+
}
|
|
107
188
|
}
|
|
108
189
|
}
|
|
109
190
|
|
|
@@ -111,5 +192,39 @@ export function classifyHotChatSessionsForSubscriptionFlush(
|
|
|
111
192
|
Array.from(previousHotSessionIds).filter((sessionId) => !active.has(sessionId) && !excluded.has(sessionId)),
|
|
112
193
|
);
|
|
113
194
|
|
|
114
|
-
return { active, finalizing };
|
|
195
|
+
return { active, finalizing, guaranteedDelivery };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detect sessions that just transitioned from an active/generating state into
|
|
200
|
+
* a settled/completed-but-unseen state, so their finalized completion tail can
|
|
201
|
+
* be flushed exactly once regardless of the recency window. Pure: the caller
|
|
202
|
+
* owns the `previousStatus` map and updates it from the returned `nextStatus`.
|
|
203
|
+
*/
|
|
204
|
+
export function detectNewlySettledCompletedSessions(
|
|
205
|
+
sessions: HotChatSessionLike[],
|
|
206
|
+
previousStatus: ReadonlyMap<string, string>,
|
|
207
|
+
options: { activeStatuses?: ReadonlySet<string> } = {},
|
|
208
|
+
): { settled: Set<string>; nextStatus: Map<string, string> } {
|
|
209
|
+
const activeStatuses = options.activeStatuses ?? DEFAULT_ACTIVE_CHAT_POLL_STATUSES;
|
|
210
|
+
const settled = new Set<string>();
|
|
211
|
+
const nextStatus = new Map<string, string>();
|
|
212
|
+
|
|
213
|
+
for (const session of sessions) {
|
|
214
|
+
const sessionId = typeof session?.id === 'string' ? session.id : '';
|
|
215
|
+
if (!sessionId) continue;
|
|
216
|
+
const status = String(session?.status || '').toLowerCase();
|
|
217
|
+
const prevStatus = previousStatus.get(sessionId);
|
|
218
|
+
nextStatus.set(sessionId, status);
|
|
219
|
+
|
|
220
|
+
const wasActive = prevStatus !== undefined && activeStatuses.has(prevStatus);
|
|
221
|
+
const isSettledNow = !activeStatuses.has(status);
|
|
222
|
+
const inboxBucket = String(session?.inboxBucket || '').toLowerCase();
|
|
223
|
+
const completedUnseen = session?.unread === true || inboxBucket === 'task_complete';
|
|
224
|
+
if (wasActive && isSettledNow && completedUnseen) {
|
|
225
|
+
settled.add(sessionId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { settled, nextStatus };
|
|
115
230
|
}
|