@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c3c7796382e28

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.
Files changed (186) hide show
  1. package/README.md +25 -12
  2. package/dist/adapter.d.ts +216 -74
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/adapter.js +1 -9
  5. package/dist/attached-model.d.ts +15 -0
  6. package/dist/attached-model.d.ts.map +1 -0
  7. package/dist/attached-model.js +64 -0
  8. package/dist/attempt.d.ts +16 -0
  9. package/dist/attempt.d.ts.map +1 -0
  10. package/dist/attempt.js +70 -0
  11. package/dist/binary.d.ts +18 -0
  12. package/dist/binary.d.ts.map +1 -0
  13. package/dist/binary.js +8 -0
  14. package/dist/bootstrap.d.ts +44 -7
  15. package/dist/bootstrap.d.ts.map +1 -1
  16. package/dist/bootstrap.js +224 -46
  17. package/dist/chain.d.ts +11 -0
  18. package/dist/chain.d.ts.map +1 -0
  19. package/dist/chain.js +64 -0
  20. package/dist/change-set.d.ts +9 -14
  21. package/dist/change-set.d.ts.map +1 -1
  22. package/dist/change-set.js +11 -60
  23. package/dist/change.d.ts +45 -0
  24. package/dist/change.d.ts.map +1 -1
  25. package/dist/change.js +34 -0
  26. package/dist/client.d.ts +37 -21
  27. package/dist/client.d.ts.map +1 -1
  28. package/dist/client.js +159 -35
  29. package/dist/collection.d.ts +14 -15
  30. package/dist/collection.d.ts.map +1 -1
  31. package/dist/collection.js +65 -63
  32. package/dist/compile.d.ts +16 -0
  33. package/dist/compile.d.ts.map +1 -0
  34. package/dist/compile.js +765 -0
  35. package/dist/connection.d.ts +67 -42
  36. package/dist/connection.d.ts.map +1 -1
  37. package/dist/connection.js +4006 -1085
  38. package/dist/conversation.d.ts +59 -21
  39. package/dist/conversation.d.ts.map +1 -1
  40. package/dist/conversation.js +786 -243
  41. package/dist/digest.d.ts +7 -0
  42. package/dist/digest.d.ts.map +1 -0
  43. package/dist/digest.js +113 -0
  44. package/dist/durability.d.ts +5 -1
  45. package/dist/durability.d.ts.map +1 -1
  46. package/dist/durability.js +5 -0
  47. package/dist/entry.d.ts +19 -0
  48. package/dist/entry.d.ts.map +1 -0
  49. package/dist/entry.js +151 -0
  50. package/dist/expectation.d.ts +21 -0
  51. package/dist/expectation.d.ts.map +1 -0
  52. package/dist/expectation.js +70 -0
  53. package/dist/failure.d.ts +13 -0
  54. package/dist/failure.d.ts.map +1 -0
  55. package/dist/failure.js +19 -0
  56. package/dist/fence.d.ts +6 -0
  57. package/dist/fence.d.ts.map +1 -0
  58. package/dist/fence.js +10 -0
  59. package/dist/file.d.ts +2 -19
  60. package/dist/file.d.ts.map +1 -1
  61. package/dist/file.js +7 -122
  62. package/dist/fulltext.d.ts +27 -0
  63. package/dist/fulltext.d.ts.map +1 -0
  64. package/dist/fulltext.js +368 -0
  65. package/dist/host.d.ts +21 -3
  66. package/dist/host.d.ts.map +1 -1
  67. package/dist/host.js +165 -72
  68. package/dist/identity.d.ts +4 -0
  69. package/dist/identity.d.ts.map +1 -1
  70. package/dist/identity.js +16 -1
  71. package/dist/index.d.ts +19 -10
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +8 -1
  74. package/dist/live.d.ts +22 -0
  75. package/dist/live.d.ts.map +1 -0
  76. package/dist/live.js +124 -0
  77. package/dist/media.d.ts +8 -0
  78. package/dist/media.d.ts.map +1 -0
  79. package/dist/media.js +208 -0
  80. package/dist/memory.d.ts +22 -8
  81. package/dist/memory.d.ts.map +1 -1
  82. package/dist/memory.js +207 -48
  83. package/dist/messages.d.ts +28 -0
  84. package/dist/messages.d.ts.map +1 -0
  85. package/dist/messages.js +90 -0
  86. package/dist/model.d.ts +23 -14
  87. package/dist/model.d.ts.map +1 -1
  88. package/dist/model.js +22 -4
  89. package/dist/name.d.ts +4 -0
  90. package/dist/name.d.ts.map +1 -0
  91. package/dist/name.js +122 -0
  92. package/dist/observation.d.ts +21 -1
  93. package/dist/observation.d.ts.map +1 -1
  94. package/dist/observation.js +128 -17
  95. package/dist/open.d.ts +2 -0
  96. package/dist/open.d.ts.map +1 -0
  97. package/dist/open.js +1 -0
  98. package/dist/opened.d.ts +22 -0
  99. package/dist/opened.d.ts.map +1 -0
  100. package/dist/opened.js +107 -0
  101. package/dist/outcome.d.ts +75 -21
  102. package/dist/outcome.d.ts.map +1 -1
  103. package/dist/outcome.js +81 -1
  104. package/dist/overlay.d.ts +15 -5
  105. package/dist/overlay.d.ts.map +1 -1
  106. package/dist/overlay.js +136 -180
  107. package/dist/paths.d.ts +0 -1
  108. package/dist/paths.d.ts.map +1 -1
  109. package/dist/paths.js +0 -16
  110. package/dist/query.d.ts +78 -7
  111. package/dist/query.d.ts.map +1 -1
  112. package/dist/query.js +126 -43
  113. package/dist/reach.d.ts +53 -0
  114. package/dist/reach.d.ts.map +1 -0
  115. package/dist/reach.js +448 -0
  116. package/dist/react.d.ts +43 -0
  117. package/dist/react.d.ts.map +1 -0
  118. package/dist/react.js +54 -0
  119. package/dist/readiness.d.ts +64 -0
  120. package/dist/readiness.d.ts.map +1 -0
  121. package/dist/readiness.js +33 -0
  122. package/dist/reads.d.ts +11 -0
  123. package/dist/reads.d.ts.map +1 -0
  124. package/dist/reads.js +100 -0
  125. package/dist/recovery.d.ts +25 -0
  126. package/dist/recovery.d.ts.map +1 -0
  127. package/dist/recovery.js +198 -0
  128. package/dist/refresh.d.ts +19 -7
  129. package/dist/refresh.d.ts.map +1 -1
  130. package/dist/refresh.js +125 -53
  131. package/dist/replica.d.ts +59 -33
  132. package/dist/replica.d.ts.map +1 -1
  133. package/dist/replica.js +324 -162
  134. package/dist/room-draft.d.ts +44 -9
  135. package/dist/room-draft.d.ts.map +1 -1
  136. package/dist/room-draft.js +191 -40
  137. package/dist/rows.d.ts +39 -34
  138. package/dist/rows.d.ts.map +1 -1
  139. package/dist/rows.js +195 -81
  140. package/dist/save.d.ts +72 -6
  141. package/dist/save.d.ts.map +1 -1
  142. package/dist/save.js +889 -179
  143. package/dist/search.d.ts +41 -29
  144. package/dist/search.d.ts.map +1 -1
  145. package/dist/search.js +37 -105
  146. package/dist/seed.d.ts +16 -0
  147. package/dist/seed.d.ts.map +1 -0
  148. package/dist/seed.js +75 -0
  149. package/dist/shared.d.ts +16 -0
  150. package/dist/shared.d.ts.map +1 -0
  151. package/dist/shared.js +431 -0
  152. package/dist/status.d.ts +6 -2
  153. package/dist/status.d.ts.map +1 -1
  154. package/dist/status.js +0 -8
  155. package/dist/store.d.ts +129 -70
  156. package/dist/store.d.ts.map +1 -1
  157. package/dist/store.js +373 -365
  158. package/dist/stored.d.ts +152 -0
  159. package/dist/stored.d.ts.map +1 -0
  160. package/dist/stored.js +571 -0
  161. package/dist/tables.d.ts +354 -0
  162. package/dist/tables.d.ts.map +1 -0
  163. package/dist/tables.js +624 -0
  164. package/dist/turn.d.ts +9 -0
  165. package/dist/turn.d.ts.map +1 -0
  166. package/dist/turn.js +75 -0
  167. package/dist/validation.d.ts +30 -0
  168. package/dist/validation.d.ts.map +1 -0
  169. package/dist/validation.js +162 -0
  170. package/dist/windows.d.ts +22 -0
  171. package/dist/windows.d.ts.map +1 -0
  172. package/dist/windows.js +89 -0
  173. package/dist/wire.d.ts +194 -19
  174. package/dist/wire.d.ts.map +1 -1
  175. package/dist/wire.js +90 -67
  176. package/dist/words.d.ts +19 -0
  177. package/dist/words.d.ts.map +1 -0
  178. package/dist/words.js +150 -0
  179. package/package.json +33 -10
  180. package/AGENTS.md +0 -210
  181. package/dist/attachment.d.ts +0 -3
  182. package/dist/attachment.d.ts.map +0 -1
  183. package/dist/attachment.js +0 -8
  184. package/dist/read-side.d.ts +0 -14
  185. package/dist/read-side.d.ts.map +0 -1
  186. package/dist/read-side.js +0 -371
