@wacando/agent 2.4.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/STATUS.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Handoff — October 15, 2026
2
2
 
3
+ ## Runtime 2.4.1 — October 17, 2026
4
+
5
+ File runtime follow-ups (lbframe/wacando-agent#24, review of #23):
6
+ - A ciphertext left without its record (crash between the two writes) is
7
+ removed by the cleanup once it is more than an hour old.
8
+ - The quarantine keeps at most 20 descriptors per conversation (100 in all),
9
+ and a full quarantine is cleaned up before a share is refused, so a peer's
10
+ forged descriptors cannot block real shares.
11
+ - A file descriptor is also detected behind a zero-width space, BOM or word
12
+ joiner, with fullwidth braces, and nested, inside an array or inside prose
13
+ together with its key; text that only names the format (also a nested
14
+ `{"wacandoFile": false}`) stays text. Detection is linear in the body size.
15
+ - `fileStatus` of an incoming file runs one step at a time with
16
+ `receiveFile`, so it never drops the record of a file being received.
17
+ - The cleanup no longer runs when a read-only tool (`wacando_read`,
18
+ `wacando_conversations`, ...) connects: it runs before the first file
19
+ operation of a connection, and at every connection only for
20
+ `wacando-agent daemon` (SDK `files.sweepOnConnect`). A record that changed
21
+ while the cleanup consulted the server is kept (closes the STOP race that
22
+ could unlist a received file), and two processes of one identity can clean
23
+ up at the same time.
24
+ See [agent tools](docs/AGENT_TOOLS.md#files-10); evidence
25
+ `docs/evidence/issue-24/`.
26
+
27
+ Embedded Wacando code rebuilt from lbframe/wacando main `87b51a9`, which
28
+ brings lbframe/wacando#200: the embedded `scramClient` now guards its own
29
+ `start()` (`guardStart`), so the runtime's local copy of that guard (added in
30
+ 2.4.0) is removed; an error while the stream opens is still an ordinary,
31
+ retried connection failure. `@xmpp/events` 0.14.0 becomes an exact dependency
32
+ ([embedded Wacando code](docs/WACANDO_PACKAGES.md)); evidence
33
+ `docs/evidence/release-2.4.1/`.
34
+
3
35
  ## Runtime 2.4.0 — October 17, 2026
4
36
 
5
37
  Verified XMPP server (lbframe/wacando-agent#26): every XMPP connection of
@@ -70,10 +70,10 @@ export function parseFlags(flags, allowed) {
70
70
  class UsageError extends Error {}
71
71
  const print = value => console.log(JSON.stringify(value, null, 2));
72
72
 
73
- async function runtime(options, report) {
73
+ async function runtime(options, report, extra = {}) {
74
74
  const { AgentRuntime } = await import('../packages/runtime/runtime.mjs');
75
75
  const { CredentialStore } = await import('../packages/onboarding/storage.mjs');
76
- return new AgentRuntime({ host: options.host ?? 'cli', origin: options.origin, store: new CredentialStore(), ...(report ? { report } : {}) });
76
+ return new AgentRuntime({ host: options.host ?? 'cli', origin: options.origin, store: new CredentialStore(), ...(report ? { report } : {}), ...extra });
77
77
  }
78
78
 
79
79
  commands.set('status', async flags => {
@@ -198,7 +198,7 @@ commands.set('daemon', async flags => {
198
198
  // Diagnostics carry only local enums and public ids (never content).
199
199
  process.stderr.write(`${JSON.stringify({ component: 'wacando-connection', ...event })}\n`);
200
200
  if (event.state === 'replaced' || event.state === 'refused') terminal(event.state);
201
- });
201
+ }, { files: { sweepOnConnect: true } }); // the daemon cleans up ended file transfers at every connection (#24)
202
202
  try {
203
203
  try { print(await agent.connect(options.agent)); }
204
204
  catch (error) {
@@ -188,16 +188,27 @@ Code: `packages/runtime/files.mjs`.
188
188
  The URL in a message never chooses where the runtime connects nor where the
189
189
  agent credential goes; the fetch URL is rebuilt from the pin. https only,
190
190
  except loopback http for local stacks.
191
- - **Receive**: a decrypted body that is a file descriptor is never shown as
192
- text: any JSON object with a `wacandoFile` key, whatever its whitespace or
193
- key order (`fileBodyKind`; a body that opens like one but is not valid
194
- JSON counts too). A review descriptor (`wacandoFileReview`) in the room is
191
+ - **Receive**: a decrypted body that is or carries a file descriptor is never
192
+ shown as text (`fileBodyKind`, lbframe/wacando-agent#21, #24): a JSON object
193
+ with a `wacandoFile` key at its top level, whatever its whitespace, key order,
194
+ escaping or values; a body that opens like one but is not valid JSON; the
195
+ same behind invisible format characters (zero-width space, BOM, word joiner)
196
+ or written with fullwidth braces; and a descriptor anywhere else (nested in
197
+ JSON, inside an array, in prose, a code block or a JSON string) when its key
198
+ is there too (an `aesgcm://…#<88 hex>` URL, or a review `secret`). Text
199
+ that only names the format without a key (e.g. a nested
200
+ `{"wacandoFile": false}` setting) stays text. Detection is linear in the
201
+ body size. A review descriptor (`wacandoFileReview`) in the room is
195
202
  `file: { state: 'refused', reason: 'review_descriptor' }`. It is parsed strictly against
196
203
  the pin: valid -> `file: { fileId, name, mediaType, size, state:
197
204
  'quarantined' }` in the transcript entry (and `wacando_wait`), otherwise
198
205
  `file: { state: 'refused', reason: 'invalid_descriptor' }`, nothing stored or
199
- fetched. At most 100 descriptors wait in quarantine on one runtime; beyond
200
- that a new share is `refused` (`too_many_quarantined`) and never stored. `wacando_receive_file` checks that the server lists the file for
206
+ fetched. At most 100 descriptors wait in quarantine on one runtime, and at
207
+ most 20 of one conversation (one peer cannot fill the quarantine for the
208
+ others). When either limit is reached the runtime first cleans up ended
209
+ transfers (a peer's forged descriptors, which the server never lists, go
210
+ then); if it is still full, the new share is `refused`
211
+ (`too_many_quarantined`) and never stored. `wacando_receive_file` checks that the server lists the file for
201
212
  this agent with the same digest, size and type, waits (bounded) while it is
202
213
  quarantined (`uploaded`), and only once the recipient owner released it
203
214
  downloads (redirects refused, size capped), verifies and decrypts
@@ -214,17 +225,28 @@ Code: `packages/runtime/files.mjs`.
214
225
  in a conversation that is not `active` (checked locally before each step and
215
226
  by the server). Past `expiresAt` (30 days) the transfer is `expired`.
216
227
  Owners keep observing exchanged files in the app until then.
217
- - **Local cleanup** (lbframe/wacando-agent#21, `FileTransfers.sweep`): at every
218
- connection of an identity (and reconnection), before every
219
- `wacando_files`/`files` listing, and before refusing a send for a full
220
- queue, the runtime deletes the **local ciphertext, keys and quarantined
228
+ - **Local cleanup** (lbframe/wacando-agent#21, #24, `FileTransfers.sweep`):
229
+ once per connection, before the first file operation (`wacando_send_file`,
230
+ `wacando_file_status`, `wacando_receive_file`, `wacando_files` and their CLI
231
+ and SDK forms; never from the read-only `wacando_contacts`,
232
+ `wacando_conversations`, `wacando_read`, `wacando_wait`); at every
233
+ connection and reconnection for `wacando-agent daemon` (SDK:
234
+ `files.sweepOnConnect: true`); before every `wacando_files`/`files` listing;
235
+ and before refusing a send or a received share for a full queue or
236
+ quarantine, the runtime deletes the **local ciphertext, keys and quarantined
221
237
  descriptors** of every transfer that can never continue: conversation no
222
238
  longer `active` (STOP) or unknown to the server, file unknown, different,
223
239
  `rejected` or `expired` on the server, or past its expiry by the local clock
224
240
  (the server's `expiresAt`, else 30 days after the local request or receipt;
225
241
  this one also works offline). A server that cannot be reached removes
226
242
  nothing else. Sent records (public metadata, no key) are kept until their
227
- expiry. Leftovers of an interrupted write older than an hour are removed.
243
+ expiry. Leftovers of an interrupted write older than an hour are removed,
244
+ including a ciphertext whose record was never written (a crash between the
245
+ two writes: without its record it has no key). A record that changed while
246
+ the sweep consulted the server is kept (a file received meanwhile stays
247
+ listed, also if STOP follows), and no transfer step of the same process runs
248
+ during a removal. Two processes of one identity sweeping at once (a daemon
249
+ and an MCP server) only ever remove ended transfers.
228
250
  The listing reports `cleaned: { outgoing, incoming }` counts when it removed
229
251
  something. Nothing is logged.
230
252
  - **Received files are kept**: a file written to the quarantine directory is
@@ -42,7 +42,17 @@ source repository; a URL is cloned into a temporary directory.
42
42
  result is byte-identical to `packages/wacando/` and `provenance.json` (it needs
43
43
  access to the private repository, so it is not part of `npm run check`).
44
44
 
45
- Current source (2.4.0): lbframe/wacando main
45
+ Current source (2.4.1): lbframe/wacando main
46
+ `87b51a96aff0a13ca5ad6650c9c841bee72e1612` (#200 merged at `e669157`). It brings lbframe/wacando#200:
47
+ the embedded `scramClient` applies `guardStart`, so an error while the XMPP
48
+ stream opens rejects `start()` without leaving an unhandled rejection behind
49
+ (the runtime's own copy of that guard is removed); `@xmpp/events` is now a
50
+ recorded external of the embedded code and an exact dependency. The entry
51
+ list is unchanged: `guardStart` is applied inside `scramClient`, and the
52
+ runtime does not use `startOrStop` (`connectSession` already stops the
53
+ reconnect loop and the client on any failure, presence echo included).
54
+
55
+ 2.4.0 embedded lbframe/wacando main
46
56
  `40dd0c30b3653095f08994f9534fd5110e1d0925`. It brings lbframe/wacando#186:
47
57
  `#wacando/xmpp` now also exports `scramClient`, `startScramClient` and
48
58
  `ScramAuthError` (SCRAM client verifying the ServerSignature `v=`, SASL2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wacando/agent",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Wacando agent runtime: CLI/daemon, MCP server and Node SDK on official libsignal OMEMO, with thin Codex, Cursor and Claude Code plugins.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -53,6 +53,7 @@
53
53
  "dependencies": {
54
54
  "@signalapp/libsignal-client": "0.102.2",
55
55
  "@xmpp/client": "0.14.0",
56
+ "@xmpp/events": "0.14.0",
56
57
  "@xmpp/jid": "0.14.0",
57
58
  "@xmpp/xml": "0.14.0",
58
59
  "base64-js": "1.5.1",
@@ -1,2 +1,2 @@
1
1
  // Must match all distributed plugin manifests; checked by the test suite.
2
- export const pluginVersion = '2.4.0';
2
+ export const pluginVersion = '2.4.1';
@@ -104,7 +104,7 @@ export function publicConversation(view, extra = {}) {
104
104
 
105
105
  /** Messaging for ONE connected identity. Created by AgentRuntime.conversations(). */
106
106
  export class ConversationClient {
107
- constructor({ runtime, agent, session, files }) {
107
+ constructor({ runtime, agent, session, files, sweepOnConnect = false }) {
108
108
  this.runtime = runtime;
109
109
  this.agent = agent;
110
110
  this.session = session;
@@ -131,8 +131,11 @@ export class ConversationClient {
131
131
  this.lifecyclesLoaded = this.loadLifecycles();
132
132
  // lbframe/wacando-agent#10: encrypted files under owner approval.
133
133
  this.files = files?.(this) ?? null;
134
- // lbframe/wacando-agent#21: clean up ended transfers at every connection.
135
- this.filesSwept = this.sweepFiles();
134
+ // lbframe/wacando-agent#21, #24: clean up ended transfers once per
135
+ // connection, at the connection only for a daemon (sweepOnConnect), else
136
+ // before the first file operation: a read-only tool never deletes anything.
137
+ this.sweepOnConnect = sweepOnConnect;
138
+ this.filesSwept = this.sweepOnConnect ? this.sweepFiles() : null;
136
139
  }
137
140
 
138
141
  /** Content-free counts of the local file records removed, or null (never throws). */
@@ -191,7 +194,7 @@ export class ConversationClient {
191
194
  this.session = session;
192
195
  this.recovery = this.attachRecovery(session);
193
196
  this.lifecyclesLoaded = this.loadLifecycles();
194
- this.filesSwept = this.sweepFiles();
197
+ this.filesSwept = this.sweepOnConnect ? this.sweepFiles() : null;
195
198
  this.tracker = new MucOriginTracker(session.client, { trustedRelayJid: this.policyJid });
196
199
  this.rooms.clear();
197
200
  for (const resolve of this.submissions.values()) resolve({ state: 'refused', reason: 'connection-lost' });
@@ -343,6 +346,8 @@ export class ConversationClient {
343
346
 
344
347
  fileTransfers() {
345
348
  if (!this.files) throw new SafeError('File transfer is not available on this runtime.');
349
+ // First file operation of this connection (none is read-only): clean up.
350
+ this.filesSwept ??= this.sweepFiles();
346
351
  return this.files;
347
352
  }
348
353
  /** Encrypt a local file, ask the owner's approval, then deposit and share it (bounded wait). */
@@ -29,8 +29,10 @@ import { protectedDirectory, SafeError, storageError } from '../onboarding/stora
29
29
  // directory (0700/0600). The runtime never opens, extracts or executes it.
30
30
  // Nothing here logs; errors carry no name, digest, URL token or key.
31
31
  //
32
- // Local retention (lbframe/wacando-agent#21): sweep() runs at every connection
33
- // and before every listing; it deletes the local ciphertext, keys and
32
+ // Local retention (lbframe/wacando-agent#21, #24): sweep() runs before the
33
+ // first file operation of each connection (at every connection for the
34
+ // daemon, never from a read-only tool), before every listing and before a
35
+ // full queue or quarantine refuses; it deletes the local ciphertext, keys and
34
36
  // quarantined descriptors of transfers that can never continue (STOP or any
35
37
  // other closed conversation, rejected, expired or unknown on the server, or
36
38
  // past their 30-day expiry by the local clock). A RECEIVED file, written to
@@ -40,11 +42,15 @@ import { protectedDirectory, SafeError, storageError } from '../onboarding/stora
40
42
  export const FILE_POLL_MS = 2000;
41
43
  export const MAX_PENDING_OUTGOING = 10;
42
44
  export const MAX_QUARANTINED_INCOMING = 100;
45
+ // One conversation (one peer) can fill only part of the quarantine (lbframe/wacando-agent#24).
46
+ export const MAX_QUARANTINED_PER_CONVERSATION = 20;
43
47
  export const FILE_RETENTION_MS = 30 * 24 * 3600 * 1000;
44
- // Leftovers of an interrupted write (staged ciphertext, .part) older than this are removed by the sweep.
48
+ // Leftovers of an interrupted write (staged ciphertext, .part, a ciphertext
49
+ // without its record) older than this are removed by the sweep.
45
50
  const STALE_LEFTOVER_MS = 3600 * 1000;
46
51
  const STAGED = /^staged-[0-9a-f-]{36}\.bin$/;
47
52
  const PART = /^\.[0-9a-f-]{36}\.part$/;
53
+ const COMMITTED = /^(fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.bin$/;
48
54
  const FILE_ID = /^fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
49
55
  const UPLOAD_PATH = /^\/v1\/uploads\/(fil_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\/[A-Za-z0-9_-]{43}$/;
50
56
  const LOOPBACK = new Set(['localhost', '127.0.0.1', '[::1]']);
@@ -66,6 +72,7 @@ export const fileErrors = {
66
72
  download: 'The encrypted file could not be downloaded from the configured Wacando upload origin. Retry later.',
67
73
  plainText: 'A message body may not be a file share; use wacando_send_file.',
68
74
  tooManyIncoming: `Too many received files are waiting in quarantine on this runtime (at most ${MAX_QUARANTINED_INCOMING}). This file share was refused and never stored; ask your owner to authorize or the sender to resend later.`,
75
+ tooManyInConversation: `Too many received files of this conversation are waiting in quarantine (at most ${MAX_QUARANTINED_PER_CONVERSATION} per conversation). This file share was refused and never stored; ask your owner to authorize or the sender to resend later.`,
69
76
  orphan: 'The file could not be kept on this device after the request: nothing will be deposited. Your owner sees a pending request without a review copy and can refuse it; it expires after 30 days.',
70
77
  noOwnerDevice: 'Your owner has no active Wacando app device to review this file: ask your owner to open the Wacando desktop app (signed in) to approve it, then call wacando_file_status. The file stays pending; nothing was deposited.',
71
78
  reviewRetry: 'The review copy for your owner could not be deposited yet; call wacando_file_status to retry. The file stays pending.',
@@ -161,23 +168,67 @@ export function ownOwnerId(view, agentId) {
161
168
  }
162
169
 
163
170
  /**
164
- * Pure: whether a message body is a Wacando file descriptor, whatever its
165
- * whitespace or key order: 'share' (file share, `wacandoFile`), 'review'
166
- * (owner review descriptor, `wacandoFileReview`, never meant for a room) or
167
- * null. Such a body is never shown nor sent as text (it carries a key).
171
+ * Pure: whether a message body is, or carries, a Wacando file descriptor:
172
+ * 'share' (file share, `wacandoFile`), 'review' (owner review descriptor,
173
+ * `wacandoFileReview`, never meant for a room) or null. Such a body is never
174
+ * shown nor sent as text: it carries a key (lbframe/wacando-agent#21, #24).
175
+ * Detected whatever the whitespace, key order or JSON escaping, behind
176
+ * invisible format characters (ZWSP, BOM, word joiner...) or compatibility
177
+ * forms (fullwidth braces):
178
+ * - a JSON object with such a key at its top level (strict: whatever its
179
+ * values), or a body that opens like one but is not valid JSON;
180
+ * - a descriptor anywhere else (nested in JSON, in an array, in prose, a code
181
+ * block or a JSON string), only when its key material is there too: an
182
+ * `aesgcm://...#<88 hex>` URL for a share, a `"secret"` of 88 hex for a
183
+ * review. Text that merely names the format (e.g. a nested
184
+ * `{"wacandoFile":false}` setting) carries no key and stays text.
185
+ * Linear in the body size (a hostile body cannot stall the event loop).
168
186
  */
169
187
  export function fileBodyKind(text) {
170
188
  if (typeof text !== 'string') return null;
171
- const trimmed = text.trim();
172
- if (!trimmed.startsWith('{')) return null;
173
- let value;
174
- try { value = JSON.parse(trimmed); } catch {
175
- // Not JSON: still a descriptor when it opens like one (never shown as text).
176
- return /^\{\s*"wacandoFileReview"\s*:/.test(trimmed) ? 'review' : /^\{\s*"wacandoFile"\s*:/.test(trimmed) ? 'share' : null;
177
- }
178
- if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
179
- if (Object.hasOwn(value, 'wacandoFileReview')) return 'review';
180
- return Object.hasOwn(value, 'wacandoFile') ? 'share' : null;
189
+ // Detection only: the body itself is never rewritten.
190
+ const plain = text.normalize('NFKC').replace(/\p{Cf}/gu, '').trim();
191
+ if (plain.startsWith('{') || plain.startsWith('[')) {
192
+ let value;
193
+ let parsed = true;
194
+ try { value = JSON.parse(plain); } catch { parsed = false; }
195
+ if (parsed) {
196
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
197
+ if (Object.hasOwn(value, 'wacandoFileReview')) return 'review';
198
+ if (Object.hasOwn(value, 'wacandoFile')) return 'share';
199
+ }
200
+ } else if (/^[[\s]*\{\s*"wacandoFileReview"\s*:/.test(plain)) return 'review';
201
+ else if (/^[[\s]*\{\s*"wacandoFile"\s*:/.test(plain)) return 'share';
202
+ }
203
+ let unescaped = plain;
204
+ for (let i = 0; i < 3; i++) {
205
+ const next = unescaped.replace(/\\u([0-9a-fA-F]{4})/g, (_, code) => String.fromCharCode(Number.parseInt(code, 16))).replace(/\\(["/\\])/g, '$1');
206
+ if (next === unescaped) break;
207
+ unescaped = next;
208
+ }
209
+ if (/"wacandoFileReview"\s*:/.test(unescaped) && /"secret"\s*:\s*"[0-9a-fA-F]{88}"/.test(unescaped)) return 'review';
210
+ if (/"wacandoFile"\s*:/.test(unescaped) && hasAesgcmKey(unescaped)) return 'share';
211
+ return null;
212
+ }
213
+
214
+ const URL_END = /[\s"#]/;
215
+ const KEY_HEX = /^[0-9a-fA-F]{88}/;
216
+ /**
217
+ * Pure: whether the text holds an `aesgcm://<no space, quote or #>#<88 hex>`
218
+ * URL, in one linear pass (the equivalent regular expression is quadratic on
219
+ * `aesgcm://` repeated without a `#`, #24 review LOW-2): every later
220
+ * `aesgcm://` inside the same URL body ends at the same character.
221
+ */
222
+ function hasAesgcmKey(text) {
223
+ let from = 0;
224
+ for (;;) {
225
+ const start = text.indexOf('aesgcm://', from);
226
+ if (start < 0) return false;
227
+ let end = start + 'aesgcm://'.length;
228
+ while (end < text.length && !URL_END.test(text[end])) end++;
229
+ if (text[end] === '#' && KEY_HEX.test(text.slice(end + 1, end + 89))) return true;
230
+ from = end;
231
+ }
181
232
  }
182
233
 
183
234
  const BARE_JID = /^[^\s/@]+@[^\s/@]+$/;
@@ -269,15 +320,24 @@ export class FileVault {
269
320
  try { await rename(join(dir, staged), join(dir, `${FileVault.id(fileId)}.bin`)); } catch { throw storageError(); }
270
321
  }
271
322
  async dropStaged(staged) { if (STAGED.test(staged ?? '')) await unlink(join(await this.dir('outgoing'), staged)).catch(() => {}); }
272
- /** Remove interrupted-write leftovers (staged ciphertext, .part files) older than `before` (ms). Returns the count. */
323
+ /**
324
+ * Remove interrupted-write leftovers older than `before` (ms): staged
325
+ * ciphertext, .part files, and a committed ciphertext whose record was never
326
+ * written (a crash between the rename and the record, lbframe/wacando-agent#24;
327
+ * without its record it has no key and can never be used). Returns the count.
328
+ */
273
329
  async dropLeftovers(before) {
274
330
  let count = 0;
275
331
  for (const kind of ['outgoing', 'incoming']) {
276
332
  const dir = await this.dir(kind);
277
333
  for (const name of await readdir(dir)) {
278
- if (!STAGED.test(name) && !PART.test(name)) continue;
334
+ const orphan = kind === 'outgoing' ? COMMITTED.exec(name)?.[1] : null;
335
+ if (!STAGED.test(name) && !PART.test(name) && !orphan) continue;
279
336
  const info = await stat(join(dir, name)).catch(() => null);
280
- if (info?.isFile() && info.mtimeMs < before && await unlink(join(dir, name)).then(() => true, () => false)) count++;
337
+ if (!info?.isFile() || info.mtimeMs >= before) continue;
338
+ // Its record may have been written since the directory was read.
339
+ if (orphan && await lstat(join(dir, `${orphan}.json`)).then(() => true, error => error.code !== 'ENOENT')) continue;
340
+ if (await unlink(join(dir, name)).then(() => true, () => false)) count++;
281
341
  }
282
342
  }
283
343
  return count;
@@ -326,6 +386,7 @@ export class FileTransfers {
326
386
  this.pollMs = pollMs;
327
387
  this.fetch = fetch;
328
388
  this.busy = new Map(); // fileId -> in-flight advance (one at a time per file)
389
+ this.claims = new WeakSet(); // busy entries that are a sweep removal
329
390
  }
330
391
 
331
392
  get pin() { return this.pinSetting ?? (this.pinSetting = uploadPin(this.c.agent.origin)); }
@@ -489,15 +550,24 @@ export class FileTransfers {
489
550
  async status(fileId, { timeoutMs = 0 } = {}) {
490
551
  FileVault.id(fileId);
491
552
  if (await this.vault.read('outgoing', fileId)) return this.once(fileId, () => this.advance(fileId, timeoutMs));
492
- const incoming = await this.vault.read('incoming', fileId);
493
- if (!incoming) throw new SafeError(fileErrors.unknown);
494
- return this.incomingState(incoming, { timeoutMs, fetch: false });
553
+ // Incoming: one step at a time per file, like receive (a record written by
554
+ // a receive in flight is never dropped by this step, #24 review LOW-1).
555
+ return this.once(fileId, async () => {
556
+ const incoming = await this.vault.read('incoming', fileId);
557
+ if (!incoming) throw new SafeError(fileErrors.unknown);
558
+ return this.incomingState(incoming, { timeoutMs, fetch: false });
559
+ });
495
560
  }
496
561
 
562
+ /**
563
+ * One transfer step at a time per file in this process: a caller joins the
564
+ * step in flight; a removal by the sweep (a claim) is waited for first.
565
+ */
497
566
  once(fileId, run) {
498
567
  const running = this.busy.get(fileId);
499
- if (running) return running;
500
- const task = run().finally(() => this.busy.delete(fileId));
568
+ if (running && !this.claims.has(running)) return running;
569
+ const task = (running ? running.then(() => run(), () => run()) : run())
570
+ .finally(() => { if (this.busy.get(fileId) === task) this.busy.delete(fileId); });
501
571
  this.busy.set(fileId, task);
502
572
  return task;
503
573
  }
@@ -617,19 +687,36 @@ export class FileTransfers {
617
687
  }
618
688
  return { ...publicIncoming(known), state: known.path ? 'received' : 'quarantined' };
619
689
  }
620
- if ((await this.vault.list('incoming')).filter(r => !r.path).length >= MAX_QUARANTINED_INCOMING) {
621
- return { state: 'refused', reason: 'too_many_quarantined', note: fileErrors.tooManyIncoming };
690
+ // Bounded quarantine, and one conversation (one peer) fills at most part of
691
+ // it. Ended transfers do not count: clean them up before refusing (#24).
692
+ let full = await this.quarantineFull(conversationId);
693
+ if (full) {
694
+ await this.sweep().catch(() => null);
695
+ full = await this.quarantineFull(conversationId);
622
696
  }
697
+ if (full) return { state: 'refused', reason: 'too_many_quarantined', note: full };
623
698
  const record = { fileId, conversationId, messageId, share, ciphertextSha256: share.ciphertextSha256, receivedAt: new Date(this.now()).toISOString() };
624
699
  await this.vault.write('incoming', fileId, record);
625
700
  return { ...publicIncoming(record), state: 'quarantined', note: `In quarantine until your owner authorizes it; then call wacando_receive_file. ${UNTRUSTED}` };
626
701
  }
627
702
 
703
+ /** The refusal note when the quarantine (or this conversation's share of it) is full, else null. */
704
+ async quarantineFull(conversationId) {
705
+ const waiting = (await this.vault.list('incoming')).filter(r => !r.path);
706
+ if (waiting.length >= MAX_QUARANTINED_INCOMING) return fileErrors.tooManyIncoming;
707
+ if (waiting.filter(r => r.conversationId === conversationId).length >= MAX_QUARANTINED_PER_CONVERSATION) return fileErrors.tooManyInConversation;
708
+ return null;
709
+ }
710
+
628
711
  /** Fetch a released incoming file into the quarantine directory (bounded wait for the release). */
629
712
  async receive(fileId, { timeoutMs = 0 } = {}) {
630
- const record = await this.vault.read('incoming', FileVault.id(fileId));
631
- if (!record) throw new SafeError(fileErrors.unknown);
632
- return this.once(fileId, () => this.incomingState(record, { timeoutMs, fetch: true }));
713
+ FileVault.id(fileId);
714
+ // Read inside the step: a removal by the sweep is never overtaken.
715
+ return this.once(fileId, async () => {
716
+ const record = await this.vault.read('incoming', fileId);
717
+ if (!record) throw new SafeError(fileErrors.unknown);
718
+ return this.incomingState(record, { timeoutMs, fetch: true });
719
+ });
633
720
  }
634
721
 
635
722
  async incomingState(record, { timeoutMs, fetch }) {
@@ -640,17 +727,17 @@ export class FileTransfers {
640
727
  const view = await this.c.conversation(record.conversationId);
641
728
  if (view.state !== 'active') {
642
729
  // STOP is terminal: this agent can never fetch it; drop the key.
643
- await this.vault.remove('incoming', record.fileId);
730
+ await this.dropIncoming(record);
644
731
  return { file: summary, state: 'refused', reason: 'conversation_closed', note: fileErrors.notActive(view.state) };
645
732
  }
646
733
  const dto = (await this.serverFiles(record.conversationId)).find(f => f.id === record.fileId);
647
734
  if (!dto || dto.recipientAgentId !== this.c.agent.agentId || dto.ciphertextSha256 !== record.share.ciphertextSha256 ||
648
735
  dto.sizeBytes !== record.share.ciphertextSize || dto.mediaType !== record.share.mediaType) {
649
- await this.vault.remove('incoming', record.fileId);
736
+ await this.dropIncoming(record);
650
737
  return { file: summary, state: 'refused', reason: 'not_approved_file', note: 'The server does not list this file as approved for this agent. It was refused and never fetched.' };
651
738
  }
652
739
  if (this.expired(dto)) {
653
- await this.vault.remove('incoming', record.fileId);
740
+ await this.dropIncoming(record);
654
741
  return { file: summary, state: 'expired', note: fileApiMessages.FILE_EXPIRED };
655
742
  }
656
743
  if (dto.status === 'released' && fetch) return this.fetchReleased(record, dto, summary);
@@ -663,6 +750,12 @@ export class FileTransfers {
663
750
  }
664
751
  }
665
752
 
753
+ /** Drop a quarantined descriptor (its key), never the record of a file received meanwhile. */
754
+ async dropIncoming(record) {
755
+ if ((await this.vault.read('incoming', record.fileId))?.path) return;
756
+ await this.vault.remove('incoming', record.fileId);
757
+ }
758
+
666
759
  async fetchReleased(record, dto, summary) {
667
760
  let file;
668
761
  try {
@@ -690,16 +783,22 @@ export class FileTransfers {
690
783
 
691
784
  /**
692
785
  * Delete the local secrets of every transfer that can never continue
693
- * (lbframe/wacando-agent#21), at connection and before each listing:
786
+ * (lbframe/wacando-agent#21, #24), before the first file operation of a
787
+ * connection (at every connection for the daemon), before each listing and
788
+ * before a full queue or quarantine refuses:
694
789
  * - pending/deposited sends (ciphertext + key) and quarantined descriptors
695
790
  * (key): conversation no longer active (STOP is terminal) or unknown to
696
791
  * the server, file unknown, different, rejected or expired on the server,
697
792
  * or past its 30-day expiry by the local clock (even offline);
698
793
  * - sent records (public metadata only, no key): past their expiry;
699
- * - interrupted-write leftovers older than an hour.
794
+ * - interrupted-write leftovers older than an hour, including a ciphertext
795
+ * whose record was never written.
700
796
  * A received file (written to the quarantine directory) and its record are
701
797
  * never touched. A server that cannot be reached removes nothing it cannot
702
- * decide locally. Transfers in progress in this process are skipped.
798
+ * decide locally. Transfers in progress in this process are skipped; a
799
+ * record that changed since it was listed (e.g. a file received meanwhile)
800
+ * is kept. Another process of the same identity may sweep at the same time:
801
+ * every step tolerates a file the other one already removed.
703
802
  * Returns content-free counts.
704
803
  */
705
804
  sweep() {
@@ -721,14 +820,31 @@ export class FileTransfers {
721
820
  if (this.busy.has(record.fileId)) continue;
722
821
  if (!(await this.ended(direction, record, now, server))) continue;
723
822
  if (this.busy.has(record.fileId)) continue;
724
- await this.vault.remove(direction, record.fileId);
725
- counts[direction]++;
823
+ if (await this.claim(direction, record)) counts[direction]++;
726
824
  }
727
825
  }
728
826
  await this.vault.dropLeftovers(now - STALE_LEFTOVER_MS).catch(() => 0);
729
827
  return counts;
730
828
  }
731
829
 
830
+ /**
831
+ * Remove an ended record unless it changed since it was listed. Set as the
832
+ * file's busy step in the same tick as the caller's last busy check, so no
833
+ * step of this process (receive, status) interleaves with the removal.
834
+ */
835
+ claim(direction, record) {
836
+ const removal = (async () => {
837
+ const fresh = await this.vault.read(direction, record.fileId);
838
+ if (!fresh || JSON.stringify(fresh) !== JSON.stringify(record)) return false;
839
+ await this.vault.remove(direction, record.fileId);
840
+ return true;
841
+ })();
842
+ const held = removal.finally(() => { if (this.busy.get(record.fileId) === held) this.busy.delete(record.fileId); });
843
+ this.claims.add(held);
844
+ this.busy.set(record.fileId, held);
845
+ return held;
846
+ }
847
+
732
848
  /** Conversation state and files as the server lists them for this agent; 'gone' when unknown; null when unreachable. */
733
849
  async serverState(conversationId) {
734
850
  let state;
@@ -30,9 +30,12 @@ export function publicIdentity(agent, state) {
30
30
  */
31
31
  export class AgentRuntime {
32
32
  /**
33
- * `files`: `{ uploadOrigin, quarantineDir }` (defaults: WACANDO_UPLOAD_ORIGIN
34
- * or the API origin; WACANDO_QUARANTINE_DIR or a directory inside the
35
- * protected runtime state) plus test hooks (`now`, `sleep`, `pollMs`, `fetch`).
33
+ * `files`: `{ uploadOrigin, quarantineDir, sweepOnConnect }` (defaults:
34
+ * WACANDO_UPLOAD_ORIGIN or the API origin; WACANDO_QUARANTINE_DIR or a
35
+ * directory inside the protected runtime state; `sweepOnConnect: true`
36
+ * cleans up ended transfers at every connection, as the daemon does,
37
+ * otherwise before the first file operation of a connection) plus test
38
+ * hooks (`now`, `sleep`, `pollMs`, `fetch`).
36
39
  */
37
40
  constructor({ host = 'cli', origin, store = new CredentialStore(), report, request, connect, onStanza, files = {} } = {}) {
38
41
  if (!/^[a-z0-9][a-z0-9_-]{0,31}$/.test(host)) throw new SafeError('Invalid runtime host name.');
@@ -147,7 +150,7 @@ export class AgentRuntime {
147
150
  if (client && client.session !== session) this.rebind(agent.agentId, session);
148
151
  if (!client) {
149
152
  const files = await this.fileTransfersFor(agent);
150
- client = new ConversationClient({ runtime: this, agent, session, files });
153
+ client = new ConversationClient({ runtime: this, agent, session, files, sweepOnConnect: this.fileOptions.sweepOnConnect === true });
151
154
  this.clients.set(agent.agentId, client);
152
155
  for (const stanza of this.buffered.get(agent.agentId) ?? []) client.handle(stanza);
153
156
  this.buffered.delete(agent.agentId);
@@ -220,7 +223,7 @@ export class AgentRuntime {
220
223
 
221
224
  /** File transfer state of one identity: private directories, pinned upload origin. */
222
225
  async fileTransfersFor(agent) {
223
- const { uploadOrigin, quarantineDir = process.env.WACANDO_QUARANTINE_DIR, ...hooks } = this.fileOptions;
226
+ const { uploadOrigin, quarantineDir = process.env.WACANDO_QUARANTINE_DIR, sweepOnConnect: _sweep, ...hooks } = this.fileOptions;
224
227
  const pin = uploadPin(agent.origin, uploadOrigin ?? process.env.WACANDO_UPLOAD_ORIGIN);
225
228
  const base = join(await this.store.directory(agent.origin, this.host), `files-${digest(agent.agentId)}`);
226
229
  const vault = new FileVault(base, quarantineDir ? join(quarantineDir, digest(`${agent.origin}\n${agent.agentId}`).slice(0, 32)) : undefined);
@@ -3,32 +3,6 @@ import { SCRAM_REFUSED, XMPP_SERVER_UNVERIFIED } from '../onboarding/service.mjs
3
3
  import { SafeError } from '../onboarding/storage.mjs';
4
4
  import { loadDevice } from './device.mjs';
5
5
 
6
- /**
7
- * @xmpp/connection 0.14.0 `start()` creates its "online" promise (rejected by
8
- * any entity 'error') BEFORE awaiting `open()`. When an 'error' is emitted
9
- * while the stream opens (Node.js 24's WebSocket reports a socket cut that
10
- * way), `open()` rejects, `start()` throws without ever awaiting that promise
11
- * and its rejection is unhandled: the whole process (daemon, MCP server)
12
- * dies. Same steps, with the "online" promise always observed.
13
- */
14
- function observeOnlinePromise(xmpp) {
15
- xmpp.start = async function start() {
16
- if (this.status !== 'offline') throw new Error('Connection is not offline');
17
- const { service, domain, lang } = this.options;
18
- await this.connect(service);
19
- const online = new Promise((resolve, reject) => {
20
- const settle = () => { this.removeListener('online', onOnline); this.removeListener('error', onError); };
21
- const onOnline = value => { settle(); resolve(value); };
22
- const onError = error => { settle(); reject(error); };
23
- this.on('online', onOnline);
24
- this.on('error', onError);
25
- });
26
- online.catch(() => {});
27
- await this.open({ domain, lang });
28
- return online;
29
- };
30
- }
31
-
32
6
  const unavailable = () => new SafeError('Secure messaging is unavailable. Retry the saved identity.');
33
7
 
34
8
  /** SafeError for a ScramAuthError: the fixed message plus its local reason enum (no secret). */
@@ -47,13 +21,18 @@ const serverUnverified = error => Object.assign(new SafeError(XMPP_SERVER_UNVERI
47
21
  * SASL1 only, and authentication succeeds only once the server's
48
22
  * ServerSignature (`v=`) was verified. A refusal closes the connection,
49
23
  * stops @xmpp/client's reconnect and rejects with `code: 'SCRAM_REFUSED'`.
24
+ * Any failure (start, presence echo or deadline) stops the reconnect loop and
25
+ * the client before rejecting: the same guarantee as the embedded
26
+ * `startOrStop`, extended to the presence echo, so that helper is not used.
50
27
  */
51
28
  export async function connectSession({ jid, password, service, stateBaseDir, onStanza, onReplaced, timeoutMs = 15000 }) {
52
29
  const provider = await loadDevice(jid, stateBaseDir);
53
30
  const at = jid.indexOf('@');
54
31
  const domain = jid.slice(at + 1);
32
+ // scramClient applies the embedded guardStart (lbframe/wacando#200): an
33
+ // 'error' while the stream opens rejects start() (an ordinary, retried
34
+ // failure here), never leaves an unhandled rejection behind.
55
35
  const xmpp = scramClient({ service, domain, username: jid.slice(0, at), password });
56
- observeOnlinePromise(xmpp);
57
36
  // One server-enforced session per agent (lbframe/wacando #15): a newer login
58
37
  // ends this one with <conflict/>. Remember it so nothing reconnects in a loop.
59
38
  let replaced = false;
@@ -1,4 +1,4 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
 
3
3
  // packages/contracts/src/errors.ts
4
4
  var DomainError = class extends Error {
@@ -1,11 +1,35 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  XMPP_DOMAIN
4
- } from "./chunk-FFLYSYV6.mjs";
4
+ } from "./chunk-4Z6TL6JY.mjs";
5
5
 
6
6
  // packages/xmpp/src/scram.ts
7
7
  import { client } from "@xmpp/client";
8
8
  import xml from "@xmpp/xml";
9
+
10
+ // packages/xmpp/src/start.ts
11
+ import { promise, timeout } from "@xmpp/events";
12
+ var guarded = /* @__PURE__ */ new WeakSet();
13
+ function guardStart(entity) {
14
+ if (guarded.has(entity)) return entity;
15
+ const target = entity;
16
+ target.start = async function start() {
17
+ if (this.status !== "offline") {
18
+ throw new Error("Connection is not offline");
19
+ }
20
+ const { service, domain, lang } = this.options;
21
+ await this.connect(service);
22
+ const online = promise(this, "online");
23
+ online.catch(() => {
24
+ });
25
+ await this.open({ domain, lang });
26
+ return online;
27
+ };
28
+ guarded.add(entity);
29
+ return entity;
30
+ }
31
+
32
+ // packages/xmpp/src/scram.ts
9
33
  var SASL1_NS = "urn:ietf:params:xml:ns:xmpp-sasl";
10
34
  var SASL2_NS = "urn:xmpp:sasl:2";
11
35
  var SCRAM_MIN_ITERATIONS = 4096;
@@ -337,6 +361,7 @@ function scramClient(options) {
337
361
  refused = new ScramAuthError(reason);
338
362
  void cut(xmpp, refused);
339
363
  });
364
+ guardStart(xmpp);
340
365
  states.set(xmpp, state);
341
366
  return xmpp;
342
367
  }
@@ -1,7 +1,7 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  DomainError
4
- } from "./chunk-FFLYSYV6.mjs";
4
+ } from "./chunk-4Z6TL6JY.mjs";
5
5
 
6
6
  // packages/omemo/src/ns.ts
7
7
  var NS_OMEMO = "eu.siacs.conversations.axolotl";
@@ -1,4 +1,4 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  BODY_MARKER,
4
4
  GCM_IV_BYTES,
@@ -29,8 +29,8 @@ import {
29
29
  signerJid,
30
30
  signerRole,
31
31
  targetJid
32
- } from "./chunks/chunk-F7J3K43U.mjs";
33
- import "./chunks/chunk-FFLYSYV6.mjs";
32
+ } from "./chunks/chunk-L6IDYNL5.mjs";
33
+ import "./chunks/chunk-4Z6TL6JY.mjs";
34
34
 
35
35
  // packages/omemo/src/policy-wire.ts
36
36
  import xml from "@xmpp/xml";
@@ -1,9 +1,9 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  authenticatedMessageOrigin,
4
4
  pepFetchItems,
5
5
  pepPublish
6
- } from "./chunks/chunk-FUNUGIGH.mjs";
6
+ } from "./chunks/chunk-5Q4655F4.mjs";
7
7
  import {
8
8
  BODY_MARKER,
9
9
  GCM_IV_BYTES,
@@ -37,13 +37,13 @@ import {
37
37
  signerJid,
38
38
  signerRole,
39
39
  utf8Encode
40
- } from "./chunks/chunk-F7J3K43U.mjs";
40
+ } from "./chunks/chunk-L6IDYNL5.mjs";
41
41
  import {
42
42
  ALLOWED_MEDIA_TYPES,
43
43
  MAX_FILE_BYTES,
44
44
  MIN_FILE_BYTES,
45
45
  looksLikePlaintext
46
- } from "./chunks/chunk-FFLYSYV6.mjs";
46
+ } from "./chunks/chunk-4Z6TL6JY.mjs";
47
47
 
48
48
  // packages/omemo/src/libsignal.ts
49
49
  import { jid as parseJid2 } from "@xmpp/jid";
@@ -1,4 +1,4 @@
1
- // Embedded from lbframe/wacando@40dd0c30b3653095f08994f9534fd5110e1d0925 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
1
+ // Embedded from lbframe/wacando@87b51a96aff0a13ca5ad6650c9c841bee72e1612 by scripts/embed-wacando.mjs. Do not edit; see provenance.json.
2
2
  import {
3
3
  MucJoinError,
4
4
  MucOriginTracker,
@@ -13,8 +13,8 @@ import {
13
13
  startScramClient,
14
14
  unwrapPolicyDelivery,
15
15
  waitForStanza
16
- } from "./chunks/chunk-FUNUGIGH.mjs";
17
- import "./chunks/chunk-FFLYSYV6.mjs";
16
+ } from "./chunks/chunk-5Q4655F4.mjs";
17
+ import "./chunks/chunk-4Z6TL6JY.mjs";
18
18
 
19
19
  // packages/xmpp/src/index.ts
20
20
  import { default as default2 } from "@xmpp/xml";
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wacando",
3
- "version": "2.4.0",
3
+ "version": "2.4.1",
4
4
  "description": "Connect agents with Wacando onboarding and view conversations in the built-in Codex browser.",
5
5
  "author": { "name": "Wacando" },
6
6
  "skills": "./skills/",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wacando-claude",
3
3
  "displayName": "Wacando",
4
- "version": "2.4.0",
4
+ "version": "2.4.1",
5
5
  "description": "Connect agents with Wacando onboarding and view live Claude Code conversations locally.",
6
6
  "author": { "name": "Wacando" },
7
7
  "keywords": ["agents", "conversation", "local", "observability"],
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "wacando-cursor",
3
3
  "displayName": "Wacando",
4
- "version": "2.4.0",
4
+ "version": "2.4.1",
5
5
  "description": "Connect agents with Wacando onboarding and view conversations in the built-in Cursor browser.",
6
6
  "author": { "name": "Wacando" },
7
7
  "keywords": ["agents", "conversation", "local", "observability"],
package/provenance.json CHANGED
@@ -240,10 +240,10 @@
240
240
  "source": {
241
241
  "repository": "lbframe/wacando",
242
242
  "branch": "main",
243
- "commit": "40dd0c30b3653095f08994f9534fd5110e1d0925",
243
+ "commit": "87b51a96aff0a13ca5ad6650c9c841bee72e1612",
244
244
  "trees": {
245
245
  "packages/contracts/src": "bf79bedbbb5bd0dc5cbf7d3a027a30f76a3f6904",
246
- "packages/xmpp/src": "d8dbc0ea4b286c26bfb3e7b347c3210b27365c9b",
246
+ "packages/xmpp/src": "06bc1e27b585ce669cf12c10b01b7771d44b91f4",
247
247
  "packages/omemo/src": "850b707c9705211a6136486c78c38882699e8b39"
248
248
  },
249
249
  "packages": [
@@ -274,6 +274,7 @@
274
274
  "externals": {
275
275
  "@signalapp/libsignal-client": "0.102.2",
276
276
  "@xmpp/client": "0.14.0",
277
+ "@xmpp/events": "0.14.0",
277
278
  "@xmpp/jid": "0.14.0",
278
279
  "@xmpp/xml": "0.14.0",
279
280
  "base64-js": "1.5.1",
@@ -329,39 +330,40 @@
329
330
  "packages/xmpp/src/scram.ts",
330
331
  "packages/xmpp/src/single-session.ts",
331
332
  "packages/xmpp/src/stanza.ts",
333
+ "packages/xmpp/src/start.ts",
332
334
  "packages/xmpp/src/timeouts.ts",
333
335
  "packages/xmpp/src/upload.ts"
334
336
  ],
335
337
  "files": [
336
338
  {
337
- "path": "packages/wacando/chunks/chunk-F7J3K43U.mjs",
338
- "bytes": 13681,
339
- "sha256": "5b2797e10cb4c5ca5c976f2f670c5c41b18b403d28dc9622a07617e71dc33e80"
339
+ "path": "packages/wacando/chunks/chunk-4Z6TL6JY.mjs",
340
+ "bytes": 2164,
341
+ "sha256": "510beba0db3b2bf41032a4bd2abd003662504f03721f69cdca8ca62da7e2491c"
340
342
  },
341
343
  {
342
- "path": "packages/wacando/chunks/chunk-FFLYSYV6.mjs",
343
- "bytes": 2164,
344
- "sha256": "d84c6ca7a529acc6be0da790124bb73a35fd6c7703beb370fb4e8003eb2ca0e0"
344
+ "path": "packages/wacando/chunks/chunk-5Q4655F4.mjs",
345
+ "bytes": 27209,
346
+ "sha256": "05463ed7058575f8d167c157ac88c8ce44d77f1296235afd6bc96ef72e82501b"
345
347
  },
346
348
  {
347
- "path": "packages/wacando/chunks/chunk-FUNUGIGH.mjs",
348
- "bytes": 26532,
349
- "sha256": "b237936a6254b8263768aef19c80c427183ed3f607c9af9f33595477bc0bddc9"
349
+ "path": "packages/wacando/chunks/chunk-L6IDYNL5.mjs",
350
+ "bytes": 13681,
351
+ "sha256": "cda8c73a2d729ba969abea7bd5a460f142c4df3b30dc84cfc4ee4092d2c448a4"
350
352
  },
351
353
  {
352
354
  "path": "packages/wacando/omemo-wire.mjs",
353
355
  "bytes": 5683,
354
- "sha256": "bf44f885b9e3e65da624f9251ec1b2148e8e647a6398c22750003cda3c961e84"
356
+ "sha256": "3cfcbc94e80e57856db5a9f9ded5f441a333cf1849481273f6f23d62ed4a4414"
355
357
  },
356
358
  {
357
359
  "path": "packages/wacando/omemo.mjs",
358
360
  "bytes": 109777,
359
- "sha256": "334fe2228f4bdc5c66e9968c707094b6bc76f5a60a8f9fa59dad4a8cb1788c49"
361
+ "sha256": "b06b6771e7f1796150611ee2da60887f682ccba26262107e1a869732e57fc5f6"
360
362
  },
361
363
  {
362
364
  "path": "packages/wacando/xmpp.mjs",
363
365
  "bytes": 2834,
364
- "sha256": "1d5536b43cf9d38414c1ccd4c73334a07db60b57c6b584f6a61916010a3453e1"
366
+ "sha256": "e95455a1bc5d8d5c8773129c45fdd2ab19f2282b16acf446f981ccd2051c220c"
365
367
  }
366
368
  ]
367
369
  }