@ours.network/cowork 1.1.4 → 1.2.0

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/README.md CHANGED
@@ -23,6 +23,8 @@ The package keeps room metadata and a per-room indexed `archive.sqlite3` in its
23
23
 
24
24
  Ordinary ours-mcp identities can join only as remote participants over the ours protocol.
25
25
 
26
+ Granted room members can create message-only scoped reply threads for an explicit subset of stable room participant IDs through the generic Ours command catalog's `start_thread` command. Selected members receive separate root copies and reply with the SDK's native `reply_to_wire_id`; Cowork maps each descendant to the recipient's local immediate-parent copy. The selected participant-ID/CID pairs are immutable, later members are not backfilled, and excluded members receive no scoped message, file notice, notification, participant-history row, command result, or routing metadata. A message without `reply_to_wire_id` remains an ordinary whole-room message. See [Room workflow](./docs/05-room-workflow.md#scoped-reply-threads) for command discovery, schema, errors, and retry behavior, and [Messaging and history](./docs/07-messaging-history.md#reply-threading) for reply and history semantics.
27
+
26
28
  Active participants can also send files through the room identity. Cowork treats
27
29
  them as opaque bytes, archives them before consuming SDK inbox state, and relays an
28
30
  SDK-authenticated metadata envelope plus the binary file to every other active seat.
@@ -42,3 +44,4 @@ ours-cowork docs web
42
44
 
43
45
  Before production use, read the limitations topic. In particular, backups require a stopped daemon and restore uses the complete state directory.
44
46
  Lost room identity leases are recovered automatically with a non-force bind and exact persisted-CID proof. Operators can invoke the same safe path explicitly with `ours-cowork room rebind <room-id>`; it never recreates an established identity or steals a live lease.
47
+ The localhost host-management CLI/API and web Archive retain full scoped-thread visibility for administrators, including excluded-member traffic that participant APIs hide. Runtime `room.history` is an authenticated, grant-gated participant view with viewer-local cursors; host cursors and participant cursors are not interchangeable. The shared SDK history remains local to each identity and does not define Cowork routing or host archive retention.
package/dist/cli.js CHANGED
@@ -4351,11 +4351,85 @@ var SHARED_ROOM_COMMANDS = [
4351
4351
  "room.role.rest.remove"
4352
4352
  ];
4353
4353
  var RUNTIME_COMMAND_NAMES = [
4354
+ "start_thread",
4354
4355
  "list-members",
4355
4356
  "remove-member",
4356
4357
  ...SHARED_ROOM_COMMANDS
4357
4358
  ];
4358
4359
 
