@ours.network/cowork 1.0.2 → 1.0.3

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 (3) hide show
  1. package/README.md +1 -1
  2. package/dist/daemon.js +773 -567
  3. package/package.json +3 -1
package/dist/daemon.js CHANGED
@@ -5736,6 +5736,20 @@ function sameReply2(stored, observed) {
5736
5736
  if (stored === void 0 || observed == null) return stored === void 0 && observed == null;
5737
5737
  return stored.wire_id === observed.wire_id && stored.sentence === observed.sentence;
5738
5738
  }
5739
+ async function queryStore(store, roomId, options) {
5740
+ if (store.query) return store.query(roomId, options);
5741
+ let records = await store.read(roomId);
5742
+ records = records.filter((record) => {
5743
+ const value = record;
5744
+ return (options.kind === void 0 || record.kind === options.kind) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.sourceMsgId === void 0 || value.source_msg_id === options.sourceMsgId) && (options.sourceFileId === void 0 || value.source_file_id === options.sourceFileId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity);
5745
+ });
5746
+ if (options.unresolvedResultKind) {
5747
+ const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
5748
+ records = records.filter((record) => !completed.has(record.record_id));
5749
+ }
5750
+ if (options.descending) records.reverse();
5751
+ return records.slice(0, options.limit);
5752
+ }
5739
5753
  function canonicalValue(value) {
5740
5754
  if (Array.isArray(value)) return value.map(canonicalValue);
5741
5755
  if (value !== null && typeof value === "object") {
@@ -5763,13 +5777,14 @@ function wireKind(category) {
5763
5777
  return "room_msg";
5764
5778
  }
5765
5779
  }
5766
- var INTAKE_BATCH_SIZE, IntakePump;
5780
+ var JOURNAL_WORK_BATCH_SIZE, INTAKE_BATCH_SIZE, IntakePump;
5767
5781
  var init_intake = __esm({
5768
5782
  "src/intake.ts"() {
5769
5783
  "use strict";
5770
5784
  init_zod();
5771
5785
  init_contracts();
5772
5786
  init_ulid();
5787
+ JOURNAL_WORK_BATCH_SIZE = 64;
5773
5788
  INTAKE_BATCH_SIZE = 32;
5774
5789
  IntakePump = class {
5775
5790
  store;
@@ -5877,8 +5892,8 @@ var init_intake = __esm({
5877
5892
  await packet.acknowledgeFile(item);
5878
5893
  return;
5879
5894
  }
5880
- const records = await this.store.read(roomId);
5881
- let file = this.findSourceFile(records, item);
5895
+ const [storedFile] = await queryStore(this.store, roomId, { kind: "file", sourceFileId: item.file_id, limit: 1 });
5896
+ let file = this.findSourceFile(storedFile === void 0 ? [] : [storedFile], item);
5882
5897
  if (!file) {
5883
5898
  const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
5884
5899
  const bytes = Buffer.from(item.data);
@@ -5916,8 +5931,8 @@ var init_intake = __esm({
5916
5931
  if (acknowledge) await this.acknowledgeMessage(roomId, packet, item);
5917
5932
  return;
5918
5933
  }
5919
- const before = await this.store.read(roomId);
5920
- let message = this.findSourceMessage(before, item);
5934
+ const [storedMessage] = await queryStore(this.store, roomId, { kind: "message", sourceMsgId: item.msg_id, limit: 1 });
5935
+ let message = this.findSourceMessage(storedMessage === void 0 ? [] : [storedMessage], item);
5921
5936
  if (!message) {
5922
5937
  const recipientIdentities = unique(room.seats.filter((recipient) => recipient.state === "active").map((recipient) => recipient.identity).filter((identity) => identity !== seat.identity));
5923
5938
  const appended = await this.store.append(roomId, {
@@ -5980,100 +5995,153 @@ var init_intake = __esm({
5980
5995
  }
5981
5996
  }
5982
5997
  async completeSnapshotIntents(roomId) {
5983
- const records = await this.store.read(roomId);
5984
- for (const message of records.filter(
5985
- (record) => record.kind === "message"
5986
- )) await this.completeMessageIntents(roomId, message);
5987
- for (const file of records.filter(
5988
- (record) => record.kind === "file"
5989
- )) await this.completeFileIntents(roomId, file);
5998
+ if (!this.store.recordsNeedingRelayIntents) {
5999
+ const records = await this.store.read(roomId);
6000
+ for (const message of records.filter(
6001
+ (record) => record.kind === "message"
6002
+ )) await this.completeMessageIntents(roomId, message);
6003
+ for (const file of records.filter(
6004
+ (record) => record.kind === "file"
6005
+ )) await this.completeFileIntents(roomId, file);
6006
+ return;
6007
+ }
6008
+ for (; ; ) {
6009
+ const records = await this.store.recordsNeedingRelayIntents(
6010
+ roomId,
6011
+ { limit: JOURNAL_WORK_BATCH_SIZE }
6012
+ );
6013
+ if (records.length === 0) return;
6014
+ for (const record of records) {
6015
+ if (record.kind === "message") await this.completeMessageIntents(roomId, record);
6016
+ else if (record.kind === "file") await this.completeFileIntents(roomId, record);
6017
+ }
6018
+ }
5990
6019
  }
5991
6020
  async completeFileIntents(roomId, file) {
5992
- const records = await this.store.read(roomId);
5993
- const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.file_id === file.file_id).map((intent) => intent.recipient_identity));
6021
+ if (this.store.relayRecipientsNeedingIntent) {
6022
+ for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, file.seq)) {
6023
+ await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
6024
+ }
6025
+ return;
6026
+ }
6027
+ const records = await queryStore(this.store, roomId, { kind: "relay_intent", fileId: file.file_id });
6028
+ const intended = new Set(records.map((record) => record.recipient_identity));
5994
6029
  for (const recipientIdentity of file.recipient_identities) {
5995
6030
  if (intended.has(recipientIdentity)) continue;
5996
- await this.store.append(roomId, {
5997
- version: 1,
5998
- kind: "relay_intent",
5999
- room_id: roomId,
6000
- at: this.now(),
6001
- file_id: file.file_id,
6002
- recipient_identity: recipientIdentity
6003
- });
6031
+ await this.appendFileIntent(roomId, file.file_id, recipientIdentity);
6004
6032
  intended.add(recipientIdentity);
6005
6033
  }
6006
6034
  }
6035
+ async appendFileIntent(roomId, fileId, recipientIdentity) {
6036
+ await this.store.append(roomId, {
6037
+ version: 1,
6038
+ kind: "relay_intent",
6039
+ room_id: roomId,
6040
+ at: this.now(),
6041
+ file_id: fileId,
6042
+ recipient_identity: recipientIdentity
6043
+ });
6044
+ }
6007
6045
  async completeMessageIntents(roomId, message) {
6008
- const records = await this.store.read(roomId);
6009
- const intended = new Set(records.filter((record) => record.kind === "relay_intent" && record.message_id === message.message_id).map((intent) => intent.recipient_identity));
6046
+ if (this.store.relayRecipientsNeedingIntent) {
6047
+ for (const recipientIdentity of await this.store.relayRecipientsNeedingIntent(roomId, message.seq)) {
6048
+ await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
6049
+ }
6050
+ return;
6051
+ }
6052
+ const records = await queryStore(this.store, roomId, { kind: "relay_intent", messageId: message.message_id });
6053
+ const intended = new Set(records.map((record) => record.recipient_identity));
6010
6054
  for (const recipientIdentity of message.recipient_identities) {
6011
6055
  if (intended.has(recipientIdentity)) continue;
6012
- await this.store.append(roomId, {
6013
- version: 1,
6014
- kind: "relay_intent",
6015
- room_id: roomId,
6016
- at: this.now(),
6017
- message_id: message.message_id,
6018
- recipient_identity: recipientIdentity
6019
- });
6056
+ await this.appendMessageIntent(roomId, message.message_id, recipientIdentity);
6020
6057
  intended.add(recipientIdentity);
6021
6058
  }
6022
6059
  }
6060
+ async appendMessageIntent(roomId, messageId, recipientIdentity) {
6061
+ await this.store.append(roomId, {
6062
+ version: 1,
6063
+ kind: "relay_intent",
6064
+ room_id: roomId,
6065
+ at: this.now(),
6066
+ message_id: messageId,
6067
+ recipient_identity: recipientIdentity
6068
+ });
6069
+ }
6023
6070
  async relayPendingUnlocked(roomId, packet) {
6024
- const records = await this.store.read(roomId);
6025
6071
  const room = await this.store.load(roomId);
6026
6072
  const activeCids = new Set(room.seats.filter((seat) => seat.state === "active").map((seat) => seat.identity));
6027
6073
  const removedCids = new Set(room.seats.filter((seat) => seat.state === "removed").map((seat) => seat.identity));
6028
- const messages = new Map(records.filter((record) => record.kind === "message").map((message) => [message.message_id, message]));
6029
- const files = new Map(records.filter((record) => record.kind === "file").map((file) => [file.file_id, file]));
6030
- const completed = new Set(records.filter((record) => record.kind === "relay_result").map((result) => result.kind === "relay_result" ? result.intent_record_id : ""));
6031
- for (const intent of records.filter(
6032
- (record) => record.kind === "relay_intent"
6033
- )) {
6034
- if (completed.has(intent.record_id)) continue;
6035
- const message = intent.message_id === void 0 ? void 0 : messages.get(intent.message_id);
6036
- const file = intent.file_id === void 0 ? void 0 : files.get(intent.file_id);
6037
- if (message === void 0 === (file === void 0)) continue;
6038
- const recipients = message?.recipient_identities ?? file.recipient_identities;
6039
- if (!recipients.includes(intent.recipient_identity)) continue;
6040
- if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6041
- const skipped = await this.store.append(roomId, {
6042
- version: 1,
6043
- kind: "relay_result",
6044
- room_id: roomId,
6045
- at: this.now(),
6046
- intent_record_id: intent.record_id,
6047
- ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6048
- ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6049
- recipient_identity: intent.recipient_identity,
6050
- status: "skipped_removed"
6051
- });
6052
- if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6053
- completed.add(intent.record_id);
6054
- continue;
6055
- }
6056
- if (file !== void 0) {
6057
- const author = file.author_alias === void 0 ? file.author : {
6058
- identity: file.author_alias.participant_id,
6059
- display_name: file.author_alias.alias,
6060
- role: file.author.role
6061
- };
6062
- const metadata = await sendRoomBody(packet, intent.recipient_identity, {
6063
- version: 1,
6064
- kind: "room_file",
6065
- room_id: roomId,
6066
- room_name: room.room_name,
6067
- file_id: file.file_id,
6068
- author,
6069
- filename: file.filename,
6070
- mime: file.mime,
6071
- size: file.size,
6072
- sha256: file.sha256,
6073
- at: file.at
6074
- });
6075
- if (metadata.status === "send_failed") {
6076
- const failed = await this.store.append(roomId, {
6074
+ let after = 0;
6075
+ for (; ; ) {
6076
+ const pending = await queryStore(this.store, roomId, {
6077
+ kind: "relay_intent",
6078
+ unresolvedResultKind: "relay_result",
6079
+ after,
6080
+ limit: JOURNAL_WORK_BATCH_SIZE
6081
+ });
6082
+ if (pending.length === 0) return;
6083
+ for (const intent of pending) {
6084
+ after = intent.seq;
6085
+ const [message] = intent.message_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "message", messageId: intent.message_id, limit: 1 });
6086
+ const [file] = intent.file_id === void 0 ? [] : await queryStore(this.store, roomId, { kind: "file", fileId: intent.file_id, limit: 1 });
6087
+ if (message === void 0 === (file === void 0)) continue;
6088
+ const recipients = message?.recipient_identities ?? file.recipient_identities;
6089
+ if (!recipients.includes(intent.recipient_identity)) continue;
6090
+ if (!activeCids.has(intent.recipient_identity) && removedCids.has(intent.recipient_identity)) {
6091
+ const skipped = await this.store.append(roomId, {
6092
+ version: 1,
6093
+ kind: "relay_result",
6094
+ room_id: roomId,
6095
+ at: this.now(),
6096
+ intent_record_id: intent.record_id,
6097
+ ...intent.message_id === void 0 ? {} : { message_id: intent.message_id },
6098
+ ...intent.file_id === void 0 ? {} : { file_id: intent.file_id },
6099
+ recipient_identity: intent.recipient_identity,
6100
+ status: "skipped_removed"
6101
+ });
6102
+ if (skipped.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6103
+ continue;
6104
+ }
6105
+ if (file !== void 0) {
6106
+ const author = file.author_alias === void 0 ? file.author : {
6107
+ identity: file.author_alias.participant_id,
6108
+ display_name: file.author_alias.alias,
6109
+ role: file.author.role
6110
+ };
6111
+ const metadata = await sendRoomBody(packet, intent.recipient_identity, {
6112
+ version: 1,
6113
+ kind: "room_file",
6114
+ room_id: roomId,
6115
+ room_name: room.room_name,
6116
+ file_id: file.file_id,
6117
+ author,
6118
+ filename: file.filename,
6119
+ mime: file.mime,
6120
+ size: file.size,
6121
+ sha256: file.sha256,
6122
+ at: file.at
6123
+ });
6124
+ if (metadata.status === "send_failed") {
6125
+ const failed = await this.store.append(roomId, {
6126
+ version: 1,
6127
+ kind: "relay_result",
6128
+ room_id: roomId,
6129
+ at: this.now(),
6130
+ intent_record_id: intent.record_id,
6131
+ file_id: file.file_id,
6132
+ recipient_identity: intent.recipient_identity,
6133
+ status: "send_failed"
6134
+ });
6135
+ if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6136
+ continue;
6137
+ }
6138
+ const outcome2 = await packet.sendFile(
6139
+ intent.recipient_identity,
6140
+ file.filename,
6141
+ file.mime,
6142
+ Buffer.from(file.data_base64, "base64")
6143
+ );
6144
+ const appended2 = await this.store.append(roomId, {
6077
6145
  version: 1,
6078
6146
  kind: "relay_result",
6079
6147
  room_id: roomId,
@@ -6081,66 +6149,45 @@ var init_intake = __esm({
6081
6149
  intent_record_id: intent.record_id,
6082
6150
  file_id: file.file_id,
6083
6151
  recipient_identity: intent.recipient_identity,
6084
- status: "send_failed"
6152
+ status: outcome2.status,
6153
+ ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6154
+ ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6085
6155
  });
6086
- if (failed.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6087
- completed.add(intent.record_id);
6156
+ if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6088
6157
  continue;
6089
6158
  }
6090
- const outcome2 = await packet.sendFile(
6091
- intent.recipient_identity,
6092
- file.filename,
6093
- file.mime,
6094
- Buffer.from(file.data_base64, "base64")
6095
- );
6096
- const appended2 = await this.store.append(roomId, {
6159
+ const unsigned = {
6160
+ version: 1,
6161
+ kind: wireKind(message.category),
6162
+ room_id: roomId,
6163
+ room_name: room.room_name,
6164
+ message_id: message.message_id,
6165
+ // An anonymous author leaves the archive only in alias form.
6166
+ author: message.author_alias === void 0 ? message.author : {
6167
+ identity: message.author_alias.participant_id,
6168
+ display_name: message.author_alias.alias,
6169
+ role: message.author.role
6170
+ },
6171
+ text: message.text,
6172
+ at: message.at,
6173
+ ...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
6174
+ ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6175
+ ...message.membership === void 0 ? {} : { membership: message.membership }
6176
+ };
6177
+ const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
6178
+ const appended = await this.store.append(roomId, {
6097
6179
  version: 1,
6098
6180
  kind: "relay_result",
6099
6181
  room_id: roomId,
6100
6182
  at: this.now(),
6101
6183
  intent_record_id: intent.record_id,
6102
- file_id: file.file_id,
6184
+ message_id: intent.message_id,
6103
6185
  recipient_identity: intent.recipient_identity,
6104
- status: outcome2.status,
6105
- ...outcome2.wire_id === void 0 || outcome2.wire_id === "" ? {} : { wire_id: outcome2.wire_id },
6106
- ...metadata.wire_id === void 0 || metadata.wire_id === "" ? {} : { metadata_wire_id: metadata.wire_id }
6186
+ status: outcome.status,
6187
+ ...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
6107
6188
  });
6108
- if (appended2.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6109
- completed.add(intent.record_id);
6110
- continue;
6189
+ if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6111
6190
  }
6112
- const unsigned = {
6113
- version: 1,
6114
- kind: wireKind(message.category),
6115
- room_id: roomId,
6116
- room_name: room.room_name,
6117
- message_id: message.message_id,
6118
- // An anonymous author leaves the archive only in alias form.
6119
- author: message.author_alias === void 0 ? message.author : {
6120
- identity: message.author_alias.participant_id,
6121
- display_name: message.author_alias.alias,
6122
- role: message.author.role
6123
- },
6124
- text: message.text,
6125
- at: message.at,
6126
- ...message.briefing_role === void 0 ? {} : { briefing_role: message.briefing_role },
6127
- ...message.briefing_version === void 0 ? {} : { briefing_version: message.briefing_version },
6128
- ...message.membership === void 0 ? {} : { membership: message.membership }
6129
- };
6130
- const outcome = await sendRoomBody(packet, intent.recipient_identity, unsigned);
6131
- const appended = await this.store.append(roomId, {
6132
- version: 1,
6133
- kind: "relay_result",
6134
- room_id: roomId,
6135
- at: this.now(),
6136
- intent_record_id: intent.record_id,
6137
- message_id: intent.message_id,
6138
- recipient_identity: intent.recipient_identity,
6139
- status: outcome.status,
6140
- ...outcome.wire_id === void 0 || outcome.wire_id === "" ? {} : { wire_id: outcome.wire_id }
6141
- });
6142
- if (appended.kind !== "relay_result") throw new Error("storage returned the wrong relay result kind");
6143
- completed.add(intent.record_id);
6144
6191
  }
6145
6192
  }
6146
6193
  findSourceMessage(records, item) {
@@ -6199,6 +6246,21 @@ function byteBoundedHistoryPage(records) {
6199
6246
  function activeSeats(room) {
6200
6247
  return room.seats.filter((seat) => seat.state === "active");
6201
6248
  }
6249
+ async function queryStore2(store, roomId, options) {
6250
+ if (store.query) return store.query(roomId, options);
6251
+ let records = await store.read(roomId);
6252
+ records = records.filter((record) => {
6253
+ const value = record;
6254
+ const membership = value.membership;
6255
+ return (options.kind === void 0 || record.kind === options.kind) && (options.after === void 0 || record.seq > options.after) && (options.messageId === void 0 || value.message_id === options.messageId) && (options.fileId === void 0 || value.file_id === options.fileId) && (options.intentRecordId === void 0 || value.intent_record_id === options.intentRecordId) && (options.recipientIdentity === void 0 || value.recipient_identity === options.recipientIdentity) && (options.category === void 0 || value.category === options.category) && (options.membershipEpoch === void 0 || membership?.epoch === options.membershipEpoch);
6256
+ });
6257
+ if (options.unresolvedResultKind) {
6258
+ const completed = new Set((await store.read(roomId)).filter((record) => record.kind === options.unresolvedResultKind).map((record) => record.intent_record_id));
6259
+ records = records.filter((record) => !completed.has(record.record_id));
6260
+ }
6261
+ if (options.descending) records.reverse();
6262
+ return records.slice(0, options.limit ?? Number.MAX_SAFE_INTEGER);
6263
+ }
6202
6264
  function isCancelledExternalSeat(seat) {
6203
6265
  return seat.state === "removed" && seat.accepted_at === void 0 && seat.requested_at !== void 0 && seat.invite_sha256 !== void 0;
6204
6266
  }
@@ -6211,7 +6273,7 @@ function uniqueIdentities(identities) {
6211
6273
  function currentContactIdentities(packet) {
6212
6274
  return new Set(packet.listContacts().map((contact) => contact.container_id));
6213
6275
  }
6214
- var CreateInviteInputSchema, HistoryOptionsSchema, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6276
+ var CreateInviteInputSchema, HistoryOptionsSchema, JOURNAL_WORK_BATCH_SIZE2, DeleteRoomInputSchema, RemoveParticipantInputSchema, ReplaceParticipantInputSchema, RoomServiceError, RoomService;
6215
6277
  var init_service = __esm({
6216
6278
  "src/service.ts"() {
6217
6279
  "use strict";
@@ -6238,6 +6300,7 @@ var init_service = __esm({
6238
6300
  limit: external_exports.number().int().positive().safe().optional(),
6239
6301
  view: external_exports.enum(["operator", "participant"]).optional()
6240
6302
  }).strict();
6303
+ JOURNAL_WORK_BATCH_SIZE2 = 64;
6241
6304
  DeleteRoomInputSchema = external_exports.object({
6242
6305
  confirm: external_exports.literal(true)
6243
6306
  }).strict();
@@ -6720,8 +6783,11 @@ var init_service = __esm({
6720
6783
  membership_epoch: Math.max(current.membership_epoch, intent.epoch)
6721
6784
  }));
6722
6785
  }
6723
- const records = await this.store.read(current.room_id);
6724
- const existing = records.find((record) => record.kind === "membership_result" && record.intent_record_id === intent.record_id);
6786
+ const [existing] = await queryStore2(this.store, current.room_id, {
6787
+ kind: "membership_result",
6788
+ intentRecordId: intent.record_id,
6789
+ limit: 1
6790
+ });
6725
6791
  let outcome;
6726
6792
  if (existing !== void 0 && existing.kind === "membership_result") {
6727
6793
  outcome = {
@@ -6760,8 +6826,13 @@ var init_service = __esm({
6760
6826
  };
6761
6827
  }
6762
6828
  async ensureMembershipNotice(room, intent) {
6763
- const records = await this.store.read(room.room_id);
6764
- const already = records.some((record) => record.kind === "message" && record.category === "membership" && record.membership?.action === "remove" && record.membership.epoch === intent.epoch);
6829
+ const records = await queryStore2(this.store, room.room_id, {
6830
+ kind: "message",
6831
+ category: "membership",
6832
+ membershipEpoch: intent.epoch,
6833
+ limit: 1
6834
+ });
6835
+ const already = records.length > 0;
6765
6836
  if (already) return;
6766
6837
  const remaining = activeSeats(room);
6767
6838
  if (remaining.length === 0) return;
@@ -6971,7 +7042,7 @@ var init_service = __esm({
6971
7042
  async history(roomId, options = {}) {
6972
7043
  const id = LowerCrockfordUlidSchema.parse(roomId);
6973
7044
  const { view, ...page } = HistoryOptionsSchema.parse(options);
6974
- const records = await this.store.read(id, view === "participant" ? { after: page.after } : page);
7045
+ const records = view === "participant" ? await queryStore2(this.store, id, { kind: "message", after: page.after, limit: page.limit }) : await this.store.read(id, page);
6975
7046
  if (view !== "participant") return byteBoundedHistoryPage(records);
6976
7047
  const projected = records.filter((record) => record.kind === "message").map((record) => {
6977
7048
  const {
@@ -7238,13 +7309,19 @@ var init_service = __esm({
7238
7309
  invites,
7239
7310
  membership_epoch: room.membership_epoch + activatedPending.length + newSeats.length
7240
7311
  });
7241
- const journal = await this.store.read(next.room_id);
7242
- const completedIntents = new Set(journal.filter((record) => record.kind === "membership_result").map((record) => record.kind === "membership_result" ? record.intent_record_id : ""));
7243
- for (const intent of journal.filter(
7244
- (record) => record.kind === "membership_intent"
7245
- )) {
7246
- if (completedIntents.has(intent.record_id)) continue;
7247
- ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7312
+ let membershipAfter = 0;
7313
+ for (; ; ) {
7314
+ const journal = await queryStore2(this.store, next.room_id, {
7315
+ kind: "membership_intent",
7316
+ unresolvedResultKind: "membership_result",
7317
+ after: membershipAfter,
7318
+ limit: JOURNAL_WORK_BATCH_SIZE2
7319
+ });
7320
+ if (journal.length === 0) break;
7321
+ for (const intent of journal) {
7322
+ membershipAfter = intent.seq;
7323
+ ({ room: next } = await this.completeRemovalUnlocked(next, intent));
7324
+ }
7248
7325
  }
7249
7326
  const requirementsMet = invites.filter((invite) => invite.state !== "revoked").every((invite) => invite.accepted_cids.length >= invite.min_accepts);
7250
7327
  const admitted = [...activatedPending, ...newSeats];
@@ -7261,23 +7338,31 @@ var init_service = __esm({
7261
7338
  const packet = this.packets.get(roomId);
7262
7339
  if (packet) {
7263
7340
  await packet.refreshContacts();
7264
- let records = await this.store.read(roomId);
7265
7341
  let contacts = currentContactIdentities(packet);
7266
- const completed = new Set(records.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7267
- const pending = records.filter(
7268
- (record) => record.kind === "close_notice_intent" && !completed.has(record.record_id)
7269
- );
7270
- for (const intent of pending) {
7271
- if (contacts.has(intent.recipient_identity)) continue;
7272
- await this.appendUncertainCloseResult(roomId, intent);
7273
- completed.add(intent.record_id);
7342
+ let closeAfter = 0;
7343
+ for (; ; ) {
7344
+ const pending = await queryStore2(this.store, roomId, {
7345
+ kind: "close_notice_intent",
7346
+ unresolvedResultKind: "close_notice_result",
7347
+ after: closeAfter,
7348
+ limit: JOURNAL_WORK_BATCH_SIZE2
7349
+ });
7350
+ if (pending.length === 0) break;
7351
+ for (const intent of pending) {
7352
+ closeAfter = intent.seq;
7353
+ if (contacts.has(intent.recipient_identity)) continue;
7354
+ await this.appendUncertainCloseResult(roomId, intent);
7355
+ }
7274
7356
  }
7275
7357
  for (const recipientIdentity of contacts) {
7276
- records = await this.store.read(roomId);
7277
- const currentCompleted = new Set(records.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7278
- let intent = [...records].reverse().find(
7279
- (record) => record.kind === "close_notice_intent" && record.recipient_identity === recipientIdentity && !currentCompleted.has(record.record_id)
7280
- );
7358
+ const [existingIntent] = await queryStore2(this.store, roomId, {
7359
+ kind: "close_notice_intent",
7360
+ recipientIdentity,
7361
+ unresolvedResultKind: "close_notice_result",
7362
+ descending: true,
7363
+ limit: 1
7364
+ });
7365
+ let intent = existingIntent;
7281
7366
  if (!intent) {
7282
7367
  const appended2 = await this.store.append(roomId, {
7283
7368
  version: 1,
@@ -7320,13 +7405,19 @@ var init_service = __esm({
7320
7405
  `room "${roomId}" live-state purge left residue: ${residue.join(", ") || "packet registry entry"}`
7321
7406
  );
7322
7407
  }
7323
- const afterPurge = await this.store.read(roomId);
7324
- const completedAfterPurge = new Set(afterPurge.filter((record) => record.kind === "close_notice_result").map((record) => record.kind === "close_notice_result" ? record.intent_record_id : ""));
7325
- for (const intent of afterPurge.filter(
7326
- (record) => record.kind === "close_notice_intent" && !completedAfterPurge.has(record.record_id)
7327
- )) {
7328
- await this.appendUncertainCloseResult(roomId, intent);
7329
- completedAfterPurge.add(intent.record_id);
7408
+ let purgeAfter = 0;
7409
+ for (; ; ) {
7410
+ const afterPurge = await queryStore2(this.store, roomId, {
7411
+ kind: "close_notice_intent",
7412
+ unresolvedResultKind: "close_notice_result",
7413
+ after: purgeAfter,
7414
+ limit: JOURNAL_WORK_BATCH_SIZE2
7415
+ });
7416
+ if (afterPurge.length === 0) break;
7417
+ for (const intent of afterPurge) {
7418
+ purgeAfter = intent.seq;
7419
+ await this.appendUncertainCloseResult(roomId, intent);
7420
+ }
7330
7421
  }
7331
7422
  return this.store.save(RoomSchema.parse({ ...room, state: "closed", closed_at: this.now() }));
7332
7423
  }
@@ -7386,18 +7477,29 @@ var init_service = __esm({
7386
7477
  });
7387
7478
  }
7388
7479
  async ensureBriefingKind(room, recipients, briefing) {
7389
- const records = await this.store.read(room.room_id);
7480
+ if (this.store.briefingDeliveryTimes) {
7481
+ const recipientIdentities = uniqueIdentities(recipients.map((seat) => seat.identity));
7482
+ const deliveries = await this.store.briefingDeliveryTimes(room.room_id, {
7483
+ category: briefing.category,
7484
+ briefingRole: briefing.briefing_role,
7485
+ briefingVersion: briefing.briefing_version
7486
+ }, recipientIdentities);
7487
+ const missing2 = recipients.filter((seat) => !deliveries.has(seat.identity));
7488
+ const appendedAt2 = await this.appendBriefingForMissing(room, missing2, briefing);
7489
+ return [...deliveries.values(), ...appendedAt2 === void 0 ? [] : [appendedAt2]].sort()[0] ?? this.now();
7490
+ }
7491
+ const records = await queryStore2(this.store, room.room_id, {
7492
+ kind: "message",
7493
+ category: briefing.category
7494
+ });
7390
7495
  const matching = records.filter((record) => record.kind === "message" && record.category === briefing.category && record.briefing_role === briefing.briefing_role && (record.briefing_version ?? 1) === briefing.briefing_version);
7391
- const intentsByMessage = /* @__PURE__ */ new Map();
7392
- for (const record of records) {
7393
- if (record.kind !== "relay_intent" || record.message_id === void 0) continue;
7394
- const intents = intentsByMessage.get(record.message_id) ?? /* @__PURE__ */ new Set();
7395
- intents.add(record.recipient_identity);
7396
- intentsByMessage.set(record.message_id, intents);
7397
- }
7398
7496
  const covered = /* @__PURE__ */ new Set();
7399
7497
  for (const message of matching) {
7400
- const intents = intentsByMessage.get(message.message_id) ?? /* @__PURE__ */ new Set();
7498
+ const intentRecords = await queryStore2(this.store, room.room_id, {
7499
+ kind: "relay_intent",
7500
+ messageId: message.message_id
7501
+ });
7502
+ const intents = new Set(intentRecords.map((record) => record.recipient_identity));
7401
7503
  for (const recipientIdentity of message.recipient_identities) {
7402
7504
  covered.add(recipientIdentity);
7403
7505
  if (!intents.has(recipientIdentity)) {
@@ -7413,35 +7515,36 @@ var init_service = __esm({
7413
7515
  }
7414
7516
  }
7415
7517
  const missing = recipients.filter((seat) => !covered.has(seat.identity));
7416
- let appendedAt;
7417
- if (missing.length > 0) {
7418
- const appended = await this.store.append(room.room_id, {
7518
+ const appendedAt = await this.appendBriefingForMissing(room, missing, briefing);
7519
+ return matching[0]?.at ?? appendedAt ?? this.now();
7520
+ }
7521
+ async appendBriefingForMissing(room, missing, briefing) {
7522
+ if (missing.length === 0) return void 0;
7523
+ const appended = await this.store.append(room.room_id, {
7524
+ version: 1,
7525
+ kind: "message",
7526
+ room_id: room.room_id,
7527
+ at: this.now(),
7528
+ message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7529
+ author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7530
+ category: briefing.category,
7531
+ ...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
7532
+ briefing_version: briefing.briefing_version,
7533
+ text: briefing.text,
7534
+ recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
7535
+ });
7536
+ if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
7537
+ for (const recipientIdentity of appended.recipient_identities) {
7538
+ await this.store.append(room.room_id, {
7419
7539
  version: 1,
7420
- kind: "message",
7540
+ kind: "relay_intent",
7421
7541
  room_id: room.room_id,
7422
7542
  at: this.now(),
7423
- message_id: LowerCrockfordUlidSchema.parse(this.nextMessageId()),
7424
- author: { identity: room.identity_cid, display_name: room.identity_name, role: ROOM_ROLE },
7425
- category: briefing.category,
7426
- ...briefing.briefing_role === void 0 ? {} : { briefing_role: briefing.briefing_role },
7427
- briefing_version: briefing.briefing_version,
7428
- text: briefing.text,
7429
- recipient_identities: uniqueIdentities(missing.map((seat) => seat.identity))
7543
+ message_id: appended.message_id,
7544
+ recipient_identity: recipientIdentity
7430
7545
  });
7431
- if (appended.kind !== "message") throw new RoomServiceError("storage returned the wrong briefing record kind");
7432
- appendedAt = appended.at;
7433
- for (const recipientIdentity of appended.recipient_identities) {
7434
- await this.store.append(room.room_id, {
7435
- version: 1,
7436
- kind: "relay_intent",
7437
- room_id: room.room_id,
7438
- at: this.now(),
7439
- message_id: appended.message_id,
7440
- recipient_identity: recipientIdentity
7441
- });
7442
- }
7443
7546
  }
7444
- return matching[0]?.at ?? appendedAt ?? this.now();
7547
+ return appended.at;
7445
7548
  }
7446
7549
  lock(roomId, work) {
7447
7550
  return this.store.mutex(roomId).runExclusive(work);
@@ -7475,8 +7578,9 @@ var init_service = __esm({
7475
7578
  import { randomBytes as randomBytes3 } from "node:crypto";
7476
7579
  import * as nodeFs2 from "node:fs";
7477
7580
  import { AsyncLocalStorage } from "node:async_hooks";
7478
- import { dirname as dirname2, join as join3 } from "node:path";
7479
- var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
7581
+ import { basename, dirname as dirname2, join as join3 } from "node:path";
7582
+ import Database from "better-sqlite3";
7583
+ var DIRECTORY_MODE2, FILE_MODE2, NO_FOLLOW2, SQLITE_SCHEMA_VERSION, DEFAULT_WORK_BATCH_SIZE, utf8Decoder, CoworkStorageError, RoomQueue, CoworkStore;
7480
7584
  var init_storage = __esm({
7481
7585
  "src/storage.ts"() {
7482
7586
  "use strict";
@@ -7485,6 +7589,8 @@ var init_storage = __esm({
7485
7589
  DIRECTORY_MODE2 = 448;
7486
7590
  FILE_MODE2 = 384;
7487
7591
  NO_FOLLOW2 = nodeFs2.constants.O_NOFOLLOW ?? 0;
7592
+ SQLITE_SCHEMA_VERSION = 1;
7593
+ DEFAULT_WORK_BATCH_SIZE = 64;
7488
7594
  utf8Decoder = new TextDecoder("utf-8", { fatal: true });
7489
7595
  CoworkStorageError = class extends Error {
7490
7596
  constructor(message, options) {
@@ -7512,20 +7618,20 @@ var init_storage = __esm({
7512
7618
  CoworkStore = class {
7513
7619
  stateDir;
7514
7620
  fs;
7515
- // The standalone daemon is the sole writer. This is intentionally an
7516
- // in-process room FIFO, not an on-disk lock with stale-owner recovery.
7621
+ beforeRecordCommit;
7517
7622
  roomMutexes = /* @__PURE__ */ new Map();
7518
7623
  lockOwnership = new AsyncLocalStorage();
7519
- nextSequences = /* @__PURE__ */ new Map();
7624
+ reconciledBlobRooms = /* @__PURE__ */ new Set();
7520
7625
  constructor(stateDir, options = {}) {
7521
7626
  if (!stateDir) throw new CoworkStorageError("state directory is required");
7522
7627
  this.stateDir = stateDir;
7523
7628
  this.fs = options.fs ?? nodeFs2;
7629
+ this.beforeRecordCommit = options.beforeRecordCommit;
7524
7630
  }
7525
7631
  mutex(roomId, work) {
7526
- const validRoomId = this.roomId(roomId);
7527
- if (work) return this.withRoomMutex(validRoomId, work);
7528
- return { runExclusive: (nested) => this.withRoomMutex(validRoomId, nested) };
7632
+ const id = this.roomId(roomId);
7633
+ if (work) return this.withRoomMutex(id, work);
7634
+ return { runExclusive: (nested) => this.withRoomMutex(id, nested) };
7529
7635
  }
7530
7636
  withRoomMutex(roomId, work) {
7531
7637
  const inherited = this.lockOwnership.getStore();
@@ -7533,15 +7639,10 @@ var init_storage = __esm({
7533
7639
  if (ownership?.active) {
7534
7640
  const nested = Promise.resolve().then(work);
7535
7641
  ownership.pending.add(nested);
7536
- void nested.then(
7537
- () => {
7538
- ownership.pending.delete(nested);
7539
- },
7540
- (error) => {
7541
- ownership.pending.delete(nested);
7542
- ownership.failures.push(error);
7543
- }
7544
- );
7642
+ void nested.then(() => ownership.pending.delete(nested), (error) => {
7643
+ ownership.pending.delete(nested);
7644
+ ownership.failures.push(error);
7645
+ });
7545
7646
  return nested;
7546
7647
  }
7547
7648
  let queue = this.roomMutexes.get(roomId);
@@ -7563,9 +7664,7 @@ var init_storage = __esm({
7563
7664
  rootFailed = true;
7564
7665
  rootFailure = error;
7565
7666
  }
7566
- while (acquired.pending.size > 0) {
7567
- await Promise.allSettled([...acquired.pending]);
7568
- }
7667
+ while (acquired.pending.size > 0) await Promise.allSettled([...acquired.pending]);
7569
7668
  } finally {
7570
7669
  acquired.active = false;
7571
7670
  }
@@ -7583,20 +7682,20 @@ var init_storage = __esm({
7583
7682
  this.rejectSymlink(roomDir, "room directory");
7584
7683
  throw new CoworkStorageError(`room "${room.room_id}" already exists`);
7585
7684
  }
7586
- let roomCreated = false;
7685
+ let created = false;
7587
7686
  try {
7588
7687
  this.fs.mkdirSync(roomDir, { mode: DIRECTORY_MODE2 });
7589
- roomCreated = true;
7688
+ created = true;
7590
7689
  this.fs.chmodSync(roomDir, DIRECTORY_MODE2);
7690
+ this.fs.mkdirSync(this.blobsDirectory(room.room_id), { mode: DIRECTORY_MODE2 });
7691
+ this.fs.chmodSync(this.blobsDirectory(room.room_id), DIRECTORY_MODE2);
7591
7692
  this.fsyncDirectory(roomDir);
7592
7693
  this.fsyncDirectory(this.roomsDirectory());
7593
- this.createArchive(room.room_id);
7694
+ this.withDatabase(room.room_id, () => void 0, true);
7594
7695
  this.atomicMetadataWrite(this.metadataPath(room.room_id), room);
7595
- this.nextSequences.set(room.room_id, 1);
7596
7696
  return room;
7597
7697
  } catch (error) {
7598
- this.nextSequences.delete(room.room_id);
7599
- if (roomCreated) {
7698
+ if (created) {
7600
7699
  try {
7601
7700
  this.fs.rmSync(roomDir, { recursive: true, force: true });
7602
7701
  this.fsyncDirectory(this.roomsDirectory());
@@ -7608,8 +7707,8 @@ var init_storage = __esm({
7608
7707
  });
7609
7708
  }
7610
7709
  async load(roomId) {
7611
- const validRoomId = this.roomId(roomId);
7612
- return this.mutex(validRoomId, () => this.loadUnlocked(validRoomId));
7710
+ const id = this.roomId(roomId);
7711
+ return this.mutex(id, () => this.loadUnlocked(id));
7613
7712
  }
7614
7713
  async save(input) {
7615
7714
  const room = RoomSchema.parse(input);
@@ -7621,176 +7720,435 @@ var init_storage = __esm({
7621
7720
  }
7622
7721
  async list() {
7623
7722
  this.ensureBaseDirectories();
7624
- const roomIds = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
7723
+ const ids = this.fs.readdirSync(this.roomsDirectory(), { withFileTypes: true }).filter((entry) => entry.isDirectory() && LowerCrockfordUlidSchema.safeParse(entry.name).success).map((entry) => entry.name).sort();
7625
7724
  const rooms = [];
7626
- for (const roomId of roomIds) rooms.push(await this.load(roomId));
7725
+ for (const id of ids) rooms.push(await this.load(id));
7627
7726
  return rooms;
7628
7727
  }
7629
7728
  async append(roomId, input) {
7630
- const validRoomId = this.roomId(roomId);
7729
+ const id = this.roomId(roomId);
7631
7730
  const draft = AppendRecordSchema.parse(input);
7632
- if (draft.room_id !== validRoomId) {
7633
- throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${validRoomId}"`);
7634
- }
7635
- return this.mutex(validRoomId, () => {
7636
- this.assertRoomDirectory(validRoomId);
7637
- const archivePath = this.archivePath(validRoomId);
7638
- this.assertRegularFile(archivePath, "room archive");
7639
- let nextSequence = this.nextSequences.get(validRoomId);
7640
- if (nextSequence === void 0) {
7641
- nextSequence = this.scanArchive(validRoomId).nextSequence;
7642
- }
7643
- const record = CommunicationRecordSchema.parse({
7644
- ...draft,
7645
- seq: nextSequence,
7646
- record_id: `${validRoomId}:${nextSequence}`
7647
- });
7648
- const bytes = Buffer.from(`${JSON.stringify(record)}
7649
- `, "utf8");
7650
- let fd;
7651
- let originalSize;
7652
- let writeStarted = false;
7731
+ if (draft.room_id !== id) throw new CoworkStorageError(`record room_id "${draft.room_id}" does not match room "${id}"`);
7732
+ return this.mutex(id, () => {
7733
+ this.assertRoomDirectory(id);
7734
+ if (!this.reconciledBlobRooms.has(id)) this.withDatabase(id, () => void 0);
7735
+ let blob;
7736
+ let storedDraft = draft;
7737
+ if (draft.kind === "file") {
7738
+ const bytes = Buffer.from(draft.data_base64, "base64");
7739
+ blob = this.persistBlob(id, draft.sha256, bytes);
7740
+ const { data_base64: _bytes, ...withoutBytes } = draft;
7741
+ storedDraft = withoutBytes;
7742
+ }
7653
7743
  try {
7654
- fd = this.fs.openSync(
7655
- archivePath,
7656
- nodeFs2.constants.O_WRONLY | nodeFs2.constants.O_APPEND | NO_FOLLOW2
7657
- );
7658
- const opened = this.validateOpenPath(fd, archivePath, "room archive", "file", true);
7659
- originalSize = opened.size;
7660
- this.fs.fchmodSync(fd, FILE_MODE2);
7661
- writeStarted = true;
7662
- this.writeAll(fd, bytes);
7663
- this.fs.fsyncSync(fd);
7664
- this.nextSequences.set(validRoomId, nextSequence + 1);
7665
- return record;
7744
+ return this.withDatabase(id, (db) => {
7745
+ const transaction = db.transaction(() => {
7746
+ const next = db.prepare("SELECT COALESCE(MAX(seq), 0) + 1 AS next FROM records").get().next;
7747
+ const record = { ...draft, seq: next, record_id: `${id}:${next}` };
7748
+ const stored = { ...storedDraft, seq: next, record_id: record.record_id };
7749
+ const values = this.indexValues(record);
7750
+ db.prepare(`INSERT INTO records
7751
+ (seq, record_id, kind, at, payload_json, blob_path, message_id, file_id, intent_record_id,
7752
+ recipient_identity, source_msg_id, source_file_id, category, briefing_role, briefing_version, membership_epoch)
7753
+ VALUES (@seq,@record_id,@kind,@at,@payload_json,@blob_path,@message_id,@file_id,@intent_record_id,
7754
+ @recipient_identity,@source_msg_id,@source_file_id,@category,@briefing_role,@briefing_version,@membership_epoch)`).run({ ...values, payload_json: JSON.stringify(stored), blob_path: blob?.path ?? null });
7755
+ if (record.kind === "message" || record.kind === "file") {
7756
+ const insert = db.prepare(`INSERT INTO record_recipients
7757
+ (record_seq, recipient_identity, category, briefing_role, briefing_version)
7758
+ VALUES (?, ?, ?, ?, ?)`);
7759
+ const enqueue = db.prepare("INSERT INTO relay_intent_work(record_seq, recipient_identity) VALUES (?, ?)");
7760
+ for (const recipient of record.recipient_identities) {
7761
+ insert.run(
7762
+ record.seq,
7763
+ recipient,
7764
+ record.kind === "message" ? record.category : null,
7765
+ record.kind === "message" ? record.briefing_role ?? null : null,
7766
+ record.kind === "message" ? record.briefing_version ?? 1 : null
7767
+ );
7768
+ enqueue.run(record.seq, recipient);
7769
+ }
7770
+ } else if (record.kind === "relay_intent") {
7771
+ const sourceColumn = record.message_id === void 0 ? "file_id" : "message_id";
7772
+ const sourceId = record.message_id ?? record.file_id;
7773
+ db.prepare(`DELETE FROM relay_intent_work
7774
+ WHERE recipient_identity = ? AND record_seq IN (
7775
+ SELECT seq FROM records WHERE kind = ? AND ${sourceColumn} = ?
7776
+ )`).run(record.recipient_identity, record.message_id === void 0 ? "file" : "message", sourceId);
7777
+ }
7778
+ this.beforeRecordCommit?.();
7779
+ return record;
7780
+ });
7781
+ return transaction.immediate();
7782
+ });
7666
7783
  } catch (error) {
7667
- this.nextSequences.delete(validRoomId);
7668
- if (fd !== void 0 && originalSize !== void 0 && writeStarted) {
7669
- try {
7670
- this.fs.ftruncateSync(fd, originalSize);
7671
- this.fs.fsyncSync(fd);
7672
- } catch {
7673
- }
7674
- }
7675
- throw this.wrap(`failed to append room "${validRoomId}" archive`, error);
7676
- } finally {
7677
- if (fd !== void 0) {
7678
- try {
7679
- this.fs.closeSync(fd);
7680
- } catch {
7681
- }
7682
- }
7784
+ if (blob?.created) this.removeUnreferencedBlob(id, blob.path);
7785
+ throw this.wrap(`failed to append room "${id}" archive`, error);
7683
7786
  }
7684
7787
  });
7685
7788
  }
7686
7789
  async read(roomId, options = {}) {
7687
- const validRoomId = this.roomId(roomId);
7790
+ const id = this.roomId(roomId);
7688
7791
  const after = options.after ?? 0;
7689
7792
  const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
7690
- if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
7793
+ this.validatePage(after, limit);
7794
+ return this.mutex(id, () => {
7795
+ this.assertRoomDirectory(id);
7796
+ return this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(
7797
+ "SELECT seq,payload_json,blob_path FROM records WHERE seq > ? ORDER BY seq ASC LIMIT ?"
7798
+ ).all(after, limit)));
7799
+ });
7800
+ }
7801
+ async query(roomId, options) {
7802
+ const id = this.roomId(roomId);
7803
+ const limit = options.limit ?? Number.MAX_SAFE_INTEGER;
7691
7804
  if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
7692
- return this.mutex(validRoomId, () => {
7693
- this.assertRoomDirectory(validRoomId);
7694
- const scan = this.scanArchive(validRoomId);
7695
- this.nextSequences.set(validRoomId, scan.nextSequence);
7696
- return scan.records.filter((record) => record.seq > after).slice(0, limit);
7805
+ return this.mutex(id, () => {
7806
+ this.assertRoomDirectory(id);
7807
+ return this.withDatabase(id, (db) => {
7808
+ const clauses = [];
7809
+ const values = [];
7810
+ const add = (column, value) => {
7811
+ if (value !== void 0) {
7812
+ clauses.push(`r.${column} = ?`);
7813
+ values.push(value);
7814
+ }
7815
+ };
7816
+ add("kind", options.kind);
7817
+ add("message_id", options.messageId);
7818
+ add("file_id", options.fileId);
7819
+ add("source_msg_id", options.sourceMsgId);
7820
+ add("source_file_id", options.sourceFileId);
7821
+ add("intent_record_id", options.intentRecordId);
7822
+ add("recipient_identity", options.recipientIdentity);
7823
+ add("category", options.category);
7824
+ add("membership_epoch", options.membershipEpoch);
7825
+ if (options.after !== void 0) {
7826
+ clauses.push("r.seq > ?");
7827
+ values.push(options.after);
7828
+ }
7829
+ if (options.unresolvedResultKind) {
7830
+ clauses.push("NOT EXISTS (SELECT 1 FROM records result WHERE result.kind = ? AND result.intent_record_id = r.record_id)");
7831
+ values.push(options.unresolvedResultKind);
7832
+ }
7833
+ values.push(limit);
7834
+ const sql = `SELECT r.seq,r.payload_json,r.blob_path FROM records r${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY r.seq ${options.descending ? "DESC" : "ASC"} LIMIT ?`;
7835
+ return this.decodeRows(id, db.prepare(sql).all(...values));
7836
+ });
7697
7837
  });
7698
7838
  }
7839
+ async recipients(roomId, recordSeq) {
7840
+ const id = this.roomId(roomId);
7841
+ return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
7842
+ "SELECT recipient_identity FROM record_recipients WHERE record_seq = ? ORDER BY recipient_identity"
7843
+ ).all(recordSeq).map((row) => row.recipient_identity)));
7844
+ }
7845
+ async recordsNeedingRelayIntents(roomId, options = {}) {
7846
+ const id = this.roomId(roomId);
7847
+ const after = options.after ?? 0;
7848
+ const limit = options.limit ?? DEFAULT_WORK_BATCH_SIZE;
7849
+ this.validatePage(after, limit);
7850
+ return this.mutex(id, () => this.withDatabase(id, (db) => this.decodeRows(id, db.prepare(`
7851
+ SELECT source.seq,source.payload_json,source.blob_path
7852
+ FROM relay_intent_work work INDEXED BY relay_work_source
7853
+ JOIN records source ON source.seq = work.record_seq
7854
+ WHERE work.record_seq > ?
7855
+ GROUP BY source.seq
7856
+ ORDER BY source.seq ASC
7857
+ LIMIT ?
7858
+ `).all(after, limit))));
7859
+ }
7860
+ async relayRecipientsNeedingIntent(roomId, recordSeq, limit = DEFAULT_WORK_BATCH_SIZE) {
7861
+ const id = this.roomId(roomId);
7862
+ if (!Number.isSafeInteger(recordSeq) || recordSeq < 1) throw new CoworkStorageError("record sequence must be a positive safe integer");
7863
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
7864
+ return this.mutex(id, () => this.withDatabase(id, (db) => db.prepare(
7865
+ "SELECT recipient_identity FROM relay_intent_work WHERE record_seq = ? ORDER BY recipient_identity LIMIT ?"
7866
+ ).all(recordSeq, limit).map((row) => row.recipient_identity)));
7867
+ }
7868
+ async briefingDeliveryTimes(roomId, key, recipientIdentities) {
7869
+ const id = this.roomId(roomId);
7870
+ if (recipientIdentities.length === 0) return /* @__PURE__ */ new Map();
7871
+ return this.mutex(id, () => this.withDatabase(id, (db) => {
7872
+ const lookup = db.prepare(`SELECT records.at FROM record_recipients recipients
7873
+ INDEXED BY recipients_briefing_delivery
7874
+ JOIN records ON records.seq = recipients.record_seq
7875
+ WHERE recipients.recipient_identity = ? AND recipients.category = ?
7876
+ AND recipients.briefing_role IS ? AND recipients.briefing_version = ?
7877
+ ORDER BY recipients.record_seq ASC LIMIT 1`);
7878
+ const deliveries = /* @__PURE__ */ new Map();
7879
+ for (const recipient of recipientIdentities) {
7880
+ const row = lookup.get(
7881
+ recipient,
7882
+ key.category,
7883
+ key.briefingRole ?? null,
7884
+ key.briefingVersion
7885
+ );
7886
+ if (row) deliveries.set(recipient, row.at);
7887
+ }
7888
+ return deliveries;
7889
+ }));
7890
+ }
7891
+ async durability(roomId) {
7892
+ const id = this.roomId(roomId);
7893
+ return this.mutex(id, () => this.withDatabase(id, (db) => ({
7894
+ journalMode: db.pragma("journal_mode", { simple: true }),
7895
+ synchronous: db.pragma("synchronous", { simple: true })
7896
+ })));
7897
+ }
7699
7898
  async delete(roomId) {
7700
- const validRoomId = this.roomId(roomId);
7701
- await this.mutex(validRoomId, () => {
7899
+ const id = this.roomId(roomId);
7900
+ await this.mutex(id, () => {
7702
7901
  this.ensureBaseDirectories();
7703
- const roomDir = this.roomDirectory(validRoomId);
7902
+ const roomDir = this.roomDirectory(id);
7704
7903
  if (!this.lstatIfPresent(roomDir)) {
7705
7904
  this.fsyncDirectory(this.roomsDirectory());
7706
- this.nextSequences.delete(validRoomId);
7707
7905
  return;
7708
7906
  }
7709
- this.ensurePrivateDirectory(roomDir, false, `room "${validRoomId}" directory`);
7710
- const archivePath = this.archivePath(validRoomId);
7711
- const metadataPath = this.metadataPath(validRoomId);
7712
- const archivePresent = this.lstatIfPresent(archivePath) !== void 0;
7713
- const metadataPresent = this.lstatIfPresent(metadataPath) !== void 0;
7907
+ this.ensurePrivateDirectory(roomDir, false, `room "${id}" directory`);
7908
+ const metadata = this.metadataPath(id);
7909
+ const metadataPresent = this.lstatIfPresent(metadata) !== void 0;
7910
+ const archivePresent = this.lstatIfPresent(this.archivePath(id)) !== void 0;
7911
+ const blobsPresent = this.lstatIfPresent(this.blobsDirectory(id)) !== void 0;
7714
7912
  if (metadataPresent) {
7715
- const room = this.loadUnlocked(validRoomId);
7716
- if (room.state !== "closed") {
7717
- throw new CoworkStorageError(`room "${validRoomId}" must be closed before deletion`);
7718
- }
7913
+ const room = this.loadUnlocked(id);
7914
+ if (room.state !== "closed") throw new CoworkStorageError(`room "${id}" must be closed before deletion`);
7719
7915
  this.removeProvisioningArtifacts(roomDir);
7720
- } else if (archivePresent) {
7721
- throw new CoworkStorageError(
7722
- `room "${validRoomId}" has archive residue without deletion metadata`
7723
- );
7916
+ } else if (archivePresent || blobsPresent) {
7917
+ throw new CoworkStorageError(`room "${id}" has archive residue without deletion metadata`);
7724
7918
  }
7725
- const expected = /* @__PURE__ */ new Set(["archive.jsonl", "room.json", "room.json.v1.bak"]);
7919
+ const expected = /* @__PURE__ */ new Set(["archive.sqlite3", "archive.sqlite3-wal", "archive.sqlite3-shm", "blobs", "room.json", "room.json.v1.bak"]);
7726
7920
  const unexpected = this.fs.readdirSync(roomDir).filter((name) => !expected.has(name));
7727
- if (unexpected.length > 0) {
7728
- throw new CoworkStorageError(`room "${validRoomId}" contains live or unexpected residue: ${unexpected.join(", ")}`);
7921
+ if (unexpected.length) throw new CoworkStorageError(`room "${id}" contains live or unexpected residue: ${unexpected.join(", ")}`);
7922
+ for (const name of ["archive.sqlite3-wal", "archive.sqlite3-shm", "archive.sqlite3", "room.json.v1.bak", "room.json"]) {
7923
+ const path = join3(roomDir, name);
7924
+ if (this.lstatIfPresent(path)) {
7925
+ this.assertRegularFile(path, name);
7926
+ this.fs.unlinkSync(path);
7927
+ }
7729
7928
  }
7730
- const backupPath = `${metadataPath}.v1.bak`;
7731
- if (this.lstatIfPresent(backupPath)) {
7732
- this.assertRegularFile(backupPath, "room metadata v1 backup");
7733
- this.fs.unlinkSync(backupPath);
7734
- this.fsyncDirectory(roomDir);
7929
+ const blobs = this.blobsDirectory(id);
7930
+ if (this.lstatIfPresent(blobs)) this.fs.rmSync(blobs, { recursive: true, force: true });
7931
+ this.fsyncDirectory(roomDir);
7932
+ this.fs.rmdirSync(roomDir);
7933
+ this.fsyncDirectory(this.roomsDirectory());
7934
+ });
7935
+ }
7936
+ withDatabase(roomId, work, create = false) {
7937
+ const path = this.archivePath(roomId);
7938
+ let guardFd;
7939
+ if (!create) {
7940
+ this.assertRegularFile(path, "room archive database");
7941
+ guardFd = this.fs.openSync(path, nodeFs2.constants.O_RDONLY | NO_FOLLOW2);
7942
+ this.validateOpenPath(guardFd, path, "room archive database", "file", true);
7943
+ }
7944
+ let db;
7945
+ try {
7946
+ this.secureSqliteFiles(path);
7947
+ db = new Database(path, { fileMustExist: !create });
7948
+ if (guardFd !== void 0) this.validateOpenPath(guardFd, path, "room archive database", "file", true);
7949
+ this.fs.chmodSync(path, FILE_MODE2);
7950
+ db.pragma("journal_mode = WAL");
7951
+ db.pragma("synchronous = FULL");
7952
+ db.pragma("foreign_keys = ON");
7953
+ db.pragma("busy_timeout = 5000");
7954
+ if (create) {
7955
+ db.exec(`CREATE TABLE records (
7956
+ seq INTEGER PRIMARY KEY, record_id TEXT NOT NULL UNIQUE, kind TEXT NOT NULL, at TEXT NOT NULL,
7957
+ payload_json TEXT NOT NULL, blob_path TEXT, message_id TEXT, file_id TEXT, intent_record_id TEXT,
7958
+ recipient_identity TEXT, source_msg_id INTEGER, source_file_id INTEGER, category TEXT,
7959
+ briefing_role TEXT, briefing_version INTEGER, membership_epoch INTEGER
7960
+ );
7961
+ CREATE TABLE record_recipients (
7962
+ record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
7963
+ recipient_identity TEXT NOT NULL, category TEXT, briefing_role TEXT,
7964
+ briefing_version INTEGER, PRIMARY KEY(record_seq, recipient_identity)
7965
+ );
7966
+ CREATE TABLE relay_intent_work (
7967
+ record_seq INTEGER NOT NULL REFERENCES records(seq) ON DELETE CASCADE,
7968
+ recipient_identity TEXT NOT NULL, PRIMARY KEY(record_seq, recipient_identity)
7969
+ );
7970
+ CREATE INDEX relay_work_source ON relay_intent_work(record_seq, recipient_identity);
7971
+ CREATE INDEX records_kind_seq ON records(kind, seq);
7972
+ CREATE INDEX records_message ON records(message_id, kind, seq);
7973
+ CREATE INDEX records_file ON records(file_id, kind, seq);
7974
+ CREATE INDEX records_intent_result ON records(intent_record_id, kind);
7975
+ CREATE INDEX records_relay_recipient ON records(kind, recipient_identity, seq);
7976
+ CREATE UNIQUE INDEX records_source_message ON records(source_msg_id) WHERE kind='message' AND source_msg_id IS NOT NULL;
7977
+ CREATE UNIQUE INDEX records_source_file ON records(source_file_id) WHERE kind='file' AND source_file_id IS NOT NULL;
7978
+ CREATE INDEX records_briefing ON records(category, briefing_role, briefing_version, seq);
7979
+ CREATE INDEX records_membership_epoch ON records(category, membership_epoch);
7980
+ CREATE INDEX recipients_identity ON record_recipients(recipient_identity, record_seq);
7981
+ CREATE INDEX recipients_briefing_delivery ON record_recipients
7982
+ (recipient_identity, category, briefing_role, briefing_version, record_seq);`);
7983
+ db.pragma(`user_version = ${SQLITE_SCHEMA_VERSION}`);
7984
+ this.reconciledBlobRooms.add(roomId);
7985
+ } else {
7986
+ const version = db.pragma("user_version", { simple: true });
7987
+ if (version !== SQLITE_SCHEMA_VERSION) {
7988
+ throw new CoworkStorageError(`unsupported room archive schema version ${version}`);
7989
+ }
7990
+ if (!this.reconciledBlobRooms.has(roomId)) {
7991
+ this.reconcileBlobDirectory(roomId, db);
7992
+ this.reconciledBlobRooms.add(roomId);
7993
+ }
7735
7994
  }
7736
- if (archivePresent) {
7737
- this.assertRegularFile(archivePath, "room archive");
7738
- this.fs.unlinkSync(archivePath);
7739
- this.fsyncDirectory(roomDir);
7995
+ this.secureSqliteFiles(path);
7996
+ const result = work(db);
7997
+ this.secureSqliteFiles(path);
7998
+ return result;
7999
+ } catch (error) {
8000
+ throw this.wrap(`failed to access room "${roomId}" SQLite archive`, error);
8001
+ } finally {
8002
+ try {
8003
+ db?.close();
8004
+ } catch {
7740
8005
  }
7741
- if (metadataPresent) {
7742
- this.assertRegularFile(metadataPath, "room metadata");
7743
- this.fs.unlinkSync(metadataPath);
7744
- this.fsyncDirectory(roomDir);
8006
+ if (guardFd !== void 0) try {
8007
+ this.fs.closeSync(guardFd);
8008
+ } catch {
8009
+ }
8010
+ this.secureSqliteFiles(path);
8011
+ }
8012
+ }
8013
+ indexValues(record) {
8014
+ const subject = record;
8015
+ return {
8016
+ seq: record.seq,
8017
+ record_id: record.record_id,
8018
+ kind: record.kind,
8019
+ at: record.at,
8020
+ message_id: subject.message_id ?? null,
8021
+ file_id: subject.file_id ?? null,
8022
+ intent_record_id: subject.intent_record_id ?? null,
8023
+ recipient_identity: subject.recipient_identity ?? null,
8024
+ source_msg_id: subject.source_msg_id ?? null,
8025
+ source_file_id: subject.source_file_id ?? null,
8026
+ category: subject.category ?? null,
8027
+ briefing_role: subject.briefing_role ?? null,
8028
+ briefing_version: subject.briefing_version ?? null,
8029
+ membership_epoch: typeof subject.membership === "object" && subject.membership !== null ? subject.membership.epoch ?? null : null
8030
+ };
8031
+ }
8032
+ decodeRows(roomId, rows) {
8033
+ return rows.map((row) => {
8034
+ let decoded;
8035
+ try {
8036
+ decoded = JSON.parse(row.payload_json);
8037
+ } catch (error) {
8038
+ throw new CoworkStorageError(`malformed JSON in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
8039
+ }
8040
+ if (row.blob_path !== null) {
8041
+ const subject = decoded;
8042
+ const expectedPath = subject.kind === "file" && typeof subject.sha256 === "string" && /^[0-9a-f]{64}$/.test(subject.sha256) ? join3("blobs", subject.sha256) : void 0;
8043
+ if (expectedPath === void 0 || row.blob_path !== expectedPath) {
8044
+ throw new CoworkStorageError(`invalid blob reference in room "${roomId}" archive at sequence ${row.seq}`);
8045
+ }
8046
+ const bytes = this.readFileNoFollow(join3(this.roomDirectory(roomId), expectedPath), "room file blob");
8047
+ decoded = { ...decoded, data_base64: bytes.toString("base64") };
8048
+ }
8049
+ try {
8050
+ const record = CommunicationRecordSchema.parse(decoded);
8051
+ if (record.room_id !== roomId || record.seq !== row.seq) throw new Error("indexed identity mismatch");
8052
+ return record;
8053
+ } catch (error) {
8054
+ throw new CoworkStorageError(`invalid record in room "${roomId}" archive at sequence ${row.seq}`, { cause: error });
7745
8055
  }
7746
- this.fs.rmdirSync(roomDir);
7747
- this.fsyncDirectory(this.roomsDirectory());
7748
- this.nextSequences.delete(validRoomId);
7749
8056
  });
7750
8057
  }
7751
- removeProvisioningArtifacts(roomDir) {
7752
- const journalPath = join3(roomDir, ".cowork-provisioning-stage");
7753
- const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
7754
- if (this.lstatIfPresent(journalPath)) {
7755
- this.assertRegularFile(journalPath, "provisioning staging journal");
7756
- const stagingName = this.fs.readFileSync(journalPath, "utf8").trim();
7757
- if (!/^live\.staging-[0-9a-f]{32}$/.test(stagingName)) {
7758
- throw new CoworkStorageError("invalid provisioning staging journal");
8058
+ persistBlob(roomId, digest, bytes) {
8059
+ const directory = this.blobsDirectory(roomId);
8060
+ this.ensurePrivateDirectory(directory, true, "room blobs directory");
8061
+ const final = join3(directory, digest);
8062
+ if (this.lstatIfPresent(final)) {
8063
+ this.assertRegularFile(final, "room file blob");
8064
+ if (this.readFileNoFollow(final, "room file blob").equals(bytes)) return { path: join3("blobs", digest), created: false };
8065
+ throw new CoworkStorageError(`immutable blob collision for ${digest}`);
8066
+ }
8067
+ const temp = join3(directory, `.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`);
8068
+ let fd;
8069
+ try {
8070
+ fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
8071
+ this.writeAll(fd, bytes);
8072
+ this.fs.fsyncSync(fd);
8073
+ this.fs.closeSync(fd);
8074
+ fd = void 0;
8075
+ this.fs.renameSync(temp, final);
8076
+ this.fsyncDirectory(directory);
8077
+ this.fs.chmodSync(final, FILE_MODE2);
8078
+ return { path: join3("blobs", digest), created: true };
8079
+ } finally {
8080
+ if (fd !== void 0) try {
8081
+ this.fs.closeSync(fd);
8082
+ } catch {
8083
+ }
8084
+ if (this.lstatIfPresent(temp)) try {
8085
+ this.fs.unlinkSync(temp);
8086
+ } catch {
7759
8087
  }
7760
- targets.push(join3(roomDir, stagingName));
7761
8088
  }
7762
- for (const target of targets) {
7763
- const stat = this.lstatIfPresent(target);
7764
- if (!stat) continue;
7765
- if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
7766
- else this.fs.rmSync(target, { recursive: true, force: true });
7767
- this.fsyncDirectory(roomDir);
8089
+ }
8090
+ removeUnreferencedBlob(roomId, relativePath) {
8091
+ const absolute = join3(this.roomDirectory(roomId), relativePath);
8092
+ try {
8093
+ const referenced = this.withDatabase(roomId, (db) => db.prepare(
8094
+ "SELECT EXISTS(SELECT 1 FROM records WHERE blob_path = ?) AS found"
8095
+ ).get(relativePath).found !== 0);
8096
+ if (referenced || !this.lstatIfPresent(absolute)) return;
8097
+ this.assertRegularFile(absolute, "unreferenced room file blob");
8098
+ this.fs.unlinkSync(absolute);
8099
+ this.fsyncDirectory(dirname2(absolute));
8100
+ } catch {
7768
8101
  }
7769
- if (this.lstatIfPresent(journalPath)) {
7770
- this.fs.unlinkSync(journalPath);
7771
- this.fsyncDirectory(roomDir);
8102
+ }
8103
+ reconcileBlobDirectory(roomId, db) {
8104
+ const directory = this.blobsDirectory(roomId);
8105
+ this.ensurePrivateDirectory(directory, true, "room blobs directory");
8106
+ const referenced = new Set(db.prepare(
8107
+ "SELECT blob_path FROM records WHERE blob_path IS NOT NULL"
8108
+ ).all().map((row) => basename(row.blob_path)));
8109
+ let changed = false;
8110
+ for (const entry of this.fs.readdirSync(directory, { withFileTypes: true })) {
8111
+ const path = join3(directory, entry.name);
8112
+ if (/^\.tmp-[0-9]+-[0-9a-f]{16}$/.test(entry.name)) {
8113
+ this.assertRegularFile(path, "crash-left room blob temporary file");
8114
+ this.fs.unlinkSync(path);
8115
+ changed = true;
8116
+ continue;
8117
+ }
8118
+ if (!/^[0-9a-f]{64}$/.test(entry.name)) throw new CoworkStorageError(`unexpected room blob residue: ${entry.name}`);
8119
+ this.assertRegularFile(path, referenced.has(entry.name) ? "room file blob" : "unreferenced room file blob");
8120
+ this.fs.chmodSync(path, FILE_MODE2);
8121
+ if (referenced.has(entry.name)) continue;
8122
+ this.fs.unlinkSync(path);
8123
+ changed = true;
8124
+ }
8125
+ if (changed) this.fsyncDirectory(directory);
8126
+ }
8127
+ secureSqliteFiles(path) {
8128
+ for (const candidate of [path, `${path}-wal`, `${path}-shm`]) {
8129
+ if (!this.lstatIfPresent(candidate)) continue;
8130
+ this.assertRegularFile(candidate, `SQLite file ${basename(candidate)}`);
8131
+ this.fs.chmodSync(candidate, FILE_MODE2);
7772
8132
  }
7773
8133
  }
8134
+ validatePage(after, limit) {
8135
+ if (!Number.isSafeInteger(after) || after < 0) throw new CoworkStorageError("after must be a non-negative safe integer");
8136
+ if (!Number.isSafeInteger(limit) || limit < 1) throw new CoworkStorageError("limit must be a positive safe integer");
8137
+ }
7774
8138
  loadUnlocked(roomId) {
7775
8139
  this.assertRoomDirectory(roomId);
7776
8140
  const path = this.metadataPath(roomId);
7777
8141
  this.assertRegularFile(path, "room metadata");
8142
+ let decoded;
7778
8143
  let bytes;
7779
8144
  try {
7780
8145
  bytes = this.readFileNoFollow(path, "room metadata");
7781
- } catch (error) {
7782
- throw this.wrap(`failed to read room "${roomId}" metadata`, error);
7783
- }
7784
- let decoded;
7785
- try {
7786
8146
  decoded = JSON.parse(utf8Decoder.decode(bytes));
7787
8147
  } catch (error) {
7788
8148
  throw this.wrap(`malformed metadata for room "${roomId}"`, error);
7789
8149
  }
7790
8150
  const room = this.isVersion1(decoded) ? this.migrateUnlocked(roomId, decoded, bytes) : RoomSchema.parse(decoded);
7791
- if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) {
7792
- this.atomicMetadataWrite(this.metadataPath(roomId), room);
7793
- }
8151
+ if (!this.isVersion1(decoded) && this.persistedRoomName(decoded) !== room.room_name) this.atomicMetadataWrite(path, room);
7794
8152
  if (room.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7795
8153
  return room;
7796
8154
  }
@@ -7800,79 +8158,54 @@ var init_storage = __esm({
7800
8158
  persistedRoomName(decoded) {
7801
8159
  return typeof decoded === "object" && decoded !== null ? decoded.room_name : void 0;
7802
8160
  }
7803
- /**
7804
- * Lazy additive v1 → v2 migration: preserve the exact pre-migration
7805
- * bytes once as room.json.v1.bak, then atomically persist the v2 metadata.
7806
- *
7807
- * THE BACKUP IS WRITTEN TEMP → FSYNC → RENAME, not opened in place, and the
7808
- * reason is a crash window that an existence check cannot see. The earlier
7809
- * version guarded with `if (!lstatIfPresent(backupPath))` and wrote straight
7810
- * into the final path under O_CREAT|O_EXCL. A crash inside that write leaves a
7811
- * PARTIAL file that nonetheless EXISTS, so the next load's existence check
7812
- * skips the backup, writes v2, and the pre-migration bytes are gone — no
7813
- * error, no warning, and the one artefact that exists to undo a bad migration
7814
- * is a truncated fragment. Measured before the fix: a 40-byte prefix of a
7815
- * 604-byte room, JSON.parse false, room.json already v2.
7816
- *
7817
- * A rename is atomic, so the final path now only ever appears complete. The
7818
- * temp file is created with O_EXCL under a pid+random name and removed on
7819
- * failure, so a crashed attempt leaves at most an orphan temp, never a
7820
- * plausible-looking backup.
7821
- *
7822
- * AND AN EXISTING BACKUP IS PARSED BEFORE IT IS TRUSTED, which repairs the
7823
- * case where a partial file is ALREADY on disk from a build without this fix —
7824
- * exactly the state a host that ran the previous code could be in right now.
7825
- * A backup that does not parse as v1 is replaced by the bytes we hold, because
7826
- * those are the real pre-migration bytes and the fragment is worthless.
7827
- *
7828
- * WHAT THE BACKUP IS NOT: restoring it is NOT a rollback. See the note on
7829
- * `restoreV1Backup` — re-migrating mints fresh participant ids.
7830
- */
7831
- migrateUnlocked(roomId, decoded, originalBytes) {
8161
+ migrateUnlocked(roomId, decoded, original) {
7832
8162
  const v1 = RoomV1Schema.parse(decoded);
7833
8163
  if (v1.room_id !== roomId) throw new CoworkStorageError(`metadata room_id does not match room "${roomId}"`);
7834
8164
  const migrated = migrateRoomV1(v1, generateUlid);
7835
- const backupPath = `${this.metadataPath(roomId)}.v1.bak`;
7836
- if (!this.hasIntactV1Backup(backupPath)) {
7837
- this.atomicBytesWrite(backupPath, originalBytes, "room metadata v1 backup");
7838
- }
8165
+ const backup = `${this.metadataPath(roomId)}.v1.bak`;
8166
+ if (!this.hasIntactV1Backup(backup)) this.atomicBytesWrite(backup, original, "room metadata v1 backup");
7839
8167
  this.atomicMetadataWrite(this.metadataPath(roomId), migrated);
7840
8168
  return migrated;
7841
8169
  }
7842
- /**
7843
- * Is there already a backup we would be willing to hand back to an operator?
7844
- *
7845
- * Existence is not the question — a partial file exists. It must parse and
7846
- * still claim to be the v1 metadata for this room; anything else is a fragment
7847
- * and is better overwritten with the bytes we are holding right now.
7848
- */
7849
- hasIntactV1Backup(backupPath) {
7850
- if (!this.lstatIfPresent(backupPath)) return false;
8170
+ hasIntactV1Backup(path) {
8171
+ if (!this.lstatIfPresent(path)) return false;
7851
8172
  try {
7852
- const decoded = JSON.parse(utf8Decoder.decode(this.readFileNoFollow(backupPath, "room metadata v1 backup")));
7853
- return this.isVersion1(decoded);
8173
+ return this.isVersion1(JSON.parse(utf8Decoder.decode(this.readFileNoFollow(path, "room metadata v1 backup"))));
7854
8174
  } catch {
7855
8175
  return false;
7856
8176
  }
7857
8177
  }
7858
- /**
7859
- * Durably place exact bytes at `path`: temp under O_EXCL, fsync, rename, fsync
7860
- * the directory. The same dance as atomicMetadataWrite, which serialises a Room
7861
- * rather than taking bytes verbatim — and taking them verbatim is the whole
7862
- * point for a backup, whose value is being byte-identical to what was there.
7863
- */
8178
+ removeProvisioningArtifacts(roomDir) {
8179
+ const journal = join3(roomDir, ".cowork-provisioning-stage");
8180
+ const targets = [join3(roomDir, "live"), join3(roomDir, "provisioning-residue")];
8181
+ if (this.lstatIfPresent(journal)) {
8182
+ this.assertRegularFile(journal, "provisioning staging journal");
8183
+ const name = this.fs.readFileSync(journal, "utf8").trim();
8184
+ if (!/^live\.staging-[0-9a-f]{32}$/.test(name)) throw new CoworkStorageError("invalid provisioning staging journal");
8185
+ targets.push(join3(roomDir, name));
8186
+ }
8187
+ for (const target of targets) {
8188
+ const stat = this.lstatIfPresent(target);
8189
+ if (!stat) continue;
8190
+ if (stat.isSymbolicLink() || !stat.isDirectory()) this.fs.unlinkSync(target);
8191
+ else this.fs.rmSync(target, { recursive: true, force: true });
8192
+ this.fsyncDirectory(roomDir);
8193
+ }
8194
+ if (this.lstatIfPresent(journal)) {
8195
+ this.fs.unlinkSync(journal);
8196
+ this.fsyncDirectory(roomDir);
8197
+ }
8198
+ }
8199
+ atomicMetadataWrite(path, room) {
8200
+ this.atomicBytesWrite(path, Buffer.from(`${JSON.stringify(room)}
8201
+ `), "room metadata");
8202
+ }
7864
8203
  atomicBytesWrite(path, bytes, label) {
7865
8204
  if (this.lstatIfPresent(path)) this.assertRegularFile(path, label);
7866
8205
  const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
7867
8206
  let fd;
7868
8207
  try {
7869
- fd = this.fs.openSync(
7870
- temp,
7871
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
7872
- FILE_MODE2
7873
- );
7874
- this.validateOpenPath(fd, temp, `temporary ${label}`, "file", true);
7875
- this.fs.fchmodSync(fd, FILE_MODE2);
8208
+ fd = this.fs.openSync(temp, nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2, FILE_MODE2);
7876
8209
  this.writeAll(fd, bytes);
7877
8210
  this.fs.fsyncSync(fd);
7878
8211
  this.fs.closeSync(fd);
@@ -7880,11 +8213,9 @@ var init_storage = __esm({
7880
8213
  this.fs.renameSync(temp, path);
7881
8214
  this.fsyncDirectory(dirname2(path));
7882
8215
  } catch (error) {
7883
- if (fd !== void 0) {
7884
- try {
7885
- this.fs.closeSync(fd);
7886
- } catch {
7887
- }
8216
+ if (fd !== void 0) try {
8217
+ this.fs.closeSync(fd);
8218
+ } catch {
7888
8219
  }
7889
8220
  try {
7890
8221
  this.fs.rmSync(temp, { force: true });
@@ -7893,84 +8224,31 @@ var init_storage = __esm({
7893
8224
  throw this.wrap(`failed to write ${label} at ${path}`, error);
7894
8225
  }
7895
8226
  }
7896
- scanArchive(roomId) {
7897
- const path = this.archivePath(roomId);
7898
- this.assertRegularFile(path, "room archive");
7899
- let bytes;
7900
- try {
7901
- bytes = this.readFileNoFollow(path, "room archive");
7902
- } catch (error) {
7903
- throw this.wrap(`failed to read room "${roomId}" archive`, error);
7904
- }
7905
- const records = [];
7906
- let byteOffset = 0;
7907
- let expectedSequence = 1;
7908
- while (byteOffset < bytes.byteLength) {
7909
- const newline = bytes.indexOf(10, byteOffset);
7910
- if (newline === -1) {
7911
- throw new CoworkStorageError(`partial JSON record in room "${roomId}" archive at byte offset ${byteOffset}`);
7912
- }
7913
- const line = bytes.subarray(byteOffset, newline);
7914
- let decoded;
7915
- try {
7916
- decoded = JSON.parse(utf8Decoder.decode(line));
7917
- } catch (error) {
7918
- throw new CoworkStorageError(
7919
- `malformed JSON in room "${roomId}" archive at byte offset ${byteOffset}`,
7920
- { cause: error }
7921
- );
7922
- }
7923
- const observedSequence = typeof decoded === "object" && decoded !== null && "seq" in decoded ? decoded.seq : void 0;
7924
- if (observedSequence !== expectedSequence) {
7925
- throw new CoworkStorageError(
7926
- `non-monotonic sequence in room "${roomId}" archive at byte offset ${byteOffset}: expected ${expectedSequence}, found ${String(observedSequence)}`
7927
- );
7928
- }
7929
- let record;
7930
- try {
7931
- record = CommunicationRecordSchema.parse(decoded);
7932
- } catch (error) {
7933
- throw new CoworkStorageError(
7934
- `invalid record in room "${roomId}" archive at byte offset ${byteOffset}`,
7935
- { cause: error }
7936
- );
7937
- }
7938
- if (record.room_id !== roomId) {
7939
- throw new CoworkStorageError(
7940
- `record room_id mismatch in room "${roomId}" archive at byte offset ${byteOffset}`
7941
- );
7942
- }
7943
- records.push(record);
7944
- expectedSequence += 1;
7945
- byteOffset = newline + 1;
7946
- }
7947
- return { records, nextSequence: expectedSequence };
7948
- }
7949
8227
  ensureBaseDirectories() {
7950
8228
  this.ensurePrivateDirectory(this.stateDir, true, "state directory");
7951
8229
  this.ensurePrivateDirectory(this.roomsDirectory(), true, "rooms directory");
7952
8230
  }
8231
+ assertRoomDirectory(roomId) {
8232
+ this.ensureBaseDirectories();
8233
+ this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
8234
+ }
7953
8235
  ensurePrivateDirectory(path, create, label) {
7954
8236
  let stat = this.lstatIfPresent(path);
7955
- const created = stat === void 0;
7956
- if (created) {
8237
+ if (!stat) {
7957
8238
  if (!create) throw new CoworkStorageError(`${label} does not exist`);
7958
8239
  this.createPrivateDirectoryTree(path, label);
7959
8240
  stat = this.fs.lstatSync(path);
7960
8241
  }
7961
- const current = stat;
7962
- if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
7963
- if (!current.isDirectory()) throw new CoworkStorageError(`${label} is not a directory`);
8242
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} is not a private directory`);
7964
8243
  this.fs.chmodSync(path, DIRECTORY_MODE2);
7965
8244
  }
7966
8245
  createPrivateDirectoryTree(path, label) {
7967
8246
  const missing = [];
7968
8247
  let cursor = path;
7969
8248
  for (; ; ) {
7970
- const existing = this.lstatIfPresent(cursor);
7971
- if (existing) {
7972
- if (existing.isSymbolicLink()) throw new CoworkStorageError(`${label} parent must not be a symbolic link (symlink)`);
7973
- if (!existing.isDirectory()) throw new CoworkStorageError(`${label} parent is not a directory`);
8249
+ const stat = this.lstatIfPresent(cursor);
8250
+ if (stat) {
8251
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new CoworkStorageError(`${label} parent is unsafe`);
7974
8252
  break;
7975
8253
  }
7976
8254
  missing.push(cursor);
@@ -7978,120 +8256,45 @@ var init_storage = __esm({
7978
8256
  if (parent === cursor) throw new CoworkStorageError(`cannot find existing parent for ${label}`);
7979
8257
  cursor = parent;
7980
8258
  }
7981
- const created = [];
7982
- try {
7983
- for (const directory of missing.reverse()) {
7984
- this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
7985
- created.push(directory);
7986
- this.fs.chmodSync(directory, DIRECTORY_MODE2);
7987
- this.fsyncDirectory(directory);
7988
- this.fsyncDirectory(dirname2(directory));
7989
- }
7990
- } catch (error) {
7991
- for (const directory of created.reverse()) {
7992
- try {
7993
- this.fs.rmdirSync(directory);
7994
- this.fsyncDirectory(dirname2(directory));
7995
- } catch {
7996
- }
7997
- }
7998
- throw error;
8259
+ for (const directory of missing.reverse()) {
8260
+ this.fs.mkdirSync(directory, { mode: DIRECTORY_MODE2 });
8261
+ this.fs.chmodSync(directory, DIRECTORY_MODE2);
8262
+ this.fsyncDirectory(directory);
8263
+ this.fsyncDirectory(dirname2(directory));
7999
8264
  }
8000
8265
  }
8001
- assertRoomDirectory(roomId) {
8002
- this.ensureBaseDirectories();
8003
- this.ensurePrivateDirectory(this.roomDirectory(roomId), false, `room "${roomId}" directory`);
8004
- }
8005
8266
  rejectSymlink(path, label) {
8006
- if (this.fs.lstatSync(path).isSymbolicLink()) {
8007
- throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8008
- }
8267
+ if (this.fs.lstatSync(path).isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8009
8268
  }
8010
8269
  assertRegularFile(path, label) {
8011
8270
  this.rejectSymlink(path, label);
8012
8271
  const stat = this.fs.lstatSync(path);
8013
- if (!stat.isFile()) throw new CoworkStorageError(`${label} is not a regular file`);
8014
- if (stat.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${stat.nlink}`);
8015
- }
8016
- lstatIfPresent(path) {
8017
- try {
8018
- return this.fs.lstatSync(path);
8019
- } catch (error) {
8020
- if (error.code === "ENOENT") return void 0;
8021
- throw error;
8022
- }
8272
+ if (!stat.isFile() || stat.nlink !== 1) throw new CoworkStorageError(`${label} is not a safe regular file`);
8023
8273
  }
8024
8274
  validateOpenPath(fd, path, label, kind, requireSingleLink) {
8025
8275
  const opened = this.fs.fstatSync(fd);
8026
8276
  const validKind = kind === "file" ? opened.isFile() : opened.isDirectory();
8027
8277
  if (!validKind) throw new CoworkStorageError(`${label} open descriptor is not a regular ${kind}`);
8028
- if (requireSingleLink && opened.nlink !== 1) {
8029
- throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
8030
- }
8278
+ if (requireSingleLink && opened.nlink !== 1) throw new CoworkStorageError(`${label} has unsafe hardlink link count ${opened.nlink}`);
8031
8279
  const current = this.fs.lstatSync(path);
8032
8280
  if (current.isSymbolicLink()) throw new CoworkStorageError(`${label} must not be a symbolic link (symlink)`);
8033
- if (current.dev !== opened.dev || current.ino !== opened.ino) {
8034
- throw new CoworkStorageError(`${label} inode changed during open`);
8035
- }
8281
+ if (current.dev !== opened.dev || current.ino !== opened.ino) throw new CoworkStorageError(`${label} inode changed during open`);
8036
8282
  return opened;
8037
8283
  }
8038
- createArchive(roomId) {
8039
- const path = this.archivePath(roomId);
8040
- let fd;
8041
- try {
8042
- fd = this.fs.openSync(
8043
- path,
8044
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
8045
- FILE_MODE2
8046
- );
8047
- this.validateOpenPath(fd, path, "room archive", "file", true);
8048
- this.fs.fchmodSync(fd, FILE_MODE2);
8049
- this.fs.fsyncSync(fd);
8050
- } finally {
8051
- if (fd !== void 0) this.fs.closeSync(fd);
8052
- }
8053
- this.fsyncDirectory(dirname2(path));
8054
- }
8055
- atomicMetadataWrite(path, room) {
8056
- if (this.lstatIfPresent(path)) this.assertRegularFile(path, "room metadata");
8057
- const temp = `${path}.tmp-${process.pid}-${randomBytes3(8).toString("hex")}`;
8058
- const bytes = Buffer.from(`${JSON.stringify(room)}
8059
- `, "utf8");
8060
- let fd;
8284
+ lstatIfPresent(path) {
8061
8285
  try {
8062
- fd = this.fs.openSync(
8063
- temp,
8064
- nodeFs2.constants.O_CREAT | nodeFs2.constants.O_EXCL | nodeFs2.constants.O_WRONLY | NO_FOLLOW2,
8065
- FILE_MODE2
8066
- );
8067
- this.validateOpenPath(fd, temp, "temporary room metadata", "file", true);
8068
- this.fs.fchmodSync(fd, FILE_MODE2);
8069
- this.writeAll(fd, bytes);
8070
- this.fs.fsyncSync(fd);
8071
- this.fs.closeSync(fd);
8072
- fd = void 0;
8073
- this.fs.renameSync(temp, path);
8074
- this.fsyncDirectory(dirname2(path));
8286
+ return this.fs.lstatSync(path);
8075
8287
  } catch (error) {
8076
- if (fd !== void 0) {
8077
- try {
8078
- this.fs.closeSync(fd);
8079
- } catch {
8080
- }
8081
- }
8082
- try {
8083
- this.fs.rmSync(temp, { force: true });
8084
- } catch {
8085
- }
8086
- throw this.wrap(`failed to atomically persist ${path}`, error);
8288
+ if (error.code === "ENOENT") return void 0;
8289
+ throw error;
8087
8290
  }
8088
8291
  }
8089
8292
  writeAll(fd, bytes) {
8090
8293
  let offset = 0;
8091
- while (offset < bytes.byteLength) {
8092
- const written = this.fs.writeSync(fd, bytes, offset, bytes.byteLength - offset, null);
8093
- if (written <= 0) throw new CoworkStorageError("write made no progress");
8094
- offset += written;
8294
+ while (offset < bytes.length) {
8295
+ const n = this.fs.writeSync(fd, bytes, offset, bytes.length - offset, null);
8296
+ if (n <= 0) throw new CoworkStorageError("write made no progress");
8297
+ offset += n;
8095
8298
  }
8096
8299
  }
8097
8300
  readFileNoFollow(path, label) {
@@ -8129,10 +8332,13 @@ var init_storage = __esm({
8129
8332
  return join3(this.roomDirectory(roomId), "room.json");
8130
8333
  }
8131
8334
  archivePath(roomId) {
8132
- return join3(this.roomDirectory(roomId), "archive.jsonl");
8335
+ return join3(this.roomDirectory(roomId), "archive.sqlite3");
8336
+ }
8337
+ blobsDirectory(roomId) {
8338
+ return join3(this.roomDirectory(roomId), "blobs");
8133
8339
  }
8134
8340
  wrap(message, error) {
8135
- return error instanceof CoworkStorageError ? new CoworkStorageError(`${message}: ${error.message}`, { cause: error }) : new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
8341
+ return new CoworkStorageError(`${message}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
8136
8342
  }
8137
8343
  };
8138
8344
  }
@@ -9043,7 +9249,7 @@ import { randomBytes as randomBytes4 } from "node:crypto";
9043
9249
  import * as http from "node:http";
9044
9250
  import * as net from "node:net";
9045
9251
  import * as nodeFs4 from "node:fs";
9046
- import { basename, dirname as dirname3, join as join5 } from "node:path";
9252
+ import { basename as basename2, dirname as dirname3, join as join5 } from "node:path";
9047
9253
  function createServiceRoutes(service) {
9048
9254
  return {
9049
9255
  "room.create": { auth: true, run: (params2) => service.createRoom(params2) },
@@ -9709,7 +9915,7 @@ var init_transports = __esm({
9709
9915
  }
9710
9916
  async cleanupStalePrivateSockets() {
9711
9917
  const directory = dirname3(this.options.socketPath);
9712
- const prefix = `${basename(this.options.socketPath)}.private-`;
9918
+ const prefix = `${basename2(this.options.socketPath)}.private-`;
9713
9919
  const uid = typeof process.getuid === "function" ? process.getuid() : void 0;
9714
9920
  const candidates = this.fs.readdirSync(directory).filter((name) => name.startsWith(prefix)).sort().slice(0, MAX_STALE_PRIVATE_SOCKET_CLEANUP);
9715
9921
  for (const name of candidates) {