@farmslot/protocol 0.78.0 → 0.79.0
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/CHANGELOG.md +5 -0
- package/dist/client-cache/cache.d.ts.map +1 -1
- package/dist/client-cache/cache.js +119 -6
- package/dist/client-cache/cache.js.map +1 -1
- package/dist/client-cache/command-method.d.ts +2 -2
- package/dist/client-cache/command-method.d.ts.map +1 -1
- package/dist/client-cache/command-method.js +2 -2
- package/dist/client-cache/command-method.js.map +1 -1
- package/dist/client-cache/index.d.ts +1 -0
- package/dist/client-cache/index.d.ts.map +1 -1
- package/dist/client-cache/index.js +1 -0
- package/dist/client-cache/index.js.map +1 -1
- package/dist/client-cache/native-conversation.d.ts +74 -0
- package/dist/client-cache/native-conversation.d.ts.map +1 -0
- package/dist/client-cache/native-conversation.js +662 -0
- package/dist/client-cache/native-conversation.js.map +1 -0
- package/dist/client-cache/native-history.d.ts.map +1 -1
- package/dist/client-cache/native-history.js +35 -9
- package/dist/client-cache/native-history.js.map +1 -1
- package/dist/client-cache/registry.d.ts +4 -4
- package/dist/client-cache/registry.js +5 -5
- package/dist/client-cache/registry.js.map +1 -1
- package/dist/client-cache/transport.d.ts +2 -0
- package/dist/client-cache/transport.d.ts.map +1 -1
- package/dist/client-cache/transport.js +1 -0
- package/dist/client-cache/transport.js.map +1 -1
- package/dist/client-cache/types.d.ts +6 -0
- package/dist/client-cache/types.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/peer-versions.d.ts +15 -0
- package/dist/peer-versions.d.ts.map +1 -0
- package/dist/peer-versions.js +69 -0
- package/dist/peer-versions.js.map +1 -0
- package/dist/performance/ranking.d.ts.map +1 -1
- package/dist/performance/ranking.js +1 -0
- package/dist/performance/ranking.js.map +1 -1
- package/dist/performance/types.d.ts +1 -0
- package/dist/performance/types.d.ts.map +1 -1
- package/dist/performance/types.js +1 -0
- package/dist/performance/types.js.map +1 -1
- package/dist/rpc/auth.d.ts +1 -0
- package/dist/rpc/auth.d.ts.map +1 -1
- package/dist/rpc/auth.js.map +1 -1
- package/dist/rpc/chat.d.ts +1 -3
- package/dist/rpc/chat.d.ts.map +1 -1
- package/dist/rpc/chat.js +1 -3
- package/dist/rpc/chat.js.map +1 -1
- package/dist/rpc/copilot-runtime.d.ts +4 -5
- package/dist/rpc/copilot-runtime.d.ts.map +1 -1
- package/dist/rpc/copilot-runtime.js.map +1 -1
- package/dist/rpc/fs.d.ts +2 -0
- package/dist/rpc/fs.d.ts.map +1 -1
- package/dist/rpc/native-session.d.ts +85 -0
- package/dist/rpc/native-session.d.ts.map +1 -1
- package/dist/rpc/native-session.js +8 -0
- package/dist/rpc/native-session.js.map +1 -1
- package/dist/rpc/nodes.d.ts +7 -2
- package/dist/rpc/nodes.d.ts.map +1 -1
- package/dist/rpc/nodes.js.map +1 -1
- package/dist/rpc/registry.d.ts +3 -3
- package/dist/rpc/registry.js +3 -3
- package/dist/rpc/registry.js.map +1 -1
- package/dist/rpc/worker-session-history.d.ts +1 -0
- package/dist/rpc/worker-session-history.d.ts.map +1 -1
- package/dist/surfaces/view-links.d.ts.map +1 -1
- package/dist/surfaces/view-links.js +1 -0
- package/dist/surfaces/view-links.js.map +1 -1
- package/dist/surfaces/view-state.js +1 -1
- package/dist/surfaces/view-state.js.map +1 -1
- package/dist/transport/events.d.ts +4 -2
- package/dist/transport/events.d.ts.map +1 -1
- package/dist/transport/events.js +1 -1
- package/dist/transport/events.js.map +1 -1
- package/dist/transport/observations.d.ts +1 -0
- package/dist/transport/observations.d.ts.map +1 -1
- package/dist/version.d.ts +3 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +6 -6
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { afterForegroundPaint } from '../performance/paint.js';
|
|
2
|
+
import { NATIVE_CONVERSATION_LIMITS, } from '../rpc/native-session.js';
|
|
3
|
+
import { Methods } from '../rpc/registry.js';
|
|
4
|
+
import { canonicalResourceJson } from './keys.js';
|
|
5
|
+
import { storedNativeHistoryMatches } from './native-history.js';
|
|
6
|
+
import { resourceQuery } from './registry.js';
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const bytes = (value) => encoder.encode(JSON.stringify(value)).byteLength;
|
|
9
|
+
const MAX_PROJECTION_BYTES = NATIVE_CONVERSATION_LIMITS.projectionBytes;
|
|
10
|
+
const MAX_PENDING_BYTES = 1024 * 1024;
|
|
11
|
+
export const NATIVE_CONVERSATION_STORAGE_PREFIX = 'native-conversation:';
|
|
12
|
+
export const nativeConversationStorageId = (target) => NATIVE_CONVERSATION_STORAGE_PREFIX +
|
|
13
|
+
canonicalResourceJson({ ...target, executionNodeId: target.executionNodeId ?? 'local' });
|
|
14
|
+
function sameCursorSource(a, b) {
|
|
15
|
+
if (!a || !b)
|
|
16
|
+
return false;
|
|
17
|
+
return (a.executionNodeId === b.executionNodeId &&
|
|
18
|
+
a.sessionId === b.sessionId &&
|
|
19
|
+
a.hostGeneration === b.hostGeneration &&
|
|
20
|
+
a.generation === b.generation &&
|
|
21
|
+
canonicalResourceJson(a.worker ?? null) === canonicalResourceJson(b.worker ?? null));
|
|
22
|
+
}
|
|
23
|
+
function snapshotMatches(target, snapshot, cached = false) {
|
|
24
|
+
if (!storedNativeHistoryMatches(target, snapshot))
|
|
25
|
+
return false;
|
|
26
|
+
const cursor = snapshot.streamCursor;
|
|
27
|
+
return !cursor
|
|
28
|
+
? cached
|
|
29
|
+
: cursor.sessionId === target.sessionId &&
|
|
30
|
+
cursor.executionNodeId === (target.executionNodeId ?? 'local') &&
|
|
31
|
+
canonicalResourceJson(cursor.worker ?? null) ===
|
|
32
|
+
canonicalResourceJson(target.worker ?? null);
|
|
33
|
+
}
|
|
34
|
+
/** One reducer for web, Companion and CLI. Input is normalized native events, never runner text. */
|
|
35
|
+
export function reduceNativeConversation(previous, update) {
|
|
36
|
+
if (update.kind === 'reset' || (!previous && update.kind === 'replay')) {
|
|
37
|
+
const snapshot = update.kind === 'reset'
|
|
38
|
+
? update.snapshot
|
|
39
|
+
: {
|
|
40
|
+
...update.page,
|
|
41
|
+
historyBefore: (update.page.events[0]?.sequence ?? update.cursor.sequence + 1) - 1,
|
|
42
|
+
truncated: true,
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
...snapshot,
|
|
46
|
+
streamCursor: update.cursor,
|
|
47
|
+
retainedBytes: snapshot.events.reduce((total, event) => total + bytes(event), 0),
|
|
48
|
+
historyRead: { pageReadCount: 0, pageAfters: [] },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (!previous || !sameCursorSource(previous.streamCursor, update.cursor))
|
|
52
|
+
throw new Error('Conversation source changed without a reset');
|
|
53
|
+
if (update.cursor.sequence < previous.cursor)
|
|
54
|
+
return previous;
|
|
55
|
+
let cursor = previous.cursor;
|
|
56
|
+
const events = [...previous.events];
|
|
57
|
+
const omissions = previous.omissions?.map((item) => ({ ...item })) ?? [];
|
|
58
|
+
let retainedBytes = previous.retainedBytes;
|
|
59
|
+
let historyBefore = previous.historyBefore;
|
|
60
|
+
for (const event of update.page.events) {
|
|
61
|
+
if (event.sequence <= cursor)
|
|
62
|
+
continue;
|
|
63
|
+
if (event.sequence !== cursor + 1 || event.sessionId !== update.cursor.sessionId)
|
|
64
|
+
throw new Error('Conversation event gap');
|
|
65
|
+
cursor = event.sequence;
|
|
66
|
+
const last = events.at(-1);
|
|
67
|
+
if (event.type === 'text.delta' &&
|
|
68
|
+
last?.type === 'text.delta' &&
|
|
69
|
+
event.commandId === last.commandId &&
|
|
70
|
+
event.generation === last.generation) {
|
|
71
|
+
const added = bytes(event.text ?? '') - 2;
|
|
72
|
+
const omitted = omissions.find((item) => item.anchorSequence === last.sequence);
|
|
73
|
+
if (omitted)
|
|
74
|
+
omitted.through = event.sequence;
|
|
75
|
+
else if (retainedBytes + added > MAX_PROJECTION_BYTES) {
|
|
76
|
+
omissions.push({
|
|
77
|
+
anchorSequence: last.sequence,
|
|
78
|
+
after: event.sequence - 1,
|
|
79
|
+
through: event.sequence,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
events[events.length - 1] = { ...last, text: (last.text ?? '') + (event.text ?? '') };
|
|
84
|
+
retainedBytes += added;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
events.push(event);
|
|
89
|
+
retainedBytes += bytes(event);
|
|
90
|
+
}
|
|
91
|
+
while ((retainedBytes > MAX_PROJECTION_BYTES ||
|
|
92
|
+
events.length > NATIVE_CONVERSATION_LIMITS.projectionRows) &&
|
|
93
|
+
events.length > 1) {
|
|
94
|
+
const removed = events.shift();
|
|
95
|
+
retainedBytes -= bytes(removed);
|
|
96
|
+
historyBefore = events[0].sequence - 1;
|
|
97
|
+
const omission = omissions.findIndex((item) => item.anchorSequence === removed.sequence);
|
|
98
|
+
if (omission >= 0)
|
|
99
|
+
omissions.splice(omission, 1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (cursor !== update.cursor.sequence)
|
|
103
|
+
throw new Error('Conversation high-water gap');
|
|
104
|
+
const page = update.page;
|
|
105
|
+
const { metadataOmitted: _omitted, ...changes } = page;
|
|
106
|
+
return {
|
|
107
|
+
...previous,
|
|
108
|
+
...changes,
|
|
109
|
+
events,
|
|
110
|
+
hasMore: false,
|
|
111
|
+
retainedBytes,
|
|
112
|
+
cursor,
|
|
113
|
+
streamCursor: update.cursor,
|
|
114
|
+
omissions,
|
|
115
|
+
historyBefore,
|
|
116
|
+
metadataOmitted: page.metadataOmitted === null
|
|
117
|
+
? undefined
|
|
118
|
+
: (page.metadataOmitted ?? previous.metadataOmitted),
|
|
119
|
+
truncated: historyBefore > (page.scope?.startAfter ?? previous.scope?.startAfter ?? 0) ||
|
|
120
|
+
omissions.length > 0,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Scoped-store owner calls connectionChanged synchronously with its own authority fence. */
|
|
124
|
+
export function createNativeConversationCache(transport, storage, connection, options = {}) {
|
|
125
|
+
const metrics = options.performance;
|
|
126
|
+
const entries = new Map();
|
|
127
|
+
const byId = new Map();
|
|
128
|
+
let disposed = false;
|
|
129
|
+
let admitted = true;
|
|
130
|
+
let epoch = 0;
|
|
131
|
+
let pending = 0;
|
|
132
|
+
let early = [];
|
|
133
|
+
let earlyBytes = 0;
|
|
134
|
+
let overflowVersion = 0;
|
|
135
|
+
const notify = (entry) => {
|
|
136
|
+
for (const listener of entry.listeners)
|
|
137
|
+
listener(entry.state);
|
|
138
|
+
};
|
|
139
|
+
const entryFor = (target) => {
|
|
140
|
+
const key = nativeConversationStorageId(target);
|
|
141
|
+
let entry = entries.get(key);
|
|
142
|
+
if (!entry) {
|
|
143
|
+
entry = {
|
|
144
|
+
target,
|
|
145
|
+
state: { status: 'loading' },
|
|
146
|
+
listeners: new Set(),
|
|
147
|
+
ticket: 0,
|
|
148
|
+
opening: false,
|
|
149
|
+
hydrated: false,
|
|
150
|
+
resets: 0,
|
|
151
|
+
denialVersion: 0,
|
|
152
|
+
paints: new Set(),
|
|
153
|
+
};
|
|
154
|
+
entries.set(key, entry);
|
|
155
|
+
}
|
|
156
|
+
return entry;
|
|
157
|
+
};
|
|
158
|
+
const failure = (entry, error) => {
|
|
159
|
+
const code = error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
|
|
160
|
+
? error.code
|
|
161
|
+
: 'NATIVE_SOURCE_UNAVAILABLE';
|
|
162
|
+
const denied = ['AUTH_FORBIDDEN', 'AUTH_REQUIRED', 'NOT_FOUND'].includes(code);
|
|
163
|
+
entry.state = {
|
|
164
|
+
...(denied ? {} : entry.state),
|
|
165
|
+
status: entry.state.data && !denied ? 'stale' : 'error',
|
|
166
|
+
error: { code, message: error instanceof Error ? error.message : String(error) },
|
|
167
|
+
};
|
|
168
|
+
if (denied) {
|
|
169
|
+
entry.denialVersion++;
|
|
170
|
+
entry.hydrated = true;
|
|
171
|
+
void storage.forget(entry.target).catch((error) => storageFailed(entry, error));
|
|
172
|
+
}
|
|
173
|
+
notify(entry);
|
|
174
|
+
};
|
|
175
|
+
const storageFailed = (entry, error) => {
|
|
176
|
+
entry.state = {
|
|
177
|
+
...entry.state,
|
|
178
|
+
storageNotice: `Conversation cache failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
+
};
|
|
180
|
+
notify(entry);
|
|
181
|
+
};
|
|
182
|
+
const save = (entry) => entry.state.data
|
|
183
|
+
? storage.save(entry.target, entry.state.data).catch((error) => storageFailed(entry, error))
|
|
184
|
+
: Promise.resolve();
|
|
185
|
+
const detach = (entry) => {
|
|
186
|
+
entry.ticket++;
|
|
187
|
+
entry.opening = false;
|
|
188
|
+
for (const cancel of entry.paints)
|
|
189
|
+
cancel();
|
|
190
|
+
entry.paints.clear();
|
|
191
|
+
const { conversation: _authority, ...held } = entry.state;
|
|
192
|
+
entry.state = { ...held, status: held.data ? 'stale' : held.status };
|
|
193
|
+
notify(entry);
|
|
194
|
+
const detachedEpoch = epoch;
|
|
195
|
+
const detachedTicket = entry.ticket;
|
|
196
|
+
const id = entry.subscriptionId;
|
|
197
|
+
entry.subscriptionId = undefined;
|
|
198
|
+
if (![...entries.values()].some((value) => value.subscriptionId || value.opening)) {
|
|
199
|
+
stopDelta?.();
|
|
200
|
+
stopDelta = undefined;
|
|
201
|
+
}
|
|
202
|
+
if (id) {
|
|
203
|
+
byId.delete(id);
|
|
204
|
+
void transport?.unsubscribe(id).catch((error) => {
|
|
205
|
+
if (!disposed && epoch === detachedEpoch && entry.ticket === detachedTicket)
|
|
206
|
+
failure(entry, error);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
function apply(entry, update, envelope) {
|
|
211
|
+
const received = metrics?.clock();
|
|
212
|
+
const metricEpoch = metrics?.epoch;
|
|
213
|
+
if (update.kind === 'terminal') {
|
|
214
|
+
if (envelope)
|
|
215
|
+
options.onEnvelope?.(envelope);
|
|
216
|
+
else if (entry.subscriptionId)
|
|
217
|
+
options.onEnvelope?.({ ...update, subscriptionId: entry.subscriptionId });
|
|
218
|
+
detach(entry);
|
|
219
|
+
if (update.reason === 'resync-required') {
|
|
220
|
+
resync(entry);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (update.reason === 'source-unavailable') {
|
|
224
|
+
resync(entry, false);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
failure(entry, Object.assign(new Error(update.reason === 'access-denied'
|
|
228
|
+
? 'Conversation access was withdrawn'
|
|
229
|
+
: 'Conversation source unavailable'), {
|
|
230
|
+
code: update.reason === 'access-denied' ? 'AUTH_FORBIDDEN' : 'NATIVE_SOURCE_UNAVAILABLE',
|
|
231
|
+
}));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
let data;
|
|
235
|
+
try {
|
|
236
|
+
data = reduceNativeConversation(entry.state.data, update);
|
|
237
|
+
if (!snapshotMatches(entry.target, data))
|
|
238
|
+
throw new Error('Conversation target changed');
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
detach(entry);
|
|
242
|
+
resync(entry);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
const previousState = entry.state.data?.session.state;
|
|
246
|
+
entry.metricSource = 'network';
|
|
247
|
+
if (update.kind === 'delta' && update.page.events.length)
|
|
248
|
+
entry.resets = 0;
|
|
249
|
+
const history = entry.state.data && sameCursorSource(entry.state.data.streamCursor, data.streamCursor)
|
|
250
|
+
? entry.state.history
|
|
251
|
+
: undefined;
|
|
252
|
+
entry.state = {
|
|
253
|
+
status: 'ready',
|
|
254
|
+
data,
|
|
255
|
+
history,
|
|
256
|
+
observedAt: Date.now(),
|
|
257
|
+
conversation: data.streamCursor,
|
|
258
|
+
};
|
|
259
|
+
if (envelope)
|
|
260
|
+
options.onEnvelope?.(envelope);
|
|
261
|
+
notify(entry);
|
|
262
|
+
if (metrics &&
|
|
263
|
+
received !== undefined &&
|
|
264
|
+
update.kind === 'delta' &&
|
|
265
|
+
update.page.events.some((event) => event.type === 'text.delta') &&
|
|
266
|
+
connection().foreground !== false) {
|
|
267
|
+
let cancel;
|
|
268
|
+
cancel = (options.afterPaint ?? afterForegroundPaint)(() => {
|
|
269
|
+
if (cancel)
|
|
270
|
+
entry.paints.delete(cancel);
|
|
271
|
+
if (!disposed &&
|
|
272
|
+
entry.listeners.size &&
|
|
273
|
+
metrics.epoch === metricEpoch &&
|
|
274
|
+
connection().foreground !== false)
|
|
275
|
+
metrics.observe('farmslot_native_text_render_seconds', {
|
|
276
|
+
method: Methods.NATIVE_SESSION_SUBSCRIBE,
|
|
277
|
+
cache_source: 'network',
|
|
278
|
+
action_class: 'interactive-read',
|
|
279
|
+
}, metrics.clock() - received);
|
|
280
|
+
});
|
|
281
|
+
if (cancel)
|
|
282
|
+
entry.paints.add(cancel);
|
|
283
|
+
}
|
|
284
|
+
if (update.kind === 'reset' || data.session.state !== previousState)
|
|
285
|
+
void save(entry);
|
|
286
|
+
}
|
|
287
|
+
function resync(entry, reset = true) {
|
|
288
|
+
if (++entry.resets > 2) {
|
|
289
|
+
failure(entry, new Error('Conversation needs an explicit retry after repeated source gaps'));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
void open(entry, reset);
|
|
293
|
+
}
|
|
294
|
+
let stopDelta;
|
|
295
|
+
function receiveDelta(payload) {
|
|
296
|
+
const entry = byId.get(payload.subscriptionId);
|
|
297
|
+
if (entry) {
|
|
298
|
+
apply(entry, payload, payload);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (!pending)
|
|
302
|
+
return;
|
|
303
|
+
earlyBytes += bytes(payload);
|
|
304
|
+
if (earlyBytes > MAX_PENDING_BYTES) {
|
|
305
|
+
overflowVersion++;
|
|
306
|
+
earlyBytes = 0;
|
|
307
|
+
early = [];
|
|
308
|
+
}
|
|
309
|
+
else
|
|
310
|
+
early.push(payload);
|
|
311
|
+
}
|
|
312
|
+
function armDelta() {
|
|
313
|
+
stopDelta ??= transport?.onDelta(receiveDelta);
|
|
314
|
+
}
|
|
315
|
+
async function open(entry, reset = false) {
|
|
316
|
+
const current = connection();
|
|
317
|
+
if (disposed ||
|
|
318
|
+
!admitted ||
|
|
319
|
+
entry.opening ||
|
|
320
|
+
entry.subscriptionId ||
|
|
321
|
+
!entry.listeners.size ||
|
|
322
|
+
!current.connected ||
|
|
323
|
+
current.foreground === false)
|
|
324
|
+
return;
|
|
325
|
+
if (!transport || current.capabilities?.conversationStreaming !== 1) {
|
|
326
|
+
entry.state = {
|
|
327
|
+
...entry.state,
|
|
328
|
+
status: 'unsupported',
|
|
329
|
+
error: {
|
|
330
|
+
code: 'NATIVE_SOURCE_UNAVAILABLE',
|
|
331
|
+
message: 'Update the gateway, node and native host for conversation streaming',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
notify(entry);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
armDelta();
|
|
338
|
+
const ticket = ++entry.ticket;
|
|
339
|
+
const openedEpoch = epoch;
|
|
340
|
+
const openedOverflow = overflowVersion;
|
|
341
|
+
entry.opening = true;
|
|
342
|
+
pending++;
|
|
343
|
+
try {
|
|
344
|
+
metrics?.reread(Methods.NATIVE_SESSION_SUBSCRIBE, entry);
|
|
345
|
+
const result = await transport.subscribe({
|
|
346
|
+
...entry.target,
|
|
347
|
+
...(!reset && entry.state.data ? { after: entry.state.data.streamCursor } : {}),
|
|
348
|
+
});
|
|
349
|
+
if (disposed ||
|
|
350
|
+
epoch !== openedEpoch ||
|
|
351
|
+
ticket !== entry.ticket ||
|
|
352
|
+
!entry.listeners.size ||
|
|
353
|
+
!connection().connected ||
|
|
354
|
+
connection().foreground === false) {
|
|
355
|
+
await transport.unsubscribe(result.subscriptionId);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
entry.subscriptionId = result.subscriptionId;
|
|
359
|
+
byId.set(result.subscriptionId, entry);
|
|
360
|
+
const wasOverflow = overflowVersion !== openedOverflow;
|
|
361
|
+
const frames = early.filter((item) => item.subscriptionId === result.subscriptionId);
|
|
362
|
+
early = early.filter((item) => item.subscriptionId !== result.subscriptionId);
|
|
363
|
+
earlyBytes = early.reduce((total, item) => total + bytes(item), 0);
|
|
364
|
+
apply(entry, result, result);
|
|
365
|
+
if (wasOverflow)
|
|
366
|
+
apply(entry, { kind: 'terminal', reason: 'resync-required' });
|
|
367
|
+
else
|
|
368
|
+
for (const payload of frames)
|
|
369
|
+
if (entry.subscriptionId === result.subscriptionId)
|
|
370
|
+
apply(entry, payload, payload);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
if (epoch === openedEpoch && entry.ticket === ticket)
|
|
374
|
+
failure(entry, error);
|
|
375
|
+
}
|
|
376
|
+
finally {
|
|
377
|
+
if (entry.ticket === ticket)
|
|
378
|
+
entry.opening = false;
|
|
379
|
+
pending--;
|
|
380
|
+
if (!pending) {
|
|
381
|
+
early = [];
|
|
382
|
+
earlyBytes = 0;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function hydrate(entry) {
|
|
387
|
+
const ticket = epoch;
|
|
388
|
+
const denialVersion = entry.denialVersion;
|
|
389
|
+
try {
|
|
390
|
+
const saved = await storage.load(entry.target);
|
|
391
|
+
if (disposed || ticket !== epoch || denialVersion !== entry.denialVersion)
|
|
392
|
+
return;
|
|
393
|
+
if (!entry.state.data && saved && snapshotMatches(entry.target, saved, true)) {
|
|
394
|
+
entry.metricSource = 'stored';
|
|
395
|
+
entry.state = { status: 'stale', data: saved };
|
|
396
|
+
notify(entry);
|
|
397
|
+
}
|
|
398
|
+
entry.hydrated = true;
|
|
399
|
+
void open(entry);
|
|
400
|
+
}
|
|
401
|
+
catch (error) {
|
|
402
|
+
if (ticket !== epoch || disposed || denialVersion !== entry.denialVersion)
|
|
403
|
+
return;
|
|
404
|
+
storageFailed(entry, error);
|
|
405
|
+
entry.hydrated = true;
|
|
406
|
+
void open(entry);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const api = {
|
|
410
|
+
async readPage(target, after) {
|
|
411
|
+
const entry = entryFor(target);
|
|
412
|
+
if (entry.state.status !== 'ready' ||
|
|
413
|
+
!entry.subscriptionId ||
|
|
414
|
+
!transport?.readPage ||
|
|
415
|
+
!entry.state.data)
|
|
416
|
+
throw new Error('Wait for the current conversation before paging its journal');
|
|
417
|
+
const source = entry.state.data;
|
|
418
|
+
const ticket = entry.ticket, readingEpoch = epoch;
|
|
419
|
+
const request = { after, loading: true };
|
|
420
|
+
entry.state = { ...entry.state, history: request };
|
|
421
|
+
notify(entry);
|
|
422
|
+
const current = () => !disposed &&
|
|
423
|
+
entry.ticket === ticket &&
|
|
424
|
+
epoch === readingEpoch &&
|
|
425
|
+
entry.state.history === request;
|
|
426
|
+
try {
|
|
427
|
+
const page = await transport.readPage({ ...target, after, limit: 200 });
|
|
428
|
+
if (!current())
|
|
429
|
+
return;
|
|
430
|
+
if (!storedNativeHistoryMatches(target, page) ||
|
|
431
|
+
page.session.ownerPrincipalId !== source.session.ownerPrincipalId ||
|
|
432
|
+
page.session.generation !== source.session.generation)
|
|
433
|
+
throw new Error('Historical page source changed');
|
|
434
|
+
const data = entry.state.data === source && source.metadataOmitted?.pendingRequests
|
|
435
|
+
? {
|
|
436
|
+
...source,
|
|
437
|
+
pendingRequests: page.pendingRequests,
|
|
438
|
+
metadataOmitted: source.metadataOmitted.commands
|
|
439
|
+
? { commands: source.metadataOmitted.commands, pendingRequests: 0 }
|
|
440
|
+
: undefined,
|
|
441
|
+
}
|
|
442
|
+
: entry.state.data;
|
|
443
|
+
entry.state = { ...entry.state, data, history: { after, loading: false, page } };
|
|
444
|
+
notify(entry);
|
|
445
|
+
}
|
|
446
|
+
catch (error) {
|
|
447
|
+
if (current()) {
|
|
448
|
+
const code = error && typeof error === 'object' && 'code' in error ? error.code : undefined;
|
|
449
|
+
if (code === 'AUTH_FORBIDDEN' || code === 'AUTH_REQUIRED' || code === 'NOT_FOUND') {
|
|
450
|
+
detach(entry);
|
|
451
|
+
failure(entry, error);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
entry.state = {
|
|
455
|
+
...entry.state,
|
|
456
|
+
history: {
|
|
457
|
+
after,
|
|
458
|
+
loading: false,
|
|
459
|
+
error: error instanceof Error ? error.message : String(error),
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
notify(entry);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
dismissHistory(target) {
|
|
467
|
+
const entry = entryFor(target);
|
|
468
|
+
entry.state = { ...entry.state, history: undefined };
|
|
469
|
+
notify(entry);
|
|
470
|
+
},
|
|
471
|
+
observe(target, listener) {
|
|
472
|
+
if (disposed)
|
|
473
|
+
throw new Error('Conversation cache is disposed');
|
|
474
|
+
const entry = entryFor(target);
|
|
475
|
+
const held = entry.state.data !== undefined;
|
|
476
|
+
const started = metrics?.clock(), metricEpoch = metrics?.epoch;
|
|
477
|
+
let first = true, active = true;
|
|
478
|
+
let cancelPaint;
|
|
479
|
+
const receive = (state) => {
|
|
480
|
+
listener(state);
|
|
481
|
+
if (!first || !state.data || !metrics || started === undefined)
|
|
482
|
+
return;
|
|
483
|
+
first = false;
|
|
484
|
+
const source = held ? 'memory' : (entry.metricSource ?? 'unknown');
|
|
485
|
+
metrics.increment('farmslot_client_cache_reads_total', {
|
|
486
|
+
method: Methods.NATIVE_SESSION_SUBSCRIBE,
|
|
487
|
+
cache_source: source,
|
|
488
|
+
});
|
|
489
|
+
cancelPaint = (options.afterPaint ?? afterForegroundPaint)(() => {
|
|
490
|
+
if (cancelPaint)
|
|
491
|
+
entry.paints.delete(cancelPaint);
|
|
492
|
+
if (!active ||
|
|
493
|
+
disposed ||
|
|
494
|
+
metrics.epoch !== metricEpoch ||
|
|
495
|
+
connection().foreground === false)
|
|
496
|
+
return;
|
|
497
|
+
metrics.observe(source === 'network'
|
|
498
|
+
? 'farmslot_client_visible_read_seconds'
|
|
499
|
+
: 'farmslot_client_cached_paint_seconds', {
|
|
500
|
+
method: Methods.NATIVE_SESSION_SUBSCRIBE,
|
|
501
|
+
cache_source: source,
|
|
502
|
+
action_class: source === 'network' ? 'interactive-read' : 'cached-paint',
|
|
503
|
+
}, metrics.clock() - started);
|
|
504
|
+
});
|
|
505
|
+
if (cancelPaint)
|
|
506
|
+
entry.paints.add(cancelPaint);
|
|
507
|
+
else
|
|
508
|
+
metrics.increment('farmslot_client_unmeasured_paints_total', {
|
|
509
|
+
method: Methods.NATIVE_SESSION_SUBSCRIBE,
|
|
510
|
+
});
|
|
511
|
+
};
|
|
512
|
+
entry.listeners.add(receive);
|
|
513
|
+
receive(entry.state);
|
|
514
|
+
if (!entry.hydrated) {
|
|
515
|
+
if (!entry.hydrating)
|
|
516
|
+
entry.hydrating = hydrate(entry).finally(() => {
|
|
517
|
+
entry.hydrating = undefined;
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else
|
|
521
|
+
void open(entry);
|
|
522
|
+
return () => {
|
|
523
|
+
active = false;
|
|
524
|
+
cancelPaint?.();
|
|
525
|
+
if (cancelPaint)
|
|
526
|
+
entry.paints.delete(cancelPaint);
|
|
527
|
+
entry.listeners.delete(receive);
|
|
528
|
+
if (!entry.listeners.size) {
|
|
529
|
+
void save(entry);
|
|
530
|
+
detach(entry);
|
|
531
|
+
}
|
|
532
|
+
const inactive = [...entries].filter(([, value]) => !value.listeners.size);
|
|
533
|
+
for (const [key] of inactive.slice(0, Math.max(0, inactive.length - 10)))
|
|
534
|
+
entries.delete(key);
|
|
535
|
+
};
|
|
536
|
+
},
|
|
537
|
+
read(target) {
|
|
538
|
+
return entryFor(target).state;
|
|
539
|
+
},
|
|
540
|
+
retry(target) {
|
|
541
|
+
const entry = entryFor(target);
|
|
542
|
+
entry.resets = 0;
|
|
543
|
+
detach(entry);
|
|
544
|
+
void open(entry);
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
return {
|
|
548
|
+
api,
|
|
549
|
+
resourcesChanged(payload) {
|
|
550
|
+
for (const entry of entries.values()) {
|
|
551
|
+
if (!entry.listeners.size || entry.subscriptionId || entry.opening)
|
|
552
|
+
continue;
|
|
553
|
+
const relevant = payload.changes.some(({ key }) => entry.target.worker
|
|
554
|
+
? key.family === 'run' && key.runId === entry.target.worker.runId
|
|
555
|
+
: (key.family === 'copilot' && (entry.target.executionNodeId ?? 'local') === 'local') ||
|
|
556
|
+
(key.family === 'native-session' &&
|
|
557
|
+
('scope' in key ||
|
|
558
|
+
(key.nodeId === (entry.target.executionNodeId ?? 'local') &&
|
|
559
|
+
(!key.sessionId || key.sessionId === entry.target.sessionId)))));
|
|
560
|
+
if (relevant) {
|
|
561
|
+
entry.resets = 0;
|
|
562
|
+
void open(entry);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
},
|
|
566
|
+
connectionChanged(withdraw, resume = true) {
|
|
567
|
+
epoch++;
|
|
568
|
+
admitted = resume;
|
|
569
|
+
early = [];
|
|
570
|
+
earlyBytes = 0;
|
|
571
|
+
for (const entry of entries.values()) {
|
|
572
|
+
if (!withdraw)
|
|
573
|
+
void save(entry);
|
|
574
|
+
else
|
|
575
|
+
entry.state = { status: 'loading' };
|
|
576
|
+
detach(entry);
|
|
577
|
+
entry.state = withdraw ? { status: 'loading' } : { ...entry.state, status: 'stale' };
|
|
578
|
+
if (withdraw)
|
|
579
|
+
entry.hydrated = false;
|
|
580
|
+
notify(entry);
|
|
581
|
+
if (resume && entry.listeners.size) {
|
|
582
|
+
if (!entry.hydrated)
|
|
583
|
+
void hydrate(entry);
|
|
584
|
+
else
|
|
585
|
+
void open(entry);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
},
|
|
589
|
+
async dispose(withdraw) {
|
|
590
|
+
if (disposed)
|
|
591
|
+
return;
|
|
592
|
+
const saves = [...entries.values()].map((entry) => {
|
|
593
|
+
const saving = withdraw ? Promise.resolve() : save(entry);
|
|
594
|
+
if (withdraw)
|
|
595
|
+
entry.state = { status: 'loading' };
|
|
596
|
+
detach(entry);
|
|
597
|
+
return saving;
|
|
598
|
+
});
|
|
599
|
+
disposed = true;
|
|
600
|
+
epoch++;
|
|
601
|
+
stopDelta?.();
|
|
602
|
+
entries.clear();
|
|
603
|
+
byId.clear();
|
|
604
|
+
await Promise.all(saves);
|
|
605
|
+
},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/** Shared public entry; metadata resource observers never own the conversation body. */
|
|
609
|
+
export function observeNativeConversation(cache, target, listener) {
|
|
610
|
+
if (!cache.conversations) {
|
|
611
|
+
listener({
|
|
612
|
+
status: 'unsupported',
|
|
613
|
+
error: {
|
|
614
|
+
code: 'NATIVE_SOURCE_UNAVAILABLE',
|
|
615
|
+
message: 'Conversation streaming is unavailable',
|
|
616
|
+
},
|
|
617
|
+
});
|
|
618
|
+
return () => undefined;
|
|
619
|
+
}
|
|
620
|
+
return cache.conversations.observe(target, listener);
|
|
621
|
+
}
|
|
622
|
+
/** Immutable pre-native histories share the scoped store, without a mutable chat facade. */
|
|
623
|
+
export function observeLegacyConversation(cache, sessionId, listener) {
|
|
624
|
+
return cache.observe(resourceQuery(Methods.CHAT_ARCHIVE, { sessionId }), listener);
|
|
625
|
+
}
|
|
626
|
+
export function nativeConversationOmissionNotice(snapshot) {
|
|
627
|
+
if (snapshot?.metadataOmitted?.pendingRequests)
|
|
628
|
+
return 'Some request details need an explicit journal read. Input waits for complete request metadata.';
|
|
629
|
+
if (snapshot?.truncated || snapshot?.metadataOmitted?.commands)
|
|
630
|
+
return 'Additional output is available in the saved journal.';
|
|
631
|
+
return '';
|
|
632
|
+
}
|
|
633
|
+
/** Legacy completed-message rows keep their original identities. Never turn TUI output into native events. */
|
|
634
|
+
export function applyLegacyConversationDelta(previous, payload, selection) {
|
|
635
|
+
if ((selection.slotId && payload.slotId !== selection.slotId) ||
|
|
636
|
+
(selection.runId && payload.runId !== selection.runId))
|
|
637
|
+
return previous;
|
|
638
|
+
const retarget = payload.reset === true && !selection.runId && payload.runId !== previous.runId;
|
|
639
|
+
if (!retarget &&
|
|
640
|
+
(payload.runId !== previous.runId ||
|
|
641
|
+
payload.role !== previous.role ||
|
|
642
|
+
payload.contextId !== previous.contextId))
|
|
643
|
+
return previous;
|
|
644
|
+
const messages = payload.reset
|
|
645
|
+
? payload.messages
|
|
646
|
+
: [...previous.messages, ...payload.messages].slice(-300);
|
|
647
|
+
return {
|
|
648
|
+
...previous,
|
|
649
|
+
runId: payload.runId,
|
|
650
|
+
role: payload.role,
|
|
651
|
+
contextId: payload.contextId,
|
|
652
|
+
runner: payload.runner,
|
|
653
|
+
model: payload.model ?? previous.model,
|
|
654
|
+
runnerSessionPath: payload.runnerSessionPath ?? previous.runnerSessionPath,
|
|
655
|
+
source: payload.source,
|
|
656
|
+
messages,
|
|
657
|
+
cursor: payload.cursor,
|
|
658
|
+
degradedReason: payload.degradedReason,
|
|
659
|
+
generatedAt: payload.generatedAt,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=native-conversation.js.map
|