4360
+ // src/thread-contracts.ts
4361
+ var ParticipantIdSchema = external_exports.string().regex(
4362
+ /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4363
+ "must be a 26-character lowercase Crockford ULID"
4364
+ );
4365
+ var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4366
+ var TopicTextSchema = external_exports.string().refine(
4367
+ (value) => Array.from(value).length >= 1 && Array.from(value).length <= 120 && value.trim().length > 0 && !/[\p{Cc}\p{Cf}]/u.test(value),
4368
+ "topic must contain 1-120 Unicode characters without control or format characters"
4369
+ );
4370
+ var InputTopicSchema = TopicTextSchema.transform((value) => value.trim());
4371
+ var StoredTopicSchema = TopicTextSchema.refine(
4372
+ (value) => value === value.trim(),
4373
+ "stored topic must already be trimmed"
4374
+ );
4375
+ var IdempotencyKeySchema = external_exports.string().regex(
4376
+ /^[A-Za-z0-9._:-]{1,128}$/,
4377
+ "must contain 1-128 portable idempotency-key characters"
4378
+ );
4379
+ var ThreadMemberSchema = external_exports.object({
4380
+ participant_id: ParticipantIdSchema,
4381
+ identity: ContainerIdSchema
4382
+ }).strict();
4383
+ var ThreadRootSchema = external_exports.object({
4384
+ schema_version: external_exports.literal(1),
4385
+ thread_id: ParticipantIdSchema,
4386
+ topic: StoredTopicSchema,
4387
+ creator_participant_id: ParticipantIdSchema,
4388
+ members: external_exports.array(ThreadMemberSchema).min(1),
4389
+ idempotency_key: IdempotencyKeySchema,
4390
+ fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/, "must be a lowercase SHA-256 digest")
4391
+ }).strict().superRefine((root, context) => {
4392
+ const participantIds = /* @__PURE__ */ new Set();
4393
+ const identities = /* @__PURE__ */ new Set();
4394
+ for (const [index, member] of root.members.entries()) {
4395
+ if (participantIds.has(member.participant_id)) {
4396
+ context.addIssue({
4397
+ code: external_exports.ZodIssueCode.custom,
4398
+ path: ["members", index, "participant_id"],
4399
+ message: "thread member participant IDs must be unique"
4400
+ });
4401
+ }
4402
+ participantIds.add(member.participant_id);
4403
+ if (identities.has(member.identity)) {
4404
+ context.addIssue({
4405
+ code: external_exports.ZodIssueCode.custom,
4406
+ path: ["members", index, "identity"],
4407
+ message: "thread member identities must be unique"
4408
+ });
4409
+ }
4410
+ identities.add(member.identity);
4411
+ }
4412
+ if (!participantIds.has(root.creator_participant_id)) {
4413
+ context.addIssue({
4414
+ code: external_exports.ZodIssueCode.custom,
4415
+ path: ["creator_participant_id"],
4416
+ message: "thread creator must be a member"
4417
+ });
4418
+ }
4419
+ });
4420
+ var ThreadScopeSchema = external_exports.object({
4421
+ thread_id: ParticipantIdSchema,
4422
+ parent_key: external_exports.string().regex(
4423
+ /^(?:message|file):[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4424
+ "must identify an immediate message or file parent"
4425
+ ).optional()
4426
+ }).strict();
4427
+ var StartThreadInputSchema = external_exports.object({
4428
+ topic: InputTopicSchema,
4429
+ participant_ids: external_exports.array(ParticipantIdSchema).min(1),
4430
+ idempotency_key: IdempotencyKeySchema
4431
+ }).strict();
4432
+
4359
4433
  // src/contracts.ts
4360
4434
  var MAX_TEXT_BYTES = 262144;
4361
4435
  var MAX_FILE_BYTES = 2 * 1024 * 1024;
@@ -4379,10 +4453,10 @@ var LowerCrockfordUlidSchema = external_exports.string().regex(
4379
4453
  /^[0-7][0-9a-hjkmnp-tv-z]{25}$/,
4380
4454
  "must be a 26-character lowercase Crockford ULID"
4381
4455
  );
4382
- var ContainerIdSchema = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4456
+ var ContainerIdSchema2 = external_exports.string().regex(/^[0-9a-f]{64}$/i, "must be a 64-character hexadecimal CID").transform((value) => value.toUpperCase());
4383
4457
  var RuntimeCommandNameSchema = external_exports.union([external_exports.enum(RUNTIME_COMMAND_NAMES), ConsumerCommandNameSchema]);
4384
4458
  var RuntimeCommandGrantSchema = external_exports.object({
4385
- caller_cid: ContainerIdSchema,
4459
+ caller_cid: ContainerIdSchema2,
4386
4460
  command: RuntimeCommandNameSchema
4387
4461
  }).strict();
4388
4462
  function isStrictRfc3339(value) {
@@ -4727,7 +4801,7 @@ var CurrentRoomSchema = external_exports.object({
4727
4801
  lifecycle_request: external_exports.object({
4728
4802
  request_id: external_exports.string().min(1).max(256),
4729
4803
  command: external_exports.enum(["room.close", "room.delete"]),
4730
- caller_cid: ContainerIdSchema,
4804
+ caller_cid: ContainerIdSchema2,
4731
4805
  accepted_at: Rfc3339Schema,
4732
4806
  state: external_exports.enum(["pending", "failed", "completed"]),
4733
4807
  error: external_exports.literal("lifecycle_failed").optional()
@@ -4881,7 +4955,7 @@ var AcceptExternalInviteInputSchema = external_exports.object({
4881
4955
  (value) => Buffer.byteLength(value, "utf8") <= MAX_EXTERNAL_INVITE_BYTES,
4882
4956
  `invite input must be at most ${MAX_EXTERNAL_INVITE_BYTES} UTF-8 bytes`
4883
4957
  ),
4884
- expected_cid: ContainerIdSchema.optional()
4958
+ expected_cid: ContainerIdSchema2.optional()
4885
4959
  }).strict();
4886
4960
  var ListMembersCommandInputSchema = external_exports.object({}).strict();
4887
4961
  var CommandIdempotencyKeySchema = external_exports.string().regex(
@@ -4956,7 +5030,9 @@ var MessageShape = {
4956
5030
  }),
4957
5031
  source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
4958
5032
  source_wire_id: NonEmptyStringSchema.optional(),
4959
- source_reply_to: ReplyReferenceSchema.optional()
5033
+ source_reply_to: ReplyReferenceSchema.optional(),
5034
+ scope: ThreadScopeSchema.optional(),
5035
+ thread_root: ThreadRootSchema.optional()
4960
5036
  };
4961
5037
  var RelayIntentShape = {
4962
5038
  kind: external_exports.literal("relay_intent"),
@@ -4964,7 +5040,12 @@ var RelayIntentShape = {
4964
5040
  file_id: LowerCrockfordUlidSchema.optional(),
4965
5041
  recipient_identity: NonEmptyStringSchema
4966
5042
  };
4967
- var RelayResultStatusSchema = external_exports.enum(["queued", "send_failed", "skipped_removed"]);
5043
+ var RelayResultStatusSchema = external_exports.enum([
5044
+ "queued",
5045
+ "send_failed",
5046
+ "skipped_removed",
5047
+ "skipped_reply_unavailable"
5048
+ ]);
4968
5049
  var RelayResultShape = {
4969
5050
  kind: external_exports.literal("relay_result"),
4970
5051
  intent_record_id: NonEmptyStringSchema,
@@ -5002,6 +5083,18 @@ var FileShape = {
5002
5083
  source_wire_id: NonEmptyStringSchema.optional(),
5003
5084
  source_reply_to: ReplyReferenceSchema.optional()
5004
5085
  };
5086
+ var IntakeRejectionShape = {
5087
+ kind: external_exports.literal("intake_rejection"),
5088
+ source_kind: external_exports.enum(["message", "file"]),
5089
+ source_msg_id: external_exports.number().int().nonnegative().safe().optional(),
5090
+ source_file_id: external_exports.number().int().nonnegative().safe().optional(),
5091
+ source_wire_id: NonEmptyStringSchema,
5092
+ sender_identity: NonEmptyStringSchema,
5093
+ sender_participant_id: LowerCrockfordUlidSchema,
5094
+ fingerprint: external_exports.string().regex(/^[0-9a-f]{64}$/),
5095
+ error: external_exports.enum(["reply_target_unavailable", "thread_files_unsupported"]),
5096
+ notification_attempt_claimed: external_exports.literal(true)
5097
+ };
5005
5098
  var MembershipIntentShape = {
5006
5099
  kind: external_exports.literal("membership_intent"),
5007
5100
  action: external_exports.enum(["remove"]),
@@ -5039,6 +5132,7 @@ var MessageRecordSchema = external_exports.object({ ...RecordCommonShape, ...Mes
5039
5132
  var FileRecordSchema = external_exports.object({ ...RecordCommonShape, ...FileShape }).strict();
5040
5133
  var RelayIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayIntentShape }).strict();
5041
5134
  var RelayResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...RelayResultShape }).strict();
5135
+ var IntakeRejectionRecordSchema = external_exports.object({ ...RecordCommonShape, ...IntakeRejectionShape }).strict();
5042
5136
  var MembershipIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipIntentShape }).strict();
5043
5137
  var MembershipResultRecordSchema = external_exports.object({ ...RecordCommonShape, ...MembershipResultShape }).strict();
5044
5138
  var CloseNoticeIntentRecordSchema = external_exports.object({ ...RecordCommonShape, ...CloseNoticeIntentShape }).strict();
@@ -5048,6 +5142,7 @@ var RawCommunicationRecordSchema = external_exports.discriminatedUnion("kind", [
5048
5142
  FileRecordSchema,
5049
5143
  RelayIntentRecordSchema,
5050
5144
  RelayResultRecordSchema,
5145
+ IntakeRejectionRecordSchema,
5051
5146
  MembershipIntentRecordSchema,
5052
5147
  MembershipResultRecordSchema,
5053
5148
  CloseNoticeIntentRecordSchema,
@@ -5063,6 +5158,18 @@ function refineRelaySubject(record, context) {
5063
5158
  });
5064
5159
  }
5065
5160
  }
5161
+ function refineIntakeRejection(record, context) {
5162
+ if (record.kind !== "intake_rejection") return;
5163
+ const hasMessage = record.source_msg_id !== void 0;
5164
+ const hasFile = record.source_file_id !== void 0;
5165
+ if (hasMessage === hasFile || (record.source_kind === "message" ? !hasMessage : !hasFile)) {
5166
+ context.addIssue({
5167
+ code: external_exports.ZodIssueCode.custom,
5168
+ path: ["source_kind"],
5169
+ message: "intake rejections require exactly one numeric source field matching source_kind"
5170
+ });
5171
+ }
5172
+ }
5066
5173
  function refineFileRecord(record, context) {
5067
5174
  if (record.kind !== "file" || record.data_base64 === void 0) return;
5068
5175
  const bytes = Buffer.from(record.data_base64, "base64");
@@ -5113,6 +5220,102 @@ function refineMessageCategory(message, context) {
5113
5220
  }
5114
5221
  }
5115
5222
  }
5223
+ function refineMessageThread(message, context) {
5224
+ if (message.thread_root !== void 0) {
5225
+ if (message.scope === void 0) {
5226
+ context.addIssue({
5227
+ code: external_exports.ZodIssueCode.custom,
5228
+ path: ["scope"],
5229
+ message: "thread root messages require scope"
5230
+ });
5231
+ return;
5232
+ }
5233
+ if (message.scope.thread_id !== message.message_id || message.thread_root.thread_id !== message.message_id) {
5234
+ context.addIssue({
5235
+ code: external_exports.ZodIssueCode.custom,
5236
+ path: ["thread_root", "thread_id"],
5237
+ message: "thread root thread_id and scope thread_id must equal message_id"
5238
+ });
5239
+ }
5240
+ if (message.scope.parent_key !== void 0) {
5241
+ context.addIssue({
5242
+ code: external_exports.ZodIssueCode.custom,
5243
+ path: ["scope", "parent_key"],
5244
+ message: "parent_key is forbidden on thread root messages"
5245
+ });
5246
+ }
5247
+ if (message.category !== "chat") {
5248
+ context.addIssue({
5249
+ code: external_exports.ZodIssueCode.custom,
5250
+ path: ["category"],
5251
+ message: "thread root messages must be chat messages"
5252
+ });
5253
+ }
5254
+ if (message.text !== `Thread: ${message.thread_root.topic}`) {
5255
+ context.addIssue({
5256
+ code: external_exports.ZodIssueCode.custom,
5257
+ path: ["text"],
5258
+ message: "thread root message text must identify its topic"
5259
+ });
5260
+ }
5261
+ const creator = message.thread_root.members.find(
5262
+ (member) => member.participant_id === message.thread_root?.creator_participant_id
5263
+ );
5264
+ if (creator?.identity !== message.author.identity) {
5265
+ context.addIssue({
5266
+ code: external_exports.ZodIssueCode.custom,
5267
+ path: ["thread_root", "creator_participant_id"],
5268
+ message: "thread root creator must match the message author"
5269
+ });
5270
+ }
5271
+ if (message.author_alias !== void 0 && message.author_alias.participant_id !== message.thread_root.creator_participant_id) {
5272
+ context.addIssue({
5273
+ code: external_exports.ZodIssueCode.custom,
5274
+ path: ["author_alias", "participant_id"],
5275
+ message: "thread root author alias must identify the creator"
5276
+ });
5277
+ }
5278
+ for (const field of ["source_msg_id", "source_wire_id", "source_reply_to"]) {
5279
+ if (message[field] !== void 0) {
5280
+ context.addIssue({
5281
+ code: external_exports.ZodIssueCode.custom,
5282
+ path: [field],
5283
+ message: `${field} is forbidden on thread root messages`
5284
+ });
5285
+ }
5286
+ }
5287
+ return;
5288
+ }
5289
+ if (message.scope === void 0) return;
5290
+ if (message.scope.parent_key === void 0) {
5291
+ context.addIssue({
5292
+ code: external_exports.ZodIssueCode.custom,
5293
+ path: ["scope", "parent_key"],
5294
+ message: "thread descendants require an immediate parent_key"
5295
+ });
5296
+ }
5297
+ if (message.scope.thread_id === message.message_id) {
5298
+ context.addIssue({
5299
+ code: external_exports.ZodIssueCode.custom,
5300
+ path: ["scope", "thread_id"],
5301
+ message: "thread descendant thread_id must identify a distinct root message"
5302
+ });
5303
+ }
5304
+ if (message.source_reply_to === void 0) {
5305
+ context.addIssue({
5306
+ code: external_exports.ZodIssueCode.custom,
5307
+ path: ["source_reply_to"],
5308
+ message: "thread descendants require source_reply_to"
5309
+ });
5310
+ }
5311
+ if (message.category !== "chat") {
5312
+ context.addIssue({
5313
+ code: external_exports.ZodIssueCode.custom,
5314
+ path: ["category"],
5315
+ message: "thread descendants must be chat messages"
5316
+ });
5317
+ }
5318
+ }
5116
5319
  var CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record, context) => {
5117
5320
  if (record.record_id !== `${record.room_id}:${record.seq}`) {
5118
5321
  context.addIssue({
@@ -5121,8 +5324,12 @@ var CommunicationRecordSchema = RawCommunicationRecordSchema.superRefine((record
5121
5324
  message: 'record_id must equal room_id + ":" + seq'
5122
5325
  });
5123
5326
  }
5124
- if (record.kind === "message") refineMessageCategory(record, context);
5327
+ if (record.kind === "message") {
5328
+ refineMessageCategory(record, context);
5329
+ refineMessageThread(record, context);
5330
+ }
5125
5331
  refineRelaySubject(record, context);
5332
+ refineIntakeRejection(record, context);
5126
5333
  refineFileRecord(record, context);
5127
5334
  });
5128
5335
  var AppendRecordSchema = external_exports.discriminatedUnion("kind", [
@@ -5130,11 +5337,16 @@ var AppendRecordSchema = external_exports.discriminatedUnion("kind", [
5130
5337
  external_exports.object({ ...AppendCommonShape, ...FileShape }).strict(),
5131
5338
  external_exports.object({ ...AppendCommonShape, ...RelayIntentShape }).strict(),
5132
5339
  external_exports.object({ ...AppendCommonShape, ...RelayResultShape }).strict(),
5340
+ external_exports.object({ ...AppendCommonShape, ...IntakeRejectionShape }).strict(),
5133
5341
  external_exports.object({ ...AppendCommonShape, ...CloseNoticeIntentShape }).strict(),
5134
5342
  external_exports.object({ ...AppendCommonShape, ...CloseNoticeResultShape }).strict()
5135
5343
  ]).superRefine((record, context) => {
5136
- if (record.kind === "message") refineMessageCategory(record, context);
5344
+ if (record.kind === "message") {
5345
+ refineMessageCategory(record, context);
5346
+ refineMessageThread(record, context);
5347
+ }
5137
5348
  refineRelaySubject(record, context);
5349
+ refineIntakeRejection(record, context);
5138
5350
  refineFileRecord(record, context);
5139
5351
  });
5140
5352