@agentshouse/mdruntime 0.1.0-cc7bd7a53214e → 0.1.0-ccbe92df1c4f6

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