@agentshouse/mdruntime 0.1.0-c3903a68dcbbf → 0.1.0-c3fa75348e547

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