@@ -1,26 +1,128 @@
1
1
  import { conversationChangeSet } from './change-set.js';
2
- import { declaredFile } from './file.js';
2
+ import { runtimeError } from './failure.js';
3
+ import { refusedForSpace } from './durability.js';
4
+ import { changeBatchIdentity, chatDraftRevision } from './identity.js';
5
+ import { OFFLINE } from './outcome.js';
6
+ import { capped, cursorOf, decodedCursor, FROM_START, messageOrder, precedes } from './windows.js';
3
7
  const QUIET = {
4
- growingText: null,
5
- plan: null,
6
- workingNote: null,
8
+ turn: null,
7
9
  questions: [],
8
10
  process: null,
9
11
  commands: [],
10
12
  options: [],
13
+ starting: null,
14
+ link: null,
15
+ deliveryUnknown: [],
11
16
  };
12
- function ordered(left, right) {
13
- if (left.position !== right.position)
14
- return left.position < right.position ? -1 : 1;
15
- if (left.identity !== right.identity)
16
- return left.identity < right.identity ? -1 : 1;
17
- return 0;
17
+ const FACTS = [
18
+ 'questions',
19
+ 'process',
20
+ 'commands',
21
+ 'options',
22
+ 'starting',
23
+ 'link',
24
+ 'deliveryUnknown',
25
+ ];
26
+ export function unheldView(conversation) {
27
+ return {
28
+ conversation,
29
+ messages: [],
30
+ turn: null,
31
+ questions: [],
32
+ process: null,
33
+ commands: [],
34
+ options: [],
35
+ starting: null,
36
+ link: null,
37
+ deliveryUnknown: [],
38
+ chatDraft: null,
39
+ };
40
+ }
41
+ export const SESSION_STARTED = 'Session started';
42
+ const NEW_CHAT = 'new:';
43
+ export const HISTORY_CONVERSATIONS = 64;
44
+ export function newChat(agent) {
45
+ return `${NEW_CHAT}${agent}`;
46
+ }
47
+ function creationOf(input) {
48
+ if (typeof input !== 'object' || input === null)
49
+ return null;
50
+ const { agent_id: agent, operation_id: operation, replaces, } = input;
51
+ return typeof agent === 'string' && typeof operation === 'string' && replaces === undefined
52
+ ? { agent, operation }
53
+ : null;
54
+ }
55
+ function startedTurn(turn, operation) {
56
+ if (operation.op === 'begin')
57
+ return { turn, started: operation.at, parts: [], context: null };
58
+ if (operation.op === 'context') {
59
+ return {
60
+ turn,
61
+ started: null,
62
+ parts: [],
63
+ context: { used: operation.used, window: operation.window },
64
+ };
65
+ }
66
+ return operation.op === 'start' && operation.index === 0
67
+ ? { turn, started: null, parts: [structuredClone(operation.part)], context: null }
68
+ : null;
69
+ }
70
+ function changedFields(part, operation) {
71
+ if (operation.op === 'append') {
72
+ return { text: `${String(part.text ?? '')}${operation.text}` };
73
+ }
74
+ if (operation.op === 'status') {
75
+ return { status: operation.status, result: structuredClone(operation.result) };
76
+ }
77
+ if (operation.op === 'count')
78
+ return { count: operation.count };
79
+ return { entries: structuredClone(operation.entries) };
80
+ }
81
+ function appliedTurn(held, operation) {
82
+ if (operation.op === 'begin')
83
+ return { ...held, started: operation.at };
84
+ if (operation.op === 'context') {
85
+ return { ...held, context: { used: operation.used, window: operation.window } };
86
+ }
87
+ if (operation.op === 'start') {
88
+ if (operation.index !== 0 && operation.index !== held.parts.length)
89
+ return null;
90
+ return {
91
+ ...held,
92
+ parts: [...held.parts.slice(0, operation.index), structuredClone(operation.part)],
93
+ };
94
+ }
95
+ const part = held.parts[operation.index];
96
+ if (typeof part !== 'object' || part === null)
97
+ return null;
98
+ const changed = { ...part, ...changedFields(part, operation) };
99
+ return {
100
+ ...held,
101
+ parts: held.parts.map((kept, at) => (at === operation.index ? changed : kept)),
102
+ };
18
103
  }
19
- function merged(held, arriving) {
104
+ function windowOf(held, arriving, limit) {
20
105
  const byIdentity = new Map(held.map((message) => [message.identity, message]));
21
106
  for (const message of arriving)
22
107
  byIdentity.set(message.identity, message);
23
- return [...byIdentity.values()].sort(ordered);
108
+ const next = [...byIdentity.values()].sort(messageOrder);
109
+ const start = Math.max(0, next.length - limit);
110
+ return { messages: next.slice(start), dropped: start > 0 };
111
+ }
112
+ function frameOf(asked, messages, more, total) {
113
+ const oldest = messages[0];
114
+ const next = !more
115
+ ? undefined
116
+ : oldest === undefined
117
+ ? (asked.after ?? FROM_START)
118
+ : cursorOf([oldest.position, oldest.identity]);
119
+ return {
120
+ ...(next === undefined ? {} : { next }),
121
+ ...(total === null ? {} : { total }),
122
+ };
123
+ }
124
+ function newestOf(arriving, from) {
125
+ return arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from);
24
126
  }
25
127
  function acceptedMessage(value) {
26
128
  if (typeof value !== 'object' || value === null)
@@ -79,90 +181,299 @@ function historyPages(result) {
79
181
  }
80
182
  return pages;
81
183
  }
82
- function turnOf(result) {
184
+ function answered(result, field) {
83
185
  if (result.kind !== 'action')
84
186
  return null;
85
187
  const answer = result.result;
86
188
  if (typeof answer !== 'object' || answer === null)
87
189
  return null;
88
- const record = answer;
89
- return typeof record.turn === 'string' ? record.turn : null;
190
+ const value = answer[field];
191
+ return typeof value === 'string' ? value : null;
90
192
  }
91
- function limitOf(options) {
92
- const { limit } = options;
93
- if (limit === undefined)
94
- return undefined;
95
- if (!Number.isInteger(limit) || limit < 0) {
96
- throw new Error(`conversation limit must be a non-negative integer, got ${String(limit)}`);
97
- }
98
- return limit;
193
+ function beforeOf(after) {
194
+ if (after === undefined)
195
+ return null;
196
+ const decoded = typeof after === 'string' ? decodedCursor(after) : null;
197
+ if (decoded?.length === 0)
198
+ return null;
199
+ const [position, identity] = decoded ?? [];
200
+ if (decoded?.length !== 2 || typeof position !== 'string' || typeof identity !== 'string') {
201
+ throw runtimeError('invalid-after', `conversation after must be the next an earlier view returned, got ${String(after)}`);
202
+ }
203
+ return [position, identity];
204
+ }
205
+ function askedOf(options) {
206
+ const { limit, after, total } = options;
207
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 0)) {
208
+ throw runtimeError('invalid-limit', `conversation limit must be a non-negative integer, got ${String(limit)}`);
209
+ }
210
+ return { limit: capped(limit), after, before: beforeOf(after), total: total === true };
211
+ }
212
+ const EXPIRED = { kind: 'rejected', code: 'expired', fields: {}, retryable: false };
213
+ const DURABLE_WRITE = {
214
+ kind: 'rejected',
215
+ code: 'durable-write',
216
+ fields: {},
217
+ retryable: true,
218
+ };
219
+ function unwritten(draft) {
220
+ return draft.text === '';
221
+ }
222
+ function pending(draft) {
223
+ return {
224
+ ...(draft.submitted === undefined ? {} : { submitted: draft.submitted }),
225
+ ...(draft.moves === undefined ? {} : { moves: draft.moves }),
226
+ };
227
+ }
228
+ function empty(draft) {
229
+ return unwritten(draft) && draft.submitted === undefined && draft.moves === undefined;
230
+ }
231
+ function withSubmitted(draft, submitted) {
232
+ const { submitted: _held, ...unsent } = draft;
233
+ return submitted.length === 0 ? unsent : { ...unsent, submitted };
234
+ }
235
+ function prepended(draft, said) {
236
+ return { text: [said.text, draft.text].filter((one) => one !== '').join('\n') };
237
+ }
238
+ function returned(draft, submission, others) {
239
+ return withSubmitted({
240
+ conversation: draft.conversation,
241
+ ...prepended(draft, submission),
242
+ revision: chatDraftRevision(),
243
+ }, others);
99
244
  }
100
245
  export function createConversationStore(deps) {
101
- const messages = new Map();
246
+ const stored = new Set();
102
247
  const reads = new Map();
248
+ const partials = new Set();
103
249
  const drafts = new Map();
104
250
  const sending = new Map();
105
251
  const behind = new Set();
252
+ const catching = new Map();
106
253
  const gaps = new Map();
107
254
  const lives = new Map();
255
+ const unsent = new Map();
108
256
  const views = new Map();
257
+ const listing = new Map();
258
+ const creations = new Set();
259
+ const listWatchers = new Set();
260
+ let listKnown = false;
261
+ const agentWatchers = new Set();
262
+ let agentNames = new Map();
263
+ let retiredAgents = new Set();
264
+ let agentsRead = null;
109
265
  let replicated = new Set();
110
266
  let handedReplicated;
111
267
  function liveOf(conversation) {
112
268
  return lives.get(conversation) ?? QUIET;
113
269
  }
114
- function viewOf(conversation, limit) {
115
- const held = messages.get(conversation) ?? [];
116
- const shown = limit === undefined ? held : held.slice(Math.max(0, held.length - limit));
270
+ function viewOf(conversation, held, frame) {
117
271
  const draft = drafts.get(conversation);
118
272
  const facts = liveOf(conversation);
119
273
  return {
120
274
  conversation,
121
- messages: shown.map(({ identity, turn, text, message }) => ({
275
+ messages: held.map(({ identity, turn, text, message }) => ({
122
276
  identity,
123
277
  turn,
124
278
  text,
125
- message: structuredClone(message),
279
+ message,
126
280
  })),
127
- growingText: facts.growingText === null ? null : { ...facts.growingText },
128
- workingNote: facts.workingNote === null ? null : { ...facts.workingNote },
129
- plan: structuredClone(facts.plan?.plan ?? null),
281
+ ...frame,
282
+ turn: structuredClone(facts.turn),
130
283
  questions: structuredClone(facts.questions),
131
284
  process: facts.process === null ? null : { ...facts.process },
132
285
  commands: structuredClone(facts.commands),
133
286
  options: structuredClone(facts.options),
134
- chatDraft: draft === undefined || draft.turn !== null || sending.get(conversation)?.draft === draft
287
+ starting: facts.starting,
288
+ link: facts.link === null ? null : { ...facts.link },
289
+ deliveryUnknown: [...facts.deliveryUnknown],
290
+ chatDraft: draft === undefined || unwritten(draft)
135
291
  ? null
136
- : { text: draft.text, files: [...draft.files] },
292
+ : { text: draft.text, revision: draft.revision },
137
293
  };
138
294
  }
139
295
  function emit(conversation) {
140
296
  if (deps.closed())
141
297
  return;
142
298
  for (const view of views.values()) {
143
- if (view.conversation !== conversation)
299
+ if (view.conversation !== conversation || view.window === null)
144
300
  continue;
145
301
  const ready = [...view.watchers].filter((watcher) => watcher.ready);
146
302
  if (ready.length === 0)
147
303
  continue;
148
- const value = viewOf(conversation, view.limit);
304
+ const value = viewOf(conversation, view.window, sharedFrame(view, view.window));
149
305
  for (const watcher of ready)
150
306
  watcher.notify(value);
151
307
  }
152
308
  }
309
+ function sharedFrame(view, window) {
310
+ return frameOf(view.asked, window, view.more, view.total);
311
+ }
312
+ async function slide(local, conversation, arriving) {
313
+ let count;
314
+ for (const view of views.values()) {
315
+ if (view.conversation !== conversation || view.window === null)
316
+ continue;
317
+ if (arriving === null) {
318
+ view.window = [];
319
+ view.more = false;
320
+ if (view.total !== null)
321
+ view.total = 0;
322
+ continue;
323
+ }
324
+ const joining = arriving.filter((message) => precedes(message, view.asked.before));
325
+ const next = windowOf(view.window, joining, view.asked.limit);
326
+ view.window = next.messages;
327
+ view.more ||= next.dropped;
328
+ if (view.total !== null && arriving.length > 0) {
329
+ count ??= await local.messageCount(conversation);
330
+ view.total = count;
331
+ }
332
+ }
333
+ }
334
+ function stateOf(conversation) {
335
+ const facts = liveOf(conversation);
336
+ if (facts.questions.length > 0)
337
+ return 'asking';
338
+ return facts.turn === null ? 'idle' : 'working';
339
+ }
340
+ function entries() {
341
+ return [...listing.values()].map((entry) => ({
342
+ conversation: entry.conversation,
343
+ agent: { identity: entry.agent, name: agentNames.get(entry.agent) ?? null },
344
+ name: entry.name,
345
+ created: entry.created,
346
+ finished: entry.finished,
347
+ outcome: entry.outcome,
348
+ waiting: entry.waiting,
349
+ state: stateOf(entry.conversation),
350
+ started: lives.get(entry.conversation)?.turn?.started ?? null,
351
+ }));
352
+ }
353
+ function emitList() {
354
+ if (deps.closed() || listWatchers.size === 0)
355
+ return;
356
+ const value = entries();
357
+ for (const watcher of listWatchers)
358
+ watcher(value);
359
+ }
360
+ function agents() {
361
+ return [...agentNames]
362
+ .filter(([identity]) => !retiredAgents.has(identity))
363
+ .map(([identity, name]) => ({ identity, name }))
364
+ .sort((left, right) => left.name === right.name
365
+ ? left.identity.localeCompare(right.identity)
366
+ : left.name.localeCompare(right.name));
367
+ }
368
+ function emitAgents() {
369
+ if (deps.closed() || agentWatchers.size === 0)
370
+ return;
371
+ const value = agents();
372
+ for (const watcher of agentWatchers)
373
+ watcher(value);
374
+ }
375
+ function setLive(conversation, next) {
376
+ const before = stateOf(conversation);
377
+ const began = lives.get(conversation)?.turn?.started;
378
+ if (next === null)
379
+ lives.delete(conversation);
380
+ else
381
+ lives.set(conversation, next);
382
+ if (!listing.has(conversation))
383
+ return;
384
+ if (stateOf(conversation) !== before || next?.turn?.started !== began)
385
+ emitList();
386
+ }
387
+ async function readAgents() {
388
+ const declaration = deps.agents();
389
+ if (declaration === undefined)
390
+ return;
391
+ const names = new Map();
392
+ const retired = new Set();
393
+ let covered = null;
394
+ for (let after;;) {
395
+ const read = await deps.documents(declaration, after);
396
+ if ('pending' in read)
397
+ return;
398
+ const coverage = JSON.stringify(read.coverage);
399
+ if (covered !== null && coverage !== covered)
400
+ return readAgents();
401
+ covered = coverage;
402
+ for (const document of read.documents) {
403
+ const identity = document.fields?.[declaration.identity];
404
+ const name = document.fields?.[declaration.name];
405
+ if (typeof identity !== 'string' || typeof name !== 'string')
406
+ continue;
407
+ names.set(identity, name);
408
+ const ended = declaration.retired === undefined ? null : document.fields?.[declaration.retired];
409
+ if (ended !== undefined && ended !== null)
410
+ retired.add(identity);
411
+ }
412
+ if (read.next === undefined)
413
+ break;
414
+ after = read.next;
415
+ }
416
+ const named = JSON.stringify([...names]) !== JSON.stringify([...agentNames]);
417
+ if (!named && JSON.stringify([...retired]) === JSON.stringify([...retiredAgents]))
418
+ return;
419
+ agentNames = names;
420
+ retiredAgents = retired;
421
+ if (named)
422
+ emitList();
423
+ emitAgents();
424
+ const ended = [...drafts.keys()].filter(retiredChat);
425
+ if (ended.length > 0)
426
+ await remove(ended);
427
+ }
428
+ function retiredChat(conversation) {
429
+ return (conversation.startsWith(NEW_CHAT) && retiredAgents.has(conversation.slice(NEW_CHAT.length)));
430
+ }
431
+ function agentsReady() {
432
+ agentsRead ??= readAgents().catch(() => {
433
+ agentsRead = null;
434
+ });
435
+ return agentsRead;
436
+ }
153
437
  function live(conversation, change) {
154
- lives.set(conversation, { ...liveOf(conversation), ...change });
438
+ const owed = unsent.get(conversation);
439
+ for (const fact of FACTS.filter((one) => one in change))
440
+ owed?.delete(fact);
441
+ setLive(conversation, { ...liveOf(conversation), ...change });
155
442
  emit(conversation);
156
443
  }
444
+ function openingEnded() {
445
+ const owed = [...unsent];
446
+ unsent.clear();
447
+ for (const [conversation, facts] of owed) {
448
+ const held = lives.get(conversation);
449
+ if (held === undefined || facts.size === 0)
450
+ continue;
451
+ setLive(conversation, {
452
+ turn: held.turn,
453
+ questions: facts.has('questions') ? QUIET.questions : held.questions,
454
+ process: facts.has('process') ? QUIET.process : held.process,
455
+ commands: facts.has('commands') ? QUIET.commands : held.commands,
456
+ options: facts.has('options') ? QUIET.options : held.options,
457
+ starting: facts.has('starting') ? QUIET.starting : held.starting,
458
+ link: facts.has('link') ? QUIET.link : held.link,
459
+ deliveryUnknown: facts.has('deliveryUnknown')
460
+ ? QUIET.deliveryUnknown
461
+ : held.deliveryUnknown,
462
+ });
463
+ emit(conversation);
464
+ }
465
+ }
466
+ function relayed(conversation, turn, operation) {
467
+ const held = liveOf(conversation).turn;
468
+ const next = held?.turn === turn ? appliedTurn(held, operation) : startedTurn(turn, operation);
469
+ if (next !== null)
470
+ live(conversation, { turn: next });
471
+ }
157
472
  function sealTurns(conversation, turns) {
158
473
  const held = lives.get(conversation);
159
- if (held === undefined)
160
- return false;
161
- const growingText = held.growingText !== null && turns.has(held.growingText.turn) ? null : held.growingText;
162
- const plan = held.plan !== null && turns.has(held.plan.turn) ? null : held.plan;
163
- if (growingText === held.growingText && plan === held.plan)
474
+ if (held?.turn == null || !turns.has(held.turn.turn))
164
475
  return false;
165
- lives.set(conversation, { ...held, growingText, plan });
476
+ setLive(conversation, { ...held, turn: null });
166
477
  return true;
167
478
  }
168
479
  function handReplicated() {
@@ -176,47 +487,25 @@ export function createConversationStore(deps) {
176
487
  handedReplicated = named;
177
488
  stream.replicate(held);
178
489
  }
179
- async function documented() {
180
- const declaration = deps.cards();
181
- if (declaration === undefined)
182
- return [];
183
- const { documents } = await deps.documents(declaration.locator);
184
- return documents.filter((document) => typeof document.type === 'string');
185
- }
186
- function carded(documents) {
187
- const declaration = deps.cards();
188
- if (declaration === undefined)
189
- return [];
190
- const cards = [];
191
- for (const card of documents) {
192
- if (card.type !== declaration.type)
193
- continue;
194
- const identity = card.fields[declaration.identity];
195
- if (typeof identity === 'string')
196
- cards.push({ conversation: identity, card });
197
- }
198
- return cards;
199
- }
200
490
  async function remove(leaving) {
201
491
  await deps.durably(async (local) => {
202
492
  await local.dropConversations(leaving);
203
- const removed = [];
204
493
  const changed = [];
205
494
  for (const conversation of leaving) {
206
- for (const message of messages.get(conversation) ?? []) {
207
- removed.push({ conversation, identity: message.identity });
208
- }
209
- if (messages.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
495
+ if (stored.has(conversation) || drafts.has(conversation) || lives.has(conversation)) {
210
496
  changed.push(conversation);
211
497
  }
212
- messages.delete(conversation);
498
+ stored.delete(conversation);
213
499
  reads.delete(conversation);
500
+ partials.delete(conversation);
214
501
  drafts.delete(conversation);
215
502
  behind.delete(conversation);
216
- lives.delete(conversation);
503
+ unsent.delete(conversation);
504
+ setLive(conversation, null);
505
+ await slide(local, conversation, null);
217
506
  }
218
507
  if (changed.length > 0)
219
- deps.publish(conversationChangeSet(removed, changed));
508
+ deps.publish(conversationChangeSet(changed));
220
509
  });
221
510
  }
222
511
  function gap(conversation) {
@@ -231,32 +520,28 @@ export function createConversationStore(deps) {
231
520
  const advances = read !== undefined &&
232
521
  !behind.has(conversation) &&
233
522
  (read === null || message.position > read);
234
- const sealed = drafts.get(conversation)?.turn === message.turn;
523
+ await sealed(local, conversation, new Set([message.turn]));
235
524
  await local.keepMessages({
236
525
  conversation,
237
526
  messages: [message],
238
- ...(advances ? { read: message.position } : {}),
239
- sealed,
527
+ ...(advances ? { read: { at: message.position, partial: false } } : {}),
240
528
  });
241
- messages.set(conversation, merged(messages.get(conversation) ?? [], [message]));
529
+ stored.add(conversation);
242
530
  if (advances)
243
531
  reads.set(conversation, message.position);
244
- if (sealed)
245
- drafts.delete(conversation);
246
532
  sealTurns(conversation, new Set([message.turn]));
247
- deps.publish(conversationChangeSet([{ conversation, identity: message.identity }], [conversation]));
533
+ await slide(local, conversation, [message]);
534
+ deps.publish(conversationChangeSet([conversation]));
248
535
  });
249
536
  }
250
- async function readHistories(conversations) {
537
+ async function readHistories(conversations, ended) {
251
538
  const generations = new Map(conversations.map((one) => [one, gaps.get(one) ?? 0]));
252
- const starts = new Map(conversations.map((one) => [one, reads.get(one)]));
253
- const arriving = new Map();
254
- const complete = new Set();
255
- let asking = conversations.map((conversation) => ({
539
+ let waiting = conversations.map((conversation) => ({
256
540
  conversation,
257
- after: starts.get(conversation) ?? undefined,
541
+ after: reads.get(conversation) ?? undefined,
258
542
  }));
259
- while (asking.length > 0) {
543
+ while (waiting.length > 0) {
544
+ const asking = waiting.slice(0, HISTORY_CONVERSATIONS);
260
545
  const result = await deps.call('conversation.history', {
261
546
  conversations: asking.map(({ conversation, after }) => ({
262
547
  conversation,
@@ -266,124 +551,289 @@ export function createConversationStore(deps) {
266
551
  const pages = historyPages(result);
267
552
  if (pages === null)
268
553
  return result;
269
- const asked = new Set(asking.map(({ conversation }) => conversation));
554
+ const asked = new Map(asking.map(({ conversation, after }) => [conversation, after]));
270
555
  const following = [];
271
556
  for (const page of pages) {
272
557
  if (!asked.has(page.conversation))
273
558
  continue;
274
- arriving.set(page.conversation, [
275
- ...(arriving.get(page.conversation) ?? []),
276
- ...page.messages,
277
- ]);
278
- if (page.next === undefined)
279
- complete.add(page.conversation);
280
- else
559
+ await keepPage(page.conversation, page.messages, asked.get(page.conversation) ?? null, page.next, generations.get(page.conversation) ?? 0);
560
+ if (page.next !== undefined) {
281
561
  following.push({ conversation: page.conversation, after: page.next });
562
+ }
282
563
  }
283
- asking = following;
284
- }
285
- for (const [conversation, held] of arriving) {
286
- if (!complete.has(conversation))
287
- continue;
288
- await keepHistory(conversation, held, starts.get(conversation), generations.get(conversation) ?? 0);
564
+ for (const { conversation } of asking) {
565
+ if (!following.some((next) => next.conversation === conversation))
566
+ ended(conversation);
567
+ }
568
+ waiting = [...waiting.slice(HISTORY_CONVERSATIONS), ...following];
289
569
  }
290
570
  return null;
291
571
  }
292
- function keepHistory(conversation, arriving, from, generation) {
572
+ function caughtUp(conversations) {
573
+ const ends = new Map();
574
+ for (const conversation of conversations) {
575
+ const held = new Promise((end) => ends.set(conversation, end));
576
+ catching.set(conversation, held);
577
+ void held.then(() => {
578
+ if (catching.get(conversation) === held)
579
+ catching.delete(conversation);
580
+ });
581
+ }
582
+ const ended = (conversation) => ends.get(conversation)?.();
583
+ const read = readHistories(conversations, ended);
584
+ const endAll = () => {
585
+ for (const conversation of conversations)
586
+ ended(conversation);
587
+ };
588
+ read.then(endAll, endAll);
589
+ return read;
590
+ }
591
+ function keepPage(conversation, arriving, from, next, generation) {
293
592
  return deps.durably(async (local) => {
294
593
  if (!replicated.has(conversation))
295
594
  return;
296
- const newest = arriving.reduce((top, message) => (top === null || message.position > top ? message.position : top), from ?? null);
297
- const turn = drafts.get(conversation)?.turn;
298
- const sealed = arriving.some((message) => message.turn === turn);
299
- await local.keepMessages({ conversation, messages: arriving, read: newest, sealed });
300
- messages.set(conversation, merged(messages.get(conversation) ?? [], arriving));
301
- reads.set(conversation, newest);
302
- if (sealed)
303
- drafts.delete(conversation);
595
+ const whole = next === undefined;
596
+ const cursor = next ?? newestOf(arriving, from);
597
+ const partial = !whole && (partials.has(conversation) || !reads.has(conversation));
598
+ const sealing = await sealed(local, conversation, new Set(arriving.map((message) => message.turn)));
599
+ await local.keepMessages({ conversation, messages: arriving, read: { at: cursor, partial } });
600
+ stored.add(conversation);
601
+ reads.set(conversation, cursor);
602
+ if (partial)
603
+ partials.add(conversation);
604
+ else
605
+ partials.delete(conversation);
304
606
  sealTurns(conversation, new Set(arriving.map((message) => message.turn)));
305
- if ((gaps.get(conversation) ?? 0) === generation)
607
+ if (whole && (gaps.get(conversation) ?? 0) === generation)
306
608
  behind.delete(conversation);
307
- if (arriving.length > 0 || sealed) {
308
- deps.publish(conversationChangeSet(arriving.map((message) => ({ conversation, identity: message.identity })), [conversation]));
609
+ await slide(local, conversation, arriving);
610
+ if (arriving.length > 0 || sealing || whole) {
611
+ deps.publish(conversationChangeSet([conversation]));
309
612
  }
310
613
  });
311
614
  }
615
+ function heard(event) {
616
+ const { conversation } = event;
617
+ if (event.kind === 'gap') {
618
+ if (replicated.has(conversation))
619
+ gap(conversation);
620
+ if (lives.has(conversation))
621
+ unsent.set(conversation, new Set(FACTS));
622
+ return;
623
+ }
624
+ if (event.kind === 'turn') {
625
+ relayed(conversation, event.turn, event.operation);
626
+ return;
627
+ }
628
+ if (event.kind === 'questions') {
629
+ live(conversation, { questions: structuredClone(event.questions) });
630
+ return;
631
+ }
632
+ if (event.kind === 'process') {
633
+ live(conversation, { process: { running: event.running } });
634
+ return;
635
+ }
636
+ if (event.kind === 'commands') {
637
+ live(conversation, { commands: structuredClone(event.commands) });
638
+ return;
639
+ }
640
+ if (event.kind === 'options') {
641
+ live(conversation, { options: structuredClone(event.options) });
642
+ return;
643
+ }
644
+ if (event.kind === 'starting') {
645
+ live(conversation, { starting: event.step });
646
+ return;
647
+ }
648
+ if (event.kind === 'link') {
649
+ live(conversation, { link: { connected: event.connected } });
650
+ return;
651
+ }
652
+ if (event.kind === 'deliveries') {
653
+ live(conversation, { deliveryUnknown: [...event.unknown] });
654
+ return;
655
+ }
656
+ const arriving = acceptedMessage(event.message);
657
+ if (arriving === null)
658
+ return;
659
+ if (replicated.has(conversation)) {
660
+ if (deps.full()) {
661
+ gap(conversation);
662
+ return;
663
+ }
664
+ void keep(conversation, arriving).catch((error) => {
665
+ if (refusedForSpace(error))
666
+ gap(conversation);
667
+ });
668
+ return;
669
+ }
670
+ if (sealTurns(conversation, new Set([arriving.turn])))
671
+ emit(conversation);
672
+ }
673
+ async function kept(local, conversation, following) {
674
+ if (following === null || empty(following)) {
675
+ await local.dropChatDraft(conversation);
676
+ drafts.delete(conversation);
677
+ }
678
+ else {
679
+ await local.keepChatDrafts([following]);
680
+ drafts.set(conversation, following);
681
+ }
682
+ }
683
+ async function sealed(local, conversation, turns) {
684
+ const held = drafts.get(conversation);
685
+ const submitted = held?.submitted ?? [];
686
+ const left = submitted.filter((one) => one.turn === null || !turns.has(one.turn));
687
+ if (held === undefined || left.length === submitted.length)
688
+ return false;
689
+ await kept(local, conversation, withSubmitted(held, left));
690
+ return true;
691
+ }
692
+ function moved(agent, operation, to) {
693
+ const from = newChat(agent);
694
+ return deps.durably(async (local) => {
695
+ const held = drafts.get(from);
696
+ const earlier = (held?.moves ?? []).filter((one) => !deps.expired(one));
697
+ if (earlier.includes(operation))
698
+ return;
699
+ const blank = {
700
+ conversation: from,
701
+ text: '',
702
+ revision: chatDraftRevision(),
703
+ };
704
+ const { moves: _answered, ...unsent } = held ?? blank;
705
+ const taken = drafts.has(to) || empty(unsent) ? [] : [{ ...unsent, conversation: to }];
706
+ const left = { ...(taken.length === 0 ? unsent : blank), moves: [...earlier, operation] };
707
+ await local.keepChatDrafts([left, ...taken]);
708
+ drafts.set(from, left);
709
+ for (const one of taken)
710
+ drafts.set(to, one);
711
+ deps.publish(conversationChangeSet([from, to]));
712
+ });
713
+ }
312
714
  function withDraft(conversation, next) {
313
715
  return deps.durably(async (local) => {
314
- const following = next(drafts.get(conversation));
716
+ const following = await next(drafts.get(conversation), local);
315
717
  if (following === undefined)
316
718
  return;
317
- if (following === null) {
318
- await local.dropChatDraft(conversation);
319
- drafts.delete(conversation);
320
- }
321
- else {
322
- await local.keepChatDraft(following);
323
- drafts.set(conversation, following);
324
- }
325
- deps.publish(conversationChangeSet([], [conversation]));
719
+ await kept(local, conversation, following);
720
+ deps.publish(conversationChangeSet([conversation]));
721
+ });
722
+ }
723
+ async function submitted(conversation, operation, draft) {
724
+ let submission;
725
+ let earlier = false;
726
+ await withDraft(conversation, (current) => {
727
+ submission = current?.submitted?.find((one) => one.operation === operation);
728
+ earlier = submission !== undefined;
729
+ if (submission !== undefined)
730
+ return undefined;
731
+ const said = draft ?? current;
732
+ submission = { operation, text: said?.text ?? '', turn: null };
733
+ return {
734
+ conversation,
735
+ ...(draft === undefined || current === undefined
736
+ ? { text: '', revision: chatDraftRevision() }
737
+ : { text: current.text, revision: current.revision }),
738
+ submitted: [...(current?.submitted ?? []), submission],
739
+ };
326
740
  });
741
+ return { submission: submission, earlier };
742
+ }
743
+ function forgotten(conversation, submission) {
744
+ return withDraft(conversation, (current) => {
745
+ const held = current?.submitted ?? [];
746
+ if (current === undefined || !held.includes(submission))
747
+ return undefined;
748
+ return withSubmitted(current, held.filter((one) => one !== submission));
749
+ });
750
+ }
751
+ async function dispatched(conversation, submission) {
752
+ const dispatch = { answered: false };
753
+ sending.set(submission.operation, dispatch);
754
+ try {
755
+ const result = await invoke('conversation.send', conversation, {
756
+ text: submission.text,
757
+ operation_id: submission.operation,
758
+ });
759
+ dispatch.answered = result.kind !== 'transport-uncertainty';
760
+ if (result.kind === 'revoked-authorization')
761
+ return result;
762
+ if (result.kind !== 'action' && result.retryable) {
763
+ deps.unanswered();
764
+ return result;
765
+ }
766
+ const turn = answered(result, 'turn');
767
+ await withDraft(conversation, async (current, local) => {
768
+ const held = current?.submitted?.find((one) => one.operation === submission.operation);
769
+ if (current === undefined || held === undefined || held.turn !== null)
770
+ return undefined;
771
+ const others = current.submitted.filter((one) => one !== held);
772
+ if (result.kind !== 'action')
773
+ return returned(current, held, others);
774
+ const arrived = turn !== null && (await local.turnArrived(conversation, turn));
775
+ return withSubmitted(current, turn === null || arrived
776
+ ? others
777
+ : current.submitted.map((one) => (one === held ? { ...held, turn } : one)));
778
+ });
779
+ return result;
780
+ }
781
+ finally {
782
+ if (sending.get(submission.operation) === dispatch)
783
+ sending.delete(submission.operation);
784
+ }
327
785
  }
328
786
  function invoke(name, conversation, extra = {}) {
329
787
  return deps.call(name, { conversation, ...extra });
330
788
  }
331
789
  const surface = {
332
790
  async list() {
333
- const declaration = deps.cards();
334
- if (declaration === undefined)
335
- return [];
336
- const documents = await documented();
337
- const entries = carded(documents).map(({ conversation, card }) => ({
338
- conversation,
339
- card,
340
- agent: documents.find((document) => document.backlinks.some((backlink) => backlink.path === card.path && backlink.field === declaration.agent)) ?? null,
341
- }));
342
- entries.sort((left, right) => (left.conversation < right.conversation ? -1 : 1));
343
- return entries;
791
+ await agentsReady();
792
+ return entries();
344
793
  },
345
794
  async read(conversation, options = {}) {
346
- const limit = limitOf(options);
347
- return deps.durably(() => Promise.resolve(viewOf(conversation, limit)));
795
+ const asked = askedOf(options);
796
+ return deps.durably(async (local) => {
797
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
798
+ const total = asked.total ? await local.messageCount(conversation) : null;
799
+ return structuredClone(viewOf(conversation, messages, frameOf(asked, messages, more, total)));
800
+ });
348
801
  },
349
- create(input) {
350
- return deps.call('conversation.create', input);
802
+ async create(input) {
803
+ const result = await deps.call('conversation.create', input);
804
+ const conversation = answered(result, 'conversation');
805
+ if (conversation === null)
806
+ return result;
807
+ if (!listing.has(conversation))
808
+ creations.add(conversation);
809
+ const creation = creationOf(input);
810
+ if (creation !== null)
811
+ await moved(creation.agent, creation.operation, conversation);
812
+ return result;
351
813
  },
352
- async send(conversation) {
353
- const draft = drafts.get(conversation);
354
- const pending = draft === undefined ? undefined : { draft };
355
- if (pending !== undefined) {
356
- sending.set(conversation, pending);
357
- deps.publish(conversationChangeSet([], [conversation]));
358
- }
359
- try {
360
- const files = await Promise.all((draft?.files ?? []).map(async (file) => ({
361
- ...(await declaredFile(file)),
362
- blob: file.blob,
363
- })));
364
- const result = await invoke('conversation.send', conversation, {
365
- text: draft?.text ?? '',
366
- files,
367
- });
368
- const turn = turnOf(result);
369
- if (turn === null || draft === undefined)
370
- return result;
371
- await withDraft(conversation, (current) => {
372
- if (current !== draft)
373
- return undefined;
374
- const sealed = (messages.get(conversation) ?? []).some((message) => message.turn === turn);
375
- return sealed ? null : { ...draft, turn };
814
+ async send(conversation, operation = changeBatchIdentity(), draft) {
815
+ if (deps.full())
816
+ return DURABLE_WRITE;
817
+ if (deps.online()) {
818
+ const held = await submitted(conversation, operation, draft).catch((error) => {
819
+ if (refusedForSpace(error))
820
+ return null;
821
+ throw error;
376
822
  });
377
- return result;
823
+ if (held === null)
824
+ return DURABLE_WRITE;
825
+ const { submission, earlier } = held;
826
+ if (!earlier || !deps.expired(operation))
827
+ return dispatched(conversation, submission);
828
+ await forgotten(conversation, submission);
829
+ return EXPIRED;
378
830
  }
379
- finally {
380
- if (pending !== undefined && sending.get(conversation) === pending) {
381
- sending.delete(conversation);
382
- if (drafts.get(conversation) === draft) {
383
- deps.publish(conversationChangeSet([], [conversation]));
384
- }
385
- }
831
+ if (draft !== undefined) {
832
+ await withDraft(conversation, (current) => current?.submitted?.some((one) => one.operation === operation) === true
833
+ ? undefined
834
+ : returned(current ?? { conversation, text: '', revision: '' }, { operation, ...draft, turn: null }, current?.submitted ?? []));
386
835
  }
836
+ return OFFLINE;
387
837
  },
388
838
  answer(conversation, answer) {
389
839
  return invoke('conversation.answer', conversation, { answer });
@@ -398,7 +848,12 @@ export function createConversationStore(deps) {
398
848
  const result = await invoke('conversation.cancel', conversation, { turn });
399
849
  if (result.kind !== 'action')
400
850
  return result;
401
- await withDraft(conversation, (current) => current === undefined || current.turn !== turn ? undefined : { ...current, turn: null });
851
+ await withDraft(conversation, (current) => {
852
+ const held = current?.submitted?.find((one) => one.turn === turn);
853
+ if (current === undefined || held === undefined)
854
+ return undefined;
855
+ return returned(current, held, current.submitted.filter((one) => one !== held));
856
+ });
402
857
  return result;
403
858
  },
404
859
  delete(conversation) {
@@ -406,38 +861,55 @@ export function createConversationStore(deps) {
406
861
  },
407
862
  };
408
863
  const chatDraft = {
409
- write(conversation, draft) {
410
- return withDraft(conversation, () => ({
864
+ write(conversation, draft, revision = chatDraftRevision()) {
865
+ return withDraft(conversation, (current) => ({
411
866
  conversation,
412
867
  text: draft.text,
413
- files: [...draft.files],
414
- turn: null,
868
+ revision,
869
+ ...(current === undefined ? {} : pending(current)),
870
+ }));
871
+ },
872
+ prepend(conversation, draft) {
873
+ return withDraft(conversation, (current) => ({
874
+ conversation,
875
+ ...(current === undefined ? { text: draft.text } : prepended(current, draft)),
876
+ revision: chatDraftRevision(),
877
+ ...(current === undefined ? {} : pending(current)),
415
878
  }));
416
879
  },
417
880
  discard(conversation) {
418
- return withDraft(conversation, (current) => (current === undefined ? undefined : null));
881
+ return withDraft(conversation, (current) => current === undefined
882
+ ? undefined
883
+ : { conversation, text: '', revision: chatDraftRevision(), ...pending(current) });
419
884
  },
420
885
  };
421
886
  return {
422
887
  surface,
423
888
  chatDraft,
424
- hydrate(held, stored) {
425
- messages.clear();
889
+ hydrate(held, kept) {
890
+ stored.clear();
426
891
  reads.clear();
892
+ partials.clear();
427
893
  for (const entry of held) {
428
- messages.set(entry.conversation, [...entry.messages].sort(ordered));
894
+ stored.add(entry.conversation);
429
895
  if (entry.read !== undefined)
430
896
  reads.set(entry.conversation, entry.read);
897
+ if (entry.partial)
898
+ partials.add(entry.conversation);
431
899
  }
432
900
  drafts.clear();
433
- for (const record of stored)
901
+ for (const record of kept)
434
902
  drafts.set(record.conversation, record);
435
903
  },
436
904
  async reconcile() {
437
- const named = new Set(carded(await documented()).map((entry) => entry.conversation));
905
+ if (!listKnown)
906
+ return;
907
+ const named = new Set(listing.keys());
438
908
  const leaving = [
439
- ...new Set([...replicated, ...messages.keys(), ...reads.keys(), ...drafts.keys()]),
440
- ].filter((conversation) => !named.has(conversation));
909
+ ...new Set([...replicated, ...stored, ...reads.keys(), ...drafts.keys()]),
910
+ ].filter((conversation) => !named.has(conversation) &&
911
+ !creations.has(conversation) &&
912
+ (!conversation.startsWith(NEW_CHAT) || retiredChat(conversation)));
441
913
  for (const conversation of named) {
442
914
  if (!replicated.has(conversation))
443
915
  behind.add(conversation);
@@ -448,36 +920,100 @@ export function createConversationStore(deps) {
448
920
  await remove(leaving);
449
921
  },
450
922
  async catchUp(progress) {
451
- const reading = [...behind].filter((conversation) => replicated.has(conversation)).sort();
452
- if (reading.length > 0 && !deps.closed()) {
453
- const failed = await readHistories(reading);
923
+ if (deps.full()) {
924
+ if (behind.size > 0)
925
+ progress.complete = false;
926
+ return;
927
+ }
928
+ const owed = (conversation) => behind.has(conversation) && replicated.has(conversation);
929
+ const read = async (conversations) => {
930
+ if (conversations.length === 0 || deps.closed())
931
+ return;
932
+ const failed = await caughtUp(conversations);
454
933
  if (failed?.kind === 'transport-uncertainty')
455
934
  progress.reachable = false;
456
- }
935
+ };
936
+ const asking = [...behind].filter(owed).sort();
937
+ const awaited = asking.filter((conversation) => catching.has(conversation));
938
+ await Promise.all([
939
+ read(asking.filter((conversation) => !catching.has(conversation))),
940
+ Promise.all(awaited.map((conversation) => catching.get(conversation))).then(() => read(awaited.filter((conversation) => owed(conversation) && !catching.has(conversation)))),
941
+ ]);
457
942
  if (behind.size > 0)
458
943
  progress.complete = false;
459
944
  },
945
+ async resend(progress) {
946
+ for (const [conversation, held] of [...drafts]) {
947
+ for (const submission of held.submitted ?? []) {
948
+ if (submission.turn !== null || sending.has(submission.operation))
949
+ continue;
950
+ if (deps.closed())
951
+ return;
952
+ if (deps.expired(submission.operation)) {
953
+ await forgotten(conversation, submission);
954
+ continue;
955
+ }
956
+ const result = await dispatched(conversation, submission);
957
+ if (result.kind === 'revoked-authorization')
958
+ return;
959
+ if (result.kind === 'transport-uncertainty') {
960
+ progress.reachable = false;
961
+ return;
962
+ }
963
+ if (result.kind !== 'action' && result.retryable) {
964
+ progress.complete = false;
965
+ return;
966
+ }
967
+ }
968
+ }
969
+ },
460
970
  current() {
461
971
  return behind.size === 0;
462
972
  },
973
+ known() {
974
+ return listKnown;
975
+ },
463
976
  reconnected() {
464
977
  for (const conversation of replicated)
465
978
  gap(conversation);
466
979
  },
980
+ reopened() {
981
+ for (const [operation, dispatch] of sending)
982
+ if (!dispatch.answered)
983
+ sending.delete(operation);
984
+ },
985
+ lost() {
986
+ catching.clear();
987
+ },
467
988
  watch(conversation, options, deliver) {
468
- const limit = limitOf(options);
469
- const key = JSON.stringify([conversation, limit ?? null]);
470
- const view = views.get(key) ?? { conversation, limit, watchers: new Set() };
989
+ const asked = askedOf(options);
990
+ const key = JSON.stringify([conversation, asked.limit, asked.after ?? null, asked.total]);
991
+ const view = views.get(key) ?? {
992
+ conversation,
993
+ asked,
994
+ watchers: new Set(),
995
+ window: null,
996
+ more: false,
997
+ total: null,
998
+ };
471
999
  views.set(key, view);
472
1000
  const watcher = { notify: deliver, ready: false };
473
1001
  view.watchers.add(watcher);
474
1002
  void deps
475
- .durably(() => Promise.resolve())
1003
+ .durably(async (local) => {
1004
+ if (view.window !== null)
1005
+ return;
1006
+ const { messages, more } = await local.messageWindow(conversation, asked.limit, asked.before);
1007
+ view.window = messages;
1008
+ view.more = more;
1009
+ view.total = asked.total ? await local.messageCount(conversation) : null;
1010
+ })
476
1011
  .then(() => {
477
1012
  if (deps.closed() || !view.watchers.has(watcher))
478
1013
  return;
479
1014
  watcher.ready = true;
480
- watcher.notify(viewOf(conversation, limit));
1015
+ const window = view.window ?? [];
1016
+ watcher.notify(viewOf(conversation, window, sharedFrame(view, window)));
481
1017
  })
482
1018
  .catch(() => { });
483
1019
  return () => {
@@ -487,71 +1023,78 @@ export function createConversationStore(deps) {
487
1023
  views.delete(key);
488
1024
  };
489
1025
  },
1026
+ watchList(deliver) {
1027
+ listWatchers.add(deliver);
1028
+ void agentsReady().then(() => {
1029
+ if (!deps.closed() && listWatchers.has(deliver))
1030
+ deliver(entries());
1031
+ });
1032
+ return () => {
1033
+ listWatchers.delete(deliver);
1034
+ };
1035
+ },
1036
+ watchAgents(deliver) {
1037
+ agentWatchers.add(deliver);
1038
+ void agentsReady().then(() => {
1039
+ if (!deps.closed() && agentWatchers.has(deliver))
1040
+ deliver(agents());
1041
+ });
1042
+ return () => {
1043
+ agentWatchers.delete(deliver);
1044
+ };
1045
+ },
490
1046
  changed(set) {
491
1047
  for (const conversation of set.conversations)
492
1048
  emit(conversation);
1049
+ const declaration = deps.agents();
1050
+ if (declaration !== undefined &&
1051
+ set.documents.some((change) => change.locator === declaration.locator)) {
1052
+ agentsRead = readAgents().catch(() => {
1053
+ agentsRead = null;
1054
+ });
1055
+ }
493
1056
  },
494
1057
  receive(event) {
495
1058
  if (deps.closed())
496
- return;
497
- const { conversation } = event;
498
- if (event.kind === 'gap') {
499
- if (replicated.has(conversation))
500
- gap(conversation);
501
- if (lives.delete(conversation))
502
- emit(conversation);
503
- return;
504
- }
505
- if (event.kind === 'growing-text') {
506
- live(conversation, { growingText: { turn: event.turn, text: event.text } });
507
- return;
508
- }
509
- if (event.kind === 'plan') {
510
- live(conversation, { plan: { turn: event.turn, plan: structuredClone(event.plan) } });
511
- return;
512
- }
513
- if (event.kind === 'working-note') {
514
- live(conversation, {
515
- workingNote: event.note === null ? null : { noteId: event.note.noteId, text: event.note.text },
516
- });
517
- return;
518
- }
519
- if (event.kind === 'questions') {
520
- live(conversation, { questions: structuredClone(event.questions) });
521
- return;
522
- }
523
- if (event.kind === 'process') {
524
- live(conversation, { process: { running: event.running } });
525
- return;
526
- }
527
- if (event.kind === 'commands') {
528
- live(conversation, { commands: structuredClone(event.commands) });
529
- return;
530
- }
531
- if (event.kind === 'options') {
532
- live(conversation, { options: structuredClone(event.options) });
533
- return;
534
- }
535
- const arriving = acceptedMessage(event.message);
536
- if (arriving === null)
537
- return;
538
- if (replicated.has(conversation)) {
539
- void keep(conversation, arriving).catch(() => { });
540
- return;
1059
+ return false;
1060
+ if (event.kind === 'list' || event.kind === 'listed' || event.kind === 'unlisted') {
1061
+ if (event.kind === 'list') {
1062
+ listing.clear();
1063
+ for (const entry of event.entries) {
1064
+ listing.set(entry.conversation, { ...entry });
1065
+ creations.delete(entry.conversation);
1066
+ }
1067
+ listKnown = true;
1068
+ openingEnded();
1069
+ }
1070
+ else if (event.kind === 'listed') {
1071
+ listing.set(event.entry.conversation, { ...event.entry });
1072
+ creations.delete(event.entry.conversation);
1073
+ }
1074
+ else {
1075
+ listing.delete(event.conversation);
1076
+ }
1077
+ emitList();
1078
+ return (event.kind !== 'list' &&
1079
+ listKnown &&
1080
+ (listing.size !== replicated.size ||
1081
+ [...listing.keys()].some((conversation) => !replicated.has(conversation))));
541
1082
  }
542
- if (sealTurns(conversation, new Set([arriving.turn])))
543
- emit(conversation);
1083
+ heard(event);
1084
+ return false;
544
1085
  },
545
- messages() {
546
- return [...messages].flatMap(([conversation, held]) => held.map(({ identity, text }) => ({ conversation, identity, text })));
1086
+ searchable() {
1087
+ return [...(listKnown ? listing.keys() : stored)].sort();
547
1088
  },
548
- message(conversation, identity) {
549
- const held = messages.get(conversation)?.find((message) => message.identity === identity);
550
- return held === undefined ? null : { conversation, identity, text: held.text };
1089
+ acquired(conversation) {
1090
+ return reads.has(conversation) && !partials.has(conversation);
551
1091
  },
552
1092
  close() {
553
1093
  views.clear();
554
1094
  lives.clear();
1095
+ unsent.clear();
1096
+ listWatchers.clear();
1097
+ agentWatchers.clear();
555
1098
  },
556
1099
  };
557
1100
  }